diff --git a/include/ui/auth_screen.hpp b/include/ui/auth_screen.hpp index 965caafc..eb1d8045 100644 --- a/include/ui/auth_screen.hpp +++ b/include/ui/auth_screen.hpp @@ -70,6 +70,7 @@ class AuthScreen { bool authenticating = false; bool showPassword = false; bool pinAutoSubmitted_ = false; + bool securityPromptFocused_ = false; // Status std::string statusMessage; diff --git a/src/auth/auth_handler.cpp b/src/auth/auth_handler.cpp index eb07a7db..716fdaa1 100644 --- a/src/auth/auth_handler.cpp +++ b/src/auth/auth_handler.cpp @@ -19,6 +19,10 @@ constexpr uint8_t kSecurityFlagPin = 0x01; // PIN grid challenge constexpr uint8_t kSecurityFlagMatrixCard = 0x02; // Matrix card (unused by most servers) constexpr uint8_t kSecurityFlagAuthenticator = 0x04; // TOTP authenticator token +bool isLegacyVanillaAuth(const ClientInfo& info) { + return info.majorVersion == 1 && info.protocolVersion < 8; +} + AuthHandler::AuthHandler() { LOG_DEBUG("AuthHandler created"); } @@ -219,6 +223,16 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) { << " build " << clientInfo.build << ", auth protocol " << static_cast(clientInfo.protocolVersion) << ")"; fail(ss.str(), /*protocolRelated=*/true); + } else if (response.result == AuthResult::BUSY && isLegacyVanillaAuth(clientInfo)) { + // Turtle/vmangos-derived realms can answer protocol 3 challenges + // with BUSY before sending the v8 security-flags byte. Let the UI + // try the alternate vanilla auth protocol instead of stopping here. + std::ostringstream ss; + ss << "LOGON_CHALLENGE failed: " + << getAuthResultString(response.result) + << " (auth protocol " << static_cast(clientInfo.protocolVersion) + << " may be incompatible)"; + fail(ss.str(), /*protocolRelated=*/true); } else { // Account-level rejections (unknown account, wrong password, banned, // suspended, in use) are NOT protocol problems — never retry those. @@ -237,6 +251,12 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) { LOG_INFO("Challenge: N=", response.N.size(), "B g=", response.g.size(), "B salt=", response.salt.size(), "B secFlags=0x", std::hex, static_cast(response.securityFlags), std::dec); + if (clientInfo.protocolVersion < 8 && response.securityFlags != 0) { + fail("Server requires auth protocol 8 security extensions", + /*protocolRelated=*/true); + return; + } + // Feed SRP with server challenge data srp->feed(response.B, response.g, response.N, response.salt); @@ -340,6 +360,10 @@ void AuthHandler::sendLogonProof() { socket->send(packet); } else { auto packet = LogonProofPacket::build(A, M1, securityFlags_, crcHashPtr, pinClientSaltPtr, pinHashPtr); + LOG_INFO("LOGON_PROOF payload: protocol=", static_cast(clientInfo.protocolVersion), + " secFlags=0x", std::hex, static_cast(securityFlags_), std::dec, + " pinData=", ((pinClientSaltPtr && pinHashPtr) ? "yes" : "no"), + " payloadSize=", packet.getSize(), "B"); socket->send(packet); if (securityFlags_ & kSecurityFlagAuthenticator) { @@ -379,7 +403,12 @@ void AuthHandler::handleLogonProofResponse(network::Packet& packet) { // Credential/account rejection — a different protocol won't help, and // retrying can trip the server's failed-login lockout. std::string reason = "Login failed: "; - reason += getAuthResultString(static_cast(response.status)); + if (response.status == static_cast(AuthResult::ACCOUNT_INVALID) && + (securityFlags_ & kSecurityFlagPin)) { + reason += "PIN/2FA proof was rejected"; + } else { + reason += getAuthResultString(static_cast(response.status)); + } fail(reason); return; } diff --git a/src/auth/auth_packets.cpp b/src/auth/auth_packets.cpp index 48654c24..f6006972 100644 --- a/src/auth/auth_packets.cpp +++ b/src/auth/auth_packets.cpp @@ -245,7 +245,6 @@ network::Packet LogonProofPacket::buildLegacy(const std::vector& A, for (int i = 0; i < 20; ++i) packet.writeUInt8(0); // CRC hash } packet.writeUInt8(0); // number of keys - packet.writeUInt8(0); // security flags return packet; } diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index e779fd64..32f5eaa4 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -466,14 +466,30 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { // Checkbox state changed } + const auto authState = authHandler.getState(); + const bool waitingForSecurityCode = (authState == auth::AuthState::PIN_REQUIRED || + authState == auth::AuthState::AUTHENTICATOR_REQUIRED); + // Optional 2FA / PIN field (some servers require this; e.g. Turtle WoW uses Google Authenticator). // Keep it visible pre-connect so we can send LOGON_PROOF immediately after the SRP challenge. { ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password; - if (authHandler.getState() == auth::AuthState::PIN_REQUIRED) { + if (waitingForSecurityCode) { pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue; + if (!securityPromptFocused_) { + ImGui::SetKeyboardFocusHere(); + securityPromptFocused_ = true; + } + } + const bool submitFromField = ImGui::InputText("2FA / PIN", pinCode, sizeof(pinCode), pinFlags); + if (waitingForSecurityCode && submitFromField) { + const std::string code = trimAscii(pinCode); + if (!code.empty()) { + authHandler.submitSecurityCode(code); + pinCode[0] = '\0'; + pinAutoSubmitted_ = true; + } } - ImGui::InputText("2FA / PIN", pinCode, sizeof(pinCode), pinFlags); ImGui::SameLine(); ImGui::TextDisabled("(optional)"); } @@ -499,6 +515,7 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { auto state = authHandler.getState(); if (state != auth::AuthState::PIN_REQUIRED && state != auth::AuthState::AUTHENTICATOR_REQUIRED) { pinAutoSubmitted_ = false; + securityPromptFocused_ = false; authTimer += ImGui::GetIO().DeltaTime; // Show progress with elapsed time @@ -526,10 +543,13 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { ImGui::SameLine(); if (ImGui::Button("Submit 2FA/PIN")) { - authHandler.submitSecurityCode(pinCode); - // Don't keep the code around longer than needed. - pinCode[0] = '\0'; - pinAutoSubmitted_ = true; + const std::string code = trimAscii(pinCode); + if (!code.empty()) { + authHandler.submitSecurityCode(code); + // Don't keep the code around longer than needed. + pinCode[0] = '\0'; + pinAutoSubmitted_ = true; + } } } @@ -651,9 +671,9 @@ void AuthScreen::attemptAuth(auth::AuthHandler& authHandler) { // Build the auth-protocol candidate chain for this attempt. The profile's // own value is always tried first; vanilla-family profiles get the other - // vanilla protocol byte appended as a fallback, because 1.12 servers - // disagree on it (vmangos-derived: 8, stock mangos/cmangos: 3) and the - // profile can only name one. TBC/WotLK have no such ambiguity. + // vanilla protocol byte appended as a fallback, because vanilla-family + // servers disagree on it (Turtle/vmangos-derived: 8, older mangos/cmangos: + // 3) and the profile can only name one. TBC/WotLK have no such ambiguity. authProtocols_.clear(); authProtocolAttempt_ = 0; { @@ -728,6 +748,7 @@ void AuthScreen::beginAuthAttempt(auth::AuthHandler& authHandler) { authTimer = 0.0f; setStatus(isRetry ? "Reconnected, authenticating..." : "Connected, authenticating...", false); pinAutoSubmitted_ = false; + securityPromptFocused_ = false; // Save login info for next session saveLoginInfo(false); diff --git a/tests/test_realm_list.cpp b/tests/test_realm_list.cpp index c4358b3c..7a9f7306 100644 --- a/tests/test_realm_list.cpp +++ b/tests/test_realm_list.cpp @@ -9,6 +9,7 @@ #include "auth/auth_packets.hpp" #include "network/packet.hpp" +#include #include using namespace wowee::auth; @@ -146,3 +147,26 @@ TEST_CASE("Realm list: empty realm list is not an error", "[realm_list]") { REQUIRE(RealmListResponseParser::parse(pkt, resp, /*legacyVanillaLayout=*/true)); CHECK(resp.realms.empty()); } + +TEST_CASE("Logon proof legacy format omits v8 security flags", "[auth_packets]") { + std::vector A(32, 0xA5); + std::vector M1(20, 0x5A); + std::array crc{}; + + auto legacy = LogonProofPacket::buildLegacy(A, M1, &crc); + CHECK(legacy.getSize() == 32 + 20 + 20 + 1); + + auto v8 = LogonProofPacket::build(A, M1, 0, &crc, nullptr, nullptr); + CHECK(v8.getSize() == 32 + 20 + 20 + 1 + 1); +} + +TEST_CASE("Logon proof PIN format includes security proof data", "[auth_packets]") { + std::vector A(32, 0xA5); + std::vector M1(20, 0x5A); + std::array crc{}; + std::array pinSalt{}; + std::array pinHash{}; + + auto v8 = LogonProofPacket::build(A, M1, 0x01, &crc, &pinSalt, &pinHash); + CHECK(v8.getSize() == 32 + 20 + 20 + 1 + 1 + 16 + 20); +}