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.
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:
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
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.
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.