Glossary

Salsa20

Salsa20 is a stream cipher designed by Daniel Bernstein in 2005 that expands a 256-bit key and an 8-byte nonce into a keystream of up to 2^70 bytes. It was submitted to eSTREAM, the ECRYPT Stream Cipher Project. Bernstein later modified it into ChaCha, and the IETF cipher in RFC 8439 descends from that. You will mostly meet Salsa20 inside NaCl-style libraries as XSalsa20, the same cipher with a 192-bit nonce.

How it works

Salsa20 is a hash function used in counter mode. The hash takes a 64-byte input and returns a 64-byte output. To make keystream block number i, the input is built from the key, the nonce, the block number and four constants, and the output is XORed with 64 bytes of plaintext. The input is 16 little-endian 32-bit words:

  • Constants: the ASCII text "expand 32-byte k" split into four words that sit on the diagonal of the 4 by 4 grid.
  • Key: 32 bytes (eight words). The specification also allows a 16-byte key, which uses the constant "expand 16-byte k".
  • Nonce: 8 bytes, called v. It must be unique per message under one key.
  • Block number: 8 bytes, called i, counting from 0.

The hash applies ten double rounds, which is 20 rounds, of quarter round operations built only from addition modulo 2^32, XOR and rotations by 7, 9, 13 and 18 bits. Then it adds the original input words back. Bernstein deliberately left out S-boxes, which are lookup tables.

Each nonce gives 2^64 blocks of 64 bytes, so a single nonce can encrypt up to 2^70 bytes. Bernstein also defines reduced-round Salsa20/12 and Salsa20/8, and recommends Salsa20/20 for typical use.

The program below implements the cipher in plain Python. It encrypts a short message, prints the ciphertext, then decrypts it with the same key and nonce.

import struct
def rotl(x, n): return ((x << n) & 0xffffffff) | (x >> (32 - n))
def core(inp):
    x = list(struct.unpack('<16I', inp)); z = x[:]
    def qr(a, b, c, d):
        z[b] ^= rotl((z[a] + z[d]) & 0xffffffff, 7)
        z[c] ^= rotl((z[b] + z[a]) & 0xffffffff, 9)
        z[d] ^= rotl((z[c] + z[b]) & 0xffffffff, 13)
        z[a] ^= rotl((z[d] + z[c]) & 0xffffffff, 18)
    for _ in range(10):
        qr(0,4,8,12); qr(5,9,13,1); qr(10,14,2,6); qr(15,3,7,11)
        qr(0,1,2,3); qr(5,6,7,4); qr(10,11,8,9); qr(15,12,13,14)
    return struct.pack('<16I', *[(a + b) & 0xffffffff for a, b in zip(z, x)])
def block(key, nonce, i):
    s = b'expand 32-byte k'
    return core(s[0:4] + key[:16] + s[4:8] + nonce + struct.pack('<Q', i) + s[8:12] + key[16:] + s[12:16])
def crypt(key, nonce, data):
    out = bytearray()
    for i in range(0, len(data), 64):
        out += bytes(a ^ b for a, b in zip(data[i:i+64], block(key, nonce, i // 64)))
    return bytes(out)
key = bytes(range(32)); nonce = bytes(range(8))
ct = crypt(key, nonce, b"attack at dawn")
print(ct.hex()); print(crypt(key, nonce, ct)); print(len(ct))
4fd97b3e7b3c09afa252d7c85f8a
b'attack at dawn'
14

What is XSalsa20?

XSalsa20 is Salsa20 with a 192-bit (24-byte) nonce instead of a 64-bit one. Bernstein's paper builds it by running a hash step on the key and the first part of the nonce to get a fresh key, then using Salsa20 with the rest. The long nonce is large enough that picking nonces at random is safe in practice, which is the reason libraries use it.

Is Salsa20 still secure?

Yes, no attack on full 20-round Salsa20 faster than a 256-bit brute-force search is known in Bernstein's own survey. The published attacks reach only reduced rounds: roughly 2^249 operations against Salsa20/8, per that paper. Treat that as the designer's summary, and check recent cryptanalysis before relying on reduced-round variants.

Common pitfalls

  • Reusing a nonce: two messages under the same key and nonce get the same keystream. XORing the ciphertexts reveals the XOR of the plaintexts.
  • Assuming it authenticates: Salsa20 only encrypts. Flipping a ciphertext bit flips the matching plaintext bit, so add a MAC such as Poly1305.
  • Choosing an 8-byte random nonce: a random 64-bit value repeats too soon at scale. Use a counter, or XSalsa20 with its 24-byte nonce.
  • Confusing it with ChaCha20: the two have different state layouts and rotation amounts, so ciphertext from one cannot be decrypted by the other.
  • Using the 16-byte key form by default: it exists in the spec, but the 32-byte key is the one Bernstein prefers.

Related terms

  • ChaCha20 — the later variant that changes the round function and fills the block differently.
  • Poly1305 — a one-time authenticator commonly paired with Salsa20 family ciphers.
  • AES — the block cipher that Salsa20 is usually compared with on speed.
  • AES-CTR — the block cipher counter mode that does the same job of making a keystream.

See also