ChaCha and Salsa20¶
Bernstein's stream ciphers: 20 rounds of add, rotate and XOR turn a key, a nonce and a counter into keystream. Fast in plain software, with no tables to leak timing.
Status and standards: see Symmetric ciphers in the main table.
What it is¶
Daniel J. Bernstein designed Salsa20 in 2005 for eSTREAM, the EU stream cipher project, and its 12-round version, Salsa20/12, is in the final eSTREAM portfolio (2008). ChaCha (2008) is a variant that mixes more per round at the same cost. RFC 8439 (2018, replacing RFC 7539 from 2015) defines the IETF version of ChaCha20. Protocols use it inside ChaCha20-Poly1305, since bare ChaCha20 only encrypts.
How it works¶
The state is a 4×4 grid of 32-bit words: four constants ("expand 32-byte k" in ASCII), eight key words, and four words of counter and nonce. The quarter-round mixes four of them:
a += b; d ^= a; d <<<= 16;
c += d; b ^= c; b <<<= 12;
a += b; d ^= a; d <<<= 8;
c += d; b ^= c; b <<<= 7;
Rounds alternate between the four columns and the four diagonals, 20 rounds in all. Then the input is added back, so the rounds can't be run backwards to the key. The result is a 64-byte keystream block. The counter goes up by one per block, and the ciphertext is the plaintext XOR the keystream.
Salsa20 is the same idea, but its quarter-round touches each word once (rotations 7, 9, 13, 18). ChaCha updates each word twice for the same number of operations, so changes spread faster.
| Version | Key | Nonce | Counter | Data per nonce |
|---|---|---|---|---|
| Salsa20, original ChaCha | 256-bit | 64-bit | 64-bit | 2⁷⁰ bytes |
| ChaCha20, RFC 8439 | 256-bit | 96-bit | 32-bit | 256 GB |
| XChaCha20 | 256-bit | 192-bit | 32-bit | 256 GB |
XChaCha20 stretches the nonce so random nonces are safe: 2⁸⁰ messages before a 2⁻³² chance of a repeat. HChaCha20 hashes the key and the first 16 nonce bytes into a new key, and ChaCha20 runs under it with the last 8 bytes. Its CFRG draft expired in 2020 and is marked dead, so there is no RFC, but libsodium, WireGuard, Tink and Go's x/crypto ship it. NaCl's secretbox uses XSalsa20, the same trick on Salsa20.
Why it is fast without AES hardware¶
- Only 32-bit adds, XORs and fixed rotations: cheap on any CPU, and no lookup tables, so no key-dependent timing. Table-based AES in software leaks through the cache.
- The quarter-rounds in a round are independent, and so are the blocks, so SIMD units (SSE, AVX2, NEON) run several at once.
- RFC 8439 quotes 2014 numbers: on a phone chip (OMAP 4460) ChaCha20-Poly1305 did 75.3 MB/s and AES-128-GCM 24.1 MB/s. With AES-NI on a Sandy Bridge Xeon, AES-GCM won, 900 to 500 MB/s.
The Linux kernel RNG¶
Since Linux 4.8 (2016), /dev/urandom and getrandom() output comes from
ChaCha20. In today's drivers/char/random.c, entropy is hashed into an
input pool with BLAKE2s (SHA-1 before Linux 5.17), and the hash becomes the
ChaCha20 key. Each CPU has its own generator, reseeded about once a minute.
Every request first runs one block and overwrites the key with its first 32
bytes ("fast key erasure"), so reading the state later can't recover
earlier output. OpenBSD's arc4random moved from RC4 to
ChaCha20 in OpenBSD 5.5 (2014).
When to use it¶
- Through an AEAD: ChaCha20-Poly1305, or XChaCha20-Poly1305 for random nonces. See AEAD.
- On CPUs without AES instructions, such as older phones and small ARM chips. It is fine everywhere else too.
- Salsa20 only to stay compatible with NaCl-era code.
Example (Python)¶
The RFC 8439 block function, checked against the test vector in section 2.3.2, then used as a stream cipher. A teaching copy: slow, and Python makes no timing promises.
import struct
M = 0xffffffff
def rotl(x, n):
return (x << n | x >> (32 - n)) & M
def qr(s, a, b, c, d): # the quarter-round, RFC 8439 section 2.1
s[a] = (s[a] + s[b]) & M; s[d] = rotl(s[d] ^ s[a], 16)
s[c] = (s[c] + s[d]) & M; s[b] = rotl(s[b] ^ s[c], 12)
s[a] = (s[a] + s[b]) & M; s[d] = rotl(s[d] ^ s[a], 8)
s[c] = (s[c] + s[d]) & M; s[b] = rotl(s[b] ^ s[c], 7)
def block(key, counter, nonce): # 32-byte key, 12-byte nonce -> 64 bytes
init = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
init += [*struct.unpack("<8I", key), counter, *struct.unpack("<3I", nonce)]
s = init[:]
for _ in range(10): # 10 double rounds = 20 rounds
qr(s, 0, 4, 8, 12); qr(s, 1, 5, 9, 13); qr(s, 2, 6, 10, 14); qr(s, 3, 7, 11, 15)
qr(s, 0, 5, 10, 15); qr(s, 1, 6, 11, 12); qr(s, 2, 7, 8, 13); qr(s, 3, 4, 9, 14)
return struct.pack("<16I", *[(x + y) & M for x, y in zip(s, init)])
def chacha20(key, counter, nonce, data): # encrypting and decrypting are the same
out = bytearray()
for i in range(0, len(data), 64):
ks = block(key, counter + i // 64, nonce)
out += bytes(a ^ b for a, b in zip(data[i:i + 64], ks))
return bytes(out)
key = bytes(range(32)) # RFC 8439 section 2.3.2 test vector
nonce = bytes.fromhex("000000090000004a00000000")
want = ("10f1e7e4d13b5915500fdd1fa32071c4c7d1f4c733c068030422aa9ac3d46c4e"
"d2826446079faa0914c2d705d98b02a2b5129cd1de164eb9cbd083e8a2503c4e")
print(block(key, 1, nonce).hex() == want) # True
ct = chacha20(key, 1, nonce, b"see you at noon")
print(ct.hex()) # 639482c4a8542c35317bfd71cc4f1f
print(chacha20(key, 1, nonce, ct)) # b'see you at noon'
Security notes¶
- Never reuse a nonce with the same key. The keystream repeats, and XORing two ciphertexts gives the XOR of the plaintexts.
- Random 96-bit nonces are good for about 2³² messages per key; beyond that use a counter or XChaCha20. One nonce covers at most 256 GB in the RFC version.
- Alone it is malleable: flip a ciphertext bit and the same plaintext bit flips. Always add Poly1305 or another MAC.
- The original (64-bit nonce) and IETF (96-bit nonce) versions give different keystreams. Check which one a library means.
- No attack comes near 20 rounds. Bernstein's list of published attacks reaches 7 rounds of ChaCha, at about 2¹⁴² operations for the best one (2026), and 8 rounds of Salsa20.
See also¶
- ChaCha20-Poly1305: the AEAD built on it
- AES: the block cipher it competes with
- Retired ciphers: RC4, the stream cipher it replaced
References¶
- Bernstein, "ChaCha, a variant of Salsa20" (2008)
- Bernstein, "The Salsa20 family of stream ciphers" (2007) / Salsa20 page with the list of attacks
- eSTREAM portfolio (archived)
- RFC 8439: ChaCha20 and Poly1305 for IETF Protocols (Sections 2.1–2.4, Appendix B) / RFC 7539
- draft-irtf-cfrg-xchacha-03: XChaCha
- Linux
drivers/char/random.c/ ChaCha20 CRNG commit (Linux 4.8) / BLAKE2s commit (Linux 5.17) - OpenBSD 5.5 release notes