DEV Community

Cover image for Preventing Timing Attacks with Constant-Time Comparisons
Doogal Simpson
Doogal Simpson

Posted on Originally published at doogal.dev

Preventing Timing Attacks with Constant-Time Comparisons

TL;DR: Using standard comparison operators like == to validate API keys or cryptographic signatures leaks secrets byte-by-byte. Because standard operators return early upon encountering a mismatch, attackers can measure tiny response-time differences to brute-force your keys. Replacing them with constant-time comparison functions completely neutralizes this vulnerability.

Imagine trying to crack a physical safe. Usually, you must guess the entire combination at once. But what if the dial gave a faint, satisfying "click" the moment you got just the first digit right? Suddenly, your search space collapses. Instead of guessing a million combinations, you only need to try ten digits to find the first one, ten for the second, and so on.

In software, standard string comparison operators do exactly this: they "click" when you get a character right, leaking your secrets one byte at a time.

How does a timing attack exploit standard string comparisons?

Standard comparison operators evaluate strings from left to right and terminate immediately upon finding the first mismatched byte. This early-return behavior allows an attacker to guess a secret byte-by-byte by measuring microsecond differences in API response times.

When you compare a 32-character API key using a standard comparison like if (apiKey == inputKey), the CPU does not analyze all 32 characters simultaneously. It evaluates them sequentially. If the very first character of the user's input is wrong, the engine is optimized to return false instantly.

However, if the first character is correct, the engine moves on to check the second character. This additional step takes a tiny fraction of a nanosecond. If an attacker repeatedly sends requests to your endpoint, they can use statistical analysis to filter out network noise and identify which characters caused the server to respond just a fraction of a nanosecond slower. By keeping the slowest-responding character and moving to the next position, they can brute-force a highly secure signature or token in a fraction of the time. This exact vulnerability famously compromised Google's Keyczar cryptography library in 2009.

How does constant-time comparison solve this vulnerability?

Constant-time comparison functions evaluate every single character in both strings regardless of where a mismatch occurs, ensuring the execution time is identical for every input. This uniformity denies attackers the timing variance they need to deduce the correct characters.

To prevent this timing leak, we must strip away the performance optimization of early-return. We need to force the CPU to walk through every single byte of both strings, comparing them all, and only returning a boolean result at the very end of the loop.

Why does Node's timingSafeEqual require a hashing pre-step?

In Node.js, crypto.timingSafeEqual() throws a critical RangeError if the compared buffers are of unequal length, which can crash your application. Hashing both inputs with SHA-256 before comparing them guarantees equal-length buffers and masks the true length of the secret key.

If you simply compare the string lengths before using timingSafeEqual, you leak the exact length of your secret key to attackers via the execution time of that length check. By hashing both the secret and the user input with SHA-256 first, you transform both values into identical-length 32-byte buffers. This bypasses the buffer-length constraint safely without crashing or leaking details.

import crypto from 'crypto';

const isSecure = (secretKey, userInput) => {
  // Hash both to fixed-length buffers to avoid RangeErrors and length-leakage
  const hashA = crypto.createHash('sha256').update(secretKey).digest();
  const hashB = crypto.createHash('sha256').update(userInput).digest();

  return crypto.timingSafeEqual(hashA, hashB);
};
Enter fullscreen mode Exit fullscreen mode

What are the best practices for implementing constant-time checks?

To implement these checks securely, you should leverage native cryptographic libraries and normalize your input lengths using hashing. This prevents compiler optimizations or unexpected runtime crashes when comparing user-supplied data against your secrets.

Most modern backend runtimes provide safe utility functions out of the box. When verifying webhooks, API tokens, or cryptographic signatures, reference the table below to select the appropriate comparison method for your stack:

Language / Runtime Standard Vulnerable Comparison Constant-Time Secure Function
Node.js a === b crypto.timingSafeEqual(bufA, bufB)
Python a == b hmac.compare_digest(a, b)
Go a == b subtle.ConstantTimeCompare(a, b)
PHP a === b hash_equals(a, b)

Can timing attacks be executed reliably over the public internet?

Yes, though it requires more statistical sampling. While public internet routing introduces substantial latency variance (jitter), attackers can easily bypass this noise by sending thousands of requests and analyzing the statistical distribution of response times to extract the timing signal.

Does using HTTPS or encryption protect against timing attacks?

No, HTTPS only encrypts the payload in transit to prevent eavesdropping. It does not alter the processing execution path on your application server, meaning an attacker can still measure the exact interval between when a request is fully sent and when the first byte of the response is received.

Should I use constant-time comparison for all string checks?

No, you only need constant-time comparisons for security-sensitive strings such as API keys, password reset tokens, signatures, and session IDs. Using it for standard routing, usernames, or public string matches adds unnecessary performance overhead without providing any security benefits.

Top comments (0)