USMP Encryption & Frame Protection

Once the handshake is complete, USMP shifts into lock-down mode. Every single post-handshake packet (`DATA`, `PING`, `PONG`, `BYE`) is protected using **AES-256...

Once the handshake is complete, USMP shifts into lock-down mode. Every single post-handshake packet (DATA, PING, PONG, BYE) is protected using AES-256-GCM (Galois/Counter Mode).

AES-256-GCM is an Authenticated Encryption with Associated Data (AEAD) cipher. It doesn't just encrypt your data; it also creates a cryptographic signature (a "tag") that guarantees the data has not been modified or reordered in transit. If an attacker changes even one bit of an encrypted frame, decryption fails immediately, and the session is torn down.

The Shipping Box Analogy: How AES-GCM Protects Us

To understand how USMP protects data, imagine mailing a package using a secure, tamper-proof shipping container:

┌────────────────────────────────────────────────────────┐
│                   THE OUTSIDE (AAD)                    │  ← Unencrypted, but signed!
│   Magic, Version, Type, Sequence Number, Length        │
├────────────────────────────────────────────────────────┤
│                   THE INSIDE (Payload)                 │  ← Fully encrypted!
│   12-byte Nonce  │  Ciphertext  │  16-byte Auth Tag    │
└────────────────────────────────────────────────────────┘
  1. The Box Contents (Ciphertext): This is your actual application payload. It is fully encrypted. To an eavesdropper, it looks like random noise.
  2. The Tamper-Proof Seal (16-byte Auth Tag): This is a signature generated by the encryption engine. If anyone alters the ciphertext or the shipping label, the seal breaks, and the package is rejected.
  3. The Shipping Label (Additional Authenticated Data / AAD): These are the header fields (like the sequence number and length) written on the outside of the box. They are left unencrypted so the network can read them, but they are cryptographically signed by the GCM engine. If an attacker tries to change the sequence number to bypass replay protections, the signature verification fails!

Deterministic Nonces: Zero Collisions, Zero Overhead

In AES-GCM, reusing a nonce (a "number used once") with the same session key is a catastrophic cryptographic failure. If a nonce is reused, an attacker can decrypt your traffic and forge messages.

Standard approaches generate random 12-byte nonces for every packet. However, on small microcontrollers, high-quality randomness (entropy) is a scarce resource, and generating random numbers for every packet is slow.

USMP solves this by using deterministic nonces. The 12-byte nonce is constructed mathematically from two variables:

  • Bytes 0..3: The 32-bit monotonic sequence number seq (encoded as a little-endian unsigned integer).
  • Bytes 4..11: The first 8 bytes of the session's random session_id.
┌─────────────────────────┬─────────────────────────────────┐
│  Sequence Number (4B)   │      Session ID [0..7] (8B)     │
└─────────────────────────┴─────────────────────────────────┘
 0                       3 4                              11

Because the session_id is unique and random for every new session, and the sequence number increments strictly by 1 for every packet sent, no two packets will ever share the same nonce/key combination. This design guarantees absolute cryptographic safety without wasting CPU cycles generating random numbers!

Implementing Nonce Generation

/* Construct deterministic GCM nonce: seq (4 bytes, LE) || session_id[0..7] (8 bytes) */
uint8_t nonce[12];
nonce[0] = seq & 0xFF;
nonce[1] = (seq >> 8) & 0xFF;
nonce[2] = (seq >> 16) & 0xFF;
nonce[3] = (seq >> 24) & 0xFF;
memcpy(nonce + 4, session_id, 8);

Additional Authenticated Data (AAD)

The AAD consists of the first 10 bytes of the USMP header. By authenticating the header, USMP ensures that hackers cannot swap packet types (e.g. changing a PING into a DATA packet) or modify length fields.

Header Fields in AAD (10 bytes):

  • magic (2 bytes) — always 0xABCD
  • version (1 byte)
  • type (1 byte)
  • seq (4 bytes)
  • length (2 bytes)

Note: The final 2-byte CRC field of the header is excluded.

Decryption & Monotonic Verification

When a packet is received, the receiver doesn't just decrypt it—it performs defense-in-depth verification:

  1. Reconstruct the expected nonce: The receiver takes the sequence number from the header and its own local session_id, and builds the expected deterministic nonce.
  2. Verify the nonce matches: The receiver compares the 12-byte nonce prepended to the payload with its locally computed nonce using a constant-time comparison. If they do not match, the packet is thrown out!
  3. Perform AES-GCM verification: The AES-GCM engine decrypts the ciphertext and verifies the 16-byte authentication tag.

Decryption Implementation

// 1. Build expected deterministic nonce
uint8_t expected_nonce[12];
expected_nonce[0] = pkt_seq & 0xFF;
expected_nonce[1] = (pkt_seq >> 8) & 0xFF;
expected_nonce[2] = (pkt_seq >> 16) & 0xFF;
expected_nonce[3] = (pkt_seq >> 24) & 0xFF;
memcpy(expected_nonce + 4, session_id, 8);

// 2. Verify received nonce matches expected nonce in constant-time
if (mbedtls_ct_memcmp(nonce_ct_tag, expected_nonce, 12) != 0) {
    return -1; // Nonce mismatch!
}

const uint8_t *ciphertext = nonce_ct_tag + 12;
size_t cipher_len         = nct_len - 12 - 16;
const uint8_t *tag        = ciphertext + cipher_len;

mbedtls_gcm_context gcm;
mbedtls_gcm_init(&gcm);
mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 256);

// 3. Decrypt and verify the authentication tag
int ret = mbedtls_gcm_auth_decrypt(&gcm, cipher_len,
                                   nonce_ct_tag, 12,
                                   aad, aad_len,
                                   tag, 16,
                                   ciphertext, out);
mbedtls_gcm_free(&gcm);
if (ret != 0) {
    return -1; // Tag verification failed!
}
*out_len = cipher_len;