← articles

web · cryptography · side channel

Timing attacks: recovering a secret from response times

An early-exit comparison leaks how many leading characters of a guess are correct.

A naive comparison returns at the first mismatching byte. The time to reject a guess grows with the length of its correct prefix, so measuring it recovers the secret one position at a time. For a 4-character alphanumeric secret, that is at most 4 × 36 guesses instead of 36⁴.

The demo exaggerates the difference so it is visible.

Timing attack live demo
POST /login · 36 guesses per character

Response time per guess. The slowest one matched one more character.

password: ____
for i := range given { if given[i] != secret[i] { return false } } // exits early
Simulated. Runs entirely in your browser.

In practice

The real difference is a few nanoseconds per byte, while network jitter is orders of magnitude larger. Attackers average many samples per guess and move closer to the target, for example into the same cloud region, to reduce noise. Brumley and Boneh showed remote timing attacks against OpenSSL in 2003.

Typical targets

  • API keys and bearer tokens compared with ==.
  • HMAC signatures on webhooks checked with a string comparison.
  • Password reset tokens and OTP codes.
  • Logins that skip password hashing for unknown users. That one leaks which accounts exist rather than a secret.

If a password can be compared byte by byte, it is stored in plaintext. Fix that first.

Fix

Compare secrets in constant time:

if !hmac.Equal(gotMAC, expectedMAC) {
	http.Error(w, "invalid signature", http.StatusUnauthorized)
	return
}

subtle.ConstantTimeCompare works on any byte slices but returns immediately when the lengths differ. Comparing fixed-length values (hashes, MACs) avoids leaking the length.

Store passwords with Argon2id or bcrypt. For unknown users, hash a dummy password so both paths take the same time. Rate-limit login and code verification.

I test web apps, APIs and LLM integrations. Contact.