""" MeshBay — AES-256-GCM cipher variant for browser-accessible groups. The ChaCha20-Poly1305 GEK used in MNP (TCP+TLS and QUIC transport) is NOT available in the WebCrypto API. For groups whose content must be decryptable by a web browser (using SubtleCrypto), an AES-256-GCM variant is used instead. The GEK wrapping (X25519 + HKDF) is identical — only the content cipher changes. The hub stores and distributes GEK bundles the same way. Cipher selection is declared per-group in the hub registry: "cipher": "chacha20-poly1305" (default, native clients) "cipher": "aes-256-gcm" (browser-compatible groups) Python side (this module): encrypt_chunk_aes / decrypt_chunk_aes JavaScript side (in static/crypto.js): Uses SubtleCrypto.importKey + SubtleCrypto.decrypt with AES-GCM. Key derivation for AES variant — same HKDF info string with suffix: info = b"file:" + file_hash + b":chunk:" + chunk_index + b":aes" This ensures AES and ChaCha20 keys are always distinct even from the same GEK. """ import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives import hashes def chunk_key_aes(gek: bytes, file_hash: bytes, chunk_index: int) -> bytes: """Derive a per-chunk AES-256 key. Distinct from ChaCha20 key.""" return HKDF( algorithm=hashes.SHA256(), length=32, salt=None, info=b"file:" + file_hash + b":chunk:" + chunk_index.to_bytes(4, "big") + b":aes", ).derive(gek) def encrypt_chunk_aes(key: bytes, plaintext: bytes) -> tuple[bytes, bytes]: """Encrypt with AES-256-GCM. Returns (nonce, ciphertext+tag).""" nonce = os.urandom(12) # 96-bit nonce (WebCrypto standard) ct = AESGCM(key).encrypt(nonce, plaintext, None) return nonce, ct def decrypt_chunk_aes(key: bytes, nonce: bytes, ciphertext: bytes) -> bytes: """Decrypt with AES-256-GCM. Raises InvalidTag on failure.""" return AESGCM(key).decrypt(nonce, ciphertext, None)