Ciphers

Salsa20

Bernstein's stream cipher from 2005. Twenty rounds of add, rotate and XOR turn key, nonce and counter into keystream. UTF-8 in, hex out, no padding.
IDsalsa2051 / 55stream · arx
Cipher / ARX

Salsa20

Bernstein's first dance, twenty rounds of add, rotate and XOR.

Keyspace
2^128 or 2^256
Decode
same options back
Works on
UTF-8 or hex, hex out
Family
6 in arx
required 2 / 4

Options

keystring
required
32 or 64 hex digits, a 128 or 256-bit key
noncestring
required
16 hex digits (8 bytes), never reused under one key
counternumber
default 0
Block counter of the first 64 bytes, 0 to 2^53 - 1
bytesstring
default text
What the plain side is: text for UTF-8 text, or hex to read and write hex there, for bytes that are not text

Access

Createcreate("salsa20")
CLIciphers encode salsa20 'ATTACK AT DAWN' --key 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f --nonce 0001020304050607
Tryplayground with the sample above
Kinrabbit, xsalsa20, chacha20, xchacha20 +1

Daniel J. Bernstein published Salsa20 in 2005 and sent it to eSTREAM. The twelve-round version, Salsa20/12, made the final software portfolio in 2008. This one runs all twenty rounds, the way Bernstein specified it.

What's inside? No S-boxes, no tables, no multiplication. Add two 32-bit words, rotate, XOR into a third. That's ARX, and it's the whole cipher. No table lookups means no cache timing leaks, at least in code that bothers to be constant time. This code doesn't.

How it runs

The state is sixteen 32-bit words in a 4×4 grid. Four constants sit on the diagonal, they spell expand 32-byte k. The key fills eight words, the nonce two, the block counter two.

Then twenty rounds. Odd rounds mix the columns, even rounds mix the rows. Each one is four quarter rounds, and each quarter round is four lines of add, rotate by 7, 9, 13 or 18, XOR. At the end the starting state gets added back word by word. Out come 64 bytes of keystream.

The next 64 bytes? Same thing with the counter one higher. The keystream gets XORed into the text, and decrypting is the same XOR again.

Keys, nonce and counter

The key is 64 hex digits, or 32 for the old 128-bit variant. That one copies the key into both halves and swaps the diagonal for expand 16-byte k. Don't pick it for something new. It's here because eSTREAM tested it and some puzzles use it.

nonce is 16 hex digits, 8 bytes, and required. counter says which block the text starts at, default 0. The counter is 64 bits in the cipher. Here it stops at 2^53 - 1, where JavaScript numbers stop being exact. Nobody encrypts that far anyway.

ts
const salsa = create("salsa20");
const key = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
const nonce = "0001020304050607";
salsa.encode("ATTACK AT DAWN", { key, nonce }).text; // "6ff95b1e5b1c098f8252f7e87faa"
salsa.decode("6ff95b1e5b1c098f8252f7e87faa", { key, nonce }).text; // "ATTACK AT DAWN"

Fourteen bytes in, fourteen out. With counter: 1 the same call skips the first 64 bytes of keystream and gives e06e7f189a4f5bccea997a85e0c2. Handy when a puzzle hands you the middle of a stream.

Raw bytes without the hex? salsa20(data, key, nonce, counter) from @agntn/ciphers/salsa takes and returns Uint8Array, and pulls in no registry.

Checked against

The eSTREAM verified vectors, set 1 vector 0 for a 128-bit key and set 3 vector 243 for a 256-bit one, both ways. Crypto++'s vector where the counter starts at 2^32 - 1, so the carry into the high word gets tested too. All 1024 bytes of it, by digest.

The nonce is the whole game

Same key, same nonce, same keystream. Encrypt two texts like that and watch:

ts
salsa.encode("ATTACK AT DAWN", { key, nonce }).text; // "6ff95b1e5b1c098f8252f7e87faa"
salsa.encode("ATTACK AT DUSK", { key, nonce }).text; // "6ff95b1e5b1c098f8252f7fc7baf"

Eleven bytes identical. XOR the two and you get DAWN XOR DUSK, no key needed. Eight bytes of nonce is too short to pick at random safely, so use a counter. Or use XSalsa20, which has 24.

Nothing checks integrity either. Flip a ciphertext bit, the same plaintext bit flips. NaCl pairs Salsa with Poly1305 for exactly that reason.

A key that isn't 32 or 64 hex digits is an InvalidOptionError, and so is a nonce that isn't 16 or a counter that isn't a whole number from 0 to 2^53 - 1. A missing key or nonce is a MissingOptionError. On decode, an odd number of hex digits is a CipherError.

Plain TypeScript, not constant time, no tag. Fun to take apart. Not for secrets.