"""AES-256-GCM cipher backend.""" import os import base64 from typing import Tuple from cryptography.hazmat.primitives.ciphers.aead import AESGCM from .base import CipherBackend class AESGCMCipher(CipherBackend): """AES-256-GCM authenticated encryption.""" def encrypt(self, plaintext: bytes) -> Tuple[bytes, str]: """ Encrypt using AES-256-GCM. Returns: Tuple of (nonce + ciphertext, base64url-encoded key) """ # Generate random 256-bit key key = AESGCM.generate_key(bit_length=256) aesgcm = AESGCM(key) # Generate random 96-bit nonce (recommended for GCM) nonce = os.urandom(12) # Encrypt and authenticate ciphertext = aesgcm.encrypt(nonce, plaintext, None) # Prepend nonce to ciphertext for storage encrypted_data = nonce + ciphertext # Encode key for URL fragment key_encoded = base64.urlsafe_b64encode(key).decode('ascii').rstrip('=') return encrypted_data, key_encoded def decrypt(self, ciphertext: bytes, key: str) -> bytes: """ Decrypt using AES-256-GCM. Args: ciphertext: Nonce (12 bytes) + encrypted data key: Base64url-encoded 256-bit key """ # Decode key (add padding if needed) padding = '=' * (4 - len(key) % 4) if len(key) % 4 else '' key_bytes = base64.urlsafe_b64decode(key + padding) if len(key_bytes) != 32: raise ValueError("Invalid key length. Expected 32 bytes for AES-256.") # Extract nonce and ciphertext nonce = ciphertext[:12] encrypted = ciphertext[12:] # Decrypt aesgcm = AESGCM(key_bytes) plaintext = aesgcm.decrypt(nonce, encrypted, None) return plaintext def get_name(self) -> str: """Return cipher name.""" return "aes-256-gcm"