Recovering a Secret From Timing Alone
When you check a password, an API key, or a message authentication code against a stored secret, the obvious way to compare them is character by character, stopping at the first character that differs. That shortcut leaks information. A guess that gets the first three characters right takes very slightly longer to reject than one that misses immediately, because the comparison runs three steps further before it gives up. Measure carefully enough and that tiny difference lets an attacker recover the secret one character at a time, without ever being told a single character of it.
This page holds a random 8-character hex token that is generated in your browser and never written into the text you can read. Pick a comparison function and press Attack. The naive compare gives the token up from timing alone. The constant-time compare does not.
Recovered so far:
Idle.
Reveal the secret (for checking the result)
The hidden token is --------. It changes every time you press “New secret”.
Why the naive compare leaks
The dangerous line is the early return: if (guess[i] != secret[i]) return false. The loop does more work the more leading characters are correct, so the time it takes to say “no” is a direct function of how much of the secret the guess already has. An attacker fixes one position at a time: try all sixteen hex values, keep whichever is consistently the slowest to be rejected, lock it in, and move to the next position. Sixteen values times eight positions is a few hundred guesses, instead of the four billion a blind search would need.
This is not hypothetical. Token and HMAC comparisons have leaked this way for years, and the Python Keyczar library once shipped a byte-by-byte signature check that let an attacker forge valid signatures by measuring how long verification took, until it was replaced with a constant-time comparison. The fix is exactly that: a comparison that always inspects every byte and folds the differences together with XOR, so the running time no longer depends on where the first mismatch is. This is why crypto libraries ship primitives like Python's hmac.compare_digest and tell you to use them for anything secret.
About the amplification
A real server leaks at the scale of nanoseconds per character, far below what a browser clock can resolve. Browsers deliberately blur performance.now() for exactly this reason, to blunt attacks like this one. So this demo amplifies the effect: the naive compare does a fixed chunk of arithmetic for every character that matches, which turns the same per-character leak into something visible at millisecond scale. The shape of the attack is real and the constant-time version genuinely closes it; only the size of the signal is exaggerated so you can watch it happen in a few seconds.
How the measurement works
Timing a single comparison is hopeless: it is faster than the clock can resolve, and any one reading is buried in noise from garbage collection, other tabs, and the operating system scheduler. So for each candidate character the attack runs the comparison many times in a tight batch and records the batch time. It repeats that for about a dozen passes, and it measures all sixteen candidates interleaved within each pass (reshuffling the order every time) so that any slow drift in CPU frequency or scheduling hits every candidate together and cancels out when they are compared. For each candidate it then keeps the average of its fastest passes, because a comparison can only be slowed down by interference and never sped up, so the fastest readings are the cleanest picture of the real work. The correct character is the one whose fastest time is reliably the highest. If no candidate leads the pack by a clear margin after two rounds, the attack reports that there is no signal and stops.
Browser timer clamping is why this is harder now than it was around 2015, when performance.now() still had microsecond resolution and these attacks were a live concern for the web. The amplification here stands in for that lost resolution. If you switch tabs mid-attack the measurement pauses on purpose, because background timers are throttled and would corrupt the samples.