from __future__ import annotations import base64 import hashlib import hmac import secrets PBKDF2_ROUNDS = 210_000 def _b64(data: bytes) -> str: return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=") def _unb64(value: str) -> bytes: padding = "=" * (-len(value) % 4) return base64.urlsafe_b64decode(value + padding) def hash_secret(secret: str) -> str: salt = secrets.token_bytes(16) digest = hashlib.pbkdf2_hmac("sha256", secret.encode("utf-8"), salt, PBKDF2_ROUNDS) return f"pbkdf2_sha256${PBKDF2_ROUNDS}${_b64(salt)}${_b64(digest)}" def verify_secret(secret: str, stored_hash: str) -> bool: try: algorithm, rounds_text, salt_text, digest_text = stored_hash.split("$", 3) if algorithm != "pbkdf2_sha256": return False rounds = int(rounds_text) salt = _unb64(salt_text) expected = _unb64(digest_text) except (ValueError, TypeError): return False actual = hashlib.pbkdf2_hmac("sha256", secret.encode("utf-8"), salt, rounds) return hmac.compare_digest(actual, expected) def generate_api_key() -> str: return "rcg_" + secrets.token_urlsafe(32) def key_prefix(secret: str) -> str: return secret[:12] def generate_csrf_token() -> str: return secrets.token_urlsafe(24)