> Aztec.nr API reference, v6.0.0 (mainnet and testnet). Index: https://docs.aztec.network/aztec-nr-api/v6.0.0/llms.txt

# Struct AES128

Module: `noir_aztec::messages::encryption::aes128`

```rust
pub struct AES128 {}
```

## Trait implementations

```rust
impl MessageEncryption for AES128
```

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

```text
+---------------------------------------------+
|                   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:

```text
+---------------------------+
|         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:

```text
+------------+-------------------------+---------+
| 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:

```text
+----------+-------------------------+-------------------+
| 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`](/aztec-nr-api/v6.0.0/noir_aztec/keys/ecdh_shared_secret/fn.compute_app_siloed_shared_secret.md)). 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.

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