Add authenticator opcode support + auth_probe tool

This commit is contained in:
Kelsi 2026-02-13 00:55:36 -08:00
parent fd468ce793
commit 6a44f02e0c
8 changed files with 188 additions and 15 deletions

View file

@ -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

View file

@ -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<uint8_t, 16> pinServerSalt_{}; // from LOGON_CHALLENGE response
std::string pendingPin_;
std::string pendingSecurityCode_;
};
} // namespace auth

View file

@ -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,
};

View file

@ -43,6 +43,9 @@ struct LogonChallengeResponse {
uint32_t pinGridSeed = 0;
std::array<uint8_t, 16> pinSalt{};
// Authenticator extension (securityFlags & 0x04)
uint8_t authenticatorRequired = 0;
bool isSuccess() const { return result == AuthResult::SUCCESS; }
};
@ -64,6 +67,12 @@ public:
const std::array<uint8_t, 20>* 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;

View file

@ -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();
}
}

View file

@ -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<uint8_t>& A,
return packet;
}
network::Packet AuthenticatorTokenPacket::build(const std::string& token) {
network::Packet packet(static_cast<uint16_t>(AuthOpcode::AUTHENTICATOR));
// TrinityCore expects: u8 len + ascii token bytes (not null-terminated)
uint8_t len = static_cast<uint8_t>(std::min<size_t>(255, token.size()));
packet.writeUInt8(len);
if (len > 0) {
packet.writeBytes(reinterpret_cast<const uint8_t*>(token.data()), len);
}
return packet;
}
bool LogonProofResponseParser::parse(network::Packet& packet, LogonProofResponse& response) {
// Note: opcode byte already consumed by handlePacket()

View file

@ -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();

111
tools/auth_probe/main.cpp Normal file
View file

@ -0,0 +1,111 @@
#include "auth/auth_packets.hpp"
#include "network/tcp_socket.hpp"
#include "network/packet.hpp"
#include "core/logger.hpp"
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string>
#include <thread>
using namespace wowee;
static void usage() {
std::cerr
<< "Usage:\n"
<< " auth_probe <host> <port> <account> <major> <minor> <patch> <build> <proto> <locale> [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<uint8_t>(major);
info.minorVersion = static_cast<uint8_t>(minor);
info.patchVersion = static_cast<uint8_t>(patch);
info.build = static_cast<uint16_t>(build);
info.protocolVersion = static_cast<uint8_t>(proto);
info.locale = locale;
info.platform = platform;
info.os = os;
std::atomic<bool> done{false};
std::atomic<int> resultCode{0xFF};
std::atomic<bool> 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<uint8_t>(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<int>(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<uint16_t>(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<std::chrono::milliseconds>(now - start).count() > 4000) {
break;
}
}
sock.disconnect();
if (!gotResponse) {
std::cerr << "Timeout waiting for response\n";
return 4;
}
return resultCode.load();
}