XChaCha20-Poly1305 Encryption Explained

The cipher behind the encryption, and why it was chosen.

What it is

XChaCha20-Poly1305 is an authenticated encryption with associated data (AEAD) construction. It does two things in a single operation: encrypts data so it can't be read, and authenticates it so tampering is detected.

The name breaks down into two parts. XChaCha20 is the stream cipher that does the encryption. Poly1305 is the message authentication code (MAC) that provides integrity. Together they guarantee that ciphertext is both confidential and unmodified.

ChaCha20 was designed by Daniel J. Bernstein as a refinement of his earlier Salsa20 cipher. The IETF standardised ChaCha20-Poly1305 in RFC 8439. The "X" variant extends the nonce from 96 bits to 192 bits, which is the version libsodium implements as its default AEAD cipher.

How it works

ChaCha20 is a stream cipher. It takes a 256-bit key and a nonce, and generates a pseudorandom keystream. The plaintext is XORed with this keystream to produce ciphertext. Decryption is the same operation: XOR the ciphertext with the same keystream to recover the plaintext.

The cipher operates on 64-byte blocks. Each block goes through 20 rounds of quarter-round operations (add, rotate, XOR) on a 4×4 matrix of 32-bit words. The state is initialised with the key, the nonce, a block counter, and a constant.

After encryption, Poly1305 computes a 128-bit authentication tag over the ciphertext and any additional authenticated data (AAD). The tag is appended to the ciphertext. Before decryption, the recipient recomputes the tag. If it doesn't match, decryption is rejected. No partial output, no garbage, just a clear failure.

The nonce advantage

This is the main reason XChaCha20 exists. The standard IETF ChaCha20-Poly1305 uses a 96-bit (12-byte) nonce. XChaCha20 extends it to 192 bits (24 bytes).

Why this matters: if you ever reuse a nonce with the same key, the security of a stream cipher breaks completely. With a 96-bit nonce, the birthday bound means you should generate no more than about 232 (roughly 4 billion) random nonces per key before collision risk becomes non-negligible. That sounds like a lot, but it's a constraint you have to actively track.

With a 192-bit nonce, the birthday bound is 296. You can generate random nonces freely without tracking or coordination, and the probability of a collision is negligible for any practical workload. For a system where every secret gets a fresh random nonce, this property is exactly what you want.

XChaCha20 achieves this through HChaCha20, a key derivation step that takes the original 256-bit key and the first 128 bits of the extended nonce, producing a subkey. The remaining 64 bits of the nonce plus the subkey feed into standard ChaCha20. The construction is simple and well-analysed.

XChaCha20-Poly1305 vs AES-GCM

AES-GCM is the most widely deployed AEAD cipher, used in TLS, disk encryption, and most cloud services. It's a strong cipher. But the two constructions have different properties that matter depending on the context.

Property XChaCha20-Poly1305 AES-256-GCM
Key size 256 bits 256 bits
Nonce size 192 bits 96 bits
Nonce-misuse impact Catastrophic (stream cipher) Catastrophic (GCM forgery + key recovery)
Safe random nonces Yes (192-bit nonce) Risky at scale (96-bit nonce)
Hardware acceleration Not needed (fast in software) Needs AES-NI for competitive speed
Browser performance Consistent across devices Slow without AES-NI (mobile, older CPUs)
Timing side-channels Resistant by design (ARX) Requires AES-NI to avoid table lookups

Both are nonce-misuse-catastrophic, meaning a reused nonce breaks the security guarantees. The difference is that XChaCha20's 192-bit nonce makes random generation safe without coordination, while AES-GCM's 96-bit nonce requires careful nonce management or a counter-based scheme.

AES-GCM's performance depends on hardware. On CPUs with AES-NI instructions, it's very fast. Without them, it falls back to software table lookups that are slower and potentially vulnerable to cache-timing attacks. ChaCha20 uses only add, rotate, and XOR operations (ARX), so it runs at consistent speed on any CPU and is inherently timing-safe.

For a browser-based tool that runs on phones, tablets, laptops, and desktops with varying hardware support, consistent software performance without timing side-channels is the right trade-off.

Libsodium

Secret.Broker doesn't implement XChaCha20-Poly1305 from scratch. It uses libsodium, a well-audited, widely-used cryptographic library originally derived from Daniel J. Bernstein's NaCl (Networking and Cryptography library).

Libsodium provides XChaCha20-Poly1305 as its recommended AEAD construction through the crypto_aead_xchacha20poly1305_ietf API. The browser uses libsodium.js, a WebAssembly/JavaScript port that runs the same code in the browser.

Using a well-tested library matters. Cryptographic implementations are notoriously difficult to get right. Subtle bugs in constant-time operations, nonce generation, or memory handling can silently destroy the security properties. Libsodium handles these details.

How Secret.Broker uses it

For each secret, the browser generates a fresh 256-bit random key using libsodium's secure random number generator. That key goes through Argon2id key derivation with a fresh 16-byte salt. The derived key feeds into XChaCha20-Poly1305 with a fresh 24-byte random nonce. The current domain is included as additional authenticated data, binding the ciphertext to secret.broker specifically.

The encrypted payload format:

[1 byte version] [16 bytes salt] [24 bytes nonce] [ciphertext + 16 bytes auth tag]

The version byte allows the cipher or parameters to change in the future without breaking old links. The protocol page covers the full process including key derivation, domain binding, and memory cleanup.

Further reading