Add phase 5 secure credential vault
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
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
|
||||
Reference in New Issue
Block a user