iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Constant-time Comparisons

A timing attack measures how long an operation takes and deduces secret information from the variance. Classic targets: password compares, MAC verification, table lookups in AES. The fix is constant-time code: branch-free comparisons that take the same number of CPU cycles regardless of input.

Constant-time compare and MAC verify

EXAMPLE
// ===== Python =====
import hmac, secrets, time

# 1) WRONG: == compares character by character, returns early on first mismatch.
def naive_eq(a: bytes, b: bytes) -> bool:
    return a == b

# 2) RIGHT: hmac.compare_digest runs in constant time
def safe_eq(a: bytes, b: bytes) -> bool:
    return hmac.compare_digest(a, b)

# Sketch a measurement showing the leak on toy data
def time_eq(fn, target):
    candidates = [target] + [b'X' * len(target)]
    candidates += [bytes([target[0]]) + b'X' * (len(target) - 1)]
    for c in candidates:
        t = time.perf_counter()
        for _ in range(50_000): fn(c, target)
        print(f'{c[:6]}... {(time.perf_counter() - t) * 1e6:.0f}us')

# ===== JavaScript / Node =====
const { timingSafeEqual, createHmac, randomBytes } = require('node:crypto');

function safeEqualUtf8(a, b) {
  const A = Buffer.from(a, 'utf8'); const B = Buffer.from(b, 'utf8');
  if (A.length !== B.length) return false;          // length leak is unavoidable
  return timingSafeEqual(A, B);
}

// MAC over a webhook payload, then constant-time compare
function verifyWebhook(signatureHeader, body, secret) {
  const expected = createHmac('sha256', secret).update(body).digest('hex');
  // Equalise lengths before timingSafeEqual; reject early on mismatch length.
  if (signatureHeader.length !== expected.length) return false;
  return timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}

// ===== Go =====
// import "crypto/subtle"
// func eq(a, b []byte) bool {
//   return subtle.ConstantTimeCompare(a, b) == 1   // returns int 1/0
// }

// ===== PHP =====
// hash_equals(\$expected, \$user_supplied)

// ===== Pitfalls that re-introduce timing leaks =====
// - Comparing strings with == AFTER you used a constant-time function.
// - Stopping early (return) the moment lengths differ — leaks length.
//   Defence: derive expected from the secret-keyed HMAC and use its length;
//   a length-mismatched attacker controls nothing useful.
// - Using string slicing / indexing as an 'optimisation' (str[:n] == other[:n]).
// - Database password compares (== in SQL) - move auth into the app layer
//   using a real password hash (argon2, bcrypt).

// ===== Beyond compares: constant-time crypto primitives =====
// - Use library primitives (libsodium, Bouncy Castle, BoringSSL) not hand-rolled.
// - AES-GCM, ChaCha20-Poly1305 over AES-CBC where possible.
// - Ed25519 signing (deterministic, no nonce-reuse footgun).
// - HKDF for key derivation; never plain SHA over the shared secret.

Why it matters

Reach for the libraries built-in constant-time compare every single time you compare anything that depends on a secret — HMACs, tokens, CSRF tokens, signed cookies, webhook signatures. Equality of secrets is not the place to outsmart the standard library.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Naive == on secrets leaks bits via timing.
// Use crypto.timingSafeEqual / hmac.compare_digest / hash_equals.
Try it Yourself »

Test yourself

Q1. Comparing secret strings with == can leak…
Q2. The fix is…
Q3. Hashing first is…

Discussion

Loading…