import os from dataclasses import dataclass from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import AESGCM from app.config import get_settings from app.errors import AppError _DEK_AAD = b"finance-import-dek-v1" @dataclass(frozen=True) class EncryptedFile: ciphertext: bytes content_nonce: bytes dek_wrapped: bytes wrap_nonce: bytes def encrypt_file(plaintext: bytes) -> EncryptedFile: """Per-file random DEK; DEK wrapped with the KEK from the secret manager.""" kek = get_settings().kek dek = os.urandom(32) content_nonce = os.urandom(12) ciphertext = AESGCM(dek).encrypt(content_nonce, plaintext, None) wrap_nonce = os.urandom(12) dek_wrapped = AESGCM(kek).encrypt(wrap_nonce, dek, _DEK_AAD) return EncryptedFile( ciphertext=ciphertext, content_nonce=content_nonce, dek_wrapped=dek_wrapped, wrap_nonce=wrap_nonce, ) def decrypt_file( *, ciphertext: bytes, content_nonce: bytes, dek_wrapped: bytes, wrap_nonce: bytes, ) -> bytes: kek = get_settings().kek try: dek = AESGCM(kek).decrypt(wrap_nonce, dek_wrapped, _DEK_AAD) return AESGCM(dek).decrypt(content_nonce, ciphertext, None) except InvalidTag as exc: raise AppError( "Encrypted document failed integrity verification", code="IMPORT_DECRYPT_FAILED", status_code=409, ) from exc def shred_dek_material() -> None: """Crypto-shred hook: deleting wrapped DEK rows in DB plus blobs removes access.""" return None