Struct AES128
pub struct AES128 {}
Trait implementations
impl MessageEncryption for AES128
pub fn encrypt<let PlaintextLen: u32>(
plaintext: [Field; PlaintextLen],
recipient: AztecAddress,
contract_address: AztecAddress,
) -> [Field; 15]
pub unconstrained fn decrypt(
ciphertext: BoundedVec<Field, 15>,
recipient: AztecAddress,
contract_address: AztecAddress,
) -> Option<BoundedVec<Field, 12>>
AES128-CBC encryption for Aztec protocol messages.
Overview
The plaintext is an array of up to
MESSAGE_PLAINTEXT_LEN(12) fields. The output is always exactlyMESSAGE_CIPHERTEXT_LEN(15) fields, regardless of plaintext size. All output fields except the ephemeral public key are uniformly randomFieldvalues to any observer without knowledge of the shared secret, making all encrypted messages indistinguishable by size or content.PKCS#7 Padding
AES operates on 16-byte blocks, so the plaintext must be padded to a multiple of 16. PKCS#7 padding always adds at least 1 byte (so the receiver can always detect and strip it), which means:
In general: if the plaintext is already a multiple of 16, a full 16-byte padding block is appended.
Encryption Steps
1. Body encryption. The plaintext fields are serialized to bytes (32 bytes per field) and AES-128-CBC encrypted. Since 32 is a multiple of 16, PKCS#7 always adds a full 16-byte padding block (see above):
2. Header encryption. The byte length of
body_ctis stored as a 2-byte big-endian integer. This 2-byte header plaintext is then AES-encrypted; PKCS#7 pads the remaining 14 bytes to fill one 16-byte AES block, producing a 16-byte header ciphertext:Wire Format
Messages are transmitted as fields, not bytes. A field is ~254 bits and can safely store 31 whole bytes, so we need to pack our byte data into 31-byte chunks. This packing drives the wire format.
Step 1 -- Assemble bytes. The ciphertexts are laid out in a byte array, padded with zero bytes to a multiple of 31 so it divides evenly into fields:
Step 2 -- Pack and mask. The byte array is split into 31-byte chunks, each stored in one field. A Poseidon2-derived mask (see
derive_shared_secret_field_mask) is added to each so that the resulting fields appear as uniformly randomFieldvalues to any observer without knowledge of the shared secret, hiding the fact that the underlying ciphertext consists of 128-bit AES blocks.Step 3 -- Assemble ciphertext. The ephemeral public key x-coordinate is prepended and random field padding is appended to fill to 15 fields:
Key Derivation
The raw ECDH shared secret point is first app-siloed into a scalar
s_appby hashing with the contract address (seecompute_app_siloed_shared_secret). Two (key, IV) pairs are then derived froms_appvia indexed Poseidon2 hashing: one pair for the body ciphertext and one for the header ciphertext.