""" Symmetric encryption for secrets stored in SystemSetting (currently just the IMAP mailbox password used by email ingestion). The Fernet key is derived from SECRET_KEY rather than a separately managed key, so no extra key-rotation story is needed for a single-tenant on-prem app — rotating SECRET_KEY (which already invalidates sessions) also invalidates encrypted secrets, which is an acceptable trade-off here. """ import base64 import hashlib from cryptography.fernet import Fernet, InvalidToken from flask import current_app def _fernet() -> Fernet: secret = current_app.config['SECRET_KEY'].encode() key = base64.urlsafe_b64encode(hashlib.sha256(secret).digest()) return Fernet(key) def encrypt_secret(plain: str) -> str: """Encrypt a secret for storage. Empty input passes through unchanged.""" if not plain: return '' return _fernet().encrypt(plain.encode()).decode() def decrypt_secret(stored: str) -> str: """Decrypt a secret previously written by encrypt_secret(). Falls back to returning the value unchanged if it isn't a valid Fernet token. This covers values saved before encryption was introduced, so existing configurations (e.g. email ingestion set up before this change) keep working without a manual data migration — the value is simply re-encrypted the next time it's saved through the settings form. """ if not stored: return '' try: return _fernet().decrypt(stored.encode()).decode() except (InvalidToken, ValueError): return stored