BareGit
#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