symmetric · level 6

Stream Ciphers

ChaCha20, RC4, and the keystream-XOR mental model.

200 XP

Stream Ciphers

A block cipher (like AES) operates on a fixed-size chunk and needs a mode of operation to handle anything longer. A stream cipher is the other family: it produces a long pseudorandom keystream and you XOR it with your plaintext, byte by byte, for as long as your message lasts.

The mental model is one line: stream cipher = key+nonce-keyed PRNG. Encryption and decryption are the same XOR, since (P ⊕ K) ⊕ K = P.

ChaCha20 — the modern default

ChaCha20 is Daniel Bernstein's design, a refinement of Salsa20. It's the stream cipher behind:

  • TLS 1.3TLS_CHACHA20_POLY1305_SHA256 is one of the three mandatory cipher suites.
  • WireGuard — uses ChaCha20-Poly1305 exclusively, no negotiation.
  • OpenSSH — preferred AEAD when both ends support it.
  • Signal — the data layer of Signal's protocol uses ChaCha20.

Why it won:

  • Constant-time on every CPU. No data-dependent branches, no S-box lookups. AES requires hardware (AES-NI, ARM crypto extensions) to be fast and constant-time; on a CPU without those, AES is either slow or vulnerable to cache-timing attacks. ChaCha20 doesn't care.
  • Mobile-friendly. Older ARM chips lack AES instructions. ChaCha20 runs at full speed on raw integer ALU.
  • Simple structure. ARX (Add, Rotate, XOR) — three operations, no Galois fields, no AES tables. A few hundred lines of audit-able code.

The parameters:

key:     256 bits (32 bytes)
nonce:    96 bits (12 bytes)   ← unique per message
counter:  32 bits (4 bytes)    ← incremented per 64-byte block
keystream block: 64 bytes per (key, nonce, counter) tuple

RC4 — the deprecated ancestor

RC4 was the dominant stream cipher of the 90s and 2000s. Tiny, fast, easy to implement. It's also fundamentally broken:

  • Biased keystream. The first few bytes have detectable statistical biases. WEP shipped these directly to the wire — letting an attacker recover the key from enough captured traffic.
  • Related-key weaknesses. RC4's key schedule leaks information about the key.
  • No modern AEAD pairing. RC4 was always used with separate MACs, often badly composed.

RFC 7465 banned RC4 from TLS in 2015. If you see RC4 anywhere today, it's a bug.

The keystream-XOR mental model

      plaintext:   D E A R   J O H N
       keystream:  k0 k1 k2 k3 k4 k5 k6 k7
                   XOR
      ciphertext:  c0 c1 c2 c3 c4 c5 c6 c7

Decryption:

      ciphertext:  c0 c1 c2 c3 c4 c5 c6 c7
       keystream:  k0 k1 k2 k3 k4 k5 k6 k7
                   XOR
       plaintext:  D E A R   J O H N

Same operation, both directions. The keystream depends only on (key, nonce, counter) — it knows nothing about your plaintext. That's the source of both the strength (no plaintext-leak, no IV chaining like CBC) and the danger (re-using a (key, nonce) pair is catastrophic).

Why nonce uniqueness is non-negotiable

If you encrypt two messages with the same (key, nonce):

  ciphertext_1 = plaintext_1 ⊕ keystream
  ciphertext_2 = plaintext_2 ⊕ keystream

  ciphertext_1 ⊕ ciphertext_2
   = (plaintext_1 ⊕ keystream) ⊕ (plaintext_2 ⊕ keystream)
   = plaintext_1 ⊕ plaintext_2

The keystream cancels. You're left with the XOR of the two plaintexts. Any structure in either plaintext (English text, JSON keys, fixed-width fields) becomes a crib that an attacker uses to recover both.

This is the same failure mode as the WW2 Venona cables (Soviet pad-reuse) and the WEP attacks (catastrophic IV-reuse on busy networks). It's so easy to misuse that modern libraries refuse to expose raw stream ciphers — they bundle nonce generation and a MAC together as AEAD (authenticated encryption with associated data) and never let the application touch the keystream directly.

XChaCha20 — the bigger nonce

A 96-bit ChaCha20 nonce is 2^96 possible values. Pick them randomly and the birthday bound says you'll see a collision around 2^48 messages — too few for a long-lived key.

XChaCha20 extends the nonce to 192 bits using an HSalsa20-style construction (HChaCha20(key, first_16_bytes_of_nonce) produces a sub-key, and the remaining 24 bytes become the standard ChaCha20 nonce). 192 random bits will never collide for any practical workload. Use XChaCha20-Poly1305 anywhere you can't guarantee strict counter-based nonces (e.g., distributed systems, file-at-rest encryption).

What this means for you

  • Use ChaCha20-Poly1305 (or XChaCha20-Poly1305 if you don't have a clean counter). Always paired with Poly1305 for integrity. Never raw ChaCha20.
  • Treat nonces as a uniqueness invariant. Counter-based for in-order systems; random + 192-bit for anything else.
  • Don't reach for AES-CTR if your platform lacks AES-NI. ChaCha20 will be faster and safer.
  • Don't write your own. libsodium, noble-ciphers, PyNaCl, and Web Crypto all expose AEAD interfaces that handle nonces, MACs, and counter-rollover correctly.

What it looks like under the hood

The ChaCha20 quarter round, repeated 80 times to produce one 64-byte keystream block:

a += b; d ^= a; d <<<= 16
c += d; b ^= c; b <<<= 12
a += b; d ^= a; d <<<=  8
c += d; b ^= c; b <<<=  7

Four 32-bit registers, four operations, four rotations. A few hundred lines of constant-time code becomes the cipher protecting most TLS-mobile, WireGuard, and OpenSSH connections you'll touch. That's modern crypto: small, simple, audit-able, and used everywhere.

Tools in the wild

4 tools
  • libsodiumfree tier

    The reference 'crypto for the rest of us' library. ChaCha20-Poly1305 with sane defaults.

    library
  • Pure-JS audited implementation of ChaCha20-Poly1305 and AES-GCM. Tiny, no native deps.

    library
  • WireGuardfree tier

    Modern VPN. Uses ChaCha20-Poly1305 exclusively for the data plane — no negotiation.

    service
  • PyNaClfree tier

    Python bindings to libsodium. SecretBox does authenticated stream encryption in two lines.

    library