Support PIN-required auth servers

This commit is contained in:
Kelsi 2026-02-13 00:22:01 -08:00
parent f247d53309
commit 62a49644a5
9 changed files with 271 additions and 13 deletions

View file

@ -89,6 +89,7 @@ set(WOWEE_SOURCES
src/auth/auth_handler.cpp
src/auth/auth_opcodes.cpp
src/auth/auth_packets.cpp
src/auth/pin_auth.cpp
src/auth/srp.cpp
src/auth/big_num.cpp
src/auth/crypto.cpp

View file

@ -5,6 +5,7 @@
#include <memory>
#include <string>
#include <functional>
#include <array>
namespace wowee {
namespace network { class TCPSocket; class Packet; }
@ -19,6 +20,7 @@ enum class AuthState {
CONNECTED,
CHALLENGE_SENT,
CHALLENGE_RECEIVED,
PIN_REQUIRED,
PROOF_SENT,
AUTHENTICATED,
REALM_LIST_REQUESTED,
@ -44,6 +46,9 @@ public:
// Authentication
void authenticate(const std::string& username, const std::string& password);
void authenticateWithHash(const std::string& username, const std::vector<uint8_t>& authHash);
// 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);
// Set client version info (call before authenticate)
void setClientInfo(const ClientInfo& info) { clientInfo = info; }
@ -96,6 +101,12 @@ private:
// Receive buffer
std::vector<uint8_t> receiveBuffer;
// Challenge security extension (PIN)
uint8_t securityFlags_ = 0;
uint32_t pinGridSeed_ = 0;
std::array<uint8_t, 16> pinServerSalt_{}; // from LOGON_CHALLENGE response
std::string pendingPin_;
};
} // namespace auth

View file

@ -5,6 +5,7 @@
#include <string>
#include <vector>
#include <cstdint>
#include <array>
namespace wowee {
namespace auth {
@ -38,6 +39,10 @@ struct LogonChallengeResponse {
std::vector<uint8_t> salt; // Salt (32 bytes)
uint8_t securityFlags;
// PIN extension (securityFlags & 0x01)
uint32_t pinGridSeed = 0;
std::array<uint8_t, 16> pinSalt{};
bool isSuccess() const { return result == AuthResult::SUCCESS; }
};
@ -52,6 +57,11 @@ class LogonProofPacket {
public:
static network::Packet build(const std::vector<uint8_t>& A,
const std::vector<uint8_t>& M1);
static network::Packet build(const std::vector<uint8_t>& A,
const std::vector<uint8_t>& M1,
uint8_t securityFlags,
const std::array<uint8_t, 16>* pinClientSalt,
const std::array<uint8_t, 20>* pinHash);
};
// LOGON_PROOF response data

28
include/auth/pin_auth.hpp Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
namespace wowee {
namespace auth {
struct PinProof {
std::array<uint8_t, 16> clientSalt{};
std::array<uint8_t, 20> hash{};
};
// Implements the "PIN" security extension used in the WoW auth protocol (securityFlags & 0x01).
// Algorithm based on documented client behavior:
// - Remap digits using pinGridSeed (a permutation of 0..9)
// - Convert user-entered PIN digits into randomized indices in that permutation
// - Compute: pin_hash = SHA1(client_salt || SHA1(server_salt || randomized_pin_ascii))
//
// PIN must be 4-10 ASCII digits.
PinProof computePinProof(const std::string& pinDigits,
uint32_t pinGridSeed,
const std::array<uint8_t, 16>& serverSalt);
} // namespace auth
} // namespace wowee

View file

@ -54,6 +54,7 @@ private:
char hostname[256] = "127.0.0.1";
char username[256] = "";
char password[256] = "";
char pinCode[32] = "";
int port = 3724;
int expansionIndex = 0; // Index into expansion registry profiles
bool authenticating = false;

View file

@ -1,9 +1,12 @@
#include "auth/auth_handler.hpp"
#include "auth/pin_auth.hpp"
#include "network/tcp_socket.hpp"
#include "network/packet.hpp"
#include "core/logger.hpp"
#include <sstream>
#include <iomanip>
#include <algorithm>
#include <cctype>
namespace wowee {
namespace auth {
@ -17,7 +20,17 @@ AuthHandler::~AuthHandler() {
}
bool AuthHandler::connect(const std::string& host, uint16_t port) {
LOG_INFO("Connecting to auth server: ", host, ":", port);
auto trimHost = [](std::string s) {
auto isSpace = [](unsigned char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; };
size_t b = 0;
while (b < s.size() && isSpace(static_cast<unsigned char>(s[b]))) ++b;
size_t e = s.size();
while (e > b && isSpace(static_cast<unsigned char>(s[e - 1]))) --e;
return s.substr(b, e - b);
};
const std::string hostTrimmed = trimHost(host);
LOG_INFO("Connecting to auth server: ", hostTrimmed, ":", port);
socket = std::make_unique<network::TCPSocket>();
@ -28,7 +41,7 @@ bool AuthHandler::connect(const std::string& host, uint16_t port) {
handlePacket(mutablePacket);
});
if (!socket->connect(host, port)) {
if (!socket->connect(hostTrimmed, port)) {
LOG_ERROR("Failed to connect to auth server");
setState(AuthState::FAILED);
return false;
@ -84,6 +97,10 @@ void AuthHandler::authenticate(const std::string& user, const std::string& pass)
username = user;
password = pass;
pendingPin_.clear();
securityFlags_ = 0;
pinGridSeed_ = 0;
pinServerSalt_ = {};
// Initialize SRP
srp = std::make_unique<SRP>();
@ -110,6 +127,10 @@ void AuthHandler::authenticateWithHash(const std::string& user, const std::vecto
username = user;
password.clear();
pendingPin_.clear();
securityFlags_ = 0;
pinGridSeed_ = 0;
pinServerSalt_ = {};
// Initialize SRP with pre-computed hash
srp = std::make_unique<SRP>();
@ -144,7 +165,7 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) {
if (response.securityFlags != 0) {
LOG_WARNING("Server sent security flags: 0x", std::hex, (int)response.securityFlags, std::dec);
if (response.securityFlags & 0x01) LOG_WARNING(" PIN required (not supported)");
if (response.securityFlags & 0x01) LOG_WARNING(" PIN required");
if (response.securityFlags & 0x02) LOG_WARNING(" Matrix card required (not supported)");
if (response.securityFlags & 0x04) LOG_WARNING(" Authenticator required (not supported)");
}
@ -155,9 +176,20 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) {
// Feed SRP with server challenge data
srp->feed(response.B, response.g, response.N, response.salt);
securityFlags_ = response.securityFlags;
if (securityFlags_ & 0x01) {
pinGridSeed_ = response.pinGridSeed;
pinServerSalt_ = response.pinSalt;
}
setState(AuthState::CHALLENGE_RECEIVED);
// Send LOGON_PROOF immediately
// If PIN is required, wait for user input.
if ((securityFlags_ & 0x01) && pendingPin_.empty()) {
setState(AuthState::PIN_REQUIRED);
return;
}
sendLogonProof();
}
@ -167,12 +199,38 @@ void AuthHandler::sendLogonProof() {
auto A = srp->getA();
auto M1 = srp->getM1();
auto packet = LogonProofPacket::build(A, M1);
std::array<uint8_t, 16> pinClientSalt{};
std::array<uint8_t, 20> pinHash{};
const std::array<uint8_t, 16>* pinClientSaltPtr = nullptr;
const std::array<uint8_t, 20>* pinHashPtr = nullptr;
if (securityFlags_ & 0x01) {
try {
PinProof proof = computePinProof(pendingPin_, pinGridSeed_, pinServerSalt_);
pinClientSalt = proof.clientSalt;
pinHash = proof.hash;
pinClientSaltPtr = &pinClientSalt;
pinHashPtr = &pinHash;
} catch (const std::exception& e) {
fail(std::string("PIN required but invalid: ") + e.what());
return;
}
}
auto packet = LogonProofPacket::build(A, M1, securityFlags_, pinClientSaltPtr, pinHashPtr);
socket->send(packet);
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) {
sendLogonProof();
}
}
void AuthHandler::handleLogonProofResponse(network::Packet& packet) {
LOG_DEBUG("Handling LOGON_PROOF response");

View file

@ -135,18 +135,38 @@ bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallenge
// Security flags
response.securityFlags = packet.readUInt8();
// Optional security extensions (protocol v8+)
if (response.securityFlags & 0x01) {
// PIN required: u32 pin_grid_seed + u8[16] pin_salt
response.pinGridSeed = packet.readUInt32();
for (size_t i = 0; i < response.pinSalt.size(); ++i) {
response.pinSalt[i] = packet.readUInt8();
}
}
LOG_DEBUG("Parsed LOGON_CHALLENGE response:");
LOG_DEBUG(" B size: ", response.B.size(), " bytes");
LOG_DEBUG(" g size: ", response.g.size(), " bytes");
LOG_DEBUG(" N size: ", response.N.size(), " bytes");
LOG_DEBUG(" salt size: ", response.salt.size(), " bytes");
LOG_DEBUG(" Security flags: ", (int)response.securityFlags);
if (response.securityFlags & 0x01) {
LOG_DEBUG(" PIN grid seed: ", response.pinGridSeed);
}
return true;
}
network::Packet LogonProofPacket::build(const std::vector<uint8_t>& A,
const std::vector<uint8_t>& M1) {
return build(A, M1, 0, nullptr, nullptr);
}
network::Packet LogonProofPacket::build(const std::vector<uint8_t>& A,
const std::vector<uint8_t>& M1,
uint8_t securityFlags,
const std::array<uint8_t, 16>* pinClientSalt,
const std::array<uint8_t, 20>* pinHash) {
if (A.size() != 32) {
LOG_ERROR("Invalid A size: ", A.size(), " (expected 32)");
}
@ -171,7 +191,17 @@ network::Packet LogonProofPacket::build(const std::vector<uint8_t>& A,
packet.writeUInt8(0);
// Security flags
packet.writeUInt8(0);
packet.writeUInt8(securityFlags);
if (securityFlags & 0x01) {
if (!pinClientSalt || !pinHash) {
LOG_ERROR("LOGON_PROOF: PIN flag set but PIN data missing");
} else {
// PIN: u8[16] client_salt + u8[20] pin_hash
packet.writeBytes(pinClientSalt->data(), pinClientSalt->size());
packet.writeBytes(pinHash->data(), pinHash->size());
}
}
LOG_DEBUG("Built LOGON_PROOF packet:");
LOG_DEBUG(" A size: ", A.size(), " bytes");

108
src/auth/pin_auth.cpp Normal file
View file

@ -0,0 +1,108 @@
#include "auth/pin_auth.hpp"
#include "auth/crypto.hpp"
#include <algorithm>
#include <random>
#include <stdexcept>
#include <vector>
namespace wowee {
namespace auth {
static std::array<uint8_t, 16> randomSalt16() {
std::array<uint8_t, 16> out{};
std::random_device rd;
for (auto& b : out) {
b = static_cast<uint8_t>(rd() & 0xFFu);
}
return out;
}
static std::array<uint8_t, 10> remapPinGrid(uint32_t seed) {
// Generates a permutation of digits 0..9 from a seed.
// Based on:
// https://gtker.com/wow_messages/docs/auth/pin.html
uint32_t v = seed;
std::array<uint8_t, 10> remapped{};
uint8_t used = 0;
for (int i = 0; i < 10; ++i) {
uint32_t divisor = 10 - i;
uint32_t remainder = v % divisor;
v /= divisor;
uint32_t index = 0;
for (uint32_t j = 0; j < 10; ++j) {
if (used & (1u << j)) {
continue;
}
if (index == remainder) {
used = static_cast<uint8_t>(used | (1u << j));
remapped[i] = static_cast<uint8_t>(j);
break;
}
++index;
}
}
return remapped;
}
static std::vector<uint8_t> randomizePinDigits(const std::string& pinDigits,
const std::array<uint8_t, 10>& remapped) {
// Transforms each pin digit into an index in the remapped permutation.
// Based on:
// https://gtker.com/wow_messages/docs/auth/pin.html
std::vector<uint8_t> out;
out.reserve(pinDigits.size());
for (char c : pinDigits) {
uint8_t d = static_cast<uint8_t>(c - '0');
uint8_t idx = 0xFF;
for (uint8_t j = 0; j < 10; ++j) {
if (remapped[j] == d) { idx = j; break; }
}
if (idx == 0xFF) {
throw std::runtime_error("PIN digit not found in remapped grid");
}
out.push_back(static_cast<uint8_t>(idx + 0x30)); // ASCII '0'+idx
}
return out;
}
PinProof computePinProof(const std::string& pinDigits,
uint32_t pinGridSeed,
const std::array<uint8_t, 16>& serverSalt) {
if (pinDigits.size() < 4 || pinDigits.size() > 10) {
throw std::runtime_error("PIN must be 4-10 digits");
}
if (!std::all_of(pinDigits.begin(), pinDigits.end(),
[](unsigned char c) { return c >= '0' && c <= '9'; })) {
throw std::runtime_error("PIN must contain only digits");
}
const auto remapped = remapPinGrid(pinGridSeed);
const auto randomizedAsciiDigits = randomizePinDigits(pinDigits, remapped);
// server_hash = SHA1(server_salt || randomized_pin_ascii)
std::vector<uint8_t> serverHashInput;
serverHashInput.reserve(serverSalt.size() + randomizedAsciiDigits.size());
serverHashInput.insert(serverHashInput.end(), serverSalt.begin(), serverSalt.end());
serverHashInput.insert(serverHashInput.end(), randomizedAsciiDigits.begin(), randomizedAsciiDigits.end());
const auto serverHash = Crypto::sha1(serverHashInput); // 20 bytes
PinProof proof;
proof.clientSalt = randomSalt16();
// final_hash = SHA1(client_salt || server_hash)
std::vector<uint8_t> finalInput;
finalInput.reserve(proof.clientSalt.size() + serverHash.size());
finalInput.insert(finalInput.end(), proof.clientSalt.begin(), proof.clientSalt.end());
finalInput.insert(finalInput.end(), serverHash.begin(), serverHash.end());
const auto finalHash = Crypto::sha1(finalInput);
std::copy_n(finalHash.begin(), proof.hash.size(), proof.hash.begin());
return proof;
}
} // namespace auth
} // namespace wowee

View file

@ -354,15 +354,26 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
// Connect button
if (authenticating) {
auto state = authHandler.getState();
if (state != auth::AuthState::PIN_REQUIRED) {
authTimer += ImGui::GetIO().DeltaTime;
// Show progress with elapsed time
char progressBuf[128];
snprintf(progressBuf, sizeof(progressBuf), "Authenticating... (%.0fs)", authTimer);
ImGui::Text("%s", progressBuf);
} else {
ImGui::TextWrapped("This server requires a PIN. Enter your PIN to continue.");
ImGui::InputText("PIN", pinCode, sizeof(pinCode), ImGuiInputTextFlags_Password);
ImGui::SameLine();
if (ImGui::Button("Submit PIN")) {
authHandler.submitPin(pinCode);
// Don't keep the PIN around longer than needed.
pinCode[0] = '\0';
}
}
// Check authentication status
auto state = authHandler.getState();
if (state == auth::AuthState::AUTHENTICATED) {
setStatus("Authentication successful!", false);
authenticating = false;
@ -390,7 +401,7 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
setStatus("Authentication failed", true);
}
authenticating = false;
} else if (authTimer >= AUTH_TIMEOUT) {
} else if (state != auth::AuthState::PIN_REQUIRED && authTimer >= AUTH_TIMEOUT) {
setStatus("Connection timed out - server did not respond", true);
authenticating = false;
authHandler.disconnect();