"""API-key generation and hashing (ADR-0008, ADR-0009). Keys are `sk_{prefix}_{secret}`. `prefix` is non-secret and indexed (`api_keys.key_prefix`); `secret` is 256 bits of `secrets.token_urlsafe` entropy, stored only as a SHA-256 hash. A random 256-bit secret does not benefit from a slow password-hashing KDF the way a human-chosen password does — the cost that defends against dictionary/brute-force guessing over a low-entropy input has nothing to defend here, and would only tax every request. Comparison is constant-time to avoid a hash-timing oracle. """ import hashlib import hmac import secrets _PREFIX_LENGTH = 16 def generate_api_key() -> tuple[str, str, str]: """Return `(key_prefix, secret, full_key)` for a newly issued key.""" key_prefix = secrets.token_hex(_PREFIX_LENGTH // 2) secret = secrets.token_urlsafe(32) return key_prefix, secret, f"sk_{key_prefix}_{secret}" def parse_api_key(full_key: str) -> tuple[str, str] | None: """Return `(key_prefix, secret)`, or `None` if the token is malformed.""" parts = full_key.split("_", 2) if len(parts) != 3 or parts[0] != "sk" or not parts[1] or not parts[2]: return None return parts[1], parts[2] def hash_secret(secret: str) -> str: return hashlib.sha256(secret.encode("utf-8")).hexdigest() def verify_secret(secret: str, key_hash: str) -> bool: return hmac.compare_digest(hash_secret(secret), key_hash)