Vanilla/Turtle WoW support: M2 loading, bone parsing, textures, auth

- Vanilla M2 bone struct (108 bytes) with 28-byte animation tracks
- Version-aware bone parsing (vanilla vs WotLK format detection)
- Fix CharSections.dbc field layout for vanilla (variation/color at 4-5)
- Remove broken CharSections.csv files (all fields marked as strings)
- Expansion data reload on profile switch (DBC cache clear, layout reload)
- Vanilla packet encryption (VanillaCrypt XOR-based header crypt)
- Extended character preview geoset range (0-99) for vanilla models
- DBC cache clear support in AssetManager
This commit is contained in:
Kelsi 2026-02-13 16:53:28 -08:00
parent 6729f66a37
commit 430c2bdcfa
34 changed files with 1066 additions and 24795 deletions

View file

@ -165,6 +165,7 @@ bool ExpansionRegistry::loadProfile(const std::string& jsonPath, const std::stri
}
p.build = static_cast<uint16_t>(jsonInt(json, "build"));
p.worldBuild = static_cast<uint16_t>(jsonInt(json, "worldBuild", p.build));
p.protocolVersion = static_cast<uint8_t>(jsonInt(json, "protocolVersion"));
// Optional client header fields (LOGON_CHALLENGE)
{

View file

@ -136,6 +136,13 @@ bool GameHandler::connect(const std::string& host,
this->accountName = accountName;
this->build = build;
this->realmId_ = realmId;
// Diagnostic: dump session key for AUTH_REJECT debugging
{
std::string hex;
for (uint8_t b : sessionKey) { char buf[4]; snprintf(buf, sizeof(buf), "%02x", b); hex += buf; }
LOG_INFO("GameHandler session key (", sessionKey.size(), "): ", hex);
}
requiresWarden_ = false;
wardenGateSeen_ = false;
wardenGateElapsed_ = 0.0f;
@ -1448,7 +1455,7 @@ void GameHandler::sendAuthSession() {
// AzerothCore enables encryption before sending AUTH_RESPONSE,
// so we need to be ready to decrypt the response
LOG_INFO("Enabling encryption immediately after AUTH_SESSION");
socket->initEncryption(sessionKey);
socket->initEncryption(sessionKey, build);
setState(WorldState::AUTH_SENT);
LOG_INFO("CMSG_AUTH_SESSION sent, encryption enabled, waiting for AUTH_RESPONSE...");
@ -1529,7 +1536,15 @@ void GameHandler::handleCharEnum(network::Packet& packet) {
LOG_INFO("Handling SMSG_CHAR_ENUM");
CharEnumResponse response;
if (!CharEnumParser::parse(packet, response)) {
bool parsed = false;
if (build <= 6005) {
// Vanilla 1.12.x format (different equipment layout, no customization flag)
ClassicPacketParsers classicParser;
parsed = classicParser.parseCharEnum(packet, response);
} else {
parsed = CharEnumParser::parse(packet, response);
}
if (!parsed) {
fail("Failed to parse SMSG_CHAR_ENUM");
return;
}
@ -1901,290 +1916,232 @@ void GameHandler::handleWardenData(network::Packet& packet) {
wardenPacketsAfterGate_ = 0;
}
// Log the raw encrypted packet
std::string hex;
hex.reserve(data.size() * 3);
for (size_t i = 0; i < data.size(); ++i) {
char b[4];
snprintf(b, sizeof(b), "%02x ", data[i]);
hex += b;
}
LOG_INFO("Received SMSG_WARDEN_DATA (len=", data.size(), ", raw: ", hex, ")");
// Initialize crypto on first packet (usually module load)
// Initialize Warden crypto from session key on first packet
if (!wardenCrypto_) {
wardenCrypto_ = std::make_unique<WardenCrypto>();
if (!wardenCrypto_->initialize(data)) {
LOG_ERROR("Warden: Failed to initialize crypto");
if (sessionKey.size() != 40) {
LOG_ERROR("Warden: No valid session key (size=", sessionKey.size(), "), cannot init crypto");
wardenCrypto_.reset();
return;
}
LOG_INFO("Warden: Crypto initialized, analyzing module structure");
// Parse module structure (37 bytes typical):
// [1 byte opcode][16 bytes seed][20 bytes challenge/hash]
std::vector<uint8_t> trailingBytes;
uint8_t opcodeByte = data[0];
if (data.size() >= 37) {
std::string trailingHex;
for (size_t i = 17; i < 37; ++i) {
trailingBytes.push_back(data[i]);
char b[4];
snprintf(b, sizeof(b), "%02x ", data[i]);
trailingHex += b;
}
LOG_INFO("Warden: Opcode byte: 0x", std::hex, (int)opcodeByte, std::dec);
LOG_INFO("Warden: Challenge/hash (20 bytes): ", trailingHex);
if (!wardenCrypto_->initFromSessionKey(sessionKey)) {
LOG_ERROR("Warden: Failed to initialize crypto from session key");
wardenCrypto_.reset();
return;
}
// For opcode 0xF6, try empty response (some servers expect nothing)
std::vector<uint8_t> moduleResponse;
LOG_INFO("Warden: Trying response strategy: Empty response (0 bytes)");
// Send empty/null response
// moduleResponse remains empty (size = 0)
LOG_INFO("Warden: Crypto initialized, sending MODULE_OK with challenge");
LOG_INFO("Warden: Opcode 0x01 + 20-byte challenge");
// Try opcode 0x01 (MODULE_OK) followed by 20-byte challenge
std::vector<uint8_t> hashResponse;
hashResponse.push_back(0x01); // WARDEN_CMSG_MODULE_OK
// Append the 20-byte challenge
for (size_t i = 0; i < trailingBytes.size(); ++i) {
hashResponse.push_back(trailingBytes[i]);
}
LOG_INFO("Warden: SHA1 hash computed (20 bytes), total response: ", hashResponse.size(), " bytes");
// Encrypt the response
std::vector<uint8_t> encryptedResponse = wardenCrypto_->encrypt(hashResponse);
// Send HASH_RESULT response
network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA));
for (uint8_t byte : encryptedResponse) {
response.writeUInt8(byte);
}
if (socket && socket->isConnected()) {
socket->send(response);
LOG_INFO("Sent CMSG_WARDEN_DATA MODULE_OK+challenge (", encryptedResponse.size(), " bytes encrypted)");
}
// Mark that we've seen the initial seed packet
wardenGateSeen_ = true;
return;
wardenState_ = WardenState::WAIT_MODULE_USE;
}
// Decrypt the packet
// Decrypt the payload
std::vector<uint8_t> decrypted = wardenCrypto_->decrypt(data);
// Log decrypted data (first 64 bytes for readability)
std::string decHex;
size_t logSize = std::min(decrypted.size(), size_t(64));
decHex.reserve(logSize * 3);
for (size_t i = 0; i < logSize; ++i) {
char b[4];
snprintf(b, sizeof(b), "%02x ", decrypted[i]);
decHex += b;
}
if (decrypted.size() > 64) {
decHex += "... (" + std::to_string(decrypted.size() - 64) + " more bytes)";
}
LOG_INFO("Warden: Decrypted (", decrypted.size(), " bytes): ", decHex);
// Check if this looks like a module download (large size)
if (decrypted.size() > 256) {
LOG_INFO("Warden: Received large packet (", decrypted.size(), " bytes) - attempting module load");
// Try to load this as a Warden module
// Compute MD5 hash of the decrypted data for module identification
std::vector<uint8_t> moduleMD5 = auth::Crypto::md5(decrypted);
// The data is already decrypted by our crypto layer, but the module loader
// expects encrypted data. We need to pass the original encrypted data.
// For now, try loading with what we have.
auto module = wardenModuleManager_->getModule(moduleMD5);
// Extract RC4 key from current crypto state (we already initialized it)
std::vector<uint8_t> dummyKey(16, 0); // Module will use existing crypto
if (module->load(decrypted, moduleMD5, dummyKey)) {
LOG_INFO("Warden: ✓ Module loaded successfully!");
// Module is now ready to process check requests
// No response needed for module download
return;
} else {
LOG_WARNING("Warden: ✗ Module load failed, falling back to fake responses");
// Log decrypted data
{
std::string hex;
size_t logSize = std::min(decrypted.size(), size_t(64));
hex.reserve(logSize * 3);
for (size_t i = 0; i < logSize; ++i) {
char b[4]; snprintf(b, sizeof(b), "%02x ", decrypted[i]); hex += b;
}
if (decrypted.size() > 64)
hex += "... (" + std::to_string(decrypted.size() - 64) + " more)";
LOG_INFO("Warden: Decrypted (", decrypted.size(), " bytes): ", hex);
}
// Prepare response data
std::vector<uint8_t> responseData;
if (decrypted.empty()) {
LOG_INFO("Warden: Empty decrypted packet");
} else {
uint8_t opcode = decrypted[0];
// If we have a loaded module, try to use it for check processing
std::vector<uint8_t> moduleMD5 = auth::Crypto::md5(decrypted);
auto module = wardenModuleManager_->getModule(moduleMD5);
if (module && module->isLoaded()) {
LOG_INFO("Warden: Using loaded module to process check request (opcode 0x",
std::hex, (int)opcode, std::dec, ")");
if (module->processCheckRequest(decrypted, responseData)) {
LOG_INFO("Warden: ✓ Module generated response (", responseData.size(), " bytes)");
// Response will be encrypted and sent below
} else {
LOG_WARNING("Warden: ✗ Module failed to process check, using fallback");
// Fall through to fake responses
}
}
// If module processing didn't generate a response, use fake responses
if (responseData.empty()) {
uint8_t opcode = decrypted[0];
// Warden check opcodes (from WoW 3.3.5a protocol)
switch (opcode) {
case 0x00: // Module info request
LOG_INFO("Warden: Module info request");
responseData.push_back(0x00);
break;
case 0x01: { // Hash request
LOG_INFO("Warden: Hash request (file/memory hash check)");
// Parse hash request structure
// Format: [0x01][seed][hash_to_check][...]
responseData.push_back(0x01);
// For each hash check, respond with matching result
if (decrypted.size() > 1) {
// Extract number of hash checks (varies by server)
size_t pos = 1;
while (pos < decrypted.size() && responseData.size() < 64) {
responseData.push_back(0x00); // Hash matches (no violation)
pos += 20; // Skip 20-byte hash (SHA1)
}
} else {
responseData.push_back(0x00); // Pass
}
LOG_INFO("Warden: Hash check response with ", responseData.size() - 1, " results");
break;
}
case 0x02: { // Lua string check
LOG_INFO("Warden: Lua string check (anti-addon detection)");
// Format: [0x02][lua_length][lua_string]
responseData.push_back(0x02);
// Return empty string = no suspicious Lua found
responseData.push_back(0x00); // String length = 0
LOG_INFO("Warden: Lua check - returned empty (no detection)");
break;
}
case 0x05: { // Memory/page check
LOG_INFO("Warden: Memory page check (anti-cheat scan)");
// Format: [0x05][seed][address_count][addresses...][length]
if (decrypted.size() >= 2) {
uint8_t numChecks = decrypted[1];
LOG_INFO("Warden: Processing ", (int)numChecks, " memory checks");
responseData.push_back(0x05);
// For each memory region checked, return "clean" data
for (uint8_t i = 0; i < numChecks; ++i) {
// Return zeroed memory (appears clean/unmodified)
responseData.push_back(0x00);
}
LOG_INFO("Warden: Memory check - all regions clean");
} else {
responseData.push_back(0x05);
responseData.push_back(0x00);
}
break;
}
case 0x04: { // Timing check
LOG_INFO("Warden: Timing check (detect speedhacks)");
// Return current timestamp
responseData.push_back(0x04);
uint32_t timestamp = static_cast<uint32_t>(std::time(nullptr));
responseData.push_back((timestamp >> 0) & 0xFF);
responseData.push_back((timestamp >> 8) & 0xFF);
responseData.push_back((timestamp >> 16) & 0xFF);
responseData.push_back((timestamp >> 24) & 0xFF);
break;
}
default:
LOG_INFO("Warden: Unknown check opcode 0x", std::hex, (int)opcode, std::dec);
LOG_INFO("Warden: Packet dump: ", decHex);
// Try to detect packet type from size/structure
if (decrypted.size() > 20) {
LOG_INFO("Warden: Large packet - might be batched checks");
// Some servers send multiple checks in one packet
// Try to respond to each one
responseData.push_back(0x00); // Generic "OK" response
} else {
// Small unknown packet - echo with success
responseData.push_back(opcode);
responseData.push_back(0x00);
}
break;
}
}
}
// If we have no response data, don't send anything
if (responseData.empty()) {
LOG_INFO("Warden: No response generated (module loaded or waiting for checks)");
LOG_WARNING("Warden: Empty decrypted payload");
return;
}
// Log plaintext response
std::string respPlainHex;
respPlainHex.reserve(responseData.size() * 3);
for (uint8_t byte : responseData) {
char b[4];
snprintf(b, sizeof(b), "%02x ", byte);
respPlainHex += b;
}
LOG_INFO("Warden: Response plaintext (", responseData.size(), " bytes): ", respPlainHex);
uint8_t wardenOpcode = decrypted[0];
// Encrypt response
std::vector<uint8_t> encrypted = wardenCrypto_->encrypt(responseData);
// Helper to send an encrypted Warden response
auto sendWardenResponse = [&](const std::vector<uint8_t>& plaintext) {
std::vector<uint8_t> encrypted = wardenCrypto_->encrypt(plaintext);
network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA));
for (uint8_t byte : encrypted) {
response.writeUInt8(byte);
}
if (socket && socket->isConnected()) {
socket->send(response);
LOG_INFO("Warden: Sent response (", plaintext.size(), " bytes plaintext)");
}
};
// Log encrypted response
std::string respEncHex;
respEncHex.reserve(encrypted.size() * 3);
for (uint8_t byte : encrypted) {
char b[4];
snprintf(b, sizeof(b), "%02x ", byte);
respEncHex += b;
}
LOG_INFO("Warden: Response encrypted (", encrypted.size(), " bytes): ", respEncHex);
switch (wardenOpcode) {
case 0x00: { // WARDEN_SMSG_MODULE_USE
// Format: [1 opcode][16 moduleHash][16 moduleKey][4 moduleSize]
if (decrypted.size() < 37) {
LOG_ERROR("Warden: MODULE_USE too short (", decrypted.size(), " bytes, need 37)");
return;
}
// Build and send response packet
network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA));
for (uint8_t byte : encrypted) {
response.writeUInt8(byte);
}
wardenModuleHash_.assign(decrypted.begin() + 1, decrypted.begin() + 17);
wardenModuleKey_.assign(decrypted.begin() + 17, decrypted.begin() + 33);
wardenModuleSize_ = static_cast<uint32_t>(decrypted[33])
| (static_cast<uint32_t>(decrypted[34]) << 8)
| (static_cast<uint32_t>(decrypted[35]) << 16)
| (static_cast<uint32_t>(decrypted[36]) << 24);
wardenModuleData_.clear();
if (socket && socket->isConnected()) {
socket->send(response);
LOG_INFO("Sent CMSG_WARDEN_DATA encrypted response");
{
std::string hashHex, keyHex;
for (auto b : wardenModuleHash_) { char s[4]; snprintf(s, 4, "%02x", b); hashHex += s; }
for (auto b : wardenModuleKey_) { char s[4]; snprintf(s, 4, "%02x", b); keyHex += s; }
LOG_INFO("Warden: MODULE_USE hash=", hashHex,
" key=", keyHex, " size=", wardenModuleSize_);
}
// Respond with MODULE_MISSING (opcode 0x00) to request the module data
std::vector<uint8_t> resp = { 0x00 }; // WARDEN_CMSG_MODULE_MISSING
sendWardenResponse(resp);
wardenState_ = WardenState::WAIT_MODULE_CACHE;
LOG_INFO("Warden: Sent MODULE_MISSING, waiting for module data chunks");
break;
}
case 0x01: { // WARDEN_SMSG_MODULE_CACHE (module data chunk)
// Format: [1 opcode][2 chunkSize LE][chunkSize bytes data]
if (decrypted.size() < 3) {
LOG_ERROR("Warden: MODULE_CACHE too short");
return;
}
uint16_t chunkSize = static_cast<uint16_t>(decrypted[1])
| (static_cast<uint16_t>(decrypted[2]) << 8);
if (decrypted.size() < 3u + chunkSize) {
LOG_ERROR("Warden: MODULE_CACHE chunk truncated (claimed ", chunkSize,
", have ", decrypted.size() - 3, ")");
return;
}
wardenModuleData_.insert(wardenModuleData_.end(),
decrypted.begin() + 3,
decrypted.begin() + 3 + chunkSize);
LOG_INFO("Warden: MODULE_CACHE chunk ", chunkSize, " bytes, total ",
wardenModuleData_.size(), "/", wardenModuleSize_);
// Check if module download is complete
if (wardenModuleData_.size() >= wardenModuleSize_) {
LOG_INFO("Warden: Module download complete (",
wardenModuleData_.size(), " bytes)");
wardenState_ = WardenState::WAIT_HASH_REQUEST;
// Send MODULE_OK (opcode 0x01)
std::vector<uint8_t> resp = { 0x01 }; // WARDEN_CMSG_MODULE_OK
sendWardenResponse(resp);
LOG_INFO("Warden: Sent MODULE_OK");
}
// No response for intermediate chunks
break;
}
case 0x05: { // WARDEN_SMSG_HASH_REQUEST
// Format: [1 opcode][16 seed]
if (decrypted.size() < 17) {
LOG_ERROR("Warden: HASH_REQUEST too short (", decrypted.size(), " bytes, need 17)");
return;
}
std::vector<uint8_t> seed(decrypted.begin() + 1, decrypted.begin() + 17);
{
std::string seedHex;
for (auto b : seed) { char s[4]; snprintf(s, 4, "%02x", b); seedHex += s; }
LOG_INFO("Warden: HASH_REQUEST seed=", seedHex);
}
// The server expects SHA1 of the module's init function stub XOR'd with the seed.
// Without the actual module execution, we compute a plausible hash.
// For now, try to use the module data we downloaded.
// The correct approach: decrypt module with moduleKey RC4, then hash the init stub.
// But we need to at least send a HASH_RESULT to not get kicked immediately.
// Decrypt the downloaded module data using the module RC4 key
// VMaNGOS: The module is RC4-encrypted with wardenModuleKey_
if (!wardenModuleData_.empty() && !wardenModuleKey_.empty()) {
LOG_INFO("Warden: Attempting to compute hash from downloaded module (",
wardenModuleData_.size(), " bytes)");
// The module data is RC4-encrypted. Decrypt it.
// Standard RC4 decryption with the module key
std::vector<uint8_t> moduleDecrypted(wardenModuleData_.size());
{
// RC4 KSA + PRGA with wardenModuleKey_
std::vector<uint8_t> S(256);
for (int i = 0; i < 256; ++i) S[i] = static_cast<uint8_t>(i);
uint8_t j = 0;
for (int i = 0; i < 256; ++i) {
j = (j + S[i] + wardenModuleKey_[i % wardenModuleKey_.size()]) & 0xFF;
std::swap(S[i], S[j]);
}
uint8_t ri = 0, rj = 0;
for (size_t k = 0; k < wardenModuleData_.size(); ++k) {
ri = (ri + 1) & 0xFF;
rj = (rj + S[ri]) & 0xFF;
std::swap(S[ri], S[rj]);
moduleDecrypted[k] = wardenModuleData_[k] ^ S[(S[ri] + S[rj]) & 0xFF];
}
}
LOG_INFO("Warden: Module decrypted, computing hash response");
// The hash is SHA1 of the seed concatenated with module data
// Actually in VMaNGOS, the init function stub computes SHA1 of seed + specific module regions.
// We'll try a simple SHA1(seed + first 16 bytes of decrypted module) approach,
// which won't pass verification but at least sends a properly formatted response.
std::vector<uint8_t> hashInput;
hashInput.insert(hashInput.end(), seed.begin(), seed.end());
// Add decrypted module data
hashInput.insert(hashInput.end(), moduleDecrypted.begin(), moduleDecrypted.end());
auto hash = auth::Crypto::sha1(hashInput);
// Send HASH_RESULT (WARDEN_CMSG_HASH_RESULT = opcode 0x04)
std::vector<uint8_t> resp;
resp.push_back(0x04); // WARDEN_CMSG_HASH_RESULT
resp.insert(resp.end(), hash.begin(), hash.end()); // 20-byte SHA1
sendWardenResponse(resp);
LOG_INFO("Warden: Sent HASH_RESULT (21 bytes)");
} else {
// No module data available, send a dummy hash
LOG_WARNING("Warden: No module data for hash computation, sending dummy");
std::vector<uint8_t> hashInput;
hashInput.insert(hashInput.end(), seed.begin(), seed.end());
auto hash = auth::Crypto::sha1(hashInput);
std::vector<uint8_t> resp;
resp.push_back(0x04);
resp.insert(resp.end(), hash.begin(), hash.end());
sendWardenResponse(resp);
}
wardenState_ = WardenState::WAIT_CHECKS;
break;
}
case 0x02: { // WARDEN_SMSG_CHEAT_CHECKS_REQUEST
LOG_INFO("Warden: CHEAT_CHECKS_REQUEST (", decrypted.size(), " bytes)");
// We don't have the module to properly process checks.
// Send a minimal valid-looking response.
// WARDEN_CMSG_CHEAT_CHECKS_RESULT (opcode 0x02):
// [1 opcode][2 length LE][4 checksum][result data]
// Minimal response: length=0, checksum=0
std::vector<uint8_t> resp;
resp.push_back(0x02); // WARDEN_CMSG_CHEAT_CHECKS_RESULT
resp.push_back(0x00); resp.push_back(0x00); // length = 0
resp.push_back(0x00); resp.push_back(0x00);
resp.push_back(0x00); resp.push_back(0x00); // checksum = 0
sendWardenResponse(resp);
LOG_INFO("Warden: Sent CHEAT_CHECKS_RESULT (minimal)");
break;
}
default:
LOG_INFO("Warden: Unknown opcode 0x", std::hex, (int)wardenOpcode, std::dec,
" (state=", (int)wardenState_, ", size=", decrypted.size(), ")");
break;
}
}

View file

@ -572,6 +572,10 @@ bool OpcodeTable::loadFromJson(const std::string& path) {
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
// Save old tables so we can restore on failure
auto savedLogicalToWire = logicalToWire_;
auto savedWireToLogical = wireToLogical_;
logicalToWire_.clear();
wireToLogical_.clear();
@ -624,8 +628,15 @@ bool OpcodeTable::loadFromJson(const std::string& path) {
pos = valEnd + 1;
}
if (loaded == 0) {
LOG_WARNING("OpcodeTable: no opcodes loaded from ", path, ", restoring previous tables");
logicalToWire_ = std::move(savedLogicalToWire);
wireToLogical_ = std::move(savedWireToLogical);
return false;
}
LOG_INFO("OpcodeTable: loaded ", loaded, " opcodes from ", path);
return loaded > 0;
return true;
}
uint16_t OpcodeTable::toWire(LogicalOpcode op) const {

View file

@ -95,6 +95,7 @@ bool UpdateFieldTable::loadFromJson(const std::string& path) {
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
auto savedFieldMap = fieldMap_;
fieldMap_.clear();
size_t loaded = 0;
size_t pos = 0;
@ -139,8 +140,14 @@ bool UpdateFieldTable::loadFromJson(const std::string& path) {
pos = valEnd + 1;
}
if (loaded == 0) {
LOG_WARNING("UpdateFieldTable: no fields loaded from ", path, ", restoring previous table");
fieldMap_ = std::move(savedFieldMap);
return false;
}
LOG_INFO("UpdateFieldTable: loaded ", loaded, " fields from ", path);
return loaded > 0;
return true;
}
uint16_t UpdateFieldTable::index(UF field) const {

View file

@ -1,4 +1,5 @@
#include "game/warden_crypto.hpp"
#include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <cstring>
#include <algorithm>
@ -6,65 +7,109 @@
namespace wowee {
namespace game {
// Warden module keys for WoW 3.3.5a (from client analysis)
// These are the standard keys used by most 3.3.5a servers
static const uint8_t WARDEN_MODULE_KEY[16] = {
0xC5, 0x35, 0xB2, 0x1E, 0xF8, 0xE7, 0x9F, 0x4B,
0x91, 0xB6, 0xD1, 0x34, 0xA7, 0x2F, 0x58, 0x8C
};
WardenCrypto::WardenCrypto()
: initialized_(false)
, inputRC4_i_(0)
, inputRC4_j_(0)
, outputRC4_i_(0)
, outputRC4_j_(0) {
, decryptRC4_i_(0)
, decryptRC4_j_(0)
, encryptRC4_i_(0)
, encryptRC4_j_(0) {
}
WardenCrypto::~WardenCrypto() {
}
bool WardenCrypto::initialize(const std::vector<uint8_t>& moduleData) {
if (moduleData.empty()) {
LOG_ERROR("Warden: Cannot initialize with empty module data");
void WardenCrypto::sha1RandxGenerate(const std::vector<uint8_t>& seed,
uint8_t* outputEncryptKey,
uint8_t* outputDecryptKey) {
// SHA1Randx / WardenKeyGenerator algorithm (from MaNGOS/VMaNGOS)
// Split the 40-byte session key in half
size_t half = seed.size() / 2;
// o1 = SHA1(first half of session key)
std::vector<uint8_t> firstHalf(seed.begin(), seed.begin() + half);
auto o1 = auth::Crypto::sha1(firstHalf);
// o2 = SHA1(second half of session key)
std::vector<uint8_t> secondHalf(seed.begin() + half, seed.end());
auto o2 = auth::Crypto::sha1(secondHalf);
// o0 = 20 zero bytes initially
std::vector<uint8_t> o0(20, 0);
uint32_t taken = 20; // Force FillUp on first Generate
// Lambda: FillUp - compute o0 = SHA1(o1 || o0 || o2)
auto fillUp = [&]() {
std::vector<uint8_t> combined;
combined.reserve(60);
combined.insert(combined.end(), o1.begin(), o1.end());
combined.insert(combined.end(), o0.begin(), o0.end());
combined.insert(combined.end(), o2.begin(), o2.end());
o0 = auth::Crypto::sha1(combined);
taken = 0;
};
// Generate first 16 bytes → encrypt key (client→server)
for (int i = 0; i < 16; ++i) {
if (taken == 20) fillUp();
outputEncryptKey[i] = o0[taken++];
}
// Generate next 16 bytes → decrypt key (server→client)
for (int i = 0; i < 16; ++i) {
if (taken == 20) fillUp();
outputDecryptKey[i] = o0[taken++];
}
}
bool WardenCrypto::initFromSessionKey(const std::vector<uint8_t>& sessionKey) {
if (sessionKey.size() != 40) {
LOG_ERROR("Warden: Session key must be 40 bytes, got ", sessionKey.size());
return false;
}
LOG_INFO("Warden: Initializing crypto with ", moduleData.size(), " byte module");
uint8_t encryptKey[16];
uint8_t decryptKey[16];
sha1RandxGenerate(sessionKey, encryptKey, decryptKey);
// Warden 3.3.5a module format (typically):
// [1 byte opcode][16 bytes seed/key][remaining bytes = encrypted module data]
if (moduleData.size() < 17) {
LOG_WARNING("Warden: Module too small (", moduleData.size(), " bytes), using default keys");
// Use default keys
inputKey_.assign(WARDEN_MODULE_KEY, WARDEN_MODULE_KEY + 16);
outputKey_.assign(WARDEN_MODULE_KEY, WARDEN_MODULE_KEY + 16);
} else {
// Extract seed from module (skip first opcode byte)
inputKey_.assign(moduleData.begin() + 1, moduleData.begin() + 17);
outputKey_ = inputKey_;
// XOR with module key for output
for (size_t i = 0; i < 16; ++i) {
outputKey_[i] ^= WARDEN_MODULE_KEY[i];
// Log derived keys
{
std::string hex;
for (int i = 0; i < 16; ++i) {
char b[4]; snprintf(b, sizeof(b), "%02x ", encryptKey[i]); hex += b;
}
LOG_INFO("Warden: Extracted 16-byte seed from module");
LOG_INFO("Warden: Encrypt key (C→S): ", hex);
hex.clear();
for (int i = 0; i < 16; ++i) {
char b[4]; snprintf(b, sizeof(b), "%02x ", decryptKey[i]); hex += b;
}
LOG_INFO("Warden: Decrypt key (S→C): ", hex);
}
// Initialize RC4 ciphers
inputRC4State_.resize(256);
outputRC4State_.resize(256);
std::vector<uint8_t> ek(encryptKey, encryptKey + 16);
std::vector<uint8_t> dk(decryptKey, decryptKey + 16);
initRC4(inputKey_, inputRC4State_, inputRC4_i_, inputRC4_j_);
initRC4(outputKey_, outputRC4State_, outputRC4_i_, outputRC4_j_);
encryptRC4State_.resize(256);
decryptRC4State_.resize(256);
initRC4(ek, encryptRC4State_, encryptRC4_i_, encryptRC4_j_);
initRC4(dk, decryptRC4State_, decryptRC4_i_, decryptRC4_j_);
initialized_ = true;
LOG_INFO("Warden: Crypto initialized successfully");
LOG_INFO("Warden: Crypto initialized from session key");
return true;
}
void WardenCrypto::replaceKeys(const std::vector<uint8_t>& newEncryptKey,
const std::vector<uint8_t>& newDecryptKey) {
encryptRC4State_.resize(256);
decryptRC4State_.resize(256);
initRC4(newEncryptKey, encryptRC4State_, encryptRC4_i_, encryptRC4_j_);
initRC4(newDecryptKey, decryptRC4State_, decryptRC4_i_, decryptRC4_j_);
LOG_INFO("Warden: RC4 keys replaced (module-specific keys)");
}
std::vector<uint8_t> WardenCrypto::decrypt(const std::vector<uint8_t>& data) {
if (!initialized_) {
LOG_WARNING("Warden: Attempted to decrypt without initialization");
@ -73,7 +118,7 @@ std::vector<uint8_t> WardenCrypto::decrypt(const std::vector<uint8_t>& data) {
std::vector<uint8_t> result(data.size());
processRC4(data.data(), result.data(), data.size(),
inputRC4State_, inputRC4_i_, inputRC4_j_);
decryptRC4State_, decryptRC4_i_, decryptRC4_j_);
return result;
}
@ -85,19 +130,17 @@ std::vector<uint8_t> WardenCrypto::encrypt(const std::vector<uint8_t>& data) {
std::vector<uint8_t> result(data.size());
processRC4(data.data(), result.data(), data.size(),
outputRC4State_, outputRC4_i_, outputRC4_j_);
encryptRC4State_, encryptRC4_i_, encryptRC4_j_);
return result;
}
void WardenCrypto::initRC4(const std::vector<uint8_t>& key,
std::vector<uint8_t>& state,
uint8_t& i, uint8_t& j) {
// Initialize permutation
for (int idx = 0; idx < 256; ++idx) {
state[idx] = static_cast<uint8_t>(idx);
}
// Key scheduling algorithm (KSA)
j = 0;
for (int idx = 0; idx < 256; ++idx) {
j = (j + state[idx] + key[idx % key.size()]) & 0xFF;

View file

@ -59,11 +59,11 @@ network::Packet AuthSessionPacket::build(uint32_t build,
packet.writeString(upperAccount);
packet.writeUInt32(0); // LoginServerType
packet.writeUInt32(clientSeed);
// Some private cores validate these fields against realmlist/worldserver settings.
// Default to 1/1 and realmId (falling back to 1) rather than all zeros.
packet.writeUInt32(1); // RegionID
packet.writeUInt32(1); // BattlegroupID
packet.writeUInt32(realmId ? realmId : 1); // RealmID
// AzerothCore ignores these fields; other cores may validate them.
// Use 0 for maximum compatibility.
packet.writeUInt32(0); // RegionID
packet.writeUInt32(0); // BattlegroupID
packet.writeUInt32(realmId); // RealmID
LOG_DEBUG(" Realm ID: ", realmId);
packet.writeUInt32(0); // DOS response (uint64)
packet.writeUInt32(0);
@ -148,10 +148,36 @@ std::vector<uint8_t> AuthSessionPacket::computeAuthHash(
// Session key (40 bytes)
hashInput.insert(hashInput.end(), sessionKey.begin(), sessionKey.end());
LOG_DEBUG("Auth hash input: ", hashInput.size(), " bytes");
// Diagnostic: dump auth hash inputs for debugging AUTH_REJECT
{
auto toHex = [](const uint8_t* data, size_t len) {
std::string s;
for (size_t i = 0; i < len; ++i) {
char buf[4]; snprintf(buf, sizeof(buf), "%02x", data[i]); s += buf;
}
return s;
};
LOG_INFO("AUTH HASH: account='", accountName, "' clientSeed=0x", std::hex, clientSeed,
" serverSeed=0x", serverSeed, std::dec);
LOG_INFO("AUTH HASH: sessionKey=", toHex(sessionKey.data(), sessionKey.size()));
LOG_INFO("AUTH HASH: input(", hashInput.size(), ")=", toHex(hashInput.data(), hashInput.size()));
}
// Compute SHA1 hash
return auth::Crypto::sha1(hashInput);
auto result = auth::Crypto::sha1(hashInput);
{
auto toHex = [](const uint8_t* data, size_t len) {
std::string s;
for (size_t i = 0; i < len; ++i) {
char buf[4]; snprintf(buf, sizeof(buf), "%02x", data[i]); s += buf;
}
return s;
};
LOG_INFO("AUTH HASH: digest=", toHex(result.data(), result.size()));
}
return result;
}
bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data) {