"""Fernet symmetric encryption for sensitive fields (API keys). The encryption key is derived from JWT_SECRET using PBKDF2-HMAC-SHA256, ensuring a stable 32-byte key suitable for Fernet. """ import base64 import hashlib from cryptography.fernet import Fernet, InvalidToken from config import settings def _derive_fernet_key(secret: str) -> bytes: """Derive a Fernet-compatible key from an arbitrary string secret.""" # PBKDF2 with a fixed salt — the secret itself provides entropy. # The salt is fixed so the same secret always yields the same key. dk = hashlib.pbkdf2_hmac( "sha256", secret.encode("utf-8"), b"promptlooper-fernet-salt", iterations=100_000, dklen=32, ) return base64.urlsafe_b64encode(dk) def get_fernet() -> Fernet: """Return a Fernet instance keyed from the current JWT_SECRET.""" return Fernet(_derive_fernet_key(settings.jwt_secret)) def encrypt_api_key(plain_key: str) -> str: """Encrypt an API key and return the ciphertext as a UTF-8 string.""" return get_fernet().encrypt(plain_key.encode("utf-8")).decode("utf-8") def decrypt_api_key(encrypted_key: str) -> str: """Decrypt an API key. Raises ValueError on failure.""" try: return get_fernet().decrypt(encrypted_key.encode("utf-8")).decode("utf-8") except InvalidToken as exc: raise ValueError("Failed to decrypt API key — JWT_SECRET may have changed") from exc