POW (aka. hashcash) stuff.

This commit is contained in:
momo5502 2016-02-22 23:35:53 +01:00
parent 00b7e3f920
commit bcdc555c4a
12 changed files with 255 additions and 45 deletions

View File

@ -145,7 +145,7 @@ namespace Assets
{ {
if (asset->lods[i].surfaces) 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; //dest->lods[i].surfaces = builder->RequireAsset(Game::XAssetType::ASSET_TYPE_XMODELSURFS, asset->lods[i].surfaces->name).surfaces;
IXModelSurfs().Save({ asset->lods[i].surfaces }, builder); IXModelSurfs().Save({ asset->lods[i].surfaces }, builder);

View File

@ -4,6 +4,9 @@ namespace Components
{ {
Auth::AuthInfo Auth::ClientAuthInfo[18]; Auth::AuthInfo Auth::ClientAuthInfo[18];
Utils::Cryptography::Token Auth::GuidToken;
Utils::Cryptography::ECDSA::Key Auth::GuidKey;
void Auth::Frame() void Auth::Frame()
{ {
for (int i = 0; i < *Game::svs_numclients; i++) 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<uint8_t>(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::Auth()
{ {
Auth::LoadKey(true);
// Only clients receive the auth request // Only clients receive the auth request
if (!Dedicated::IsDedicated()) if (!Dedicated::IsDedicated())
{ {
@ -93,11 +186,12 @@ namespace Components
// Ensure our certificate is loaded // Ensure our certificate is loaded
Steam::SteamUser()->GetSteamID(); Steam::SteamUser()->GetSteamID();
if (!Steam::User::GuidKey.IsValid()) return; if (!Auth::GuidKey.IsValid()) return;
Proto::Auth::Response response; Proto::Auth::Response response;
response.set_publickey(Steam::User::GuidKey.GetPublicKey()); response.set_token(Auth::GuidToken.ToString());
response.set_signature(Utils::Cryptography::ECDSA::SignMessage(Steam::User::GuidKey, data)); response.set_publickey(Auth::GuidKey.GetPublicKey());
response.set_signature(Utils::Cryptography::ECDSA::SignMessage(Auth::GuidKey, data));
Network::SendCommand(address, "xuidAuthResp", response.SerializeAsString()); Network::SendCommand(address, "xuidAuthResp", response.SerializeAsString());
}); });
@ -118,7 +212,7 @@ namespace Components
unsigned int id = static_cast<unsigned int>(~0x110000100000000 & client->steamid); unsigned int id = static_cast<unsigned int>(~0x110000100000000 & client->steamid);
// Check if response is valid // 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; info->state = Auth::STATE_INVALID;
Game::SV_KickClientError(client, "XUID authentication response was invalid!"); Game::SV_KickClientError(client, "XUID authentication response was invalid!");
@ -137,11 +231,22 @@ namespace Components
info->publicKey.Set(response.publickey()); info->publicKey.Set(response.publickey());
if (Utils::Cryptography::ECDSA::VerifyMessage(info->publicKey, info->challenge, response.signature())) if (Utils::Cryptography::ECDSA::VerifyMessage(info->publicKey, info->challenge, response.signature()))
{
uint32_t ourLevel = static_cast<uint32_t>(Dvar::Var("sv_securityLevel").Get<int>());
uint32_t userLevel = Auth::GetZeroBits(response.token(), response.publickey());
if (userLevel >= ourLevel)
{ {
info->state = Auth::STATE_VALID; info->state = Auth::STATE_VALID;
Logger::Print("Verified XUID %llX from %s\n", client->steamid, address.GetString()); Logger::Print("Verified XUID %llX from %s\n", client->steamid, address.GetString());
} }
else 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
{ {
info->state = Auth::STATE_INVALID; info->state = Auth::STATE_INVALID;
Game::SV_KickClientError(client, "Challenge signature was invalid!"); Game::SV_KickClientError(client, "Challenge signature was invalid!");
@ -157,17 +262,48 @@ namespace Components
Dedicated::OnFrame(Auth::Frame); Dedicated::OnFrame(Auth::Frame);
Renderer::OnFrame(Auth::Frame); Renderer::OnFrame(Auth::Frame);
// Register dvar
Dvar::Register<int>("sv_securityLevel", 20, 0, 512, Game::dvar_flag::DVAR_FLAG_SERVERINFO, "Security level for GUID certificates (POW)");
// Install registration hook // Install registration hook
Utils::Hook(0x478A12, Auth::RegisterClientStub, HOOK_JUMP).Install()->Quick(); 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<uint32_t>(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::~Auth()
{ {
Auth::StoreKey();
} }
bool Auth::UnitTest() 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; Utils::Cryptography::Token t;
for (int i = 0; i < 1'000'000; ++i, ++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()); 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::milliseconds>(std::chrono::high_resolution_clock::now() - startTime).count();
// };
//
// for (int j = 10; j < 30; ++j)
// {
// printf("\nTesting security level %i:\n", j);
//
// std::vector<long long> 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; return true;
} }

View File

@ -8,6 +8,14 @@ namespace Components
const char* GetName() { return "Auth"; }; const char* GetName() { return "Auth"; };
bool UnitTest(); 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: private:
enum AuthState enum AuthState
@ -28,6 +36,9 @@ namespace Components
static AuthInfo ClientAuthInfo[18]; static AuthInfo ClientAuthInfo[18];
static Utils::Cryptography::Token GuidToken;
static Utils::Cryptography::ECDSA::Key GuidKey;
static void Frame(); static void Frame();
static void RegisterClient(int clientNum); static void RegisterClient(int clientNum);

View File

@ -284,6 +284,7 @@ namespace Components
info.Set("mapname", Dvar::Var("mapname").Get<const char*>()); info.Set("mapname", Dvar::Var("mapname").Get<const char*>());
info.Set("isPrivate", (Dvar::Var("g_password").Get<std::string>().size() ? "1" : "0")); info.Set("isPrivate", (Dvar::Var("g_password").Get<std::string>().size() ? "1" : "0"));
info.Set("hc", (Dvar::Var("g_hardcore").Get<bool>() ? "1" : "0")); info.Set("hc", (Dvar::Var("g_hardcore").Get<bool>() ? "1" : "0"));
info.Set("securityLevel", Utils::VA("%i", Dvar::Var("sv_securityLevel").Get<int>()));
// Ensure mapname is set // Ensure mapname is set
if (info.Get("mapname").empty()) if (info.Get("mapname").empty())
@ -325,11 +326,16 @@ namespace Components
Party::Container.Valid = false; Party::Container.Valid = false;
int matchType = atoi(info.Get("matchtype").data()); int matchType = atoi(info.Get("matchtype").data());
uint32_t securityLevel = static_cast<uint32_t>(atoi(info.Get("securityLevel").data()));
if (info.Get("challenge") != Party::Container.Challenge) if (info.Get("challenge") != Party::Container.Challenge)
{ {
Party::ConnectError("Invalid join response: Challenge mismatch."); 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) else if (!matchType)
{ {
Party::ConnectError("Server is not hosting a match."); Party::ConnectError("Server is not hosting a match.");

View File

@ -6,6 +6,7 @@ message Response
{ {
bytes signature = 1; bytes signature = 1;
bytes publickey = 2; bytes publickey = 2;
bytes token = 3;
} }
message Certificate message Certificate

View File

@ -108,4 +108,4 @@
#define Assert_Size(x, size) static_assert(sizeof(x) == size, STRINGIZE(x) " structure has an invalid size.") #define Assert_Size(x, size) static_assert(sizeof(x) == size, STRINGIZE(x) " structure has an invalid size.")
// Enable unit-test flag for release builds // Enable unit-test flag for release builds
//#define FORCE_UNIT_TESTS #define FORCE_UNIT_TESTS

View File

@ -2,9 +2,6 @@
namespace Steam namespace Steam
{ {
::Utils::Cryptography::Token User::GuidToken;
::Utils::Cryptography::ECDSA::Key User::GuidKey;
int User::GetHSteamUser() int User::GetHSteamUser()
{ {
return NULL; return NULL;
@ -27,32 +24,9 @@ namespace Steam
{ {
subId = ~0xDED1CA7E; subId = ~0xDED1CA7E;
} }
else if (Components::Singleton::IsFirstInstance()) // Hardware guid else if (Components::Singleton::IsFirstInstance()) // ECDSA guid
{ {
if (!User::GuidKey.IsValid()) subId = Components::Auth::GetKeyHash();
{
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());
} }
else // Random guid else // Random guid
{ {

View File

@ -20,8 +20,5 @@ namespace Steam
virtual void EndAuthSession(SteamID steamID); virtual void EndAuthSession(SteamID steamID);
virtual void CancelAuthTicket(unsigned int hAuthTicket); virtual void CancelAuthTicket(unsigned int hAuthTicket);
virtual unsigned int UserHasLicenseForApp(SteamID steamID, unsigned int appID); virtual unsigned int UserHasLicenseForApp(SteamID steamID, unsigned int appID);
static ::Utils::Cryptography::Token GuidToken;
static ::Utils::Cryptography::ECDSA::Key GuidKey;
}; };
} }

View File

@ -6,6 +6,7 @@ namespace Utils
{ {
namespace Cryptography namespace Cryptography
{ {
#pragma region Rand #pragma region Rand
prng_state Rand::State; prng_state Rand::State;
@ -113,8 +114,46 @@ namespace Utils
int result = 0; int result = 0;
return (rsa_verify_hash(reinterpret_cast<const uint8_t*>(signature.data()), signature.size(), reinterpret_cast<const uint8_t*>(message.data()), message.size(), find_hash("sha1"), 0, &result, key.GetKeyPtr()) == CRYPT_OK && result != 0); return (rsa_verify_hash(reinterpret_cast<const uint8_t*>(signature.data()), signature.size(), reinterpret_cast<const uint8_t*>(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<const uint8_t*>(data.data()), data.size());
sha256_done(&state, buffer);
std::string hash(reinterpret_cast<char*>(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<const uint8_t*>(data.data()), data.size());
sha512_done(&state, buffer);
std::string hash(reinterpret_cast<char*>(buffer), sizeof(buffer));
if (!hex) return hash;
return Utils::DumpHex(hash, "");
} }
#pragma endregion #pragma endregion
} }
}

View File

@ -221,5 +221,17 @@ namespace Utils
static std::string SignMessage(Key key, std::string message); static std::string SignMessage(Key key, std::string message);
static bool VerifyMessage(Key key, std::string message, std::string signature); 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);
};
} }
} }

View File

@ -30,7 +30,7 @@ namespace Utils
return (strstr(haystack.data(), needle.data()) == (haystack.data() + haystack.size() - needle.size())); 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; std::string result;
@ -38,7 +38,7 @@ namespace Utils
{ {
if (i > 0) if (i > 0)
{ {
result.append(" "); result.append(separator);
} }
result.append(Utils::VA("%02X", data[i] & 0xFF)); result.append(Utils::VA("%02X", data[i] & 0xFF));

View File

@ -20,7 +20,7 @@ namespace Utils
void WriteFile(std::string file, std::string data); void WriteFile(std::string file, std::string data);
std::string ReadFile(std::string file); 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); bool MemIsSet(void* mem, char chr, size_t length);