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;
+}