Post

Timing Attack - A Hidden Risk When Comparing Secrets

Learn why comparing API keys or signatures using === is insecure, and how a simple timing attack can expose your secrets. Includes a clear example in TypeScript and how to fix it using constant-time comparison.

Timing Attack - A Hidden Risk When Comparing Secrets

Many developers compare API keys, tokens, or webhook signatures like this:

1
if (apiKey === expectedKey) { ... }

This looks fine, but it’s actually unsafe.


What’s the Problem?

Normal string comparison (like ===) stops at the first mismatch. That means:

  • More correct characters → longer comparison time
  • Hackers can measure the time to guess your API key, token, or signature one character at a time

This is called a timing attack.


How to Fix It

Use constant-time comparison, which takes the same time no matter how similar the strings are.

For example, in Node.js:

1
2
3
4
5
6
7
8
import { timingSafeEqual } from 'crypto';

const a = Buffer.from(signature);
const b = Buffer.from(expectedSignature);

if (a.length !== b.length || !timingSafeEqual(a, b)) {
  throw new Error('Invalid signature');
}

Other languages:

LanguageSafe Compare Function
Node.jscrypto.timingSafeEqual()
Pythonhmac.compare_digest()
JavaMessageDigest.isEqual()
Gocrypto/subtle.ConstantTimeCompare()

Demo in TypeScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function sleep(ms: number) {
  const start = Date.now();
  while (Date.now() - start < ms);
}

function insecureCompare(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false;
    sleep(5); // Simulate delay
  }
  return true;
}

function guessSecret(real: string, charset: string): string {
  let guessed = '';
  while (guessed.length < real.length) {
    let bestChar = '';
    let maxTime = 0;

    for (const ch of charset) {
      const tryGuess = guessed + ch + 'a'.repeat(real.length - guessed.length - 1);
      const t0 = performance.now();
      insecureCompare(tryGuess, real);
      const t1 = performance.now();
      if (t1 - t0 > maxTime) {
        maxTime = t1 - t0;
        bestChar = ch;
      }
    }

    guessed += bestChar;
    console.log(`Guessed: ${guessed}`);
  }
  return guessed;
}

const realKey = 'abc1';
guessSecret(realKey, 'abcdefghijklmnopqrstuvwxyz0123456789');

Play it on Typescript Playground


Summary

  • Use constant-time comparison for API keys, tokens, HMAC signatures
  • Don’t use === or simple string comparison
  • One mistake could let attackers guess your secrets
This post is licensed under CC BY 4.0 by the author.