2026-02-13 01:32:15 -08:00
|
|
|
#include "auth/integrity.hpp"
|
|
|
|
|
#include "auth/crypto.hpp"
|
|
|
|
|
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <sstream>
|
2026-02-25 09:26:34 -08:00
|
|
|
#include <vector>
|
2026-02-13 01:32:15 -08:00
|
|
|
|
|
|
|
|
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.
|
2026-02-13 01:41:59 -08:00
|
|
|
//
|
|
|
|
|
// Turtle WoW ships a custom loader DLL. Some Turtle auth servers appear to validate integrity against
|
|
|
|
|
// that distribution rather than a stock 1.12.1 client, so when using Turtle's executable we include
|
|
|
|
|
// Turtle-specific DLLs as well.
|
|
|
|
|
const bool isTurtleExe = (exeName == "TurtleWoW.exe");
|
2026-02-25 09:26:34 -08:00
|
|
|
// Some macOS client layouts use FMOD dylib naming instead of fmod.dll.
|
|
|
|
|
// We accept the first matching filename in each alias group.
|
|
|
|
|
std::vector<std::vector<std::string>> fileGroups = {
|
|
|
|
|
{ exeName },
|
|
|
|
|
{ "fmod.dll", "fmod.dylib", "libfmod.dylib", "fmodex.dll", "fmodex.dylib", "libfmod.so" },
|
|
|
|
|
{ "ijl15.dll" },
|
|
|
|
|
{ "dbghelp.dll" },
|
|
|
|
|
{ "unicows.dll" },
|
2026-02-13 01:32:15 -08:00
|
|
|
};
|
2026-02-13 01:41:59 -08:00
|
|
|
if (isTurtleExe) {
|
2026-02-25 09:26:34 -08:00
|
|
|
fileGroups.push_back({ "twloader.dll" });
|
|
|
|
|
fileGroups.push_back({ "twdiscord.dll" });
|
2026-02-13 01:41:59 -08:00
|
|
|
}
|
2026-02-13 01:32:15 -08:00
|
|
|
|
|
|
|
|
std::vector<uint8_t> allFiles;
|
2026-02-25 09:26:34 -08:00
|
|
|
for (const auto& group : fileGroups) {
|
|
|
|
|
bool foundInGroup = false;
|
|
|
|
|
std::string groupErr;
|
|
|
|
|
|
|
|
|
|
for (const auto& nameStr : group) {
|
|
|
|
|
std::vector<uint8_t> bytes;
|
|
|
|
|
std::string path = miscDir;
|
|
|
|
|
if (!path.empty() && path.back() != '/') path += '/';
|
|
|
|
|
path += nameStr;
|
|
|
|
|
|
|
|
|
|
std::string err;
|
|
|
|
|
if (!readWholeFile(path, bytes, err)) {
|
|
|
|
|
if (groupErr.empty()) groupErr = err;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
allFiles.insert(allFiles.end(), bytes.begin(), bytes.end());
|
|
|
|
|
foundInGroup = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!foundInGroup) {
|
|
|
|
|
outError = groupErr.empty() ? "missing required integrity file group" : groupErr;
|
2026-02-13 01:32:15 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|