From 5435796a98caa4657875549a39b18cc383fdb3d5 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Feb 2026 01:32:15 -0800 Subject: [PATCH] Add integrity hash support and SRP tuning options --- CMakeLists.txt | 23 +++ include/auth/auth_handler.hpp | 1 + include/auth/auth_packets.hpp | 2 + include/auth/integrity.hpp | 35 ++++ include/auth/srp.hpp | 11 + src/auth/auth_handler.cpp | 67 ++++-- src/auth/auth_packets.cpp | 17 +- src/auth/integrity.cpp | 91 +++++++++ src/auth/srp.cpp | 16 +- tools/auth_login_probe/main.cpp | 350 ++++++++++++++++++++++++++++++++ 10 files changed, 591 insertions(+), 22 deletions(-) create mode 100644 include/auth/integrity.hpp create mode 100644 src/auth/integrity.cpp create mode 100644 tools/auth_login_probe/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9729a187..dc8e9674 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,7 @@ set(WOWEE_SOURCES src/auth/auth_opcodes.cpp src/auth/auth_packets.cpp src/auth/pin_auth.cpp + src/auth/integrity.cpp src/auth/srp.cpp src/auth/big_num.cpp src/auth/crypto.cpp @@ -444,6 +445,28 @@ set_target_properties(auth_probe PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin ) +# ---- Tool: auth_login_probe (challenge + proof probe) ---- +add_executable(auth_login_probe + tools/auth_login_probe/main.cpp + src/auth/auth_packets.cpp + src/auth/auth_opcodes.cpp + src/auth/crypto.cpp + src/auth/integrity.cpp + src/auth/big_num.cpp + src/auth/srp.cpp + src/network/packet.cpp + src/network/socket.cpp + src/network/tcp_socket.cpp + src/core/logger.cpp +) +target_include_directories(auth_login_probe PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) +target_link_libraries(auth_login_probe PRIVATE Threads::Threads OpenSSL::Crypto) +set_target_properties(auth_login_probe PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin +) + # ---- Tool: blp_convert (BLP ↔ PNG) ---- add_executable(blp_convert tools/blp_convert/main.cpp diff --git a/include/auth/auth_handler.hpp b/include/auth/auth_handler.hpp index 2f015885..3916d0a1 100644 --- a/include/auth/auth_handler.hpp +++ b/include/auth/auth_handler.hpp @@ -111,6 +111,7 @@ private: uint8_t securityFlags_ = 0; uint32_t pinGridSeed_ = 0; std::array pinServerSalt_{}; // from LOGON_CHALLENGE response + std::array checksumSalt_{}; // from LOGON_CHALLENGE response (integrity salt) std::string pendingSecurityCode_; }; diff --git a/include/auth/auth_packets.hpp b/include/auth/auth_packets.hpp index fefabc6c..a6ff0ed0 100644 --- a/include/auth/auth_packets.hpp +++ b/include/auth/auth_packets.hpp @@ -37,6 +37,7 @@ struct LogonChallengeResponse { std::vector g; // Generator (variable, usually 1 byte) std::vector N; // Prime modulus (variable, usually 256 bytes) std::vector salt; // Salt (32 bytes) + std::array checksumSalt{}; // aka "crc_salt"/integrity salt uint8_t securityFlags; // PIN extension (securityFlags & 0x01) @@ -66,6 +67,7 @@ public: static network::Packet build(const std::vector& A, const std::vector& M1, uint8_t securityFlags, + const std::array* crcHash, const std::array* pinClientSalt, const std::array* pinHash); }; diff --git a/include/auth/integrity.hpp b/include/auth/integrity.hpp new file mode 100644 index 00000000..1201455a --- /dev/null +++ b/include/auth/integrity.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace auth { + +// Computes the LOGON_PROOF "CRC hash" / integrity hash for the legacy WoW login protocol. +// +// Algorithm (per WoWDev/gtker docs): +// checksum = HMAC_SHA1(checksumSalt, concatenated_file_bytes) +// crc_hash = SHA1(clientPublicKey || checksum) +// +// clientPublicKey is the 32-byte A as sent on the wire. +// +// Returns false if any file is missing/unreadable. +bool computeIntegrityHashWin32(const std::array& checksumSalt, + const std::vector& clientPublicKeyA, + const std::string& miscDir, + std::array& outHash, + std::string& outError); + +// Same as computeIntegrityHashWin32, but allows selecting the EXE filename used in the file set. +bool computeIntegrityHashWin32WithExe(const std::array& checksumSalt, + const std::vector& clientPublicKeyA, + const std::string& miscDir, + const std::string& exeName, + std::array& outHash, + std::string& outError); + +} // namespace auth +} // namespace wowee diff --git a/include/auth/srp.hpp b/include/auth/srp.hpp index 00fd2c1d..1cb8cae4 100644 --- a/include/auth/srp.hpp +++ b/include/auth/srp.hpp @@ -27,6 +27,15 @@ public: const std::vector& N, const std::vector& salt); + // Some SRP implementations use k = H(N|g) instead of the WoW-specific k=3. + // Default is false (k=3). + void setUseHashedK(bool enabled) { useHashedK_ = enabled; } + + // Controls how SHA1 outputs are interpreted when converted to big integers (x, u, optionally k). + // Many SRP implementations treat hash outputs as big-endian integers. + // Default is false (treat hash outputs as little-endian integers). + void setHashBigEndian(bool enabled) { hashBigEndian_ = enabled; } + // Get client public ephemeral (A) - send to server std::vector getA() const; @@ -73,6 +82,8 @@ private: std::vector stored_auth_hash; // Pre-computed SHA1(UPPER(user):UPPER(pass)) bool initialized = false; + bool useHashedK_ = false; + bool hashBigEndian_ = false; }; } // namespace auth diff --git a/src/auth/auth_handler.cpp b/src/auth/auth_handler.cpp index f0e3c891..e9fa8b46 100644 --- a/src/auth/auth_handler.cpp +++ b/src/auth/auth_handler.cpp @@ -1,5 +1,6 @@ #include "auth/auth_handler.hpp" #include "auth/pin_auth.hpp" +#include "auth/integrity.hpp" #include "network/tcp_socket.hpp" #include "network/packet.hpp" #include "core/logger.hpp" @@ -7,6 +8,7 @@ #include #include #include +#include namespace wowee { namespace auth { @@ -105,6 +107,7 @@ void AuthHandler::authenticate(const std::string& user, const std::string& pass, securityFlags_ = 0; pinGridSeed_ = 0; pinServerSalt_ = {}; + checksumSalt_ = {}; // Initialize SRP srp = std::make_unique(); @@ -139,6 +142,7 @@ void AuthHandler::authenticateWithHash(const std::string& user, const std::vecto securityFlags_ = 0; pinGridSeed_ = 0; pinServerSalt_ = {}; + checksumSalt_ = {}; // Initialize SRP with pre-computed hash srp = std::make_unique(); @@ -196,6 +200,7 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) { srp->feed(response.B, response.g, response.N, response.salt); securityFlags_ = response.securityFlags; + checksumSalt_ = response.checksumSalt; if (securityFlags_ & 0x01) { pinGridSeed_ = response.pinGridSeed; pinServerSalt_ = response.pinSalt; @@ -222,6 +227,8 @@ void AuthHandler::sendLogonProof() { std::array pinHash{}; const std::array* pinClientSaltPtr = nullptr; const std::array* pinHashPtr = nullptr; + std::array crcHash{}; + const std::array* crcHashPtr = nullptr; if (securityFlags_ & 0x01) { try { @@ -236,20 +243,54 @@ void AuthHandler::sendLogonProof() { } } - // Protocol < 8 uses a shorter proof packet (no securityFlags byte). - if (clientInfo.protocolVersion < 8) { - auto packet = LogonProofPacket::buildLegacy(A, M1); - socket->send(packet); - } else { - auto packet = LogonProofPacket::build(A, M1, securityFlags_, pinClientSaltPtr, pinHashPtr); - socket->send(packet); - - if (securityFlags_ & 0x04) { - // TrinityCore-style Google Authenticator token: send immediately after proof. - const std::string token = pendingSecurityCode_; - auto tokPkt = AuthenticatorTokenPacket::build(token); - socket->send(tokPkt); + // Legacy client integrity hash (aka "CRC hash"). Some servers enforce this for classic builds. + // We compute it when checksumSalt was provided (always present on success challenge) and files exist. + { + std::vector candidateDirs; + if (const char* env = std::getenv("WOWEE_INTEGRITY_DIR")) { + if (env && *env) candidateDirs.push_back(env); } + // Default local extraction layout + candidateDirs.push_back("Data/misc"); + // Common turtle repack location used in this workspace + if (const char* home = std::getenv("HOME")) { + if (home && *home) { + candidateDirs.push_back(std::string(home) + "/Downloads/twmoa_1180"); + candidateDirs.push_back(std::string(home) + "/twmoa_1180"); + } + } + + const char* candidateExes[] = { "WoW.exe", "TurtleWoW.exe", "Wow.exe" }; + bool ok = false; + std::string lastErr; + for (const auto& dir : candidateDirs) { + for (const char* exe : candidateExes) { + std::string err; + if (computeIntegrityHashWin32WithExe(checksumSalt_, A, dir, exe, crcHash, err)) { + crcHashPtr = &crcHash; + LOG_INFO("Integrity hash computed from ", dir, " (", exe, ")"); + ok = true; + break; + } + lastErr = err; + } + if (ok) break; + } + if (!ok) { + LOG_WARNING("Integrity hash not computed (", lastErr, + "). Server may reject classic clients without it. " + "Set WOWEE_INTEGRITY_DIR to your client folder."); + } + } + + auto packet = LogonProofPacket::build(A, M1, securityFlags_, crcHashPtr, pinClientSaltPtr, pinHashPtr); + socket->send(packet); + + if ((securityFlags_ & 0x04) && clientInfo.protocolVersion >= 8) { + // TrinityCore-style Google Authenticator token: send immediately after proof. + const std::string token = pendingSecurityCode_; + auto tokPkt = AuthenticatorTokenPacket::build(token); + socket->send(tokPkt); } setState(AuthState::PROOF_SENT); diff --git a/src/auth/auth_packets.cpp b/src/auth/auth_packets.cpp index 0082efa9..924a15b5 100644 --- a/src/auth/auth_packets.cpp +++ b/src/auth/auth_packets.cpp @@ -126,9 +126,9 @@ bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallenge response.salt[i] = packet.readUInt8(); } - // Unknown/padding - 16 bytes - for (int i = 0; i < 16; ++i) { - packet.readUInt8(); + // Integrity salt / CRC salt - 16 bytes + for (size_t i = 0; i < response.checksumSalt.size(); ++i) { + response.checksumSalt[i] = packet.readUInt8(); } // Security flags @@ -162,7 +162,7 @@ bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallenge network::Packet LogonProofPacket::build(const std::vector& A, const std::vector& M1) { - return build(A, M1, 0, nullptr, nullptr); + return build(A, M1, 0, nullptr, nullptr, nullptr); } network::Packet LogonProofPacket::buildLegacy(const std::vector& A, @@ -185,6 +185,7 @@ network::Packet LogonProofPacket::buildLegacy(const std::vector& A, network::Packet LogonProofPacket::build(const std::vector& A, const std::vector& M1, uint8_t securityFlags, + const std::array* crcHash, const std::array* pinClientSalt, const std::array* pinHash) { if (A.size() != 32) { @@ -202,9 +203,11 @@ network::Packet LogonProofPacket::build(const std::vector& A, // M1 (client proof) - 20 bytes packet.writeBytes(M1.data(), M1.size()); - // CRC hash - 20 bytes (zeros) - for (int i = 0; i < 20; ++i) { - packet.writeUInt8(0); + // CRC hash / integrity hash - 20 bytes + if (crcHash) { + packet.writeBytes(crcHash->data(), crcHash->size()); + } else { + for (int i = 0; i < 20; ++i) packet.writeUInt8(0); } // Number of keys diff --git a/src/auth/integrity.cpp b/src/auth/integrity.cpp new file mode 100644 index 00000000..8b569c14 --- /dev/null +++ b/src/auth/integrity.cpp @@ -0,0 +1,91 @@ +#include "auth/integrity.hpp" +#include "auth/crypto.hpp" + +#include +#include + +namespace wowee { +namespace auth { + +static bool readWholeFile(const std::string& path, std::vector& out, std::string& err) { + std::ifstream f(path, std::ios::binary); + if (!f.is_open()) { + err = "missing: " + path; + return false; + } + f.seekg(0, std::ios::end); + std::streamoff size = f.tellg(); + if (size < 0) size = 0; + f.seekg(0, std::ios::beg); + out.resize(static_cast(size)); + if (size > 0) { + f.read(reinterpret_cast(out.data()), size); + if (!f) { + err = "read failed: " + path; + return false; + } + } + return true; +} + +bool computeIntegrityHashWin32WithExe(const std::array& checksumSalt, + const std::vector& clientPublicKeyA, + const std::string& miscDir, + const std::string& exeName, + std::array& outHash, + std::string& outError) { + // Files expected by 1.12.x Windows clients for the integrity check. + // If this needs to vary by build, make it data-driven in expansion.json later. + const char* kFiles[] = { + nullptr, // exeName + "fmod.dll", + "ijl15.dll", + "dbghelp.dll", + "unicows.dll", + }; + + std::vector allFiles; + std::string err; + for (size_t idx = 0; idx < (sizeof(kFiles) / sizeof(kFiles[0])); ++idx) { + const char* name = kFiles[idx]; + std::string nameStr = name ? std::string(name) : exeName; + std::vector bytes; + std::string path = miscDir; + if (!path.empty() && path.back() != '/') path += '/'; + path += nameStr; + if (!readWholeFile(path, bytes, err)) { + outError = err; + return false; + } + allFiles.insert(allFiles.end(), bytes.begin(), bytes.end()); + } + + // HMAC_SHA1(checksumSalt, allFiles) + std::vector key(checksumSalt.begin(), checksumSalt.end()); + const std::vector checksum = Crypto::hmacSHA1(key, allFiles); // 20 bytes + + // SHA1(A || checksum) + std::vector shaIn; + shaIn.reserve(clientPublicKeyA.size() + checksum.size()); + shaIn.insert(shaIn.end(), clientPublicKeyA.begin(), clientPublicKeyA.end()); + shaIn.insert(shaIn.end(), checksum.begin(), checksum.end()); + const std::vector finalHash = Crypto::sha1(shaIn); + + if (finalHash.size() != outHash.size()) { + outError = "unexpected sha1 size"; + return false; + } + std::copy(finalHash.begin(), finalHash.end(), outHash.begin()); + return true; +} + +bool computeIntegrityHashWin32(const std::array& checksumSalt, + const std::vector& clientPublicKeyA, + const std::string& miscDir, + std::array& outHash, + std::string& outError) { + return computeIntegrityHashWin32WithExe(checksumSalt, clientPublicKeyA, miscDir, "WoW.exe", outHash, outError); +} + +} // namespace auth +} // namespace wowee diff --git a/src/auth/srp.cpp b/src/auth/srp.cpp index c427e0d1..48438ce3 100644 --- a/src/auth/srp.cpp +++ b/src/auth/srp.cpp @@ -58,6 +58,18 @@ void SRP::feed(const std::vector& B_bytes, this->N = BigNum(N_bytes, true); this->s = BigNum(salt_bytes, true); + if (useHashedK_) { + // k = H(N | g) (SRP-6a style) + std::vector Ng; + Ng.insert(Ng.end(), N_bytes.begin(), N_bytes.end()); + Ng.insert(Ng.end(), g_bytes.begin(), g_bytes.end()); + std::vector k_bytes = Crypto::sha1(Ng); + k = BigNum(k_bytes, !hashBigEndian_); + LOG_DEBUG("Using hashed SRP multiplier k=H(N|g)"); + } else { + k = BigNum(K_VALUE); + } + LOG_DEBUG("SRP challenge data loaded"); // Now compute everything in sequence @@ -72,7 +84,7 @@ void SRP::feed(const std::vector& B_bytes, x_input.insert(x_input.end(), salt_bytes.begin(), salt_bytes.end()); x_input.insert(x_input.end(), auth_hash.begin(), auth_hash.end()); std::vector x_bytes = Crypto::sha1(x_input); - x = BigNum(x_bytes, true); + x = BigNum(x_bytes, !hashBigEndian_); LOG_DEBUG("Computed x (salted password hash)"); // 3. Generate client ephemeral (a, A) @@ -151,7 +163,7 @@ void SRP::computeSessionKey() { AB.insert(AB.end(), B_bytes_u.begin(), B_bytes_u.end()); std::vector u_bytes = Crypto::sha1(AB); - u = BigNum(u_bytes, true); + u = BigNum(u_bytes, !hashBigEndian_); LOG_DEBUG("Scrambler u calculated"); diff --git a/tools/auth_login_probe/main.cpp b/tools/auth_login_probe/main.cpp new file mode 100644 index 00000000..5e597b0b --- /dev/null +++ b/tools/auth_login_probe/main.cpp @@ -0,0 +1,350 @@ +#include "auth/auth_packets.hpp" +#include "auth/crypto.hpp" +#include "auth/integrity.hpp" +#include "auth/srp.hpp" +#include "network/tcp_socket.hpp" +#include "network/packet.hpp" +#include "core/logger.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace wowee; + +static void usage() { + std::cerr + << "Usage:\n" + << " auth_login_probe \\\n" + << " (--password | --hash ) [--proof legacy|v8|auto]\n" + << "\n" + << "Notes:\n" + << " - --hash expects SHA1(UPPER(user):UPPER(pass)) in hex.\n" + << " - This tool only probes auth; it does not connect to world.\n"; +} + +static std::vector hexToBytes(const std::string& hex) { + std::vector out; + std::string h; + h.reserve(hex.size()); + for (char c : hex) { + if (!std::isspace(static_cast(c))) h.push_back(c); + } + if (h.size() % 2 != 0) throw std::runtime_error("hex length must be even"); + out.reserve(h.size() / 2); + for (size_t i = 0; i < h.size(); i += 2) { + auto byteStr = h.substr(i, 2); + uint8_t b = static_cast(std::stoul(byteStr, nullptr, 16)); + out.push_back(b); + } + return out; +} + +static std::string upperAscii(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return static_cast(std::toupper(c)); }); + return s; +} + +enum class ProofFormat { Auto, Legacy, V8 }; +enum class CrcAFormat { Wire, BigEndian }; +enum class WireAFormat { Little, Big }; + +int main(int argc, char** argv) { + if (argc < 11) { + usage(); + return 2; + } + + const std::string host = argv[1]; + const int port = std::atoi(argv[2]); + const std::string account = argv[3]; + const int major = std::atoi(argv[4]); + const int minor = std::atoi(argv[5]); + const int patch = std::atoi(argv[6]); + const int build = std::atoi(argv[7]); + const int proto = std::atoi(argv[8]); + const std::string locale = argv[9]; + + std::string password; + std::vector authHash; + bool havePassword = false; + bool haveHash = false; + ProofFormat proofFmt = ProofFormat::Auto; + CrcAFormat crcA = CrcAFormat::Wire; + WireAFormat wireA = WireAFormat::Little; + std::string integrityExe = "WoW.exe"; + bool serverValuesBigEndian = false; + std::string miscDir = "Data/misc"; + bool useHashedK = false; + bool hashBigEndian = false; + + for (int i = 10; i < argc; ++i) { + std::string a = argv[i]; + if (a == "--password" && i + 1 < argc) { + password = argv[++i]; + havePassword = true; + continue; + } + if (a == "--hash" && i + 1 < argc) { + authHash = hexToBytes(argv[++i]); + haveHash = true; + continue; + } + if (a == "--proof" && i + 1 < argc) { + std::string v = argv[++i]; + if (v == "auto") proofFmt = ProofFormat::Auto; + else if (v == "legacy") proofFmt = ProofFormat::Legacy; + else if (v == "v8") proofFmt = ProofFormat::V8; + else { + std::cerr << "Unknown --proof value: " << v << "\n"; + return 2; + } + continue; + } + if (a == "--crc-a" && i + 1 < argc) { + std::string v = argv[++i]; + if (v == "wire") crcA = CrcAFormat::Wire; + else if (v == "be") crcA = CrcAFormat::BigEndian; + else { + std::cerr << "Unknown --crc-a value: " << v << " (expected wire|be)\n"; + return 2; + } + continue; + } + if (a == "--integrity-exe" && i + 1 < argc) { + integrityExe = argv[++i]; + continue; + } + if (a == "--misc-dir" && i + 1 < argc) { + miscDir = argv[++i]; + continue; + } + if (a == "--server-values" && i + 1 < argc) { + std::string v = argv[++i]; + if (v == "le") serverValuesBigEndian = false; + else if (v == "be") serverValuesBigEndian = true; + else { + std::cerr << "Unknown --server-values value: " << v << " (expected le|be)\n"; + return 2; + } + continue; + } + if (a == "--wire-a" && i + 1 < argc) { + std::string v = argv[++i]; + if (v == "le") wireA = WireAFormat::Little; + else if (v == "be") wireA = WireAFormat::Big; + else { + std::cerr << "Unknown --wire-a value: " << v << " (expected le|be)\n"; + return 2; + } + continue; + } + if (a == "--k" && i + 1 < argc) { + std::string v = argv[++i]; + if (v == "3") useHashedK = false; + else if (v == "hashed") useHashedK = true; + else { + std::cerr << "Unknown --k value: " << v << " (expected 3|hashed)\n"; + return 2; + } + continue; + } + if (a == "--hash-endian" && i + 1 < argc) { + std::string v = argv[++i]; + if (v == "le") hashBigEndian = false; + else if (v == "be") hashBigEndian = true; + else { + std::cerr << "Unknown --hash-endian value: " << v << " (expected le|be)\n"; + return 2; + } + continue; + } + std::cerr << "Unknown arg: " << a << "\n"; + return 2; + } + + if (!havePassword && !haveHash) { + std::cerr << "Must supply --password or --hash\n"; + return 2; + } + + auth::ClientInfo info; + info.majorVersion = static_cast(major); + info.minorVersion = static_cast(minor); + info.patchVersion = static_cast(patch); + info.build = static_cast(build); + info.protocolVersion = static_cast(proto); + info.locale = locale; + info.platform = "x86"; + info.os = "Win"; + + std::atomic done{false}; + std::atomic sawDisconnect{false}; + std::atomic challengeOk{false}; + std::atomic proofStatus{-1}; + std::atomic chalCode{-1}; + + network::TCPSocket sock; + std::unique_ptr srp; + uint8_t securityFlags = 0; + uint32_t pinSeed = 0; + std::array pinSalt{}; + std::array checksumSalt{}; + + auto sendProof = [&]() { + if (!srp) return; + auto A = srp->getA(); + if (wireA == WireAFormat::Big) { + std::reverse(A.begin(), A.end()); + } + auto M1 = srp->getM1(); + + ProofFormat fmt = proofFmt; + if (fmt == ProofFormat::Auto) { + fmt = (info.protocolVersion < 8) ? ProofFormat::Legacy : ProofFormat::V8; + } + + // Try to compute the classic client integrity hash using local Data/misc. + std::array crcHash{}; + const std::array* crcHashPtr = nullptr; + { + std::string err; + std::vector crcABytes = A; + if (crcA == CrcAFormat::BigEndian) { + std::reverse(crcABytes.begin(), crcABytes.end()); + } + if (auth::computeIntegrityHashWin32WithExe(checksumSalt, crcABytes, miscDir, integrityExe, crcHash, err)) { + crcHashPtr = &crcHash; + std::cerr << "Computed integrity hash using " << miscDir << " (" << integrityExe << ")\n"; + } else { + std::cerr << "Integrity hash not computed: " << err << "\n"; + } + } + + if (fmt == ProofFormat::Legacy) { + auto pkt = auth::LogonProofPacket::buildLegacy(A, M1); + sock.send(pkt); + std::cerr << "Sent LOGON_PROOF legacy (proto=" << (int)info.protocolVersion << ")\n"; + } else { + auto pkt = auth::LogonProofPacket::build(A, M1, securityFlags, crcHashPtr, nullptr, nullptr); + sock.send(pkt); + std::cerr << "Sent LOGON_PROOF v8 (secFlags=0x" << std::hex << (int)securityFlags << std::dec << ")\n"; + } + }; + + sock.setPacketCallback([&](const network::Packet& p) { + network::Packet pkt = p; + if (pkt.getSize() < 1) return; + + uint8_t opcode = pkt.readUInt8(); + if (opcode == static_cast(auth::AuthOpcode::LOGON_CHALLENGE)) { + auth::LogonChallengeResponse resp{}; + if (!auth::LogonChallengeResponseParser::parse(pkt, resp)) { + std::cerr << "Challenge parse failed\n"; + done = true; + return; + } + chalCode = static_cast(resp.result); + if (!resp.isSuccess()) { + std::cerr << "Challenge FAIL: " << auth::getAuthResultString(resp.result) + << " (0x" << std::hex << (int)resp.result << std::dec << ")\n"; + done = true; + return; + } + + challengeOk = true; + securityFlags = resp.securityFlags; + pinSeed = resp.pinGridSeed; + pinSalt = resp.pinSalt; + checksumSalt = resp.checksumSalt; + + srp = std::make_unique(); + srp->setUseHashedK(useHashedK); + srp->setHashBigEndian(hashBigEndian); + if (haveHash) { + srp->initializeWithHash(account, authHash); + } else { + srp->initialize(account, password); + } + if (serverValuesBigEndian) { + auto rev = [](std::vector v) { + std::reverse(v.begin(), v.end()); + return v; + }; + srp->feed(rev(resp.B), rev(resp.g), rev(resp.N), rev(resp.salt)); + } else { + srp->feed(resp.B, resp.g, resp.N, resp.salt); + } + + sendProof(); + return; + } + + if (opcode == static_cast(auth::AuthOpcode::LOGON_PROOF)) { + auth::LogonProofResponse resp{}; + if (!auth::LogonProofResponseParser::parse(pkt, resp)) { + std::cerr << "Proof parse failed\n"; + done = true; + return; + } + proofStatus = resp.status; + if (resp.isSuccess()) { + std::cerr << "Proof SUCCESS\n"; + } else { + std::cerr << "Proof FAIL status=0x" << std::hex << (int)resp.status << std::dec << "\n"; + } + done = true; + return; + } + }); + + if (!sock.connect(host, static_cast(port))) { + std::cerr << "Connect failed\n"; + return 3; + } + + auto chal = auth::LogonChallengePacket::build(account, info); + sock.send(chal); + + auto start = std::chrono::steady_clock::now(); + while (!done) { + sock.update(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + if (!sock.isConnected() && !done) { + sawDisconnect = true; + done = true; + break; + } + + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - start).count() > 6000) { + break; + } + } + + sock.disconnect(); + + if (!done && sock.isConnected()) { + std::cerr << "Timeout\n"; + return 4; + } + + if (sawDisconnect && challengeOk && proofStatus.load() < 0) { + std::cerr << "Server disconnected after challenge (no proof response parsed)\n"; + return 6; + } + + if (chalCode.load() >= 0 && chalCode.load() != 0) return chalCode.load(); + if (proofStatus.load() >= 0) return proofStatus.load(); + return 0; +}