import secrets from typing import Optional import bcrypt from cryptography.fernet import Fernet, InvalidToken from app.core.config import settings def generate_api_key(prefix_len: int = 8, token_bytes: int = 32) -> str: prefix = secrets.token_urlsafe(prefix_len)[:prefix_len] token = secrets.token_urlsafe(token_bytes) return f"{prefix}.{token}" def get_prefix(api_key: str) -> str: return api_key.split(".", 1)[0] def hash_api_key(api_key: str) -> str: hashed = bcrypt.hashpw(api_key.encode("utf-8"), bcrypt.gensalt()) return hashed.decode("utf-8") def verify_api_key(api_key: str, api_key_hash: str) -> bool: return bcrypt.checkpw(api_key.encode("utf-8"), api_key_hash.encode("utf-8")) def _get_fernet() -> Fernet: if not settings.API_KEY_ENC_SECRET: raise ValueError("API_KEY_ENC_SECRET is not configured") # Expect a base64 urlsafe key; if plaintext is provided, derive a Fernet key secret = settings.API_KEY_ENC_SECRET # If length is 44 and endswith '=', assume already a Fernet key if len(secret) == 44 and secret.endswith("="): key = secret.encode("utf-8") else: # Derive deterministic Fernet key from secret (simple approach) import base64 import hashlib digest = hashlib.sha256(secret.encode("utf-8")).digest() key = base64.urlsafe_b64encode(digest) return Fernet(key) def encrypt_api_key(plain_key: str) -> str: f = _get_fernet() return f.encrypt(plain_key.encode("utf-8")).decode("utf-8") def decrypt_api_key(cipher_text: str) -> Optional[str]: try: f = _get_fernet() return f.decrypt(cipher_text.encode("utf-8")).decode("utf-8") except (InvalidToken, ValueError): return None