Bcrypt and Timing Attack Prevention

Learn how bcrypt prevents timing attacks through constant-time comparison. Understand what timing attacks are, how they exploit naive string comparison, and why bcrypt's built-in compare is essential.

Best Practices

Detailed Explanation

Bcrypt and Timing Attack Prevention

A timing attack exploits the fact that different inputs cause different execution times in a program. In password verification, a naive comparison can leak information about how much of the hash matched, allowing an attacker to guess the hash byte by byte.

How Timing Attacks Work

Consider a naive string comparison:

function unsafeCompare(a, b) {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false; // Returns early!
  }
  return true;
}

This function returns false as soon as it finds the first mismatched character. An attacker can measure response times:

  • If the first character is wrong: ~1 comparison → fast response
  • If the first 10 characters match: ~10 comparisons → slightly slower response
  • If all characters match: ~60 comparisons → slowest response

By trying different inputs and measuring response times with microsecond precision, an attacker can reconstruct the hash one character at a time.

Constant-Time Comparison

Bcrypt’s built-in compare() function uses constant-time comparison — it always examines every character, regardless of where mismatches occur:

function safeCompare(a, b) {
  let result = a.length ^ b.length; // XOR lengths
  for (let i = 0; i < Math.max(a.length, b.length); i++) {
    result |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
  }
  return result === 0;
}

This always takes the same time whether zero characters match or all characters match.

Why You Must Use bcrypt.compare()

Never verify bcrypt hashes with direct string comparison:

// WRONG - vulnerable to timing attacks
if (storedHash === computedHash) { ... }

// CORRECT - constant-time comparison
if (await bcrypt.compare(password, storedHash)) { ... }

Practical Exploitability

Timing attacks on password hashes over a network are difficult but not impossible:

  • Local attacks (same machine): highly feasible, microsecond precision achievable
  • LAN attacks: feasible with statistical analysis over many requests
  • Internet attacks: challenging due to network jitter, but demonstrated in research with enough samples
  • Shared hosting: feasible, as timing jitter is lower

Additional Protections

Beyond constant-time comparison, defend against timing attacks with:

  1. Rate limiting — limit login attempts per account and per IP
  2. Account lockout — temporarily lock accounts after repeated failures
  3. Fixed response time — add artificial delay so all login responses take the same time
  4. Hash on failure — when a username is not found, hash a dummy password anyway to prevent timing differences between "user exists" and "user not found"
app.post('/login', async (req, res) => {
  const user = await db.findUser(req.body.email);
  // Hash even if user not found (prevents user enumeration via timing)
  const hashToCompare = user?.passwordHash || DUMMY_HASH;
  const valid = await bcrypt.compare(req.body.password, hashToCompare);
  if (!user || !valid) return res.status(401).json({ error: 'Invalid credentials' });
  // ...
});

Use Case

Timing attack prevention is a critical concern in security reviews and penetration testing. Developers who implement their own verification logic (instead of using the library’s compare function) may inadvertently introduce timing vulnerabilities. This guide helps security-conscious teams understand the threat, verify their code uses constant-time comparison, and implement additional protections like dummy hashing to prevent user enumeration.

Try It — Bcrypt Generator

Open full tool