"""Fernet-based symmetric encryption for storage credentials. The encryption key must be a 32-byte URL-safe base64-encoded string, generated once via: Fernet.generate_key().decode() and stored in the STORAGE_ENCRYPTION_KEY environment variable. No credentials are ever stored in plaintext — only the encrypted blob is written to the database. """ from __future__ import annotations import json from cryptography.fernet import Fernet, InvalidToken def encrypt_credentials(key: str, data: dict) -> str: """Serialize *data* to JSON and encrypt it with Fernet. Returns a URL-safe base64-encoded ciphertext string safe to store in TEXT columns. """ f = Fernet(key.encode()) plaintext = json.dumps(data, separators=(",", ":")).encode() return f.encrypt(plaintext).decode() def decrypt_credentials(key: str, blob: str) -> dict: """Decrypt and deserialize a blob previously created by :func:`encrypt_credentials`. Raises ``cryptography.fernet.InvalidToken`` if the key is wrong or the blob is tampered. """ f = Fernet(key.encode()) try: plaintext = f.decrypt(blob.encode()) except InvalidToken: raise InvalidToken("Failed to decrypt storage credentials — wrong key or corrupted blob") return json.loads(plaintext)