Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/ui/auth_screen.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class AuthScreen {
bool authenticating = false;
bool showPassword = false;
bool pinAutoSubmitted_ = false;
bool securityPromptFocused_ = false;

// Status
std::string statusMessage;
Expand Down
31 changes: 30 additions & 1 deletion src/auth/auth_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -219,6 +223,16 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) {
<< " build " << clientInfo.build
<< ", auth protocol " << static_cast<int>(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<int>(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.
Expand All @@ -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<int>(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);

Expand Down Expand Up @@ -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<int>(clientInfo.protocolVersion),
" secFlags=0x", std::hex, static_cast<int>(securityFlags_), std::dec,
" pinData=", ((pinClientSaltPtr && pinHashPtr) ? "yes" : "no"),
" payloadSize=", packet.getSize(), "B");
socket->send(packet);

if (securityFlags_ & kSecurityFlagAuthenticator) {
Expand Down Expand Up @@ -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<AuthResult>(response.status));
if (response.status == static_cast<uint8_t>(AuthResult::ACCOUNT_INVALID) &&
(securityFlags_ & kSecurityFlagPin)) {
reason += "PIN/2FA proof was rejected";
} else {
reason += getAuthResultString(static_cast<AuthResult>(response.status));
}
fail(reason);
return;
}
Expand Down
1 change: 0 additions & 1 deletion src/auth/auth_packets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,6 @@ network::Packet LogonProofPacket::buildLegacy(const std::vector<uint8_t>& 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;
}

Expand Down
39 changes: 30 additions & 9 deletions src/ui/auth_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
}
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
}
}

Expand Down Expand Up @@ -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;
{
Expand Down Expand Up @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions tests/test_realm_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "auth/auth_packets.hpp"
#include "network/packet.hpp"

#include <array>
#include <cstring>

using namespace wowee::auth;
Expand Down Expand Up @@ -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<uint8_t> A(32, 0xA5);
std::vector<uint8_t> M1(20, 0x5A);
std::array<uint8_t, 20> 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<uint8_t> A(32, 0xA5);
std::vector<uint8_t> M1(20, 0x5A);
std::array<uint8_t, 20> crc{};
std::array<uint8_t, 16> pinSalt{};
std::array<uint8_t, 20> pinHash{};

auto v8 = LogonProofPacket::build(A, M1, 0x01, &crc, &pinSalt, &pinHash);
CHECK(v8.getSize() == 32 + 20 + 20 + 1 + 1 + 16 + 20);
}
Loading