02 Science — authenticated encryption
Break the seal. Then try to forge it.
Real ChaCha20-Poly1305 runs in this page. Decrypt a message, seal your own, flip one byte — and watch Poly1305 refuse the entire frame.
01Feel — a real sealed frame
Tap to decrypt.
This is a real ciphertext, sealed with the same primitive family the app ships. Your tap runs the actual open() — the resolve you see is the plaintext arriving.
Real ChaCha20-Poly1305 (RFC 8439), compiled from Rust to WebAssembly, running in your browser. Same primitive family as the QERYX protocol; demo build, not the shipped crypto-core module.
02Touch — seal it, then break it
Seal your own. Then attack it.
Write anything — it never leaves this page. Seal it under a fresh 256-bit key, decrypt it back, then flip a single byte and watch the authenticator refuse the whole message rather than return a garbled copy.
Real ChaCha20-Poly1305 (RFC 8439), compiled from Rust to WebAssembly, running in your browser. Same primitive family as the QERYX protocol; demo build, not the shipped crypto-core module.
03Understand — RFC 8439, two primitives
Read the construction under your thumb.
Two primitives, one composition: ChaCha20 turns a key and nonce into keystream; Poly1305 makes the ciphertext unforgeable. RFC 8439 fixes how they join.
a += b; d ^= a; d <<<= 16;
c += d; b ^= c; b <<<= 12;
a += b; d ^= a; d <<<= 8;
c += d; b ^= c; b <<<= 7;
Add, rotate, xor — on a 4×4 state of 32-bit words, 20 rounds per block. Three operations every CPU runs in constant time: no lookup tables, no data-dependent branches, nothing for a timing attack to read.
tag = ( ( m₁·rⁿ + m₂·rⁿ⁻¹ + … + mₙ·r ) mod 2¹³⁰ − 5 ) + s mod 2¹²⁸
The message is read as coefficients of a polynomial, evaluated at a secret point r over the prime field 2¹³⁰−5, then offset by s. Both r and s are used for exactly one message.
(r, s) = first ChaCha20 keystream block of (key, nonce) ct = plaintext ⊕ ChaCha20(key, nonce, counter ≥ 1) tag = Poly1305( r, s, AAD ‖ ct ‖ len(AAD) ‖ len(ct) ) open(): recompute tag, compare in constant time — refuse on any mismatch
The tag covers the ciphertext and the associated data. open() verifies before it decrypts — a tampered frame yields a refusal, never plaintext.
| Parameter | Value | Source |
|---|---|---|
| Key | 256 bits | RFC 8439 |
| Nonce | 96 bits | RFC 8439 |
| Tag | 128 bits | RFC 8439 |
| Cipher rounds | 20 | RFC 8439 |
| Poly1305 field | prime 2¹³⁰ − 5 | RFC 8439 |
Why refuse instead of returning garbled text?
Garbled output leaks structure and invites oracles: an attacker who can watch you process corrupted plaintext learns things. An AEAD returns exactly two outcomes — the true plaintext, or a refusal. The flip-a-byte demo above is that property, live.
What breaks if a nonce repeats under one key?
Everything. Two messages under the same (key, nonce) share keystream, which exposes their xor — and reuse also exposes the Poly1305 key, enabling forgeries. This is the sharpest edge of the construction; the demo draws a fresh random key and nonce for every seal, and QERYX derives fresh message keys through Q-Ratchet so the pair never repeats.
Why a software-friendly cipher?
ChaCha20 runs in constant time on every CPU, with no lookup tables and no special instructions required — the same speed and the same timing profile on a flagship phone and a ten-year-old handset. The AES-256-GCM suite is implemented and reserved as suite 0x0002, staged for rollout.
- Y. Nir, A. Langley, "ChaCha20 and Poly1305 for IETF Protocols," RFC 8439 (2018)
- D. J. Bernstein, "ChaCha, a variant of Salsa20" (2008)
- D. J. Bernstein, "The Poly1305-AES message-authentication code," FSE (2005)
04In QERYX — one key per message, Q-Ratchet derived
Follow the seal into the protocol.
Every message frame is sealed with ChaCha20-Poly1305 (RFC 8439) under a key that exists for that message alone — Q-Ratchet derives a fresh one each time from the X25519 + ML-KEM-1024 hybrid agreement.
The relay carries the sealed frame and cannot open it: the key never leaves the two devices. What you just did to a tampered byte is what every QERYX client does to a tampered message — refuse it whole.
05The record — the questions we get asked hardest
The honest questions.
Is this demo the app's crypto?
It is real ChaCha20-Poly1305 (RFC 8439), compiled from Rust to WebAssembly and self-tested against the RFC's test vector before first use — the same primitive family as the QERYX protocol, as a demo build rather than the shipped crypto-core module. The label under each demo states exactly this.
Does my message leave the page?
No. Key, nonce, plaintext, and ciphertext live and die in this tab. The playground makes no network request of any kind.
What happens on a wrong key?
The same refusal as a flipped byte: Poly1305 recomputes a tag that cannot match, and open() rejects before a single block is decrypted. Wrong key, wrong nonce, tampered ciphertext, tampered associated data — one indistinguishable refusal for all four.