diff --git a/CMakeLists.txt b/CMakeLists.txt index b06605c4..7c6102db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,7 @@ set(WOWEE_SOURCES src/auth/auth_handler.cpp src/auth/auth_opcodes.cpp src/auth/auth_packets.cpp + src/auth/pin_auth.cpp src/auth/srp.cpp src/auth/big_num.cpp src/auth/crypto.cpp diff --git a/include/auth/auth_handler.hpp b/include/auth/auth_handler.hpp index 103711e7..14bfe0bd 100644 --- a/include/auth/auth_handler.hpp +++ b/include/auth/auth_handler.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace wowee { namespace network { class TCPSocket; class Packet; } @@ -19,6 +20,7 @@ enum class AuthState { CONNECTED, CHALLENGE_SENT, CHALLENGE_RECEIVED, + PIN_REQUIRED, PROOF_SENT, AUTHENTICATED, REALM_LIST_REQUESTED, @@ -44,6 +46,9 @@ public: // Authentication void authenticate(const std::string& username, const std::string& password); void authenticateWithHash(const std::string& username, const std::vector& authHash); + // Optional: when the auth server requires a PIN (securityFlags & 0x01), call this to continue. + // PIN must be 4-10 digits. + void submitPin(const std::string& pin); // Set client version info (call before authenticate) void setClientInfo(const ClientInfo& info) { clientInfo = info; } @@ -96,6 +101,12 @@ private: // Receive buffer std::vector receiveBuffer; + + // Challenge security extension (PIN) + uint8_t securityFlags_ = 0; + uint32_t pinGridSeed_ = 0; + std::array pinServerSalt_{}; // from LOGON_CHALLENGE response + std::string pendingPin_; }; } // namespace auth diff --git a/include/auth/auth_packets.hpp b/include/auth/auth_packets.hpp index 7a86deaa..d11c531c 100644 --- a/include/auth/auth_packets.hpp +++ b/include/auth/auth_packets.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace wowee { namespace auth { @@ -38,6 +39,10 @@ struct LogonChallengeResponse { std::vector salt; // Salt (32 bytes) uint8_t securityFlags; + // PIN extension (securityFlags & 0x01) + uint32_t pinGridSeed = 0; + std::array pinSalt{}; + bool isSuccess() const { return result == AuthResult::SUCCESS; } }; @@ -52,6 +57,11 @@ class LogonProofPacket { public: static network::Packet build(const std::vector& A, const std::vector& M1); + static network::Packet build(const std::vector& A, + const std::vector& M1, + uint8_t securityFlags, + const std::array* pinClientSalt, + const std::array* pinHash); }; // LOGON_PROOF response data diff --git a/include/auth/pin_auth.hpp b/include/auth/pin_auth.hpp new file mode 100644 index 00000000..00211776 --- /dev/null +++ b/include/auth/pin_auth.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace auth { + +struct PinProof { + std::array clientSalt{}; + std::array hash{}; +}; + +// Implements the "PIN" security extension used in the WoW auth protocol (securityFlags & 0x01). +// Algorithm based on documented client behavior: +// - Remap digits using pinGridSeed (a permutation of 0..9) +// - Convert user-entered PIN digits into randomized indices in that permutation +// - Compute: pin_hash = SHA1(client_salt || SHA1(server_salt || randomized_pin_ascii)) +// +// PIN must be 4-10 ASCII digits. +PinProof computePinProof(const std::string& pinDigits, + uint32_t pinGridSeed, + const std::array& serverSalt); + +} // namespace auth +} // namespace wowee + diff --git a/include/ui/auth_screen.hpp b/include/ui/auth_screen.hpp index 257e08b4..6c7ebc79 100644 --- a/include/ui/auth_screen.hpp +++ b/include/ui/auth_screen.hpp @@ -54,6 +54,7 @@ private: char hostname[256] = "127.0.0.1"; char username[256] = ""; char password[256] = ""; + char pinCode[32] = ""; int port = 3724; int expansionIndex = 0; // Index into expansion registry profiles bool authenticating = false; diff --git a/src/auth/auth_handler.cpp b/src/auth/auth_handler.cpp index 3867454c..5deaa32d 100644 --- a/src/auth/auth_handler.cpp +++ b/src/auth/auth_handler.cpp @@ -1,9 +1,12 @@ #include "auth/auth_handler.hpp" +#include "auth/pin_auth.hpp" #include "network/tcp_socket.hpp" #include "network/packet.hpp" #include "core/logger.hpp" #include #include +#include +#include namespace wowee { namespace auth { @@ -17,7 +20,17 @@ AuthHandler::~AuthHandler() { } bool AuthHandler::connect(const std::string& host, uint16_t port) { - LOG_INFO("Connecting to auth server: ", host, ":", port); + auto trimHost = [](std::string s) { + auto isSpace = [](unsigned char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; }; + size_t b = 0; + while (b < s.size() && isSpace(static_cast(s[b]))) ++b; + size_t e = s.size(); + while (e > b && isSpace(static_cast(s[e - 1]))) --e; + return s.substr(b, e - b); + }; + + const std::string hostTrimmed = trimHost(host); + LOG_INFO("Connecting to auth server: ", hostTrimmed, ":", port); socket = std::make_unique(); @@ -28,7 +41,7 @@ bool AuthHandler::connect(const std::string& host, uint16_t port) { handlePacket(mutablePacket); }); - if (!socket->connect(host, port)) { + if (!socket->connect(hostTrimmed, port)) { LOG_ERROR("Failed to connect to auth server"); setState(AuthState::FAILED); return false; @@ -84,6 +97,10 @@ void AuthHandler::authenticate(const std::string& user, const std::string& pass) username = user; password = pass; + pendingPin_.clear(); + securityFlags_ = 0; + pinGridSeed_ = 0; + pinServerSalt_ = {}; // Initialize SRP srp = std::make_unique(); @@ -110,6 +127,10 @@ void AuthHandler::authenticateWithHash(const std::string& user, const std::vecto username = user; password.clear(); + pendingPin_.clear(); + securityFlags_ = 0; + pinGridSeed_ = 0; + pinServerSalt_ = {}; // Initialize SRP with pre-computed hash srp = std::make_unique(); @@ -144,7 +165,7 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) { if (response.securityFlags != 0) { LOG_WARNING("Server sent security flags: 0x", std::hex, (int)response.securityFlags, std::dec); - if (response.securityFlags & 0x01) LOG_WARNING(" PIN required (not supported)"); + if (response.securityFlags & 0x01) LOG_WARNING(" PIN required"); if (response.securityFlags & 0x02) LOG_WARNING(" Matrix card required (not supported)"); if (response.securityFlags & 0x04) LOG_WARNING(" Authenticator required (not supported)"); } @@ -155,9 +176,20 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) { // Feed SRP with server challenge data srp->feed(response.B, response.g, response.N, response.salt); + securityFlags_ = response.securityFlags; + if (securityFlags_ & 0x01) { + pinGridSeed_ = response.pinGridSeed; + pinServerSalt_ = response.pinSalt; + } + setState(AuthState::CHALLENGE_RECEIVED); - // Send LOGON_PROOF immediately + // If PIN is required, wait for user input. + if ((securityFlags_ & 0x01) && pendingPin_.empty()) { + setState(AuthState::PIN_REQUIRED); + return; + } + sendLogonProof(); } @@ -167,12 +199,38 @@ void AuthHandler::sendLogonProof() { auto A = srp->getA(); auto M1 = srp->getM1(); - auto packet = LogonProofPacket::build(A, M1); + std::array pinClientSalt{}; + std::array pinHash{}; + const std::array* pinClientSaltPtr = nullptr; + const std::array* pinHashPtr = nullptr; + + if (securityFlags_ & 0x01) { + try { + PinProof proof = computePinProof(pendingPin_, pinGridSeed_, pinServerSalt_); + pinClientSalt = proof.clientSalt; + pinHash = proof.hash; + pinClientSaltPtr = &pinClientSalt; + pinHashPtr = &pinHash; + } catch (const std::exception& e) { + fail(std::string("PIN required but invalid: ") + e.what()); + return; + } + } + + auto packet = LogonProofPacket::build(A, M1, securityFlags_, pinClientSaltPtr, pinHashPtr); socket->send(packet); setState(AuthState::PROOF_SENT); } +void AuthHandler::submitPin(const std::string& pin) { + pendingPin_ = pin; + // If we're waiting on a PIN, continue immediately. + if (state == AuthState::PIN_REQUIRED) { + sendLogonProof(); + } +} + void AuthHandler::handleLogonProofResponse(network::Packet& packet) { LOG_DEBUG("Handling LOGON_PROOF response"); diff --git a/src/auth/auth_packets.cpp b/src/auth/auth_packets.cpp index 6afc8242..8d99f4c6 100644 --- a/src/auth/auth_packets.cpp +++ b/src/auth/auth_packets.cpp @@ -135,18 +135,38 @@ bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallenge // Security flags response.securityFlags = packet.readUInt8(); + // Optional security extensions (protocol v8+) + if (response.securityFlags & 0x01) { + // PIN required: u32 pin_grid_seed + u8[16] pin_salt + response.pinGridSeed = packet.readUInt32(); + for (size_t i = 0; i < response.pinSalt.size(); ++i) { + response.pinSalt[i] = packet.readUInt8(); + } + } + LOG_DEBUG("Parsed LOGON_CHALLENGE response:"); LOG_DEBUG(" B size: ", response.B.size(), " bytes"); LOG_DEBUG(" g size: ", response.g.size(), " bytes"); LOG_DEBUG(" N size: ", response.N.size(), " bytes"); LOG_DEBUG(" salt size: ", response.salt.size(), " bytes"); LOG_DEBUG(" Security flags: ", (int)response.securityFlags); + if (response.securityFlags & 0x01) { + LOG_DEBUG(" PIN grid seed: ", response.pinGridSeed); + } return true; } network::Packet LogonProofPacket::build(const std::vector& A, const std::vector& M1) { + return build(A, M1, 0, nullptr, nullptr); +} + +network::Packet LogonProofPacket::build(const std::vector& A, + const std::vector& M1, + uint8_t securityFlags, + const std::array* pinClientSalt, + const std::array* pinHash) { if (A.size() != 32) { LOG_ERROR("Invalid A size: ", A.size(), " (expected 32)"); } @@ -171,7 +191,17 @@ network::Packet LogonProofPacket::build(const std::vector& A, packet.writeUInt8(0); // Security flags - packet.writeUInt8(0); + packet.writeUInt8(securityFlags); + + if (securityFlags & 0x01) { + if (!pinClientSalt || !pinHash) { + LOG_ERROR("LOGON_PROOF: PIN flag set but PIN data missing"); + } else { + // PIN: u8[16] client_salt + u8[20] pin_hash + packet.writeBytes(pinClientSalt->data(), pinClientSalt->size()); + packet.writeBytes(pinHash->data(), pinHash->size()); + } + } LOG_DEBUG("Built LOGON_PROOF packet:"); LOG_DEBUG(" A size: ", A.size(), " bytes"); diff --git a/src/auth/pin_auth.cpp b/src/auth/pin_auth.cpp new file mode 100644 index 00000000..289616ae --- /dev/null +++ b/src/auth/pin_auth.cpp @@ -0,0 +1,108 @@ +#include "auth/pin_auth.hpp" +#include "auth/crypto.hpp" +#include +#include +#include +#include + +namespace wowee { +namespace auth { + +static std::array randomSalt16() { + std::array out{}; + std::random_device rd; + for (auto& b : out) { + b = static_cast(rd() & 0xFFu); + } + return out; +} + +static std::array remapPinGrid(uint32_t seed) { + // Generates a permutation of digits 0..9 from a seed. + // Based on: + // https://gtker.com/wow_messages/docs/auth/pin.html + uint32_t v = seed; + std::array remapped{}; + uint8_t used = 0; + for (int i = 0; i < 10; ++i) { + uint32_t divisor = 10 - i; + uint32_t remainder = v % divisor; + v /= divisor; + + uint32_t index = 0; + for (uint32_t j = 0; j < 10; ++j) { + if (used & (1u << j)) { + continue; + } + if (index == remainder) { + used = static_cast(used | (1u << j)); + remapped[i] = static_cast(j); + break; + } + ++index; + } + } + return remapped; +} + +static std::vector randomizePinDigits(const std::string& pinDigits, + const std::array& remapped) { + // Transforms each pin digit into an index in the remapped permutation. + // Based on: + // https://gtker.com/wow_messages/docs/auth/pin.html + std::vector out; + out.reserve(pinDigits.size()); + + for (char c : pinDigits) { + uint8_t d = static_cast(c - '0'); + uint8_t idx = 0xFF; + for (uint8_t j = 0; j < 10; ++j) { + if (remapped[j] == d) { idx = j; break; } + } + if (idx == 0xFF) { + throw std::runtime_error("PIN digit not found in remapped grid"); + } + out.push_back(static_cast(idx + 0x30)); // ASCII '0'+idx + } + + return out; +} + +PinProof computePinProof(const std::string& pinDigits, + uint32_t pinGridSeed, + const std::array& serverSalt) { + if (pinDigits.size() < 4 || pinDigits.size() > 10) { + throw std::runtime_error("PIN must be 4-10 digits"); + } + if (!std::all_of(pinDigits.begin(), pinDigits.end(), + [](unsigned char c) { return c >= '0' && c <= '9'; })) { + throw std::runtime_error("PIN must contain only digits"); + } + + const auto remapped = remapPinGrid(pinGridSeed); + const auto randomizedAsciiDigits = randomizePinDigits(pinDigits, remapped); + + // server_hash = SHA1(server_salt || randomized_pin_ascii) + std::vector serverHashInput; + serverHashInput.reserve(serverSalt.size() + randomizedAsciiDigits.size()); + serverHashInput.insert(serverHashInput.end(), serverSalt.begin(), serverSalt.end()); + serverHashInput.insert(serverHashInput.end(), randomizedAsciiDigits.begin(), randomizedAsciiDigits.end()); + const auto serverHash = Crypto::sha1(serverHashInput); // 20 bytes + + PinProof proof; + proof.clientSalt = randomSalt16(); + + // final_hash = SHA1(client_salt || server_hash) + std::vector finalInput; + finalInput.reserve(proof.clientSalt.size() + serverHash.size()); + finalInput.insert(finalInput.end(), proof.clientSalt.begin(), proof.clientSalt.end()); + finalInput.insert(finalInput.end(), serverHash.begin(), serverHash.end()); + const auto finalHash = Crypto::sha1(finalInput); + std::copy_n(finalHash.begin(), proof.hash.size(), proof.hash.begin()); + + return proof; +} + +} // namespace auth +} // namespace wowee + diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index f1e259cf..0bea71b1 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -354,15 +354,26 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { // Connect button if (authenticating) { - authTimer += ImGui::GetIO().DeltaTime; + auto state = authHandler.getState(); + if (state != auth::AuthState::PIN_REQUIRED) { + authTimer += ImGui::GetIO().DeltaTime; - // Show progress with elapsed time - char progressBuf[128]; - snprintf(progressBuf, sizeof(progressBuf), "Authenticating... (%.0fs)", authTimer); - ImGui::Text("%s", progressBuf); + // Show progress with elapsed time + char progressBuf[128]; + snprintf(progressBuf, sizeof(progressBuf), "Authenticating... (%.0fs)", authTimer); + ImGui::Text("%s", progressBuf); + } else { + ImGui::TextWrapped("This server requires a PIN. Enter your PIN to continue."); + ImGui::InputText("PIN", pinCode, sizeof(pinCode), ImGuiInputTextFlags_Password); + ImGui::SameLine(); + if (ImGui::Button("Submit PIN")) { + authHandler.submitPin(pinCode); + // Don't keep the PIN around longer than needed. + pinCode[0] = '\0'; + } + } // Check authentication status - auto state = authHandler.getState(); if (state == auth::AuthState::AUTHENTICATED) { setStatus("Authentication successful!", false); authenticating = false; @@ -390,7 +401,7 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { setStatus("Authentication failed", true); } authenticating = false; - } else if (authTimer >= AUTH_TIMEOUT) { + } else if (state != auth::AuthState::PIN_REQUIRED && authTimer >= AUTH_TIMEOUT) { setStatus("Connection timed out - server did not respond", true); authenticating = false; authHandler.disconnect();