POW (aka. hashcash) stuff.
This commit is contained in:
parent
00b7e3f920
commit
bcdc555c4a
@ -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);
|
||||
|
@ -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<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::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<unsigned int>(~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<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;
|
||||
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<int>("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<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::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::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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -284,6 +284,7 @@ namespace Components
|
||||
info.Set("mapname", Dvar::Var("mapname").Get<const char*>());
|
||||
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("securityLevel", Utils::VA("%i", Dvar::Var("sv_securityLevel").Get<int>()));
|
||||
|
||||
// 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<uint32_t>(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.");
|
||||
|
@ -6,6 +6,7 @@ message Response
|
||||
{
|
||||
bytes signature = 1;
|
||||
bytes publickey = 2;
|
||||
bytes token = 3;
|
||||
}
|
||||
|
||||
message Certificate
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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<const uint8_t*>(message.data()), message.size(), buffer, &length, NULL, find_prng("sprng"), find_hash("sha1"), 0, key.GetKeyPtr());
|
||||
|
||||
return std::string(reinterpret_cast<char*>(buffer), length);
|
||||
@ -113,8 +114,46 @@ namespace Utils
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#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
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user