Ciphers

XChaCha20

ChaCha20 with a 24-byte nonce you can pick at random. HChaCha20 turns the key and 16 nonce bytes into a subkey. UTF-8 in, hex out, no padding.
IDxchacha2054 / 55stream · arx
Cipher / ARX

XChaCha20

ChaCha20 with a 24-byte nonce, so nobody has to count.

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 4294967295
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("xchacha20")
CLIciphers encode xchacha20 'ATTACK AT DAWN' --key 808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f --nonce 404142434445464748494a4b4c4d4e4f5051525354555658
Tryplayground with the sample above
Kinrabbit, salsa20, xsalsa20, chacha20 +1

Remember how XSalsa20 stretched the Salsa20 nonce? XChaCha20 does exactly that to ChaCha20. libsodium has it, and draft-irtf-cfrg-xchacha by Scott Arciszewski writes it down.

Why bother? Twelve random bytes of nonce collide after about 2^48 messages. Sounds like a lot until you run a busy server for a few years. Twenty-four bytes, and you can stop counting.

How it runs

Two steps, both cheap. HChaCha20 runs the ChaCha rounds over the key and the first 16 nonce bytes, where counter and nonce would sit. Nothing gets added back. The first and last rows of the result are a fresh 256-bit subkey.

Then ChaCha20 runs under that subkey. Its 12-byte nonce is four zero bytes and the last 8 bytes of yours.

Keys, nonce and counter

The key is 64 hex digits. nonce is 48 hex digits and required. counter is the ChaCha20 block counter, 0 to 4294967295, default 0.

ts
const xchacha = create("xchacha20");
const key = "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f";
const nonce = "404142434445464748494a4b4c4d4e4f5051525354555658";
xchacha.encode("ATTACK AT DAWN", { key, nonce }).text; // "50659adb696b8e4c33e8d71d20c7"
xchacha.decode("50659adb696b8e4c33e8d71d20c7", { key, nonce }).text; // "ATTACK AT DAWN"

That's the key and nonce from the draft's examples. Spot the 58 at the end? It's not a typo here. The draft really skips 57.

The draft has two examples, one from counter 0 and one from counter 1, and both decrypt the same text. libsodium's crypto_stream_xchacha20 starts at 0. Its AEAD starts at 1, same as ChaCha20-Poly1305.

xchacha20(data, key, nonce, counter) from @agntn/ciphers/chacha does the same on Uint8Array.

Checked against

The HChaCha20 vector from section 2.2.1 of draft-irtf-cfrg-xchacha-03, and the dhole text from appendix A.2 at both counters. Every byte of both ciphertexts, by digest.

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

Random nonces are fine now. Reused ones still aren't, and there's still no tag. Plain TypeScript, puzzles only.