import base64 import hashlib from typing import Optional from cryptography.fernet import Fernet, InvalidToken from .config import settings ENCRYPTED_PREFIX = "enc:v1:" SENSITIVE_SETTING_KEYS = frozenset( { "jellystat_api_key", "magent_ssl_certificate_pem", "magent_ssl_private_key_pem", "magent_notify_email_smtp_password", "magent_notify_discord_webhook_url", "magent_notify_telegram_bot_token", "magent_notify_push_token", "magent_notify_push_user_key", "magent_notify_webhook_url", "jellyseerr_api_key", "jellyfin_api_key", "sonarr_api_key", "radarr_api_key", "bazarr_api_key", "prowlarr_api_key", "qbittorrent_password", } ) def _fernet_key() -> bytes: configured = str(settings.settings_encryption_key or "").strip() if configured: try: decoded = base64.urlsafe_b64decode(configured.encode("ascii")) except Exception as exc: raise RuntimeError("SETTINGS_ENCRYPTION_KEY must be a valid Fernet key") from exc if len(decoded) != 32: raise RuntimeError("SETTINGS_ENCRYPTION_KEY must decode to exactly 32 bytes") return configured.encode("ascii") jwt_secret = str(settings.jwt_secret or "").strip() if len(jwt_secret) < 32 or jwt_secret == "change-me": raise RuntimeError( "SETTINGS_ENCRYPTION_KEY is required when JWT_SECRET is not a strong migration key" ) derived = hashlib.sha256(("magent-settings-v1:" + jwt_secret).encode("utf-8")).digest() return base64.urlsafe_b64encode(derived) def is_sensitive_setting(key: str) -> bool: return str(key or "").strip().lower() in SENSITIVE_SETTING_KEYS def validate_secret_storage_configuration() -> None: """Validate the configured or JWT-derived Fernet key without touching stored data.""" Fernet(_fernet_key()) def encrypt_setting_value(key: str, value: Optional[str]) -> Optional[str]: if value is None or not is_sensitive_setting(key): return value text = str(value) if text.startswith(ENCRYPTED_PREFIX): return text token = Fernet(_fernet_key()).encrypt(text.encode("utf-8")).decode("ascii") return ENCRYPTED_PREFIX + token def decrypt_setting_value(key: str, value: Optional[str]) -> Optional[str]: if value is None or not is_sensitive_setting(key): return value text = str(value) if not text.startswith(ENCRYPTED_PREFIX): return text try: return Fernet(_fernet_key()).decrypt( text[len(ENCRYPTED_PREFIX) :].encode("ascii") ).decode("utf-8") except InvalidToken as exc: raise RuntimeError( f"Stored secret '{key}' cannot be decrypted with the configured key" ) from exc