"""Symmetric encryption for sensitive user prefs (e.g. ntfy password). Key priority: 1. ENCRYPTION_SECRET_KEY env var (recommended — dedicated key, easily rotatable) 2. Derived from JWT_SECRET_KEY (fallback for existing installs) Generate a dedicated key: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" """ import base64 import hashlib from cryptography.fernet import Fernet _PREFIX = "enc:" _fernet_instance: Fernet | None = None def _fernet() -> Fernet: global _fernet_instance if _fernet_instance is None: from app.config import settings if settings.ENCRYPTION_SECRET_KEY: # Use dedicated key directly (must be a valid 32-byte base64url key) _fernet_instance = Fernet(settings.ENCRYPTION_SECRET_KEY.encode()) else: # Fallback: derive from JWT secret key_bytes = hashlib.sha256(settings.JWT_SECRET_KEY.encode()).digest() _fernet_instance = Fernet(base64.urlsafe_b64encode(key_bytes)) return _fernet_instance def encrypt_secret(plaintext: str) -> str: """Encrypt a string and return a prefixed ciphertext.""" if not plaintext: return plaintext return _PREFIX + _fernet().encrypt(plaintext.encode()).decode() def decrypt_secret(value: str) -> str: """Decrypt a value produced by encrypt_secret. Returns plaintext as-is (legacy support).""" if not value or not value.startswith(_PREFIX): return value # legacy plaintext — return unchanged return _fernet().decrypt(value[len(_PREFIX):].encode()).decode()