Stream ciphers and AEAD construction
Stream ciphers, ChaCha20, GCM, Poly1305: how authenticated encryption is actually built, why nonce reuse is catastrophic, and how to choose between AES-GCM and ChaCha20-Poly1305.
The previous module ended with a deliberately incomplete result: AES alone is not a working encryption scheme, ECB is unsafe, and even CBC and CTR provide only confidentiality. This module finishes the construction. We'll build a stream-cipher view of encryption, see why authenticated encryption with associated data (AEAD) is the modern default, and walk through the two AEAD constructions that account for almost all real-world TLS, WireGuard, and SSH traffic in 2026: AES-GCM and ChaCha20-Poly1305. The polemical center of this module is nonce discipline: get nonce handling right and AEAD is a beautiful primitive; get it wrong and the protocol becomes catastrophically insecure even with correct cipher math.
Prerequisites
- Module 2.1 — Symmetric encryption, block ciphers, and AES. You need the block-cipher / mode-of-operation distinction and the IND-CPA framing as starting points.
Learning objectives
By the end of this module you should be able to:
- Explain the difference between a stream cipher, a block cipher in counter mode, and a full AEAD scheme.
- Describe why nonce reuse under stream-like constructions is catastrophic, and why "two-time pad" is a real attack vector with named history.
- Explain what associated data is, why authenticated encryption beats "encrypt now, MAC later," and how AEAD APIs actually combine the two.
- Compare AES-GCM and ChaCha20-Poly1305 as engineering choices: hardware acceleration, software speed, nonce-misuse behavior, side channels.
- Recognize misuse patterns — nonce reuse, partial integrity, associated-data manipulation — when they appear in real protocol designs and code reviews.
Stream ciphers as keystream generators
A stream cipher generates a long pseudo-random keystream from a key (and usually a nonce). Encryption is XOR: ciphertext = plaintext XOR keystream. Decryption is the same XOR — the keystream is derivable on either end given the key and nonce.
The mental model is the one-time pad: if the keystream is truly random, used only once, and as long as the message, the encryption is information-theoretically secure. No amount of computation breaks it because there's no statistical structure for an attacker to exploit.
Real stream ciphers approximate this with pseudo-random keystreams. The keystream isn't truly random; it's a deterministic function of (key, nonce). For a good cipher, the keystream is computationally indistinguishable from random to anyone without the key. Each (key, nonce) pair produces a unique keystream; reusing them produces the same keystream twice.
The stream-cipher model has a beautiful clarity:
- No padding required — encrypt arbitrary-length data.
- Parallelizable on the keystream-generation side.
- Conceptually simple: generate a stream, XOR with bytes.
It also has a catastrophic failure mode that sits at the center of every stream-cipher disaster: reusing (key, nonce) reuses the keystream, which collapses the security model. We'll come back to this.
CTR mode: making block ciphers act like stream ciphers
Block ciphers (AES) and stream ciphers (ChaCha20) come from different design lineages but converge in modern protocols because of CTR mode.
CTR (Counter Mode) treats a block cipher as a keystream generator:
keystream_block_i = AES_encrypt(key, nonce || counter_i)
ciphertext_block_i = plaintext_block_i XOR keystream_block_i
The block cipher encrypts successive counter values; the resulting blocks form a keystream; XOR with plaintext gives ciphertext. Padding isn't needed (you can XOR a partial block with a partial keystream block). Different blocks are independent — encryption is fully parallel.
Critically: CTR mode inherits the nonce-reuse catastrophe. If you use the same (key, nonce) for two messages, you generate the same keystream twice. The attacker can XOR the two ciphertexts to get the XOR of the two plaintexts, which is the canonical "two-time pad" leakage:
C1 = P1 XOR keystream
C2 = P2 XOR keystream
C1 XOR C2 = P1 XOR P2
If the attacker knows part of either plaintext (a known header, a guessable structure), they can recover that portion of the other. With enough samples and known structure, recovery becomes nearly complete. This is not a theoretical concern; the WEP protocol's collapse and many other practical breaks have come from exactly this pattern.
CTR mode is the foundation of AES-GCM. Understanding nonce reuse in CTR is the same understanding you need for GCM.
ChaCha20 at the engineering level
ChaCha20 is a modern stream cipher designed by Daniel J. Bernstein in 2008, as a refinement of his earlier Salsa20. It generates a keystream by repeatedly applying a small, fast round function — the quarter round — to a 4×4 state of 32-bit words. The state contains the key, nonce, counter, and a fixed constant; after 20 rounds (10 double-rounds, hence "ChaCha20"), the result is squeezed out as 64 bytes of keystream.
The engineering virtues:
- Software speed without hardware help. ChaCha20 is fast on any CPU with 32-bit ALU and basic shift instructions. No special crypto extensions required. On platforms without AES-NI — older mobile chips, embedded devices, some virtualized environments — ChaCha20 is dramatically faster than software AES.
- Constant-time by design. The quarter round uses only addition, XOR, and rotation — none of which depend on the data values for their timing. No table lookups means no cache-timing leakage.
- Simple to implement correctly. The reference implementation is a few hundred lines. Side-channel safety is built in rather than bolted on.
ChaCha20's 8-byte (64-bit) counter and 12-byte (96-bit) nonce gives it 2^96 distinct nonces per key — comfortably large for protocols that use random nonces. Each nonce permits up to 2^64 64-byte blocks (1 EB of data) before counter overflow.
ChaCha20 is what Google, Cloudflare, and others use to keep TLS performant on devices that don't have AES-NI. The standard pairs it with Poly1305 as a MAC to form ChaCha20-Poly1305 — the AEAD construction we'll get to.
Message authentication: the integrity gap
Confidentiality without integrity is not enough. A plain encryption mode — CTR, CBC, ChaCha20 alone — doesn't detect tampering. An attacker who flips bits in ciphertext doesn't decrypt it (they don't have the key) but does cause unpredictable changes in the recovered plaintext. The receiver may notice if the resulting bytes are nonsense; may also notice nothing if the bytes happen to remain parseable.
Worse, for stream-like ciphers, bit-flipping is targeted. Flip ciphertext bit i; plaintext bit i flips on decryption. An attacker who knows part of the plaintext can flip specific bits to specific values. This is much worse than "introducing errors" — it's controlled modification of the plaintext.
The fix is a Message Authentication Code (MAC). The sender computes a tag over the ciphertext (and any other context); the receiver recomputes the tag and rejects the message if it doesn't match. Because the MAC is keyed, an attacker who modifies the ciphertext can't produce a valid tag without the key. The most important property: a single bit of difference in the ciphertext causes the recomputed tag to be unrelated to the transmitted tag, so the modification is detected.
A naive composition might encrypt the plaintext, then compute a separate MAC, then transmit (ciphertext, tag). This works if the MAC and encryption are independent and the order is right (MAC-then-encrypt? Encrypt-then-MAC?). It also adds two separate cryptographic operations, two key-management concerns, and two possible misuse vectors.
AEAD (Authenticated Encryption with Associated Data) bundles confidentiality, integrity, and authentication into a single primitive. One call encrypts and authenticates. One call verifies and decrypts. The implementation knows how to combine them safely; the application doesn't have to design the composition.
This is the modern default. Plain encryption (without integrity) is essentially never the right choice in 2026.
Poly1305 and GHASH
The MAC inside an AEAD construction has to be fast — every byte you encrypt is also a byte you authenticate. Two MACs dominate practical use:
Poly1305 (RFC 8439) is a one-time MAC built on 130-bit modular arithmetic. The key is a one-time secret derived per message from the cipher state; never reused. To authenticate a message, you split it into 16-byte chunks, treat each as a coefficient of a polynomial, evaluate the polynomial at the secret key (mod 2^130 - 5), and combine with another secret value. The result is a 16-byte tag.
The algebraic structure means Poly1305 keys must never be reused. Reusing a Poly1305 key across messages allows an attacker to recover the key from two known message-tag pairs by polynomial interpolation. This is why ChaCha20-Poly1305 derives a fresh Poly1305 key from the first ChaCha20 keystream block, per message.
GHASH is the MAC inside AES-GCM. It's a polynomial-evaluation MAC over GF(2^128) — similar in spirit to Poly1305 but in a different finite field. The hash is then encrypted under a counter mode block to produce the final tag. GHASH key is also single-use per message.
The point common to both: the MAC's security depends on never reusing its key. That's the deeper reason nonce reuse in AEAD is catastrophic: it isn't just keystream reuse (which leaks plaintexts), it's MAC-key reuse (which lets an attacker forge tags on arbitrary other ciphertexts).
The AEAD interface
AEAD APIs follow the RFC 5116 shape:
encrypt(key, nonce, plaintext, associated_data) -> ciphertext_with_tag
decrypt(key, nonce, ciphertext_with_tag, associated_data) -> plaintext_or_error
Five inputs/outputs to keep straight:
- Key. Symmetric, fixed length (typically 32 bytes for AES-256, ChaCha20, both with 16- or 32-byte keys depending on configuration).
- Nonce. A "number used once" — must be unique per
(key, message). Length depends on the AEAD: 12 bytes for AES-GCM and ChaCha20-Poly1305 standard variants. Random or counter-derived; the only requirement is uniqueness. - Plaintext. What you want to encrypt and authenticate.
- Associated data (AD). What you want to authenticate but not encrypt. Common uses: protocol headers (TLS record type and version), sequence numbers, frame metadata. The receiver gets the AD in cleartext but verifies it was bound to the ciphertext at encryption time.
- Ciphertext (with tag). The encrypted plaintext concatenated with the authentication tag. Length is
plaintext_length + tag_length(16 bytes for the standard tag).
The decryption operation either returns the plaintext (success) or an error (tampering detected). It does not return "the plaintext but it might be wrong" — there is no partial trust. Either the AEAD authenticated everything correctly, or the entire message is rejected.
The associated-data field is operationally important. Consider a TLS record: the header carries record type (0x16 handshake, 0x17 application data, etc.) and length. Both must be visible to the receiver before decryption begins (the receiver needs to know how much to read). But if the record type weren't authenticated, an attacker could rewrite it to confuse the parser. AEAD's associated data binds the cleartext header to the ciphertext: an attacker who modifies the header invalidates the tag, and the modification is detected even though the header itself isn't encrypted.
The general principle: everything that influences how the receiver interprets the ciphertext must be either inside the plaintext or in the associated data. Otherwise you have unauthenticated cleartext that the protocol can be forced to misinterpret.
AES-GCM vs ChaCha20-Poly1305
The two AEAD constructions that account for almost all 2026 real-world traffic:
AES-GCM. AES in counter mode for confidentiality, GHASH for the MAC. Inputs: 128-bit (or 256-bit) key, 96-bit nonce. With AES-NI hardware acceleration, AES-GCM hits 5+ GB/s on a single core. With pure software it's much slower than ChaCha20.
ChaCha20-Poly1305. ChaCha20 stream cipher for confidentiality, Poly1305 for the MAC. Inputs: 256-bit key, 96-bit nonce. Software-fast on every modern CPU; competitive with hardware-accelerated AES-GCM on platforms with AES-NI, decisively faster on platforms without.
The deployment guidance:
- Server-to-server traffic on x86 boxes with AES-NI. Either is fine. AES-GCM is usually marginally faster.
- Mobile devices. Many smartphones don't have AES hardware; ChaCha20-Poly1305 is faster and more battery-efficient. Google chose it for HTTPS on Android specifically for this reason.
- Embedded / constrained devices. ChaCha20-Poly1305 wins on virtually everything below the desktop class. Smaller code footprint, no table lookups, no side-channel concerns.
- Cipher suite negotiation. TLS 1.3 includes both. Clients typically advertise both; servers pick based on hardware (with-AES-NI servers prefer GCM, without-AES-NI prefer ChaCha20-Poly1305).
A misconception worth correcting: ChaCha20-Poly1305 is sometimes treated as the "second-best" choice, used only when AES-GCM doesn't fit. That framing is wrong. The two have different performance profiles on different hardware; both are equally secure cryptographically; pick the one that actually runs faster on your target.
The nonce-reuse catastrophe
Here's the critical safety property of every AEAD covered above:
For each
(key, nonce)pair, encrypt at most one message.
Reusing a (key, nonce) pair under AES-GCM:
- Reveals XOR-of-plaintexts (because the keystream is reused).
- Reveals the GHASH key (because the same MAC sub-key is used).
- An attacker who recovers the GHASH key can forge tags on arbitrary other ciphertexts under the same key.
Same under ChaCha20-Poly1305: keystream reuse plus Poly1305 key reuse, with similar consequences. The attacker can forge messages that the receiver will accept as legitimate.
This is the polemical center of modern cryptographic engineering: nonce reuse is the failure mode that breaks AEAD constructions in practice. It's not a hygiene issue; it's a complete protocol break. Protocols that get nonce handling wrong have been broken in production: WPA2 (KRACK attack), various IoT device firmware, occasional cloud SDK bugs.
How nonces are generated matters. Three workable strategies:
- Random nonces. Generate cryptographically random 12-byte values. With 2^96 nonces per key, collision probability is negligible up to 2^48 messages per key (the birthday bound). Used by TLS 1.3.
- Counter nonces. Each message uses an incrementing counter. Trivial to implement, zero collision risk, but requires per-key state. Used by WireGuard.
- Hybrid: counter + random. A per-session random prefix combined with a counter. Used by some protocols to balance simplicity and stateless behavior.
The strategies fail if:
- Random sources have insufficient entropy (early-boot RNGs are notorious).
- Counters reset (after a crash or migration).
- Different processes share a key without coordinating their nonce sequences.
A common protocol pattern: derive a fresh key per session via key exchange (TLS 1.3, WireGuard) and use simple counter nonces within the session. Both the key freshness and counter discipline keep nonce reuse impossible by construction.
Misuse-resistant variants
For situations where nonce discipline can't be guaranteed — distributed systems with replay possibilities, applications that re-encrypt the same data, scenarios where engineers will inevitably make mistakes — there are AEAD variants that survive nonce reuse with degraded but non-catastrophic security.
AES-GCM-SIV (RFC 8452) computes a synthetic nonce by hashing the plaintext under a sub-key, then uses that as the nonce for GCM. Two messages encrypted with the same nonce input but different plaintexts produce different synthetic nonces, so the catastrophic break of nonce reuse is replaced by a much milder leak (an attacker can detect that the same plaintext was encrypted twice but can't recover plaintext).
AES-SIV (RFC 5297) is a similar misuse-resistant construction without the GCM-specific structure.
These are slower than plain GCM and more complex to implement. Use them when nonce discipline is genuinely impossible to guarantee — typically in distributed systems, key-wrapping schemes, or formats designed to be re-encrypted and stored. For interactive protocols where you control the nonce generation, plain AEAD plus good nonce discipline is fine.
Hands-on exercise
Exercise 1 — Show two-time pad leakage
"""Demonstrate why nonce reuse leaks plaintext relations."""
import os
KEYSTREAM = os.urandom(64) # pretend this is ChaCha20 output
P1 = b"GET /admin HTTP/1.1\r\nHost: example.com\r\nAuthorization: hunter2"
P2 = b"GET /home HTTP/1.1\r\nHost: example.com\r\nAuthorization: ABCDEF7"
# Pad/truncate so they're equal length and fit the keystream.
n = min(len(P1), len(P2), len(KEYSTREAM))
P1, P2 = P1[:n], P2[:n]
C1 = bytes(p ^ k for p, k in zip(P1, KEYSTREAM))
C2 = bytes(p ^ k for p, k in zip(P2, KEYSTREAM))
# An attacker who has C1 and C2 can compute:
xor_of_plaintexts = bytes(c1 ^ c2 for c1, c2 in zip(C1, C2))
print("XOR of plaintexts (an attacker computes this from C1, C2):")
print(xor_of_plaintexts)
print()
print("Verify: this equals P1 XOR P2:")
print(bytes(p1 ^ p2 for p1, p2 in zip(P1, P2)))
The two outputs match. Anyone who captured both ciphertexts can XOR them to get the XOR of the plaintexts. If the attacker knows or guesses any portion of either plaintext (HTTP headers are partly predictable), they can recover the corresponding portion of the other. With enough samples and structure, near-complete recovery is possible.
This is the entire reason nonce reuse is catastrophic. It's not a small leak; it's recovering plaintexts.
Exercise 2 — AEAD with associated data
"""Show how associated data binds metadata to ciphertext."""
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
import os
key = ChaCha20Poly1305.generate_key() # 32 bytes
aead = ChaCha20Poly1305(key)
nonce = os.urandom(12)
plaintext = b"the actual confidential message"
header = b'{"version":1,"type":"data"}'
ciphertext = aead.encrypt(nonce, plaintext, associated_data=header)
# Normal decrypt: works.
decrypted = aead.decrypt(nonce, ciphertext, associated_data=header)
print("decrypt OK:", decrypted)
# Tamper with the ciphertext: fails.
try:
bad_ct = bytes([ciphertext[0] ^ 1]) + ciphertext[1:]
aead.decrypt(nonce, bad_ct, associated_data=header)
except Exception as e:
print("ciphertext tamper detected:", type(e).__name__)
# Tamper with the associated data: also fails.
try:
bad_header = b'{"version":2,"type":"data"}'
aead.decrypt(nonce, ciphertext, associated_data=bad_header)
except Exception as e:
print("AD tamper detected:", type(e).__name__)
The first decrypt succeeds. Both tampering attempts fail. The AD is bound to the ciphertext: changing it invalidates the tag, even though the AD itself wasn't encrypted. This is the structural protection that lets protocols put visible-but-authenticated metadata (record types, sequence numbers, version fields) in associated data.
Common misconceptions
"A stream cipher is just weaker than a block cipher." They're different design families. ChaCha20 is a stream cipher and is one of the most secure ciphers in use; AES is a block cipher and is also one of the most secure ciphers in use. The block-vs-stream distinction is about how they operate on data; security is independent of that and depends on correct construction and use.
"Nonce reuse is a minor hygiene issue." For stream-like AEAD constructions (GCM, ChaCha20-Poly1305) it's a complete protocol break. The attacker can recover plaintexts and forge tags. There is no acceptable amount of nonce reuse in these schemes.
"Associated data is optional metadata nobody checks." It is checked — that's the whole point. Failed AD verification rejects the entire message. AD is the place to put protocol-critical context (TLS record headers, IP addresses, sequence numbers) that must be bound to the ciphertext but doesn't need to be encrypted.
"GCM provides authentication, so any implementation is fine." GCM with bad nonce discipline is broken regardless of implementation quality. GCM with side-channel-leaky multiplications can leak the key. GCM in protocols that don't authenticate the AD properly can be confused. The cipher suite name doesn't fix the implementation.
"Encrypt-then-MAC, MAC-then-encrypt, and AEAD are interchangeable in practice." They are different APIs with different misuse risks. Encrypt-then-MAC is the only safe ordering of separately-implemented primitives, but it's also the one engineers most often get wrong. AEAD bundles the two operations into one call and removes the ordering question entirely. Modern protocols all use AEAD.
Further reading
- RFC 5116 — An Interface and Algorithms for Authenticated Encryption. The clean AEAD interface specification. All modern AEAD constructions follow this shape.
- RFC 8439 — ChaCha20 and Poly1305 for IETF Protocols. The standard for ChaCha20-Poly1305. Includes the test vectors and a clear algorithm description.
- NIST SP 800-38D — Galois/Counter Mode (GCM). AES-GCM specification.
- RFC 8452 — AES-GCM-SIV. The misuse-resistant variant for cases where nonce discipline can't be guaranteed.
- Daniel J. Bernstein, ChaCha, a variant of Salsa20, 2008. The original paper. Short, readable.
- Martin Albrecht et al., Plaintext-Recovery Attacks Against TLS 1.0 — POODLE and BEAST family, various IEEE conferences 2010–2014. A series of papers showing what happens to CBC modes without good integrity. Worth reading once for "this is what plain encryption gets you" intuition.
- Lucky Thirteen, padding oracles, and the reasons CBC was eventually deprecated — search the IETF archives. The bug-history of TLS's pre-AEAD era is a good lesson in why "encrypt then MAC carefully" lost to "use AEAD."
The next module — Hash functions and message authentication — picks up from MACs and looks at the broader hash-function ecosystem: SHA-2, SHA-3, BLAKE3, HMAC, and where each fits in modern protocol design.
// related reading
Decoy routing and refraction networking
Telex, TapDance, Slitheen, and Conjure: how cooperative infrastructure on ordinary network paths changes the evasion game.
Hysteria and QUIC-based transports
Why QUIC became an evasive substrate, how Hysteria uses it, and what QUIC-based camouflage still leaks to modern detectors.
Operational anonymity for engineers
Compartmentation, browser discipline, transport choice, telemetry minimization, and how to turn anonymity theory into a survivable daily operating model.