#include "identity.hpp"
#include <array>
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <sys/types.h>
#include <string>
#include <string_view>
#include <sys/random.h>
#include <openssl/crypto.h>
#include <openssl/evp.h>
namespace nethack_mcp
{
namespace
{
constexpr char BASE64URL_ALPHABET[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
std::string encodeBase64Url(const unsigned char* bytes, std::size_t size)
{
std::string encoded;
encoded.reserve((size * 4 + 2) / 3);
for(std::size_t index = 0; index < size; index += 3)
{
const std::uint32_t first = bytes[index];
const std::uint32_t second = index + 1 < size ? bytes[index + 1] : 0;
const std::uint32_t third = index + 2 < size ? bytes[index + 2] : 0;
const std::uint32_t block = (first << 16) | (second << 8) | third;
encoded.push_back(BASE64URL_ALPHABET[(block >> 18) & 0x3f]);
encoded.push_back(BASE64URL_ALPHABET[(block >> 12) & 0x3f]);
if(index + 1 < size)
{
encoded.push_back(BASE64URL_ALPHABET[(block >> 6) & 0x3f]);
}
if(index + 2 < size)
{
encoded.push_back(BASE64URL_ALPHABET[block & 0x3f]);
}
}
return encoded;
}
bool lowerHex(char value)
{
return (value >= '0' && value <= '9')
|| (value >= 'a' && value <= 'f');
}
} // namespace
std::string makeGameId()
{
std::array<unsigned char, 16> bytes{};
if(!secureRandom(bytes.data(), bytes.size()))
{
return {};
}
const auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
const std::uint64_t timestamp = static_cast<std::uint64_t>(now);
for(int index = 5; index >= 0; --index)
{
bytes[static_cast<std::size_t>(index)] =
static_cast<unsigned char>(timestamp >> (8 * (5 - index)));
}
bytes[6] = static_cast<unsigned char>((bytes[6] & 0x0f) | 0x70);
bytes[8] = static_cast<unsigned char>((bytes[8] & 0x3f) | 0x80);
constexpr char HEX[] = "0123456789abcdef";
std::string game_id;
game_id.reserve(36);
for(std::size_t index = 0; index < bytes.size(); ++index)
{
if(index == 4 || index == 6 || index == 8 || index == 10)
{
game_id.push_back('-');
}
game_id.push_back(HEX[bytes[index] >> 4]);
game_id.push_back(HEX[bytes[index] & 0x0f]);
}
return game_id;
}
bool validGameId(std::string_view game_id)
{
if(game_id.size() != 36 || game_id[8] != '-' || game_id[13] != '-'
|| game_id[18] != '-' || game_id[23] != '-')
{
return false;
}
for(std::size_t index = 0; index < game_id.size(); ++index)
{
if(index == 8 || index == 13 || index == 18 || index == 23)
{
continue;
}
if(!lowerHex(game_id[index]))
{
return false;
}
}
return game_id[14] == '7'
&& (game_id[19] == '8' || game_id[19] == '9'
|| game_id[19] == 'a' || game_id[19] == 'b');
}
std::string makeControlToken()
{
std::array<unsigned char, 32> bytes{};
if(!secureRandom(bytes.data(), bytes.size()))
{
return {};
}
return encodeBase64Url(bytes.data(), bytes.size());
}
std::array<unsigned char, 32> hashControlToken(
const std::array<unsigned char, 32>& salt, std::string_view token)
{
std::array<unsigned char, 32> digest{};
EVP_MD_CTX* context = EVP_MD_CTX_new();
if(context == nullptr)
{
return digest;
}
unsigned int digest_size = 0;
const bool success = EVP_DigestInit_ex(context, EVP_sha256(), nullptr) == 1
&& EVP_DigestUpdate(context, salt.data(), salt.size()) == 1
&& EVP_DigestUpdate(context, token.data(), token.size()) == 1
&& EVP_DigestFinal_ex(context, digest.data(), &digest_size) == 1
&& digest_size == digest.size();
EVP_MD_CTX_free(context);
if(!success)
{
digest.fill(0);
}
return digest;
}
bool secureDigestEqual(const std::array<unsigned char, 32>& left,
const std::array<unsigned char, 32>& right)
{
return CRYPTO_memcmp(left.data(), right.data(), left.size()) == 0;
}
bool secureRandom(void* destination, std::size_t size)
{
auto* output = static_cast<unsigned char*>(destination);
std::size_t offset = 0;
while(offset < size)
{
const ssize_t count = ::getrandom(output + offset, size - offset, 0);
if(count < 0)
{
if(errno == EINTR)
{
continue;
}
return false;
}
offset += static_cast<std::size_t>(count);
}
return true;
}
} // namespace nethack_mcp