Ciphers

ChaCha20-Poly1305

The RFC 8439 AEAD of TLS 1.3 and WireGuard. ChaCha20 encrypts, Poly1305 tags, and decoding refuses a changed byte. Hex out with a 16-byte tag.
IDchacha20-poly130555 / 55stream · arx
Cipher / ARX

ChaCha20-Poly1305

ChaCha20 with a Poly1305 tag, so a changed byte gets caught.

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
24 hex digits (12 bytes), never reused under one key
aadstring
optional
Associated data in hex: covered by the tag, not encrypted
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("chacha20-poly1305")
CLIciphers encode chacha20-poly1305 'ATTACK AT DAWN' --key 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f --nonce 070000004041424344454647
Tryplayground with the sample above
Kinrabbit, salsa20, xsalsa20, chacha20 +1

Plain ChaCha20 decrypts whatever you give it. Flip a bit, get a flipped bit, nobody complains. This one complains. It's ChaCha20 with Poly1305 on top, both by Bernstein, put together in RFC 8439. TLS 1.3 has it as a cipher suite, and WireGuard runs every packet through it.

How it runs

Block 0 of the ChaCha20 keystream isn't used for the text. Its first 32 bytes are a one-time Poly1305 key, fresh for every nonce. The text gets encrypted from block 1.

Then Poly1305 reads the associated data, padded to 16 bytes, then the ciphertext, padded the same way, then both lengths. It's a polynomial evaluated modulo 2^130 - 5, which is where the name comes from. Out comes a 16-byte tag, glued to the end of the ciphertext.

decode recomputes the tag first. Wrong tag, no text, just an error.

The options

The key is 64 hex digits. nonce is 24 hex digits and required. aad is optional hex that the tag covers but nobody encrypts, like a packet header a router has to read. No counter here. The RFC fixes it at 1.

ts
const aead = create("chacha20-poly1305");
const key = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
const nonce = "070000004041424344454647";
const aad = "46524f4d3a2048512e"; // "FROM: HQ." in hex
aead.encode("ATTACK AT DAWN", { key, nonce, aad }).text;
// "08b82563520a4c81cdc90976213c" + "cdabbd7363a02df0262f5ad3936c7d87"

Fourteen bytes of ciphertext, sixteen of tag. Node's createCipheriv("chacha20-poly1305") with the same setAAD gives the same bytes. Leave aad out and the first fourteen stay put, only the tag changes. The text never touches aad.

Try to cheat

DAWN to DUSK is 00140405 XORed into the last four bytes. Plain ChaCha20 would take that. And this one?

ts
aead.decode("08b82563520a4c81cdc909622539cdabbd7363a02df0262f5ad3936c7d87", { key, nonce, aad });
// CipherError: [chacha20-poly1305] Tag does not match: wrong key, nonce or aad, or the ciphertext was changed

Nice try. The bytes under the tag do decrypt to ATTACK AT DUSK, but decode never shows them. The tag was computed over DAWN, and a new one needs the key. A wrong nonce or a different aad ends the same way.

Checked against

RFC 8439 section 2.8.2, the sunscreen text sealed byte for byte, and appendix A.5, a real message that opens to text with curly quotes. Poly1305 alone gets section 2.5.2 and test vectors #5 to #11 from appendix A.3. Those are the nasty ones, built to break carries and reductions.

What the tag can't fix

A reused nonce. Same key, same nonce, same keystream, so XOR of two ciphertexts is still XOR of the texts. Worse, the one-time Poly1305 key repeats too. Two tags under one key let an attacker forge new ones. The RFC says it plainly: the nonce "MUST not be repeated for the same key".

A key that isn't 64 hex digits is an InvalidOptionError, and so is a nonce that isn't 24 or an aad that isn't whole bytes of hex. A missing key or nonce is a MissingOptionError. On decode, a ciphertext shorter than the tag or a tag that doesn't match is a CipherError.

Plain TypeScript, BigInt in Poly1305, not constant time. Great for seeing what a tag buys you. Terrible for anything real.