Add integrity hash support and SRP tuning options

This commit is contained in:
Kelsi 2026-02-13 01:32:15 -08:00
parent b3001a4b5b
commit 5435796a98
10 changed files with 591 additions and 22 deletions

View file

@ -1,5 +1,6 @@
#include "auth/auth_handler.hpp"
#include "auth/pin_auth.hpp"
#include "auth/integrity.hpp"
#include "network/tcp_socket.hpp"
#include "network/packet.hpp"
#include "core/logger.hpp"
@ -7,6 +8,7 @@
#include <iomanip>
#include <algorithm>
#include <cctype>
#include <cstdlib>
namespace wowee {
namespace auth {
@ -105,6 +107,7 @@ void AuthHandler::authenticate(const std::string& user, const std::string& pass,
securityFlags_ = 0;
pinGridSeed_ = 0;
pinServerSalt_ = {};
checksumSalt_ = {};
// Initialize SRP
srp = std::make_unique<SRP>();
@ -139,6 +142,7 @@ void AuthHandler::authenticateWithHash(const std::string& user, const std::vecto
securityFlags_ = 0;
pinGridSeed_ = 0;
pinServerSalt_ = {};
checksumSalt_ = {};
// Initialize SRP with pre-computed hash
srp = std::make_unique<SRP>();
@ -196,6 +200,7 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) {
srp->feed(response.B, response.g, response.N, response.salt);
securityFlags_ = response.securityFlags;
checksumSalt_ = response.checksumSalt;
if (securityFlags_ & 0x01) {
pinGridSeed_ = response.pinGridSeed;
pinServerSalt_ = response.pinSalt;
@ -222,6 +227,8 @@ void AuthHandler::sendLogonProof() {
std::array<uint8_t, 20> pinHash{};
const std::array<uint8_t, 16>* pinClientSaltPtr = nullptr;
const std::array<uint8_t, 20>* pinHashPtr = nullptr;
std::array<uint8_t, 20> crcHash{};
const std::array<uint8_t, 20>* crcHashPtr = nullptr;
if (securityFlags_ & 0x01) {
try {
@ -236,20 +243,54 @@ void AuthHandler::sendLogonProof() {
}
}
// Protocol < 8 uses a shorter proof packet (no securityFlags byte).
if (clientInfo.protocolVersion < 8) {
auto packet = LogonProofPacket::buildLegacy(A, M1);
socket->send(packet);
} else {
auto packet = LogonProofPacket::build(A, M1, securityFlags_, pinClientSaltPtr, pinHashPtr);
socket->send(packet);
if (securityFlags_ & 0x04) {
// TrinityCore-style Google Authenticator token: send immediately after proof.
const std::string token = pendingSecurityCode_;
auto tokPkt = AuthenticatorTokenPacket::build(token);
socket->send(tokPkt);
// Legacy client integrity hash (aka "CRC hash"). Some servers enforce this for classic builds.
// We compute it when checksumSalt was provided (always present on success challenge) and files exist.
{
std::vector<std::string> candidateDirs;
if (const char* env = std::getenv("WOWEE_INTEGRITY_DIR")) {
if (env && *env) candidateDirs.push_back(env);
}
// Default local extraction layout
candidateDirs.push_back("Data/misc");
// Common turtle repack location used in this workspace
if (const char* home = std::getenv("HOME")) {
if (home && *home) {
candidateDirs.push_back(std::string(home) + "/Downloads/twmoa_1180");
candidateDirs.push_back(std::string(home) + "/twmoa_1180");
}
}
const char* candidateExes[] = { "WoW.exe", "TurtleWoW.exe", "Wow.exe" };
bool ok = false;
std::string lastErr;
for (const auto& dir : candidateDirs) {
for (const char* exe : candidateExes) {
std::string err;
if (computeIntegrityHashWin32WithExe(checksumSalt_, A, dir, exe, crcHash, err)) {
crcHashPtr = &crcHash;
LOG_INFO("Integrity hash computed from ", dir, " (", exe, ")");
ok = true;
break;
}
lastErr = err;
}
if (ok) break;
}
if (!ok) {
LOG_WARNING("Integrity hash not computed (", lastErr,
"). Server may reject classic clients without it. "
"Set WOWEE_INTEGRITY_DIR to your client folder.");
}
}
auto packet = LogonProofPacket::build(A, M1, securityFlags_, crcHashPtr, pinClientSaltPtr, pinHashPtr);
socket->send(packet);
if ((securityFlags_ & 0x04) && clientInfo.protocolVersion >= 8) {
// TrinityCore-style Google Authenticator token: send immediately after proof.
const std::string token = pendingSecurityCode_;
auto tokPkt = AuthenticatorTokenPacket::build(token);
socket->send(tokPkt);
}
setState(AuthState::PROOF_SENT);

View file

@ -126,9 +126,9 @@ bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallenge
response.salt[i] = packet.readUInt8();
}
// Unknown/padding - 16 bytes
for (int i = 0; i < 16; ++i) {
packet.readUInt8();
// Integrity salt / CRC salt - 16 bytes
for (size_t i = 0; i < response.checksumSalt.size(); ++i) {
response.checksumSalt[i] = packet.readUInt8();
}
// Security flags
@ -162,7 +162,7 @@ bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallenge
network::Packet LogonProofPacket::build(const std::vector<uint8_t>& A,
const std::vector<uint8_t>& M1) {
return build(A, M1, 0, nullptr, nullptr);
return build(A, M1, 0, nullptr, nullptr, nullptr);
}
network::Packet LogonProofPacket::buildLegacy(const std::vector<uint8_t>& A,
@ -185,6 +185,7 @@ network::Packet LogonProofPacket::buildLegacy(const std::vector<uint8_t>& A,
network::Packet LogonProofPacket::build(const std::vector<uint8_t>& A,
const std::vector<uint8_t>& M1,
uint8_t securityFlags,
const std::array<uint8_t, 20>* crcHash,
const std::array<uint8_t, 16>* pinClientSalt,
const std::array<uint8_t, 20>* pinHash) {
if (A.size() != 32) {
@ -202,9 +203,11 @@ network::Packet LogonProofPacket::build(const std::vector<uint8_t>& A,
// M1 (client proof) - 20 bytes
packet.writeBytes(M1.data(), M1.size());
// CRC hash - 20 bytes (zeros)
for (int i = 0; i < 20; ++i) {
packet.writeUInt8(0);
// CRC hash / integrity hash - 20 bytes
if (crcHash) {
packet.writeBytes(crcHash->data(), crcHash->size());
} else {
for (int i = 0; i < 20; ++i) packet.writeUInt8(0);
}
// Number of keys

91
src/auth/integrity.cpp Normal file
View file

@ -0,0 +1,91 @@
#include "auth/integrity.hpp"
#include "auth/crypto.hpp"
#include <fstream>
#include <sstream>
namespace wowee {
namespace auth {
static bool readWholeFile(const std::string& path, std::vector<uint8_t>& out, std::string& err) {
std::ifstream f(path, std::ios::binary);
if (!f.is_open()) {
err = "missing: " + path;
return false;
}
f.seekg(0, std::ios::end);
std::streamoff size = f.tellg();
if (size < 0) size = 0;
f.seekg(0, std::ios::beg);
out.resize(static_cast<size_t>(size));
if (size > 0) {
f.read(reinterpret_cast<char*>(out.data()), size);
if (!f) {
err = "read failed: " + path;
return false;
}
}
return true;
}
bool computeIntegrityHashWin32WithExe(const std::array<uint8_t, 16>& checksumSalt,
const std::vector<uint8_t>& clientPublicKeyA,
const std::string& miscDir,
const std::string& exeName,
std::array<uint8_t, 20>& outHash,
std::string& outError) {
// Files expected by 1.12.x Windows clients for the integrity check.
// If this needs to vary by build, make it data-driven in expansion.json later.
const char* kFiles[] = {
nullptr, // exeName
"fmod.dll",
"ijl15.dll",
"dbghelp.dll",
"unicows.dll",
};
std::vector<uint8_t> allFiles;
std::string err;
for (size_t idx = 0; idx < (sizeof(kFiles) / sizeof(kFiles[0])); ++idx) {
const char* name = kFiles[idx];
std::string nameStr = name ? std::string(name) : exeName;
std::vector<uint8_t> bytes;
std::string path = miscDir;
if (!path.empty() && path.back() != '/') path += '/';
path += nameStr;
if (!readWholeFile(path, bytes, err)) {
outError = err;
return false;
}
allFiles.insert(allFiles.end(), bytes.begin(), bytes.end());
}
// HMAC_SHA1(checksumSalt, allFiles)
std::vector<uint8_t> key(checksumSalt.begin(), checksumSalt.end());
const std::vector<uint8_t> checksum = Crypto::hmacSHA1(key, allFiles); // 20 bytes
// SHA1(A || checksum)
std::vector<uint8_t> shaIn;
shaIn.reserve(clientPublicKeyA.size() + checksum.size());
shaIn.insert(shaIn.end(), clientPublicKeyA.begin(), clientPublicKeyA.end());
shaIn.insert(shaIn.end(), checksum.begin(), checksum.end());
const std::vector<uint8_t> finalHash = Crypto::sha1(shaIn);
if (finalHash.size() != outHash.size()) {
outError = "unexpected sha1 size";
return false;
}
std::copy(finalHash.begin(), finalHash.end(), outHash.begin());
return true;
}
bool computeIntegrityHashWin32(const std::array<uint8_t, 16>& checksumSalt,
const std::vector<uint8_t>& clientPublicKeyA,
const std::string& miscDir,
std::array<uint8_t, 20>& outHash,
std::string& outError) {
return computeIntegrityHashWin32WithExe(checksumSalt, clientPublicKeyA, miscDir, "WoW.exe", outHash, outError);
}
} // namespace auth
} // namespace wowee

View file

@ -58,6 +58,18 @@ void SRP::feed(const std::vector<uint8_t>& B_bytes,
this->N = BigNum(N_bytes, true);
this->s = BigNum(salt_bytes, true);
if (useHashedK_) {
// k = H(N | g) (SRP-6a style)
std::vector<uint8_t> Ng;
Ng.insert(Ng.end(), N_bytes.begin(), N_bytes.end());
Ng.insert(Ng.end(), g_bytes.begin(), g_bytes.end());
std::vector<uint8_t> k_bytes = Crypto::sha1(Ng);
k = BigNum(k_bytes, !hashBigEndian_);
LOG_DEBUG("Using hashed SRP multiplier k=H(N|g)");
} else {
k = BigNum(K_VALUE);
}
LOG_DEBUG("SRP challenge data loaded");
// Now compute everything in sequence
@ -72,7 +84,7 @@ void SRP::feed(const std::vector<uint8_t>& B_bytes,
x_input.insert(x_input.end(), salt_bytes.begin(), salt_bytes.end());
x_input.insert(x_input.end(), auth_hash.begin(), auth_hash.end());
std::vector<uint8_t> x_bytes = Crypto::sha1(x_input);
x = BigNum(x_bytes, true);
x = BigNum(x_bytes, !hashBigEndian_);
LOG_DEBUG("Computed x (salted password hash)");
// 3. Generate client ephemeral (a, A)
@ -151,7 +163,7 @@ void SRP::computeSessionKey() {
AB.insert(AB.end(), B_bytes_u.begin(), B_bytes_u.end());
std::vector<uint8_t> u_bytes = Crypto::sha1(AB);
u = BigNum(u_bytes, true);
u = BigNum(u_bytes, !hashBigEndian_);
LOG_DEBUG("Scrambler u calculated");