aztec-nr - noir_aztec::messages::encryption::aes128

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]

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 exactly MESSAGE_CIPHERTEXT_LEN (15) fields, regardless of plaintext size. All output fields except the ephemeral public key are uniformly random Field values 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:

  • 1 B plaintext -> 15 B padding -> 16 B total
  • 15 B plaintext -> 1 B padding -> 16 B total
  • 16 B plaintext -> 16 B padding -> 32 B total (full extra block)

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):

+---------------------------------------------+
|                   body ct                    |
|            PlaintextLen*32 + 16 B            |
+-------------------------------+--------------+
| encrypted plaintext fields    | PKCS#7 (16B) |
| (serialized at 32 B each)     |              |
+-------------------------------+--------------+

2. Header encryption. The byte length of body_ct is 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:

+---------------------------+
|         header ct         |
|            16 B           |
+--------+------------------+
| body ct| PKCS#7 (14B)     |
| length |                  |
| (2 B)  |                  |
+--------+------------------+

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:

+------------+-------------------------+---------+
| header ct  |        body ct          | byte pad|
|   16 B     | PlaintextLen*32 + 16 B  | (zeros) |
+------------+-------------------------+---------+
|<-------- padded to a multiple of 31 B -------->|

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 random Field values 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:

+----------+-------------------------+-------------------+
| eph_pk.x |  masked message fields   | random field pad  |
|          | (packed 31 B per field)  |  (fills to 15)    |
+----------+-------------------------+-------------------+
|<---------- MESSAGE_CIPHERTEXT_LEN = 15 fields -------->|

Key Derivation

The raw ECDH shared secret point is first app-siloed into a scalar s_app by hashing with the contract address (see compute_app_siloed_shared_secret). Two (key, IV) pairs are then derived from s_app via indexed Poseidon2 hashing: one pair for the body ciphertext and one for the header ciphertext.

pub unconstrained fn decrypt( ciphertext: BoundedVec<Field, 15>, recipient: AztecAddress, contract_address: AztecAddress, ) -> Option<BoundedVec<Field, 12>>