"""Symmetric encryption for secrets stored at rest, keyed by SECRET_KEY. Used for the per-user e621 API keys: a database dump (or backup) alone does not expose them. Rotating SECRET_KEY makes existing values undecryptable, exactly like the signed media URLs stop verifying — users then re-enter their e621 key. """ import base64 import hashlib from cryptography.fernet import Fernet, InvalidToken from django.conf import settings PREFIX = "enc:" def _fernet() -> Fernet: # Derive a stable Fernet key from the project secret. digest = hashlib.sha256( f"j621-secret-v1:{settings.SECRET_KEY}".encode() ).digest() return Fernet(base64.urlsafe_b64encode(digest)) def encrypt_secret(value: str) -> str: """Encrypt for storage; empty stays empty and already-encrypted passes through.""" if not value: return "" if value.startswith(PREFIX): return value return PREFIX + _fernet().encrypt(value.encode()).decode() def decrypt_secret(value: str) -> str: """Decrypt a stored value; legacy plaintext passes through. Returns "" when the value cannot be decrypted (e.g. SECRET_KEY changed), which reads as "not configured" and asks the user for the key again. """ if not value: return "" if not value.startswith(PREFIX): return value try: return _fernet().decrypt(value[len(PREFIX) :].encode()).decode() except InvalidToken: return ""