""" MeshBay — the content cipher: AES-256-GCM, per-chunk keys derived from the GEK. AES-GCM because it is what WebCrypto offers, and one cipher serves every client: the browser, the desktop client (the same engine) and the Python side here. Python side (this module): chunk_key_aes / encrypt_chunk_aes / decrypt_chunk_aes JavaScript side (in static/crypto.js): SubtleCrypto.importKey + SubtleCrypto.decrypt with AES-GCM. Key derivation: info = b"file:" + file_hash + b":chunk:" + chunk_index + b":aes" The `:aes` suffix dates from a ChaCha20-Poly1305 variant derived from the same GEK without it, which nothing used and which is gone. It stays: it is part of every chunk key in existence, and changing it would change them all. """ import os from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.kdf.hkdf import HKDF def chunk_key_aes(gek: bytes, file_hash: bytes, chunk_index: int) -> bytes: """Derive a per-chunk AES-256 key from the GEK.""" 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)