from __future__ import annotations import base64 import hashlib import hmac from cryptography.fernet import Fernet, InvalidToken from app.core.settings import get_settings class VaultCryptoError(RuntimeError): pass def _master_key() -> bytes: settings = get_settings() raw = (getattr(settings, "VAULT_MASTER_KEY", "") or settings.SECRET_KEY or "").strip() if not raw or raw == "change-me-to-a-long-random-string": raise VaultCryptoError("VAULT_MASTER_KEY must be configured with a strong production secret before using the credential vault.") return raw.encode("utf-8") def _fernet(tenant_id: int) -> Fernet: digest = hmac.new(_master_key(), f"audit-firm-vault:tenant:{tenant_id}:v1".encode(), hashlib.sha256).digest() return Fernet(base64.urlsafe_b64encode(digest)) def encrypt_value(tenant_id: int, value: str | None) -> str | None: if value is None or value == "": return None return _fernet(tenant_id).encrypt(value.encode("utf-8")).decode("ascii") def decrypt_value(tenant_id: int, token: str | None) -> str: if not token: return "" try: return _fernet(tenant_id).decrypt(token.encode("ascii")).decode("utf-8") except InvalidToken as exc: raise VaultCryptoError("The credential could not be decrypted with the configured vault key.") from exc