From bcdc555c4ac7695c5e38089543bc2f1609ee0afe Mon Sep 17 00:00:00 2001 From: momo5502 Date: Mon, 22 Feb 2016 23:35:53 +0100 Subject: [PATCH] POW (aka. hashcash) stuff. --- .../Modules/AssetInterfaces/IXModel.cpp | 2 +- src/Components/Modules/Auth.cpp | 184 +++++++++++++++++- src/Components/Modules/Auth.hpp | 11 ++ src/Components/Modules/Party.cpp | 6 + src/Proto/auth.proto | 1 + src/STDInclude.hpp | 2 +- src/Steam/Interfaces/SteamUser.cpp | 30 +-- src/Steam/Interfaces/SteamUser.hpp | 3 - src/Utils/Cryptography.cpp | 43 +++- src/Utils/Cryptography.hpp | 12 ++ src/Utils/Utils.cpp | 4 +- src/Utils/Utils.hpp | 2 +- 12 files changed, 255 insertions(+), 45 deletions(-) diff --git a/src/Components/Modules/AssetInterfaces/IXModel.cpp b/src/Components/Modules/AssetInterfaces/IXModel.cpp index bc929012..c170acf9 100644 --- a/src/Components/Modules/AssetInterfaces/IXModel.cpp +++ b/src/Components/Modules/AssetInterfaces/IXModel.cpp @@ -145,7 +145,7 @@ namespace Assets { if (asset->lods[i].surfaces) { - // Requiring this asset is not possible, as it has to be loaded after the model + // Requiring this asset is not possible, as it has to be loaded as part of the model //dest->lods[i].surfaces = builder->RequireAsset(Game::XAssetType::ASSET_TYPE_XMODELSURFS, asset->lods[i].surfaces->name).surfaces; IXModelSurfs().Save({ asset->lods[i].surfaces }, builder); diff --git a/src/Components/Modules/Auth.cpp b/src/Components/Modules/Auth.cpp index a1ef2b97..2d80f3cf 100644 --- a/src/Components/Modules/Auth.cpp +++ b/src/Components/Modules/Auth.cpp @@ -4,6 +4,9 @@ namespace Components { Auth::AuthInfo Auth::ClientAuthInfo[18]; + Utils::Cryptography::Token Auth::GuidToken; + Utils::Cryptography::ECDSA::Key Auth::GuidKey; + void Auth::Frame() { for (int i = 0; i < *Game::svs_numclients; i++) @@ -79,8 +82,98 @@ namespace Components } } + unsigned int Auth::GetKeyHash() + { + Auth::LoadKey(); + std::string key = Auth::GuidKey.GetPublicKey(); + return (Utils::OneAtATime(key.data(), key.size())); + } + + void Auth::StoreKey() + { + Proto::Auth::Certificate cert; + cert.set_token(Auth::GuidToken.ToString()); + cert.set_privatekey(Auth::GuidKey.Export(PK_PRIVATE)); + + Utils::WriteFile("players/guid.dat", cert.SerializeAsString()); + } + + void Auth::LoadKey(bool force) + { + if (!force && Auth::GuidKey.IsValid()) return; + + Proto::Auth::Certificate cert; + if (cert.ParseFromString(::Utils::ReadFile("players/guid.dat"))) + { + Auth::GuidKey.Import(cert.privatekey(), PK_PRIVATE); + Auth::GuidToken = cert.token(); + } + else + { + Auth::GuidKey.Free(); + } + + if (!Auth::GuidKey.IsValid()) + { + Auth::GuidToken.Clear(); + Auth::GuidKey = Utils::Cryptography::ECDSA::GenerateKey(512); + Auth::StoreKey(); + } + } + + uint32_t Auth::GetSecurityLevel() + { + return Auth::GetZeroBits(Auth::GuidToken, Auth::GuidKey.GetPublicKey()); + } + + uint32_t Auth::GetZeroBits(Utils::Cryptography::Token token, std::string publicKey) + { + std::string message = publicKey + token.ToString(); + std::string hash = Utils::Cryptography::SHA512::Compute(message, false); + + uint32_t bits = 0; + + for (unsigned int i = 0; i < hash.size(); ++i) + { + if (hash[i] == '\0') + { + bits += 8; + continue; + } + + uint8_t value = static_cast(hash[i]); + for (int j = 7; j >= 0; --j) + { + if ((value >> j) & 1) + { + return bits; + } + + bits++; + } + } + + return bits; + } + + void Auth::IncrementToken(Utils::Cryptography::Token& token, std::string publicKey, uint32_t zeroBits) + { + if (zeroBits > 512) return; // Not possible, due to SHA512 + + Utils::Cryptography::Token tempToken(token); + + while (Auth::GetZeroBits(tempToken, publicKey) < zeroBits) + { + ++tempToken; + } + + token = tempToken; + } + Auth::Auth() { + Auth::LoadKey(true); + // Only clients receive the auth request if (!Dedicated::IsDedicated()) { @@ -93,11 +186,12 @@ namespace Components // Ensure our certificate is loaded Steam::SteamUser()->GetSteamID(); - if (!Steam::User::GuidKey.IsValid()) return; + if (!Auth::GuidKey.IsValid()) return; Proto::Auth::Response response; - response.set_publickey(Steam::User::GuidKey.GetPublicKey()); - response.set_signature(Utils::Cryptography::ECDSA::SignMessage(Steam::User::GuidKey, data)); + response.set_token(Auth::GuidToken.ToString()); + response.set_publickey(Auth::GuidKey.GetPublicKey()); + response.set_signature(Utils::Cryptography::ECDSA::SignMessage(Auth::GuidKey, data)); Network::SendCommand(address, "xuidAuthResp", response.SerializeAsString()); }); @@ -118,7 +212,7 @@ namespace Components unsigned int id = static_cast(~0x110000100000000 & client->steamid); // Check if response is valid - if (!response.ParseFromString(data) || response.signature().empty() || response.publickey().empty()) + if (!response.ParseFromString(data) || response.signature().empty() || response.publickey().empty() || response.token().empty()) { info->state = Auth::STATE_INVALID; Game::SV_KickClientError(client, "XUID authentication response was invalid!"); @@ -138,8 +232,19 @@ namespace Components if (Utils::Cryptography::ECDSA::VerifyMessage(info->publicKey, info->challenge, response.signature())) { - info->state = Auth::STATE_VALID; - Logger::Print("Verified XUID %llX from %s\n", client->steamid, address.GetString()); + uint32_t ourLevel = static_cast(Dvar::Var("sv_securityLevel").Get()); + uint32_t userLevel = Auth::GetZeroBits(response.token(), response.publickey()); + + if (userLevel >= ourLevel) + { + info->state = Auth::STATE_VALID; + Logger::Print("Verified XUID %llX from %s\n", client->steamid, address.GetString()); + } + else + { + info->state = Auth::STATE_INVALID; + Game::SV_KickClientError(client, Utils::VA("Your security level (%d) does not match the server's security level (%d)", userLevel, ourLevel)); + } } else { @@ -157,17 +262,48 @@ namespace Components Dedicated::OnFrame(Auth::Frame); Renderer::OnFrame(Auth::Frame); + // Register dvar + Dvar::Register("sv_securityLevel", 20, 0, 512, Game::dvar_flag::DVAR_FLAG_SERVERINFO, "Security level for GUID certificates (POW)"); + // Install registration hook Utils::Hook(0x478A12, Auth::RegisterClientStub, HOOK_JUMP).Install()->Quick(); + + // Guid command + Command::Add("guid", [] (Command::Params params) + { + Logger::Print("Your guid: %llX\n", Steam::SteamUser()->GetSteamID().Bits); + }); + + Command::Add("securityLevel", [] (Command::Params params) + { + if (params.Length() < 2) + { + Logger::Print("Your current security level is %d\n", Auth::GetZeroBits(Auth::GuidToken, Auth::GuidKey.GetPublicKey())); + } + else + { + uint32_t level = static_cast(atoi(params[1])); + Logger::Print("Incrementing security level from %d to %d...\n", Auth::GetSecurityLevel(), level); + Auth::IncrementToken(Auth::GuidToken, Auth::GuidKey.GetPublicKey(), level); + Logger::Print("Your new security level is %d\n", Auth::GetSecurityLevel()); + } + }); } Auth::~Auth() { - + Auth::StoreKey(); } bool Auth::UnitTest() { +// Utils::Cryptography::Token t; +// auto _key = Utils::Cryptography::ECDSA::GenerateKey(512); +// Auth::IncrementToken(t, _key.GetPublicKey(), 22); +// +// Utils::WriteFile("pubKey.dat", _key.GetPublicKey()); +// Utils::WriteFile("token.dat", t.ToString()); + /* Utils::Cryptography::Token t; for (int i = 0; i < 1'000'000; ++i, ++t) @@ -175,6 +311,40 @@ namespace Components printf("%s\n", Utils::DumpHex(t.ToString()).data()); } */ +// auto testSecurityLevel = [](size_t level, std::string key) +// { +// auto startTime = std::chrono::high_resolution_clock::now(); +// Utils::Cryptography::Token t; +// Auth::IncrementToken(t, key, level); +// return std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - startTime).count(); +// }; +// +// for (int j = 10; j < 30; ++j) +// { +// printf("\nTesting security level %i:\n", j); +// +// std::vector times; +// +// for (int i = 0; i < 10; ++i) +// { +// auto key = Utils::Cryptography::ECDSA::GenerateKey(512); +// +// auto time = testSecurityLevel(j, key.GetPublicKey()); +// times.push_back(time); +// printf("\t%i: %llims\n", i, time); +// } +// +// long long average = 0; +// +// for (auto time : times) +// { +// average += time; +// } +// +// average /= times.size(); +// +// printf("\n Average: %llims\n", average); +// } return true; } diff --git a/src/Components/Modules/Auth.hpp b/src/Components/Modules/Auth.hpp index 20de3c6f..93dd1db5 100644 --- a/src/Components/Modules/Auth.hpp +++ b/src/Components/Modules/Auth.hpp @@ -8,6 +8,14 @@ namespace Components const char* GetName() { return "Auth"; }; bool UnitTest(); + static void StoreKey(); + static void LoadKey(bool force = false); + static unsigned int GetKeyHash(); + + static uint32_t GetSecurityLevel(); + static uint32_t GetZeroBits(Utils::Cryptography::Token token, std::string publicKey); + static void IncrementToken(Utils::Cryptography::Token& token, std::string publicKey, uint32_t zeroBits); + private: enum AuthState @@ -28,6 +36,9 @@ namespace Components static AuthInfo ClientAuthInfo[18]; + static Utils::Cryptography::Token GuidToken; + static Utils::Cryptography::ECDSA::Key GuidKey; + static void Frame(); static void RegisterClient(int clientNum); diff --git a/src/Components/Modules/Party.cpp b/src/Components/Modules/Party.cpp index 908b7173..f096a07a 100644 --- a/src/Components/Modules/Party.cpp +++ b/src/Components/Modules/Party.cpp @@ -284,6 +284,7 @@ namespace Components info.Set("mapname", Dvar::Var("mapname").Get()); info.Set("isPrivate", (Dvar::Var("g_password").Get().size() ? "1" : "0")); info.Set("hc", (Dvar::Var("g_hardcore").Get() ? "1" : "0")); + info.Set("securityLevel", Utils::VA("%i", Dvar::Var("sv_securityLevel").Get())); // Ensure mapname is set if (info.Get("mapname").empty()) @@ -325,11 +326,16 @@ namespace Components Party::Container.Valid = false; int matchType = atoi(info.Get("matchtype").data()); + uint32_t securityLevel = static_cast(atoi(info.Get("securityLevel").data())); if (info.Get("challenge") != Party::Container.Challenge) { Party::ConnectError("Invalid join response: Challenge mismatch."); } + else if (securityLevel > Auth::GetSecurityLevel()) + { + Party::ConnectError(Utils::VA("Your security level (%d) is lower than the server's (%d)", Auth::GetSecurityLevel(), securityLevel)); + } else if (!matchType) { Party::ConnectError("Server is not hosting a match."); diff --git a/src/Proto/auth.proto b/src/Proto/auth.proto index dc1acdb7..c9642679 100644 --- a/src/Proto/auth.proto +++ b/src/Proto/auth.proto @@ -6,6 +6,7 @@ message Response { bytes signature = 1; bytes publickey = 2; + bytes token = 3; } message Certificate diff --git a/src/STDInclude.hpp b/src/STDInclude.hpp index 51adec2a..a5e2b806 100644 --- a/src/STDInclude.hpp +++ b/src/STDInclude.hpp @@ -108,4 +108,4 @@ #define Assert_Size(x, size) static_assert(sizeof(x) == size, STRINGIZE(x) " structure has an invalid size.") // Enable unit-test flag for release builds -//#define FORCE_UNIT_TESTS +#define FORCE_UNIT_TESTS diff --git a/src/Steam/Interfaces/SteamUser.cpp b/src/Steam/Interfaces/SteamUser.cpp index c1e1c8bf..63820663 100644 --- a/src/Steam/Interfaces/SteamUser.cpp +++ b/src/Steam/Interfaces/SteamUser.cpp @@ -2,9 +2,6 @@ namespace Steam { - ::Utils::Cryptography::Token User::GuidToken; - ::Utils::Cryptography::ECDSA::Key User::GuidKey; - int User::GetHSteamUser() { return NULL; @@ -27,32 +24,9 @@ namespace Steam { subId = ~0xDED1CA7E; } - else if (Components::Singleton::IsFirstInstance()) // Hardware guid + else if (Components::Singleton::IsFirstInstance()) // ECDSA guid { - if (!User::GuidKey.IsValid()) - { - Proto::Auth::Certificate cert; - - if (cert.ParseFromString(::Utils::ReadFile("players/guid.dat"))) - { - User::GuidKey.Import(cert.privatekey(), PK_PRIVATE); - User::GuidToken = cert.token(); - } - - if (!User::GuidKey.IsValid()) - { - User::GuidToken.Clear(); - User::GuidKey = ::Utils::Cryptography::ECDSA::GenerateKey(512); - - cert.set_token(User::GuidToken.ToString()); - cert.set_privatekey(User::GuidKey.Export(PK_PRIVATE)); - - ::Utils::WriteFile("players/guid.dat", cert.SerializeAsString()); - } - } - - std::string publicKey = User::GuidKey.GetPublicKey(); - subId = ::Utils::OneAtATime(publicKey.data(), publicKey.size()); + subId = Components::Auth::GetKeyHash(); } else // Random guid { diff --git a/src/Steam/Interfaces/SteamUser.hpp b/src/Steam/Interfaces/SteamUser.hpp index 02cf487d..7d6140ce 100644 --- a/src/Steam/Interfaces/SteamUser.hpp +++ b/src/Steam/Interfaces/SteamUser.hpp @@ -20,8 +20,5 @@ namespace Steam virtual void EndAuthSession(SteamID steamID); virtual void CancelAuthTicket(unsigned int hAuthTicket); virtual unsigned int UserHasLicenseForApp(SteamID steamID, unsigned int appID); - - static ::Utils::Cryptography::Token GuidToken; - static ::Utils::Cryptography::ECDSA::Key GuidKey; }; } diff --git a/src/Utils/Cryptography.cpp b/src/Utils/Cryptography.cpp index 3f8b6dba..d7f6083c 100644 --- a/src/Utils/Cryptography.cpp +++ b/src/Utils/Cryptography.cpp @@ -6,6 +6,7 @@ namespace Utils { namespace Cryptography { + #pragma region Rand prng_state Rand::State; @@ -96,7 +97,7 @@ namespace Utils register_hash(&sha1_desc); ltc_mp = ltm_desc; - + rsa_sign_hash(reinterpret_cast(message.data()), message.size(), buffer, &length, NULL, find_prng("sprng"), find_hash("sha1"), 0, key.GetKeyPtr()); return std::string(reinterpret_cast(buffer), length); @@ -113,8 +114,46 @@ namespace Utils int result = 0; return (rsa_verify_hash(reinterpret_cast(signature.data()), signature.size(), reinterpret_cast(message.data()), message.size(), find_hash("sha1"), 0, &result, key.GetKeyPtr()) == CRYPT_OK && result != 0); } - } #pragma endregion +#pragma region SHA256 + + std::string SHA256::Compute(std::string data, bool hex) + { + uint8_t buffer[32] = { 0 }; + + hash_state state; + sha256_init(&state); + sha256_process(&state, reinterpret_cast(data.data()), data.size()); + sha256_done(&state, buffer); + + std::string hash(reinterpret_cast(buffer), sizeof(buffer)); + if (!hex) return hash; + + return Utils::DumpHex(hash, ""); + } + +#pragma endregion + +#pragma region SHA512 + + std::string SHA512::Compute(std::string data, bool hex) + { + uint8_t buffer[64] = { 0 }; + + hash_state state; + sha512_init(&state); + sha512_process(&state, reinterpret_cast(data.data()), data.size()); + sha512_done(&state, buffer); + + std::string hash(reinterpret_cast(buffer), sizeof(buffer)); + if (!hex) return hash; + + return Utils::DumpHex(hash, ""); + } + +#pragma endregion + + } } diff --git a/src/Utils/Cryptography.hpp b/src/Utils/Cryptography.hpp index 0b5c8175..24e0309b 100644 --- a/src/Utils/Cryptography.hpp +++ b/src/Utils/Cryptography.hpp @@ -221,5 +221,17 @@ namespace Utils static std::string SignMessage(Key key, std::string message); static bool VerifyMessage(Key key, std::string message, std::string signature); }; + + class SHA256 + { + public: + static std::string Compute(std::string data, bool hex = false); + }; + + class SHA512 + { + public: + static std::string Compute(std::string data, bool hex = false); + }; } } diff --git a/src/Utils/Utils.cpp b/src/Utils/Utils.cpp index b7faa52c..3af315e1 100644 --- a/src/Utils/Utils.cpp +++ b/src/Utils/Utils.cpp @@ -30,7 +30,7 @@ namespace Utils return (strstr(haystack.data(), needle.data()) == (haystack.data() + haystack.size() - needle.size())); } - std::string DumpHex(std::string data) + std::string DumpHex(std::string data, std::string separator) { std::string result; @@ -38,7 +38,7 @@ namespace Utils { if (i > 0) { - result.append(" "); + result.append(separator); } result.append(Utils::VA("%02X", data[i] & 0xFF)); diff --git a/src/Utils/Utils.hpp b/src/Utils/Utils.hpp index 1967358d..4baa4f39 100644 --- a/src/Utils/Utils.hpp +++ b/src/Utils/Utils.hpp @@ -20,7 +20,7 @@ namespace Utils void WriteFile(std::string file, std::string data); std::string ReadFile(std::string file); - std::string DumpHex(std::string data); + std::string DumpHex(std::string data, std::string separator = " "); bool MemIsSet(void* mem, char chr, size_t length);