A timing attack exploits the fact that operations on sensitive data can take slightly different amounts of time depending on the data itself, and that this difference can be measured.

Let’s see this in action. Imagine a simple password checker.

import time

def check_password(password_attempt):
    correct_password = "supersecretpassword123"
    if len(password_attempt) != len(correct_password):
        return False

    for i in range(len(correct_password)):
        if password_attempt[i] != correct_password[i]:
            return False
        # Simulate a tiny delay that might vary based on system load
        time.sleep(0.00001)
    return True

# Attacker tries to guess the password
for char_code in range(ord('a'), ord('z') + 1):
    guess = chr(char_code)
    start_time = time.time()
    check_password(guess)
    end_time = time.time()
    duration = end_time - start_time
    print(f"Attempted '{guess}': {duration:.6f} seconds")

# If the first character is 's', the loop runs once.
# If the first character is 'x', the loop runs once.
# The difference is subtle, but if the attacker knows the *first* character is correct,
# they'll try to find the *second* correct character.
# The loop continues until a mismatch is found. A correct character means the loop
# continues to the next iteration. An incorrect character causes an early exit.
# The *total time* taken for the function to run gives a clue.

If the correct password is "abc" and the attacker tries "abd", the loop will compare 'a' (match), then 'b' (match), then 'c' vs 'd' (mismatch). The function returns False after two full comparisons. If the attacker tries "abe", it also exits after two comparisons. But if the attacker tries "abX" (where X is not 'c'), the comparison at index 2 will fail. The exact point of failure is what the attacker is looking for.

The core problem a timing attack solves is extracting information from a system when direct observation of the data is impossible, but the timing of operations is observable. This is often used against cryptographic primitives like encryption algorithms or password hashing functions. The attacker doesn’t see the secret key or password directly, but by repeatedly sending slightly different inputs and measuring the response time, they can infer bits of the secret.

Internally, the vulnerability arises from conditional logic that branches based on secret data. For example, comparing two strings character by character. If the comparison stops at the first differing character, a longer execution time implies more characters matched. In many programming languages and CPUs, this character-by-character comparison is implemented in a way that reveals this timing difference. Optimized string comparison routines often short-circuit, meaning they stop as soon as a difference is found.

The fundamental lever an attacker controls is their ability to craft inputs and precisely measure the time it takes for the system to process them. They are essentially performing a binary search on the secret data, using execution time as their "feedback" signal. A slightly longer execution time indicates the input is "closer" to the secret in some way.

One thing most people don’t know is that even seemingly constant-time operations can leak information through microarchitectural side channels. For instance, CPU caches can introduce timing variations. If a secret value is used to access memory, and that memory location is already in the CPU’s cache, the access will be faster. An attacker can measure these tiny cache-hit/cache-miss timing differences to infer secret data, even if the algorithm itself appears to have no conditional branches based on the secret.

The next concept you’ll run into is how to defend against these attacks, specifically by implementing constant-time algorithms.

Want structured learning?

Take the full Cryptography course →