Mercurial
diff auth/auth_crypto.c @ 264:04fee26ecce0
add authenticated JRPG conversation platform
Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 07:34:12 -0700 |
| parents | |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/auth/auth_crypto.c Fri Aug 07 07:34:12 2026 -0700 @@ -0,0 +1,872 @@ +#include "auth/auth_crypto.h" + +#include <stdio.h> +#include <string.h> + +#include <openssl/crypto.h> +#include <openssl/evp.h> +#include <openssl/hmac.h> +#include <openssl/rand.h> + +#define AUTH_CRYPTO_SCRYPT_N 32768 +#define AUTH_CRYPTO_SCRYPT_R 8 +#define AUTH_CRYPTO_SCRYPT_P 1 +#define AUTH_CRYPTO_SCRYPT_MAXMEM (64ULL * 1024ULL * 1024ULL) + +#define AUTH_CRYPTO_PASSWORD_PREFIX "zenbu-scrypt$v=1$N=32768$r=8$p=1$" +#define AUTH_CRYPTO_COOKIE_VERSION "v1" +#define AUTH_CRYPTO_COOKIE_HMAC_DOMAIN "zenbu-auth-guest-cookie-v1:" +#define AUTH_CRYPTO_IP_HMAC_DOMAIN "zenbu-auth-ip-binding-v1:" +#define AUTH_CRYPTO_HMAC_INPUT_SIZE 384 + +static boolean auth__bounded_length( + const char *text, + size_t maximum, + size_t *p_length) +{ + if (!text || !p_length) + { + return FALSE; + } + + for (size_t i = 0; i <= maximum; ++i) + { + if (text[i] == '\0') + { + *p_length = i; + return TRUE; + } + } + + return FALSE; +} + +static void auth__hex_encode( + const uint8 *bytes, + size_t byte_count, + char *hex) +{ + static const char alphabet[] = "0123456789abcdef"; + + for (size_t i = 0; i < byte_count; ++i) + { + hex[i * 2] = alphabet[bytes[i] >> 4]; + hex[i * 2 + 1] = alphabet[bytes[i] & 0x0f]; + } + hex[byte_count * 2] = '\0'; +} + +static int auth__hex_value(char value) +{ + if (value >= '0' && value <= '9') + { + return value - '0'; + } + if (value >= 'a' && value <= 'f') + { + return value - 'a' + 10; + } + return -1; +} + +static boolean auth__hex_decode( + const char *hex, + size_t hex_length, + uint8 *bytes, + size_t byte_count) +{ + if (hex_length != byte_count * 2) + { + return FALSE; + } + + for (size_t i = 0; i < byte_count; ++i) + { + int high = auth__hex_value(hex[i * 2]); + int low = auth__hex_value(hex[i * 2 + 1]); + if (high < 0 || low < 0) + { + return FALSE; + } + bytes[i] = (uint8)((high << 4) | low); + } + + return TRUE; +} + +static boolean auth__secret_is_valid( + const uint8 *cookie_secret, + size_t cookie_secret_length) +{ + return cookie_secret && + cookie_secret_length >= AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES && + cookie_secret_length <= AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES; +} + +static boolean auth__uuid_is_valid(const char *uuid, size_t uuid_length) +{ + if (uuid_length != AUTH_CRYPTO_GUEST_UUID_SIZE - 1) + { + return FALSE; + } + + for (size_t i = 0; i < uuid_length; ++i) + { + if (i == 8 || i == 13 || i == 18 || i == 23) + { + if (uuid[i] != '-') + { + return FALSE; + } + } + else if (auth__hex_value(uuid[i]) < 0) + { + return FALSE; + } + } + + return TRUE; +} + +static boolean auth__parse_uint64( + const char *digits, + size_t digit_count, + uint64 *p_value) +{ + if (!digits || !p_value || digit_count == 0 || digit_count > 20 || + (digit_count > 1 && digits[0] == '0')) + { + return FALSE; + } + + uint64 value = 0; + uint64 maximum = (uint64)-1; + for (size_t i = 0; i < digit_count; ++i) + { + if (digits[i] < '0' || digits[i] > '9') + { + return FALSE; + } + + uint8 digit = (uint8)(digits[i] - '0'); + if (value > (maximum - digit) / 10) + { + return FALSE; + } + value = value * 10 + digit; + } + + *p_value = value; + return TRUE; +} + +static Auth_Crypto_Result auth__hmac_sha256( + const uint8 *secret, + size_t secret_length, + const char *domain, + const char *value, + size_t value_length, + uint8 digest[32]) +{ + size_t domain_length = strlen(domain); + if (domain_length + value_length > AUTH_CRYPTO_HMAC_INPUT_SIZE) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + + uint8 input[AUTH_CRYPTO_HMAC_INPUT_SIZE]; + memcpy(input, domain, domain_length); + memcpy(input + domain_length, value, value_length); + + unsigned int digest_length = 0; + uint8 *result = HMAC( + EVP_sha256(), + secret, + (int)secret_length, + input, + domain_length + value_length, + digest, + &digest_length); + OPENSSL_cleanse(input, sizeof(input)); + + if (!result || digest_length != 32) + { + OPENSSL_cleanse(digest, 32); + return AUTH_CRYPTO_OPERATION_FAILED; + } + + return AUTH_CRYPTO_OK; +} + +static boolean auth__split_cookie( + const char *cookie, + size_t cookie_length, + const char *segments[5], + size_t segment_lengths[5]) +{ + size_t segment_start = 0; + size_t segment_count = 0; + + for (size_t i = 0; i <= cookie_length; ++i) + { + if (i == cookie_length || cookie[i] == '.') + { + if (segment_count >= 5 || i == segment_start) + { + return FALSE; + } + segments[segment_count] = cookie + segment_start; + segment_lengths[segment_count] = i - segment_start; + ++segment_count; + segment_start = i + 1; + } + } + + return segment_count == 5; +} + +static void auth__base64url_32( + const uint8 bytes[AUTH_CRYPTO_TOKEN_BYTES], + char token[AUTH_CRYPTO_TOKEN_SIZE]) +{ + static const char alphabet[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + size_t input_index = 0; + size_t output_index = 0; + + while (input_index + 3 <= AUTH_CRYPTO_TOKEN_BYTES) + { + uint32 value = ((uint32)bytes[input_index] << 16) | + ((uint32)bytes[input_index + 1] << 8) | + bytes[input_index + 2]; + token[output_index++] = alphabet[(value >> 18) & 63]; + token[output_index++] = alphabet[(value >> 12) & 63]; + token[output_index++] = alphabet[(value >> 6) & 63]; + token[output_index++] = alphabet[value & 63]; + input_index += 3; + } + + uint32 remainder = ((uint32)bytes[input_index] << 8) | + bytes[input_index + 1]; + token[output_index++] = alphabet[(remainder >> 10) & 63]; + token[output_index++] = alphabet[(remainder >> 4) & 63]; + token[output_index++] = alphabet[(remainder << 2) & 63]; + token[output_index] = '\0'; +} + +Auth_Crypto_Result Auth_Crypto_Password_Hash( + const char *password, + char *encoded_hash, + size_t encoded_hash_capacity) +{ + if (!password || !encoded_hash) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + if (encoded_hash_capacity < AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE) + { + return AUTH_CRYPTO_BUFFER_TOO_SMALL; + } + encoded_hash[0] = '\0'; + + size_t password_length = 0; + if (!auth__bounded_length( + password, AUTH_CRYPTO_PASSWORD_MAX_BYTES, &password_length)) + { + return AUTH_CRYPTO_PASSWORD_TOO_LONG; + } + + uint8 salt[AUTH_CRYPTO_PASSWORD_SALT_BYTES]; + uint8 hash[AUTH_CRYPTO_PASSWORD_HASH_BYTES]; + if (RAND_bytes(salt, sizeof(salt)) != 1) + { + OPENSSL_cleanse(salt, sizeof(salt)); + return AUTH_CRYPTO_RANDOM_FAILED; + } + + if (EVP_PBE_scrypt( + password, + password_length, + salt, + sizeof(salt), + AUTH_CRYPTO_SCRYPT_N, + AUTH_CRYPTO_SCRYPT_R, + AUTH_CRYPTO_SCRYPT_P, + AUTH_CRYPTO_SCRYPT_MAXMEM, + hash, + sizeof(hash)) != 1) + { + OPENSSL_cleanse(salt, sizeof(salt)); + OPENSSL_cleanse(hash, sizeof(hash)); + return AUTH_CRYPTO_OPERATION_FAILED; + } + + char salt_hex[AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2 + 1]; + char hash_hex[AUTH_CRYPTO_PASSWORD_HASH_BYTES * 2 + 1]; + auth__hex_encode(salt, sizeof(salt), salt_hex); + auth__hex_encode(hash, sizeof(hash), hash_hex); + + int written = snprintf( + encoded_hash, + encoded_hash_capacity, + "%s%s$%s", + AUTH_CRYPTO_PASSWORD_PREFIX, + salt_hex, + hash_hex); + + OPENSSL_cleanse(salt, sizeof(salt)); + OPENSSL_cleanse(hash, sizeof(hash)); + OPENSSL_cleanse(hash_hex, sizeof(hash_hex)); + + if (written < 0 || (size_t)written >= encoded_hash_capacity) + { + encoded_hash[0] = '\0'; + return AUTH_CRYPTO_BUFFER_TOO_SMALL; + } + + return AUTH_CRYPTO_OK; +} + +Auth_Crypto_Result Auth_Crypto_Password_Verify( + const char *password, + const char *encoded_hash) +{ + if (!password || !encoded_hash) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + + size_t password_length = 0; + if (!auth__bounded_length( + password, AUTH_CRYPTO_PASSWORD_MAX_BYTES, &password_length)) + { + return AUTH_CRYPTO_PASSWORD_TOO_LONG; + } + + size_t encoded_length = 0; + if (!auth__bounded_length( + encoded_hash, + AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE - 1, + &encoded_length)) + { + return AUTH_CRYPTO_MALFORMED; + } + + size_t prefix_length = sizeof(AUTH_CRYPTO_PASSWORD_PREFIX) - 1; + size_t expected_length = prefix_length + + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2 + 1 + + AUTH_CRYPTO_PASSWORD_HASH_BYTES * 2; + if (encoded_length != expected_length || + memcmp(encoded_hash, AUTH_CRYPTO_PASSWORD_PREFIX, prefix_length) != 0 || + encoded_hash[prefix_length + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2] != '$') + { + return AUTH_CRYPTO_MALFORMED; + } + + uint8 salt[AUTH_CRYPTO_PASSWORD_SALT_BYTES]; + uint8 expected_hash[AUTH_CRYPTO_PASSWORD_HASH_BYTES]; + uint8 calculated_hash[AUTH_CRYPTO_PASSWORD_HASH_BYTES]; + const char *salt_hex = encoded_hash + prefix_length; + const char *hash_hex = + salt_hex + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2 + 1; + + if (!auth__hex_decode( + salt_hex, + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2, + salt, + sizeof(salt)) || + !auth__hex_decode( + hash_hex, + AUTH_CRYPTO_PASSWORD_HASH_BYTES * 2, + expected_hash, + sizeof(expected_hash))) + { + return AUTH_CRYPTO_MALFORMED; + } + + if (EVP_PBE_scrypt( + password, + password_length, + salt, + sizeof(salt), + AUTH_CRYPTO_SCRYPT_N, + AUTH_CRYPTO_SCRYPT_R, + AUTH_CRYPTO_SCRYPT_P, + AUTH_CRYPTO_SCRYPT_MAXMEM, + calculated_hash, + sizeof(calculated_hash)) != 1) + { + OPENSSL_cleanse(salt, sizeof(salt)); + OPENSSL_cleanse(expected_hash, sizeof(expected_hash)); + OPENSSL_cleanse(calculated_hash, sizeof(calculated_hash)); + return AUTH_CRYPTO_OPERATION_FAILED; + } + + int comparison = CRYPTO_memcmp( + calculated_hash, expected_hash, sizeof(calculated_hash)); + OPENSSL_cleanse(salt, sizeof(salt)); + OPENSSL_cleanse(expected_hash, sizeof(expected_hash)); + OPENSSL_cleanse(calculated_hash, sizeof(calculated_hash)); + + return comparison == 0 + ? AUTH_CRYPTO_OK + : AUTH_CRYPTO_AUTHENTICATION_FAILED; +} + +Auth_Crypto_Result Auth_Crypto_Password_Hash_Validate(const char *encoded_hash) +{ + if (!encoded_hash) + return AUTH_CRYPTO_INVALID_ARGUMENT; + + size_t encoded_length = 0; + if (!auth__bounded_length( + encoded_hash, + AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE - 1, + &encoded_length)) + return AUTH_CRYPTO_MALFORMED; + + size_t prefix_length = sizeof(AUTH_CRYPTO_PASSWORD_PREFIX) - 1; + size_t expected_length = prefix_length + + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2 + 1 + + AUTH_CRYPTO_PASSWORD_HASH_BYTES * 2; + if (encoded_length != expected_length || + memcmp(encoded_hash, AUTH_CRYPTO_PASSWORD_PREFIX, prefix_length) != 0 || + encoded_hash[prefix_length + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2] != '$') + return AUTH_CRYPTO_MALFORMED; + + /* Validate hex characters in salt and hash fields. */ + const char *salt_hex = encoded_hash + prefix_length; + const char *hash_hex = salt_hex + AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2 + 1; + for (size_t i = 0; i < AUTH_CRYPTO_PASSWORD_SALT_BYTES * 2; i++) + if (auth__hex_value(salt_hex[i]) < 0) + return AUTH_CRYPTO_MALFORMED; + for (size_t i = 0; i < AUTH_CRYPTO_PASSWORD_HASH_BYTES * 2; i++) + if (auth__hex_value(hash_hex[i]) < 0) + return AUTH_CRYPTO_MALFORMED; + + return AUTH_CRYPTO_OK; +} + +Auth_Crypto_Result Auth_Crypto_Token_Generate( + char *token, + size_t token_capacity) +{ + if (!token) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + if (token_capacity < AUTH_CRYPTO_TOKEN_SIZE) + { + return AUTH_CRYPTO_BUFFER_TOO_SMALL; + } + token[0] = '\0'; + + uint8 random_bytes[AUTH_CRYPTO_TOKEN_BYTES]; + if (RAND_bytes(random_bytes, sizeof(random_bytes)) != 1) + { + OPENSSL_cleanse(random_bytes, sizeof(random_bytes)); + return AUTH_CRYPTO_RANDOM_FAILED; + } + + auth__base64url_32(random_bytes, token); + OPENSSL_cleanse(random_bytes, sizeof(random_bytes)); + return AUTH_CRYPTO_OK; +} + +Auth_Crypto_Result Auth_Crypto_Token_Digest( + const char *token, + char *digest_hex, + size_t digest_hex_capacity) +{ + if (!token || !digest_hex) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + if (digest_hex_capacity < AUTH_CRYPTO_TOKEN_DIGEST_SIZE) + { + return AUTH_CRYPTO_BUFFER_TOO_SMALL; + } + digest_hex[0] = '\0'; + + size_t token_length = 0; + if (!auth__bounded_length(token, AUTH_CRYPTO_TOKEN_SIZE - 1, &token_length) || + token_length != AUTH_CRYPTO_TOKEN_SIZE - 1) + { + return AUTH_CRYPTO_MALFORMED; + } + for (size_t i = 0; i < token_length; ++i) + { + char value = token[i]; + if (!((value >= 'A' && value <= 'Z') || + (value >= 'a' && value <= 'z') || + (value >= '0' && value <= '9') || + value == '-' || value == '_')) + { + return AUTH_CRYPTO_MALFORMED; + } + } + + uint8 digest[AUTH_CRYPTO_TOKEN_DIGEST_BYTES]; + unsigned int digest_length = 0; + if (EVP_Digest( + token, + token_length, + digest, + &digest_length, + EVP_sha256(), + NULL) != 1 || + digest_length != AUTH_CRYPTO_TOKEN_DIGEST_BYTES) + { + OPENSSL_cleanse(digest, sizeof(digest)); + return AUTH_CRYPTO_OPERATION_FAILED; + } + + auth__hex_encode(digest, sizeof(digest), digest_hex); + OPENSSL_cleanse(digest, sizeof(digest)); + return AUTH_CRYPTO_OK; +} + +Auth_Crypto_Result Auth_Crypto_IP_Binding_Digest( + const uint8 *cookie_secret, + size_t cookie_secret_length, + const char *canonical_peer_ip, + char *digest_hex, + size_t digest_hex_capacity) +{ + if (!auth__secret_is_valid(cookie_secret, cookie_secret_length) || + !canonical_peer_ip || !digest_hex) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + if (digest_hex_capacity < AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE) + { + return AUTH_CRYPTO_BUFFER_TOO_SMALL; + } + digest_hex[0] = '\0'; + + size_t ip_length = 0; + if (!auth__bounded_length( + canonical_peer_ip, AUTH_CRYPTO_IP_MAX_BYTES, &ip_length) || + ip_length == 0) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + + uint8 digest[32]; + Auth_Crypto_Result result = auth__hmac_sha256( + cookie_secret, + cookie_secret_length, + AUTH_CRYPTO_IP_HMAC_DOMAIN, + canonical_peer_ip, + ip_length, + digest); + if (result != AUTH_CRYPTO_OK) + { + return result; + } + + auth__hex_encode(digest, sizeof(digest), digest_hex); + OPENSSL_cleanse(digest, sizeof(digest)); + return AUTH_CRYPTO_OK; +} + +Auth_Crypto_Result Auth_Crypto_Guest_Cookie_Create( + const uint8 *cookie_secret, + size_t cookie_secret_length, + const char *guest_uuid, + uint64 expiration_unix, + const char *ip_binding_digest, + char *cookie, + size_t cookie_capacity) +{ + if (!auth__secret_is_valid(cookie_secret, cookie_secret_length) || + !guest_uuid || !ip_binding_digest || !cookie || + expiration_unix == 0) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + if (cookie_capacity < AUTH_CRYPTO_GUEST_COOKIE_SIZE) + { + return AUTH_CRYPTO_BUFFER_TOO_SMALL; + } + cookie[0] = '\0'; + + size_t uuid_length = 0; + if (!auth__bounded_length( + guest_uuid, AUTH_CRYPTO_GUEST_UUID_SIZE - 1, &uuid_length) || + !auth__uuid_is_valid(guest_uuid, uuid_length)) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + + size_t ip_digest_length = 0; + uint8 ip_digest_bytes[32]; + if (!auth__bounded_length( + ip_binding_digest, + AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE - 1, + &ip_digest_length) || + !auth__hex_decode( + ip_binding_digest, + ip_digest_length, + ip_digest_bytes, + sizeof(ip_digest_bytes))) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + OPENSSL_cleanse(ip_digest_bytes, sizeof(ip_digest_bytes)); + + char payload[AUTH_CRYPTO_GUEST_COOKIE_SIZE]; + int payload_written = snprintf( + payload, + sizeof(payload), + "%s.%s.%llu.%s", + AUTH_CRYPTO_COOKIE_VERSION, + guest_uuid, + (unsigned long long)expiration_unix, + ip_binding_digest); + if (payload_written < 0 || (size_t)payload_written >= sizeof(payload)) + { + return AUTH_CRYPTO_OPERATION_FAILED; + } + + uint8 signature[32]; + Auth_Crypto_Result result = auth__hmac_sha256( + cookie_secret, + cookie_secret_length, + AUTH_CRYPTO_COOKIE_HMAC_DOMAIN, + payload, + (size_t)payload_written, + signature); + if (result != AUTH_CRYPTO_OK) + { + return result; + } + + char signature_hex[65]; + auth__hex_encode(signature, sizeof(signature), signature_hex); + OPENSSL_cleanse(signature, sizeof(signature)); + + int cookie_written = snprintf( + cookie, + cookie_capacity, + "%s.%s", + payload, + signature_hex); + OPENSSL_cleanse(signature_hex, sizeof(signature_hex)); + if (cookie_written < 0 || (size_t)cookie_written >= cookie_capacity) + { + cookie[0] = '\0'; + return AUTH_CRYPTO_BUFFER_TOO_SMALL; + } + + return AUTH_CRYPTO_OK; +} + +Auth_Crypto_Result Auth_Crypto_Guest_Cookie_Verify( + const uint8 *cookie_secret, + size_t cookie_secret_length, + const char *cookie, + uint64 current_unix, + const char *expected_ip_binding_digest, + Auth_Crypto_Guest_Cookie *guest_cookie) +{ + if (!auth__secret_is_valid(cookie_secret, cookie_secret_length) || + !cookie || !expected_ip_binding_digest || !guest_cookie) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + memset(guest_cookie, 0, sizeof(*guest_cookie)); + + size_t expected_ip_length = 0; + uint8 expected_ip[32]; + if (!auth__bounded_length( + expected_ip_binding_digest, + AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE - 1, + &expected_ip_length) || + !auth__hex_decode( + expected_ip_binding_digest, + expected_ip_length, + expected_ip, + sizeof(expected_ip))) + { + return AUTH_CRYPTO_INVALID_ARGUMENT; + } + + size_t cookie_length = 0; + if (!auth__bounded_length( + cookie, AUTH_CRYPTO_GUEST_COOKIE_SIZE - 1, &cookie_length)) + { + OPENSSL_cleanse(expected_ip, sizeof(expected_ip)); + return AUTH_CRYPTO_MALFORMED; + } + + const char *segments[5]; + size_t segment_lengths[5]; + if (!auth__split_cookie( + cookie, cookie_length, segments, segment_lengths) || + segment_lengths[0] != sizeof(AUTH_CRYPTO_COOKIE_VERSION) - 1 || + memcmp( + segments[0], + AUTH_CRYPTO_COOKIE_VERSION, + sizeof(AUTH_CRYPTO_COOKIE_VERSION) - 1) != 0 || + !auth__uuid_is_valid(segments[1], segment_lengths[1])) + { + OPENSSL_cleanse(expected_ip, sizeof(expected_ip)); + return AUTH_CRYPTO_MALFORMED; + } + + uint64 expiration_unix = 0; + uint8 cookie_ip[32]; + uint8 provided_signature[32]; + if (!auth__parse_uint64( + segments[2], segment_lengths[2], &expiration_unix) || + expiration_unix == 0 || + !auth__hex_decode( + segments[3], + segment_lengths[3], + cookie_ip, + sizeof(cookie_ip)) || + !auth__hex_decode( + segments[4], + segment_lengths[4], + provided_signature, + sizeof(provided_signature))) + { + OPENSSL_cleanse(expected_ip, sizeof(expected_ip)); + return AUTH_CRYPTO_MALFORMED; + } + + size_t payload_length = (size_t)(segments[4] - cookie - 1); + uint8 calculated_signature[32]; + Auth_Crypto_Result result = auth__hmac_sha256( + cookie_secret, + cookie_secret_length, + AUTH_CRYPTO_COOKIE_HMAC_DOMAIN, + cookie, + payload_length, + calculated_signature); + if (result != AUTH_CRYPTO_OK) + { + OPENSSL_cleanse(expected_ip, sizeof(expected_ip)); + OPENSSL_cleanse(cookie_ip, sizeof(cookie_ip)); + OPENSSL_cleanse(provided_signature, sizeof(provided_signature)); + return result; + } + + int signature_comparison = CRYPTO_memcmp( + calculated_signature, + provided_signature, + sizeof(calculated_signature)); + int ip_comparison = CRYPTO_memcmp( + cookie_ip, + expected_ip, + sizeof(cookie_ip)); + OPENSSL_cleanse(calculated_signature, sizeof(calculated_signature)); + OPENSSL_cleanse(provided_signature, sizeof(provided_signature)); + OPENSSL_cleanse(cookie_ip, sizeof(cookie_ip)); + OPENSSL_cleanse(expected_ip, sizeof(expected_ip)); + + if (signature_comparison != 0) + { + return AUTH_CRYPTO_AUTHENTICATION_FAILED; + } + if (ip_comparison != 0) + { + return AUTH_CRYPTO_IP_MISMATCH; + } + if (current_unix >= expiration_unix) + { + return AUTH_CRYPTO_EXPIRED; + } + + memcpy( + guest_cookie->guest_uuid, + segments[1], + AUTH_CRYPTO_GUEST_UUID_SIZE - 1); + guest_cookie->guest_uuid[AUTH_CRYPTO_GUEST_UUID_SIZE - 1] = '\0'; + guest_cookie->expiration_unix = expiration_unix; + return AUTH_CRYPTO_OK; +} + +const char *Auth_Crypto_Result_String(Auth_Crypto_Result result) +{ + switch (result) + { + case AUTH_CRYPTO_OK: + return "ok"; + case AUTH_CRYPTO_INVALID_ARGUMENT: + return "invalid argument"; + case AUTH_CRYPTO_BUFFER_TOO_SMALL: + return "buffer too small"; + case AUTH_CRYPTO_PASSWORD_TOO_LONG: + return "password too long"; + case AUTH_CRYPTO_RANDOM_FAILED: + return "secure random generation failed"; + case AUTH_CRYPTO_OPERATION_FAILED: + return "cryptographic operation failed"; + case AUTH_CRYPTO_MALFORMED: + return "malformed input"; + case AUTH_CRYPTO_AUTHENTICATION_FAILED: + return "authentication failed"; + case AUTH_CRYPTO_EXPIRED: + return "expired"; + case AUTH_CRYPTO_IP_MISMATCH: + return "IP binding mismatch"; + } + + return "unknown result"; +} + +size_t Auth_Crypto_Base64url_Encode( + const uint8 *bytes, + size_t byte_count, + char *out, + size_t out_capacity) +{ + if (!bytes || !out || out_capacity == 0) + return 0; + + /* Unpadded output length: full groups + partial group chars */ + size_t full_groups = byte_count / 3; + size_t remainder = byte_count % 3; + size_t unpadded_len = full_groups * 4 + + (remainder == 0 ? 0 : remainder + 1); + if (out_capacity < unpadded_len + 1) + { + out[0] = '\0'; + return 0; + } + + static const char kAlpha[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + + size_t in_idx = 0, out_idx = 0; + + /* Full 3-byte groups */ + while (in_idx + 2 < byte_count) + { + uint32 v = ((uint32)bytes[in_idx] << 16) | + ((uint32)bytes[in_idx + 1] << 8) | + bytes[in_idx + 2]; + out[out_idx++] = kAlpha[(v >> 18) & 0x3f]; + out[out_idx++] = kAlpha[(v >> 12) & 0x3f]; + out[out_idx++] = kAlpha[(v >> 6) & 0x3f]; + out[out_idx++] = kAlpha[(v ) & 0x3f]; + in_idx += 3; + } + + /* Remaining 1 or 2 bytes (no padding) */ + if (in_idx < byte_count) + { + uint32 v = (uint32)bytes[in_idx] << 16; + if (in_idx + 1 < byte_count) + v |= (uint32)bytes[in_idx + 1] << 8; + out[out_idx++] = kAlpha[(v >> 18) & 0x3f]; + out[out_idx++] = kAlpha[(v >> 12) & 0x3f]; + if (in_idx + 1 < byte_count) + out[out_idx++] = kAlpha[(v >> 6) & 0x3f]; + } + + out[out_idx] = '\0'; + return out_idx; +}