"""AES-256-GCM helpers matching static/js/app.js's encryptBytes/decryptBytes (Web Crypto AES-GCM, 12-byte IV hex-encoded, ciphertext base64, key handled as raw bytes) — diora's books are end-to-end encrypted client-side, so the server (and this module) only ever sees ciphertext plus the key the user supplies out-of-band. """ from __future__ import annotations import base64 from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import AESGCM class DecryptError(Exception): """Ciphertext could not be decrypted with the given key (wrong key, or corrupt data).""" def decrypt(key_b64: str, iv_hex: str, ciphertext_b64: str) -> bytes: try: key = base64.b64decode(key_b64) iv = bytes.fromhex(iv_hex) ct = base64.b64decode(ciphertext_b64) return AESGCM(key).decrypt(iv, ct, None) except (InvalidTag, ValueError) as e: raise DecryptError(str(e)) from e