Ciphers

XSalsa20

Salsa20 with a 24-byte nonce, the stream inside NaCl's secretbox. HSalsa20 turns the key and half the nonce into a subkey. UTF-8 in, hex out.
IDxsalsa2052 / 55stream · arx
Cipher / ARX

XSalsa20

Salsa20 with a nonce long enough to pick at random, the NaCl one.

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

Options

keystring
required
64 hex digits, a 256-bit key
noncestring
required
48 hex digits (24 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("xsalsa20")
CLIciphers encode xsalsa20 'ATTACK AT DAWN' --key 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f --nonce 000102030405060708090a0b0c0d0e0f1011121314151617
Tryplayground with the sample above
Kinrabbit, salsa20, chacha20, xchacha20 +1

Eight bytes of nonce is a problem. Pick them at random and two messages share one after about four billion messages. That's the birthday bound, and four billion isn't that many. Bernstein's fix, in "Extending the Salsa20 nonce" from 2008, was XSalsa20. NaCl and libsodium encrypt crypto_secretbox with it.

How it runs

The nonce is 24 bytes. The first 16 go through HSalsa20 with the key. That's the Salsa20 rounds on a state where nonce and counter would be, except nothing gets added back at the end. Eight of the output words become a new 256-bit key.

Then plain Salsa20 runs under that subkey, with the last 8 nonce bytes as its nonce. One extra pass of the rounds per message, and that's the whole cost.

Why does it work? A different nonce gives a different subkey. Collisions now need 24 equal bytes, and that's far enough for random nonces.

Keys, nonce and counter

The key is 64 hex digits, 256 bits only. nonce is 48 hex digits and required. counter works as in Salsa20, default 0.

ts
const xsalsa = create("xsalsa20");
const key = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
const nonce = "000102030405060708090a0b0c0d0e0f1011121314151617";
xsalsa.encode("ATTACK AT DAWN", { key, nonce }).text; // "3de234ee9ed5e607db77992c737d"
xsalsa.decode("3de234ee9ed5e607db77992c737d", { key, nonce }).text; // "ATTACK AT DAWN"

Got a NaCl secretbox? The first 32 bytes of its stream go to the Poly1305 key, and the message starts after them. So counter: 0 gives you the MAC key, and the text sits at byte 32 of block 0. This package doesn't open secretboxes. It gives you the stream to do it by hand.

xsalsa20(data, key, nonce, counter) from @agntn/ciphers/salsa runs the same thing on Uint8Array.

Checked against

Two of the XSalsa20 vectors in Crypto++, which Wei Dai made with naclcrypto-20090308. The 139-byte stream of the first by digest, the other one both ways.

A key that isn't 64 hex digits is an InvalidOptionError, and so is a nonce that isn't 48 or a bad counter. A missing key or nonce is a MissingOptionError.

Random nonces stop being scary here. A reused one is still as bad as in Salsa20, and there's still no tag. Puzzles and learning only.