mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 09:33:51 +00:00
Add integrity hash support and SRP tuning options
This commit is contained in:
parent
b3001a4b5b
commit
5435796a98
10 changed files with 591 additions and 22 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
91
src/auth/integrity.cpp
Normal 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
|
||||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue