From 6a44f02e0cf37ad3684712a96ad49619bdefaaef Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Feb 2026 00:55:36 -0800 Subject: [PATCH] Add authenticator opcode support + auth_probe tool --- CMakeLists.txt | 19 ++++++ include/auth/auth_handler.hpp | 5 +- include/auth/auth_opcodes.hpp | 1 + include/auth/auth_packets.hpp | 9 +++ src/auth/auth_handler.cpp | 30 ++++++--- src/auth/auth_packets.cpp | 15 +++++ src/ui/auth_screen.cpp | 13 ++-- tools/auth_probe/main.cpp | 111 ++++++++++++++++++++++++++++++++++ 8 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 tools/auth_probe/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c6102db..9729a187 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -425,6 +425,25 @@ set_target_properties(dbc_to_csv PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin ) +# ---- Tool: auth_probe (LOGON_CHALLENGE probe) ---- +add_executable(auth_probe + tools/auth_probe/main.cpp + src/auth/auth_packets.cpp + src/auth/auth_opcodes.cpp + src/auth/crypto.cpp + src/network/packet.cpp + src/network/socket.cpp + src/network/tcp_socket.cpp + src/core/logger.cpp +) +target_include_directories(auth_probe PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) +target_link_libraries(auth_probe PRIVATE Threads::Threads OpenSSL::Crypto) +set_target_properties(auth_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 8f382915..2f015885 100644 --- a/include/auth/auth_handler.hpp +++ b/include/auth/auth_handler.hpp @@ -21,6 +21,7 @@ enum class AuthState { CHALLENGE_SENT, CHALLENGE_RECEIVED, PIN_REQUIRED, + AUTHENTICATOR_REQUIRED, PROOF_SENT, AUTHENTICATED, REALM_LIST_REQUESTED, @@ -51,6 +52,8 @@ public: // 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); + // Generic continuation for PIN / authenticator-required servers. + void submitSecurityCode(const std::string& code); // Set client version info (call before authenticate) void setClientInfo(const ClientInfo& info) { clientInfo = info; } @@ -108,7 +111,7 @@ private: uint8_t securityFlags_ = 0; uint32_t pinGridSeed_ = 0; std::array pinServerSalt_{}; // from LOGON_CHALLENGE response - std::string pendingPin_; + std::string pendingSecurityCode_; }; } // namespace auth diff --git a/include/auth/auth_opcodes.hpp b/include/auth/auth_opcodes.hpp index 08399143..84630c20 100644 --- a/include/auth/auth_opcodes.hpp +++ b/include/auth/auth_opcodes.hpp @@ -11,6 +11,7 @@ enum class AuthOpcode : uint8_t { LOGON_PROOF = 0x01, RECONNECT_CHALLENGE = 0x02, RECONNECT_PROOF = 0x03, + AUTHENTICATOR = 0x04, // TrinityCore-style Google Authenticator token REALM_LIST = 0x10, }; diff --git a/include/auth/auth_packets.hpp b/include/auth/auth_packets.hpp index d11c531c..ffe603a5 100644 --- a/include/auth/auth_packets.hpp +++ b/include/auth/auth_packets.hpp @@ -43,6 +43,9 @@ struct LogonChallengeResponse { uint32_t pinGridSeed = 0; std::array pinSalt{}; + // Authenticator extension (securityFlags & 0x04) + uint8_t authenticatorRequired = 0; + bool isSuccess() const { return result == AuthResult::SUCCESS; } }; @@ -64,6 +67,12 @@ public: const std::array* pinHash); }; +// AUTHENTICATOR token packet builder (opcode 0x04 on many TrinityCore-derived servers) +class AuthenticatorTokenPacket { +public: + static network::Packet build(const std::string& token); +}; + // LOGON_PROOF response data struct LogonProofResponse { uint8_t status; diff --git a/src/auth/auth_handler.cpp b/src/auth/auth_handler.cpp index b5b91c96..2926d866 100644 --- a/src/auth/auth_handler.cpp +++ b/src/auth/auth_handler.cpp @@ -101,7 +101,7 @@ void AuthHandler::authenticate(const std::string& user, const std::string& pass, username = user; password = pass; - pendingPin_ = pin; + pendingSecurityCode_ = pin; securityFlags_ = 0; pinGridSeed_ = 0; pinServerSalt_ = {}; @@ -135,7 +135,7 @@ void AuthHandler::authenticateWithHash(const std::string& user, const std::vecto username = user; password.clear(); - pendingPin_ = pin; + pendingSecurityCode_ = pin; securityFlags_ = 0; pinGridSeed_ = 0; pinServerSalt_ = {}; @@ -203,9 +203,9 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) { setState(AuthState::CHALLENGE_RECEIVED); - // If PIN is required, wait for user input. - if ((securityFlags_ & 0x01) && pendingPin_.empty()) { - setState(AuthState::PIN_REQUIRED); + // If a security code is required, wait for user input. + if (((securityFlags_ & 0x04) || (securityFlags_ & 0x01)) && pendingSecurityCode_.empty()) { + setState((securityFlags_ & 0x04) ? AuthState::AUTHENTICATOR_REQUIRED : AuthState::PIN_REQUIRED); return; } @@ -225,7 +225,7 @@ void AuthHandler::sendLogonProof() { if (securityFlags_ & 0x01) { try { - PinProof proof = computePinProof(pendingPin_, pinGridSeed_, pinServerSalt_); + PinProof proof = computePinProof(pendingSecurityCode_, pinGridSeed_, pinServerSalt_); pinClientSalt = proof.clientSalt; pinHash = proof.hash; pinClientSaltPtr = &pinClientSalt; @@ -239,13 +239,25 @@ void AuthHandler::sendLogonProof() { auto packet = LogonProofPacket::build(A, M1, securityFlags_, pinClientSaltPtr, pinHashPtr); socket->send(packet); + if (securityFlags_ & 0x04) { + // TrinityCore-style Google Authenticator token: send immediately after proof. + // Token is typically 6 digits. + const std::string token = pendingSecurityCode_; + auto tokPkt = AuthenticatorTokenPacket::build(token); + socket->send(tokPkt); + } + 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) { + submitSecurityCode(pin); +} + +void AuthHandler::submitSecurityCode(const std::string& code) { + pendingSecurityCode_ = code; + // If we're waiting on a security code, continue immediately. + if (state == AuthState::PIN_REQUIRED || state == AuthState::AUTHENTICATOR_REQUIRED) { sendLogonProof(); } } diff --git a/src/auth/auth_packets.cpp b/src/auth/auth_packets.cpp index cbfbd1a4..35babd8d 100644 --- a/src/auth/auth_packets.cpp +++ b/src/auth/auth_packets.cpp @@ -142,6 +142,10 @@ bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallenge response.pinSalt[i] = packet.readUInt8(); } } + if (response.securityFlags & 0x04) { + // Authenticator required (TrinityCore): u8 requiredFlag (usually 1) + response.authenticatorRequired = packet.readUInt8(); + } LOG_DEBUG("Parsed LOGON_CHALLENGE response:"); LOG_DEBUG(" B size: ", response.B.size(), " bytes"); @@ -210,6 +214,17 @@ network::Packet LogonProofPacket::build(const std::vector& A, return packet; } +network::Packet AuthenticatorTokenPacket::build(const std::string& token) { + network::Packet packet(static_cast(AuthOpcode::AUTHENTICATOR)); + // TrinityCore expects: u8 len + ascii token bytes (not null-terminated) + uint8_t len = static_cast(std::min(255, token.size())); + packet.writeUInt8(len); + if (len > 0) { + packet.writeBytes(reinterpret_cast(token.data()), len); + } + return packet; +} + bool LogonProofResponseParser::parse(network::Packet& packet, LogonProofResponse& response) { // Note: opcode byte already consumed by handlePacket() diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index 55d6fa48..2d4c64c5 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -367,7 +367,7 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { // Connect button if (authenticating) { auto state = authHandler.getState(); - if (state != auth::AuthState::PIN_REQUIRED) { + if (state != auth::AuthState::PIN_REQUIRED && state != auth::AuthState::AUTHENTICATOR_REQUIRED) { pinAutoSubmitted_ = false; authTimer += ImGui::GetIO().DeltaTime; @@ -385,8 +385,10 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { for (size_t i = 0; i < len; ++i) { if (pinCode[i] < '0' || pinCode[i] > '9') { digitsOnly = false; break; } } - if (digitsOnly && len >= 4 && len <= 10) { - authHandler.submitPin(pinCode); + // Auto-submit if the user prefilled a plausible code. + // PIN-grid: 4-10 digits. Authenticator (TOTP): typically 6 digits. + if (digitsOnly && ((len >= 4 && len <= 10) || len == 6)) { + authHandler.submitSecurityCode(pinCode); pinCode[0] = '\0'; pinAutoSubmitted_ = true; } @@ -394,7 +396,7 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { ImGui::SameLine(); if (ImGui::Button("Submit 2FA/PIN")) { - authHandler.submitPin(pinCode); + authHandler.submitSecurityCode(pinCode); // Don't keep the code around longer than needed. pinCode[0] = '\0'; pinAutoSubmitted_ = true; @@ -429,7 +431,8 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { setStatus("Authentication failed", true); } authenticating = false; - } else if (state != auth::AuthState::PIN_REQUIRED && authTimer >= AUTH_TIMEOUT) { + } else if (state != auth::AuthState::PIN_REQUIRED && state != auth::AuthState::AUTHENTICATOR_REQUIRED + && authTimer >= AUTH_TIMEOUT) { setStatus("Connection timed out - server did not respond", true); authenticating = false; authHandler.disconnect(); diff --git a/tools/auth_probe/main.cpp b/tools/auth_probe/main.cpp new file mode 100644 index 00000000..6c35eb9f --- /dev/null +++ b/tools/auth_probe/main.cpp @@ -0,0 +1,111 @@ +#include "auth/auth_packets.hpp" +#include "network/tcp_socket.hpp" +#include "network/packet.hpp" +#include "core/logger.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace wowee; + +static void usage() { + std::cerr + << "Usage:\n" + << " auth_probe [platform] [os]\n" + << "Example:\n" + << " auth_probe logon.turtle-server-eu.kz 3724 test 1 12 1 5875 8 enGB x86 Win\n"; +} + +int main(int argc, char** argv) { + if (argc < 10) { + 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]; + const std::string platform = (argc >= 11) ? argv[10] : "x86"; + const std::string os = (argc >= 12) ? argv[11] : "Win"; + + 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 = platform; + info.os = os; + + std::atomic done{false}; + std::atomic resultCode{0xFF}; + std::atomic gotResponse{false}; + + network::TCPSocket sock; + sock.setPacketCallback([&](const network::Packet& p) { + network::Packet pkt = p; + if (pkt.getSize() < 3) { + return; + } + uint8_t opcode = pkt.readUInt8(); + if (opcode != static_cast(auth::AuthOpcode::LOGON_CHALLENGE)) { + return; + } + + auth::LogonChallengeResponse resp{}; + if (!auth::LogonChallengeResponseParser::parse(pkt, resp)) { + std::cerr << "Parse failed\n"; + resultCode = 0xFE; + } else { + resultCode = static_cast(resp.result); + if (resp.isSuccess()) { + std::cerr << "SUCCESS secFlags=0x" << std::hex << (int)resp.securityFlags << std::dec << "\n"; + } else { + std::cerr << "FAIL code=0x" << std::hex << (int)resp.result << std::dec + << " (" << auth::getAuthResultString(resp.result) << ")\n"; + } + } + gotResponse = true; + done = true; + }); + + if (!sock.connect(host, static_cast(port))) { + std::cerr << "Connect failed\n"; + return 3; + } + + auto challenge = auth::LogonChallengePacket::build(account, info); + sock.send(challenge); + + auto start = std::chrono::steady_clock::now(); + while (!done) { + sock.update(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - start).count() > 4000) { + break; + } + } + + sock.disconnect(); + + if (!gotResponse) { + std::cerr << "Timeout waiting for response\n"; + return 4; + } + + return resultCode.load(); +} +