Cryptography

Authenticated Encryption (AEAD)

Encrypt and authenticate in one shot — so a single flipped bit gets rejected, not decrypted

Authenticated encryption (AEAD) combines a cipher with a message authentication tag so any tampering with the ciphertext is detected before decryption, defeating the bit-flipping and padding-oracle attacks that plague encrypt-only modes.

  • Standard modesAES-GCM, ChaCha20-Poly1305
  • Tag size128 bits
  • Forgery probability≈ 2⁻¹²⁸
  • Composition ruleEncrypt-then-MAC
  • Nonce reuseCatastrophic (GCM)

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

The intuition: a tamper-evident seal

Encryption hides a message. It does not, on its own, tell you whether the ciphertext you received is the ciphertext that was sent. That sounds like a footnote, but it is the source of an entire genre of real-world breaks. With a stream cipher or AES in counter mode, ciphertext is just plaintext XOR keystream. Flip bit 17 of the ciphertext and you flip bit 17 of the decrypted plaintext — without knowing the key. Change "amount=10" to "amount=99" in a wire-transfer message and the server happily decrypts your forgery into something it trusts.

Authenticated encryption fixes this by shipping a short authentication tag alongside the ciphertext — a keyed fingerprint that only a holder of the secret key can produce. Tamper with even one bit of the ciphertext (or the header it covers) and the tag no longer matches. The decryptor checks the tag first; if it fails, it returns a single, uniform error and never looks at the plaintext. The seal is broken, so the package is thrown away unopened.

AEAD — Authenticated Encryption with Associated Data — adds one more idea: some bytes need to be authenticated but not encrypted. A network packet's routing header, a database row's primary key, a protocol version number — the recipient must read these in the clear, yet they still must not be forgeable. AEAD covers them with the tag while leaving them outside the ciphertext. So an AEAD primitive takes four inputs (key, nonce, plaintext, associated data) and produces two outputs (ciphertext, tag).

How it works: confidentiality plus a keyed tag

An AEAD scheme is two cryptographic jobs welded together so you can't get the join wrong:

  1. Confidentiality comes from a stream/counter cipher. AES-GCM runs AES in CTR mode; ChaCha20-Poly1305 runs the ChaCha20 stream cipher. Both turn (key, nonce, counter) into a keystream that is XORed with the plaintext. The same keystream never repeats as long as the nonce never repeats under a given key.
  2. Integrity / authenticity comes from a one-time message authentication code computed over the associated data and the ciphertext. GCM uses GHASH, a polynomial evaluation in the binary field GF(2¹²⁸); Poly1305 evaluates a polynomial modulo the prime 2¹³⁰ − 5. Both are universal hash + key constructions: fast, and provably hard to forge.

The non-negotiable design rule is Encrypt-then-MAC (EtM): encrypt the plaintext, then authenticate the resulting ciphertext (and the associated data). Bellare and Namprempre proved in 2000 that EtM is the only one of the three generic compositions that always yields a scheme secure against chosen-ciphertext attacks. The alternatives — MAC-then-Encrypt (used by old TLS CBC, the source of Lucky 13) and Encrypt-and-MAC (used by SSH) — are fragile and have produced practical attacks. AEAD modes are EtM by construction, which is the whole point: a developer can't accidentally reorder the steps.

The mechanism and its bounds

Concretely, AES-GCM works like this. AES is keyed once. The hash subkey is H = AES_K(0¹²⁸) — AES applied to an all-zero block. The 96-bit nonce is concatenated with a 32-bit counter that starts at 1 and increments per 128-bit block, and each counter block is encrypted and XORed into the plaintext to form the ciphertext. Then GHASH accumulates the associated data and ciphertext into a single field element:

X_0 = 0
X_i = (X_{i-1} XOR block_i) · H      // multiplication in GF(2^128)
tag = GHASH(AAD, C) XOR AES_K(nonce ‖ counter=0)

The final XOR with AES_K(J_0) (the counter-0 keystream block) is what turns a forgeable universal hash into an unforgeable MAC. Throughput is essentially the cost of CTR-mode AES plus one GF(2¹²⁸) multiply per block — O(n) in the message length, single-pass, parallelizable, and friendly to the PCLMULQDQ carry-less-multiply instruction.

The security bounds are sharp and worth memorizing:

  • Forgery probability per attempt ≈ 2⁻¹²⁸ with a full 128-bit tag — but it degrades to about (ℓ/2¹²⁸) per attempt for messages of ℓ blocks, because GHASH is a degree-ℓ polynomial whose roots an attacker can search.
  • Nonce uniqueness is mandatory. With a 96-bit random nonce, the birthday bound says you should rekey before ~2³² messages to keep collision risk negligible. NIST SP 800-38D caps a single (key, IV) combination at 2³² invocations for exactly this reason.
  • Data limit per key: roughly 2³⁹ − 256 bits (about 64 GB) per single message in GCM, due to the 32-bit block counter.

When to use AEAD (and when not to)

  • Any time you encrypt data that crosses a trust boundary — over a network, into a cookie, onto disk an attacker might touch. This is essentially always. "Encrypt-only" should be treated as a bug.
  • Protocols with cleartext headers — TLS records, QUIC packets, encrypted database columns with visible row IDs — use the associated-data slot to bind those headers to the ciphertext.
  • When you cannot guarantee unique nonces (e.g. stateless servers, backup/restore that may replay counters), reach for a nonce-misuse-resistant mode like AES-GCM-SIV instead of plain GCM.

When is plain AEAD not enough? It gives you confidentiality and integrity of one message, but it does not provide replay protection (an attacker can resend a valid old message) or ordering across a stream — those are the transport protocol's job (TLS sequence numbers, QUIC packet numbers). AEAD also doesn't hide message length or timing.

AEAD modes compared

AES-GCMChaCha20-Poly1305AES-GCM-SIVAES-CCMAES-OCB3AES-CBC + HMAC (legacy EtM)
Cipher coreAES-CTRChaCha20AES-CTRAES-CTRAES (offset)AES-CBC
AuthenticatorGHASH (GF(2¹²⁸))Poly1305 (mod 2¹³⁰−5)POLYVALCBC-MACbuilt-inHMAC-SHA-256
Passes over data112212
Nonce-reuse behaviorCatastrophicCatastrophicGraceful (reveals only equality)CatastrophicCatastrophicCatastrophic (IV reuse)
Speed with AES-NI1–4 GB/s/core~1.5 GB/s/core~0.7× GCM~0.5× GCMfastest AES AEADslowest (2 passes)
Speed without AES hardwareslow + timing-leak riskfast, constant-timeslowslowslowslow
Tag size128 bits128 bits128 bits≤128 bits128 bits256 bits (full HMAC)
Where usedTLS 1.3, IPsec, AWSTLS 1.3, WireGuard, SSHRFC 8452 high-volume keysWPA2, Bluetooth, IoTniche, was patent-encumberedold TLS 1.2 suites

The headline split is hardware. With AES-NI + PCLMULQDQ, GCM wins on raw throughput; without it (older ARM, microcontrollers) ChaCha20-Poly1305 wins and avoids the cache-timing leaks that software AES suffers. That's exactly why Google added ChaCha20-Poly1305 to TLS for mobile in 2014 and why WireGuard uses it exclusively.

What the numbers actually say

  • Hardware AES-GCM: 1–4 GB/s per core. On a modern x86 core with AES-NI, GCM runs around 1–2 cycles/byte, so a single core saturates a 10 GbE link. Software AES without AES-NI is roughly 10× slower and leaks via cache timing.
  • The overhead is tiny. A 16-byte tag plus a 12-byte nonce adds 28 bytes per message. On a 1,400-byte TLS record that's a 2% expansion — negligible against the cost of a single round trip.
  • Nonce reuse is not "weakened," it's broken. The 2016 study by Böck, Zauner, Devlin, Somorovsky and Jovanovic scanned the IPv4 internet and found 184 HTTPS servers actually repeating GCM nonces — fully breaking authenticity and leaking their GHASH key, so anyone could forge traffic to those endpoints — plus over 70,000 more using random nonces that put them at risk of a collision.
  • Forgery odds with a full tag ≈ 1 in 3.4 × 10³⁸. Even at a billion forgery attempts per second, the expected time to a single accepted forgery is about 10²² years — exceeding the age of the universe by ~12 orders of magnitude.

JavaScript implementation (Web Crypto AES-GCM)

Browsers and Node ship AES-GCM in the SubtleCrypto API. Note how the associated data, nonce, and tag are first-class — and how a tampered ciphertext throws rather than returning garbage.

// AEAD seal/open with AES-256-GCM via Web Crypto.
async function seal(keyBytes, plaintext, aad) {
  const key = await crypto.subtle.importKey(
    'raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
  const nonce = crypto.getRandomValues(new Uint8Array(12)); // 96-bit, MUST be unique
  const ct = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv: nonce, additionalData: aad, tagLength: 128 },
    key,
    new TextEncoder().encode(plaintext)
  );
  // Web Crypto appends the 16-byte tag to the ciphertext automatically.
  return { nonce, ciphertext: new Uint8Array(ct) };
}

async function open(keyBytes, nonce, ciphertext, aad) {
  const key = await crypto.subtle.importKey(
    'raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']);
  try {
    const pt = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv: nonce, additionalData: aad, tagLength: 128 },
      key, ciphertext);
    return new TextDecoder().decode(pt);
  } catch {
    // Tag mismatch — tampering, wrong key, or wrong AAD. ONE uniform error.
    throw new Error('decryption failed: authentication tag invalid');
  }
}

// Demo: any single-bit change to ciphertext or aad makes open() throw.
const key = crypto.getRandomValues(new Uint8Array(32));
const aad = new TextEncoder().encode('v=1;route=/transfer');
const { nonce, ciphertext } = await seal(key, 'amount=10', aad);
ciphertext[0] ^= 0x01;                 // attacker flips one bit
await open(key, nonce, ciphertext, aad); // ← throws, never reveals plaintext

Two things to flag. First, decrypt verifies the tag before producing any plaintext, so a forgery never leaks a partially-decrypted result. Second, the nonce is generated fresh and returned with the ciphertext; never derive it from a counter you might reset, and never hard-code it.

Python implementation (cryptography library)

from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
import os

def seal(key: bytes, plaintext: bytes, aad: bytes) -> tuple[bytes, bytes]:
    aead = AESGCM(key)                 # key is 16, 24, or 32 bytes
    nonce = os.urandom(12)             # 96-bit nonce — MUST be unique per key
    ct = aead.encrypt(nonce, plaintext, aad)  # ct = ciphertext ‖ 16-byte tag
    return nonce, ct

def open_(key: bytes, nonce: bytes, ct: bytes, aad: bytes) -> bytes:
    aead = AESGCM(key)
    # Raises cryptography.exceptions.InvalidTag if anything was tampered with.
    return aead.decrypt(nonce, ct, aad)

# --- Demonstrate tamper-evidence and the associated-data binding ---
key   = AESGCM.generate_key(bit_length=256)
aad   = b"v=1;route=/transfer"
nonce, ct = seal(key, b"amount=10", aad)

# 1) Flip a ciphertext bit -> InvalidTag
forged = bytearray(ct); forged[0] ^= 0x01
try:
    open_(key, nonce, bytes(forged), aad)
except Exception as e:
    print("rejected forged ciphertext:", type(e).__name__)   # InvalidTag

# 2) Change the associated data the attacker doesn't control -> InvalidTag
try:
    open_(key, nonce, ct, b"v=1;route=/refund")
except Exception as e:
    print("rejected mismatched AAD:", type(e).__name__)       # InvalidTag

# ChaCha20-Poly1305 is a drop-in replacement with the identical API:
#   aead = ChaCha20Poly1305(key)   # 32-byte key, 12-byte nonce, 16-byte tag

The API shape is deliberately identical across AES-GCM and ChaCha20-Poly1305 — both take (nonce, data, associated_data) and both raise InvalidTag on any mismatch. That uniform failure is the property that kills padding oracles: there is exactly one error, in constant time, regardless of why verification failed.

Variants worth knowing

AES-GCM-SIV (RFC 8452). A nonce-misuse-resistant variant: it derives the encryption from a synthetic IV computed over the plaintext, so reusing a nonce only leaks whether two messages were identical — it never leaks the key. The cost is a second pass over the data. Use it when you can't guarantee unique nonces, such as on stateless or distributed encryptors.

XChaCha20-Poly1305. Extends ChaCha20's 96-bit nonce to 192 bits via an HChaCha20 key-derivation step. With a 192-bit nonce you can pick nonces at random and never worry about collisions — which is why libsodium exposes it as crypto_aead_xchacha20poly1305_ietf for exactly the stateless, random-nonce cases plain ChaCha20-Poly1305 can't safely cover.

AES-CCM (Counter with CBC-MAC). The AEAD mode in WPA2 Wi-Fi, Bluetooth LE, and many IoT stacks. It's a two-pass MAC-then-... construction standardized before GCM; simpler to implement on constrained hardware but slower and pickier about parameters.

OCB3. A one-pass, single-key AEAD that's the fastest AES-based mode of all. It languished for years under software patents (now expired) and so never displaced GCM; it lives on in RFC 7253 and was a finalist-class design.

The CAESAR competition (2014–2019) ran a public, multi-year bake-off for next-generation AEAD, yielding the Ascon family — which NIST selected in 2023 as the standard for lightweight cryptography (constrained IoT devices), now SP 800-232.

Common bugs and edge cases

  • Reusing a nonce. The single most damaging AEAD mistake. With GCM/ChaCha20 it leaks the keystream and the authentication key. Use a per-message counter you persist, a random 96-bit nonce with rekeying before 2³² messages, or a misuse-resistant mode.
  • Releasing plaintext before verifying the tag. Streaming decryptors sometimes emit decrypted bytes before the final tag check. An attacker who can observe that output gets a decryption oracle. Verify first, or use a framed AEAD like the TLS record layer.
  • Forgetting the associated data on the open side, or mismatching it. If you authenticate a header on encrypt but pass empty AAD on decrypt (or vice-versa), every legitimate message fails to verify. The AAD must be byte-identical on both ends.
  • Truncating the GCM tag. Short tags raise forgery odds and, for GCM specifically, accelerate recovery of the GHASH key under repeated forgery attempts. Keep the full 128 bits.
  • Treating the tag failure as recoverable. On InvalidTag, discard everything — don't retry with "fixed" padding, don't log the offending bytes verbosely. Any behavioral difference is an oracle.
  • Assuming AEAD gives replay protection. It authenticates one message in isolation. Replay, reordering, and truncation defenses belong to the protocol layer (sequence numbers, packet counters).

Frequently asked questions

What does the "AD" in AEAD stand for?

Associated Data — header bytes that are authenticated but not encrypted. Think of packet routing fields, a database row ID, or a protocol version: the recipient must read them in the clear, but they still need to be protected from tampering. AEAD folds them into the authentication tag without putting them inside the ciphertext.

Why can't I just encrypt and then add a separate hash like SHA-256?

A plain hash isn't keyed, so an attacker who edits the ciphertext can simply recompute the hash to match. You need a keyed MAC (like HMAC or Poly1305), applied over the ciphertext, with the order Encrypt-then-MAC. AEAD modes bake this in correctly so you can't get the composition wrong.

What happens if you reuse a nonce with AES-GCM?

It's catastrophic. Two messages under the same key and nonce let an attacker XOR the ciphertexts to recover the keystream, and — worse — recover the GHASH authentication subkey H, which forges valid tags for arbitrary messages. The 2016 "Nonce-Disrespecting Adversaries" study found 184 HTTPS servers actually repeating GCM nonces (plus over 70,000 more using risky random nonces).

AES-GCM or ChaCha20-Poly1305 — which should I use?

On modern x86/ARM with AES-NI hardware instructions, AES-GCM is faster (often 1–4 GB/s per core). On devices without AES hardware — older phones, microcontrollers, some IoT — ChaCha20-Poly1305 is faster and runs in constant time in software, sidestepping cache-timing leaks. TLS 1.3 offers both.

What is a padding oracle attack and how does AEAD stop it?

In MAC-then-Encrypt or encrypt-only CBC, the receiver decrypts first and reveals (via timing or error messages) whether padding was valid. An attacker uses that one-bit leak to decrypt byte by byte. AEAD verifies the tag before any decryption or padding check, so a forged ciphertext is rejected with a single constant-time "fail" — no oracle.

How big is the authentication tag and why does the size matter?

Both AES-GCM and ChaCha20-Poly1305 use a 128-bit (16-byte) tag. Each forgery attempt has roughly a 1-in-2^128 chance of being accepted. Truncating the tag to 64 bits saves 8 bytes but raises forgery odds to 1-in-2^64 and, for GCM specifically, can leak the authentication key faster under repeated forgery attempts — so don't truncate GCM tags.