Lion Cipher Lab
Close the Token-Length Side Channel in a Streaming LLM Response
- AI Security
- Advanced
- ~80 min
- Free · no account required
Why it matters
Everyone knows HTTPS hides what you say to an AI assistant. Almost nobody checks what it does not hide. When an assistant streams its answer to you token by token -- the little chunks of text a model generates, arriving one at a time so the reply appears to type itself -- each token is pushed onto the wire in its own small message. Encryption scrambles the CONTENT of that message. It does not change its SIZE. And the size of an encrypted record is the size of the plaintext plus a fixed overhead, so an observer who cannot read a single byte of your conversation can still read off the exact…
What you'll learn
- modelling an encrypted streaming LLM response end to end in pure-stdlib Python: a token stream, one Server-Sent-Events frame per token, and the TLS record length each frame produces -- record length = constant envelope + token byte-length + constant AEAD overhead, which is exactly why the ciphertext size leaks the plaintext size
- building the threat model correctly: the observer never decrypts anything and never touches the model, and sees ONLY the ordered list of record lengths -- a passive on-path attacker with a packet capture and no keys
- MEASURING the leak against a shipped, seeded 400-response corpus: proving the length-minus-constant equals the token byte-length exactly on all 9,000+ records, that 85.8% of 1-byte tokens are punctuation (so record sizes expose sentence structure), and that 100% of the 400 candidate responses carry a length sequence shared with no other response
- quantifying how EARLY the leak resolves -- a median of 10 records identifies which of 400 replies is on the wire, so the observer knows the answer before the user has finished reading the first line
- measuring four defenses on the same corpus and reading the trade-offs: fixed-bucket padding, random padding, token batching, and not streaming at all -- with top-1 recovery and bandwidth cost for each
- the statistical honesty random padding demands: it looks like the strongest defense at one observation (10.6% top-1) and decays to 74.5% once the observer averages 100 observations of the same reply, because independent noise averages out while the signal does not
- the counter-intuitive result that batching alone is a weak defense (83.8% top-1 at 8 tokens per record) even though it is the change that costs the most perceived responsiveness -- and that batching COMBINED with bucket padding reaches 2.3% while cutting bandwidth 69.5%
- the judgment layer: total response length leaks even when every record is padded, padding costs real bandwidth, the mitigation belongs at the server/proxy layer rather than in the model, and this is one instance of a well-known family (SSH keystroke timing, encrypted-VoIP and web-page fingerprinting, CPU cache side channels)
Steps in this lab
- The attack in one breath: the model is not attacked, the transport is
- Why the length leaks: SSE frames, TLS records and AEAD overhead
- Set up: offline, key-free, deterministic
- Build the wire model, part 1: framing and the tokenizer (stream.py)
- Build the wire model, part 2: the corpus and the observer (stream.py)
- Build the wire model, part 3: the metrics, and prove the leak is exact
- Stage 1: measure what the length sequence alone reveals (attack.py)
- Read Stage 1: the observer wins, and wins early
- Stage 2: build and measure the four defenses (defend.py)
- Read the table: three of the four obvious defenses disappoint
- The combination is the answer -- and it is cheaper than doing nothing
- Judgment: what still leaks, where the fix belongs, and the family this joins
- What you built -- and the one sentence to keep