The Verification Venue · a bias you can watch walk out of the noise
The Second Byte Leans Toward Zero
Run RC4's key schedule on a handful of random keys and its output looks flawlessly uniform, and every small-sample check agrees. It is not uniform. The second keystream byte is zero about twice as often as it should be, the flaw follows from the key schedule's first two steps, and this page measures the bias, derives its direction, and shows its own work.
Below is a histogram of the second output byte over independent random 16-byte keys, filling as keys are drawn. A preview of two million keys starts immediately. When you are ready, launch the full run of a hundred and fifty million keys, watch one bin stand clear of the other two hundred and fifty-five, then drag the sample-count slider back down and watch the evidence disappear again.
Keystream byte under the microscope
Convention, stated once: byte positions are 1-indexed, keys are 16 random bytes, the first byte is not discarded. The readouts always describe byte two; the first byte is there to be compared against. ↓
Keys drawn
0
preview running
P(second byte = 0)
·
uniform would be 0.00390625
Distance from uniform
·
in standard errors, zero bin vs 1/256
Reading
waiting
interval appears once keys are drawn
Snap to the checkpoints a run has saved. Slide toward a thousand keys and the interval closes over uniform: the small run does not merely lack precision, it returns the opposite answer.
Runs across every core the browser offers. Interruptible; a stopped run is labelled partial, never an answer.
Run state
idle · the battery and the sabotage check run first, and nothing displays until they pass
Operations performed (counted)
0
inner-loop bodies the engine actually executed
Wall time
·
· workers
Against main-thread JavaScript
·×
extrapolated from a slice; the slice fraction prints beside it
Partial?
no run yet
an interrupted run is a partial, in those words, always
Why the second byte? Trace the key schedule on a key whose first two bytes cancel, K[0] + K[1] ≡ 0 (mod 256). Step one sets j = K[0] and swaps positions 0 and K[0], planting the value 0 at index K[0]. Step two computes j = K[0] + S[1] + K[1], and because nothing has touched position 1 yet, S[1] is still 1, so the sum is K[0] + 1 − K[0] = 1: the swap is S[1] with itself, a no-op. From there the question is how often the planted zero, the untouched S[1], and the arithmetic that reads them survive the remaining 254 swapping steps. They survive often enough to leave a surplus, and the surplus lands on exactly one output, because the second byte's value is read from a sum that involves precisely those entries. The panel below measures the survival exactly, over every two-byte key that exists.
The derivation, measured exactly rather than recalled
| system | keys swept | P(Z₂ = 0) | × uniform | P(Z₂ = 0 | K₀+K₁ ≡ 0) |
|---|
The first row is the real RC4 with every two-byte key enumerated exhaustively. The rows beneath are reduced analogues, RC4 with a smaller state, whole key space swept. They show the mechanism is not an accident of one parameter choice. An analogue is not a proof about the full system, and the full-system number on this page rests on the measurement above, not on these rows.
Consequence: the reused-key attack, made operable
Vote reliable (≥ 90%) from
·
ciphertexts of one known plaintext byte
Model vs direct resampling
·
at 1,000 ciphertexts, 200 trials each
Give the attacker one known plaintext byte encrypted under many independent keys, and let them vote: for each candidate value of the second byte, count how often ciphertext xor candidate repeats, and take the mode. The true value wins once its bin, the zero bin, out-polls 255 rivals that each sit near uniform. The curve plots the vote's accuracy against the number of ciphertexts, simulated from this page's own measured histogram. This is the number that turned a statistical curiosity into a rule: a key must never be reused.
Model: per-candidate counts drawn as independent Poisson variables from the measured histogram; validated below against direct resampling, which makes no independence assumption. Both are estimates, with visible trial counts.
The check · run in front of you, or the page refuses to speak
Two implementations, one contract. The compiled engine and a slow, deliberately obvious JavaScript reference ran the battery below in your browser before this page agreed to compute anything. Fingerprints are over all 515 counters of each accumulator.
| battery row | engine fingerprint | reference fingerprint | agree |
|---|
A check that has never failed is a claim about the code, not evidence about it. Press the button and watch this one go red:
Corrupts one step of the reference's key schedule and requires the battery to notice. If sabotage goes unnoticed, the agreement check above is decoration.
sabotage check: armed
planted control (a keystream uniform by construction, fed through the unmodified measurement): pending
full-size decomposition check (250 chunks vs 128: both cuts' histograms must sum to exactly the sample count, and the fingerprints must be bit-identical): awaiting the full run
full-size invariants (histograms sum to the sample count; zero failed permutations; second-byte deviation beats the first byte's and clears 8σ; interval excludes uniform): awaiting the full run
engine binary: fetching…
What none of this rules out. Two decompositions agreeing rules out boundary errors and very little else; the battery rules out translation mistakes on small cases; neither rules out a shared misunderstanding of RC4 itself, which is why the derivation panel re-derives the bias's direction from the key schedule by exhaustion. Nor can a differential fingerprint comparison see a fault that strikes both decompositions identically, which is why the decomposition gate checks each side's histogram sums before it compares anything. No amount of it certifies anything about any deployed system, which this page does not examine.
Run it yourself, offline: node research/the-second-byte-leans-toward-zero/verify-the-second-byte-leans-toward-zero.mjs. That file also rebuilds the engine from its C and hashes it, which a browser cannot do.
What's exact here, what's a model, and what's measured
Exactly true. Every histogram count is an integer tally. The permutation check runs on every single key schedule, no subset. The derivation panel's fractions are exhaustive: all 65,536 two-byte keys of the real system, and the entire key spaces of the reduced analogues. The planted control's source is uniform by construction, so its zero deviation is exact, forever.
A model. The majority-vote curve treats per-candidate counts as independent Poisson draws from the measured histogram. Multinomial counts are mildly negatively correlated, so the independence is an approximation; the page validates it against direct categorical resampling at a matched sample size and prints both estimates beside each other. The intervals are two standard errors wide by stated convention.
Measured. Everything else, always with its interval attached, and never from memory: no published bias constant appears anywhere on this page, because published values differ in convention, key length, and whether the first byte is discarded, and a recalled constant is exactly what this run forbids. If a figure you expected is missing, that is why.
Deliberately not claimed. This is one known weakness among several, not a break of RC4, and it implies nothing about any system you use. Its scope is the two sentences the measurement supports: the second byte leans toward zero by the measured amount, and the lean follows from the key schedule.