One-shot decrypt
single tag over the whole fileIdle β press βRun both decryptsβ.
- Peak footprint
- β
- Needs to finish
- β
- Buffer allocations
- β
Streaming AEAD Β· XChaCha20-Poly1305 Β· secretstream-style chaining
Watch a one-shot decrypt buffer a whole file into an OOM kill while a chunked stream holds flat β then truncate, reorder and drop segments against a real chained verifier, and against a naive split that never notices.
Authenticated encryption gives you one tag β one short fingerprint β that proves a message came from someone holding the key and was not altered. To check that tag you need the whole message, which is fine for a chat line and impossible for a 10 GiB video. So real systems cut the file into segments and authenticate each one separately.
That fixes the memory problem and quietly creates a new one. Each segment is now a valid little message on its own, so an attacker who cannot read or change a single byte can still rearrange them: play chapter five before chapter three, delete the paragraph they dislike, cut the file short. Nothing was forged, so nothing fails β unless the segments were chained together in the first place.
This page shows both halves: the memory wall that forces you to chunk, and the authentication trap that chunking walks you into.
One-shot AEAD cannot release a single byte of plaintext until the tag over the entire file verifies. So the entire ciphertext has to be resident first, and then the plaintext buffer is allocated on top of it. Push the file size up and watch where that ends.
Memory footprint is modelled from tracked buffer allocations, not measured process RSS. The chunk-chaining below uses real cryptographic operations.
Idle β press βRun both decryptsβ.
Idle β press βRun both decryptsβ.
Both meters are drawn to the same scale, so the flat line really is that flat.
A Poly1305 tag is computed over every byte of the ciphertext. Until the last byte has gone through the
accumulator you do not know whether the first byte is trustworthy, so a correct one-shot implementation
refuses to emit plaintext early β that refusal is the whole security property. The ciphertext therefore has
to be fully resident. Decryption then writes into a separate output buffer (this is what
crypto_aead_xchacha20poly1305_ietf_decrypt and WebCrypto's decrypt() both do), and
for a moment both buffers are live: peak β 2 Γ file size.
A chunked stream verifies a tag every segment, so it can hand that segment onward and free it before reading the next. Peak β 2 Γ segment size β a number you choose, rather than one your users choose for you by picking a big file.
Everything below runs actual XChaCha20-Poly1305 and SHA-256 in your browser. The message is a settlement batch, cut into six segments β chosen so that moving a line changes what a bank would do, not just which bytes it received.
DOMAIN = "crypto-lab/stream-ward/v1" chain[0] = SHA-256( DOMAIN β "chain-init" β header ) nonce[i] = SHA-256( DOMAIN β "nonce" β chain[i] β LE64(i) )[0..24] aad[i] = DOMAIN β "seg" β LE64(i) β chain[i] inner[i] = flag[i] β plaintext[i] flag = 0x01 on the last segment, else 0x00 ct[i] = XChaCha20-Poly1305( key, nonce[i], aad[i] ).encrypt( inner[i] ) chain[i+1] = SHA-256( DOMAIN β "chain" β chain[i] β LE64(i) β ct[i] )
Three things follow, and they are exactly the three attacks. chain[i] absorbs every earlier
ciphertext including its Poly1305 tag, so a segment that moves lands on a chain state its tag does
not cover. LE64(i) sits in the associated data, so a segment also cannot change position
without changing what it was authenticated against. And the FINAL flag lives inside the
encrypted block β as it does in libsodium's crypto_secretstream β so a stream that stops early
ends on a segment that authenticates as MESSAGE, and the verifier can say so.
chain[i] is not secret. It is derived from the public header and the ciphertexts, and an attacker can compute it too. Confidentiality comes from the key alone; the chain buys ordering and completeness, nothing more.
You are now the attacker. You do not have the key, and you will not change a single byte inside any segment β you only get to move segments around or throw them away. Then the real verifier runs.
No attack applied β the stream is exactly as it was sealed.
Six experiments. Each cell fills in with what the verifier actually reported when you ran that combination β nothing here is pre-written. 0 of 6 run.
| Attack | Chained β Stream Ward | Naive split |
|---|
@noble/ciphers, pinned by the draft-irtf-cfrg-xchacha-03 KATs.@noble/hashes, pinned by FIPS 180-4 KATs.crypto_secretstream_xchacha20poly1305, or an age/Tink streaming AEAD.