Attacks & Pitfalls

Side Channels

Timing, power, and cache attacks: leaking secrets without breaking math.

A side channel leaks a secret through a system’s physical or observable behavior rather than through its cryptographic math. The algorithm can be perfect and the key still walks out the door because a comparison returned a microsecond faster, a CPU cache was warmer, or the power draw dipped at the wrong moment. Side channels are humbling: they are the reason you use vetted libraries instead of your own “obviously correct” code.

Why This Matters

Real hardware and real software leak. The Spectre and Meltdown (2018) attacks read memory across security boundaries through cache timing. Lucky Thirteen broke TLS through timing differences of nanoseconds. Countless authentication bypasses come from == comparing a secret token and returning early on the first mismatched byte. If your threat model includes an attacker who can measure your system, and it should, side channels are in scope.

The Main Channels

ChannelWhat leaksClassic example
TimingHow long an operation takesNon-constant-time compare, RSA timing attacks
CacheWhich memory addresses were accessedSpectre, Flush+Reload on AES tables
PowerCurrent draw during computationDifferential power analysis on smart cards
ElectromagneticEM emissions correlated to dataTEMPEST-style attacks
Error/behaviorDistinguishable responsesPadding oracles (sibling page)

Timing Attacks and Non-Constant-Time Comparison

The most common and most fixable side channel is the string comparison that returns early. Comparing a submitted token against a secret with a naive loop stops at the first differing byte, so a token that matches the first byte takes marginally longer to reject than one that matches none. By measuring which guesses are slower, an attacker recovers the secret one byte at a time, the same divide-and-conquer shape as the padding oracle.

A Hands-On Timing-Leak Demonstration

Build a tiny naive comparator and measure that a matching prefix is slower to reject. The effect is small and noisy, which is exactly why real attacks average over many samples.

cd ~/crypto-lab
cat > timing.py <<'PY'
import time
SECRET = b"S3CR3T-TOKEN-abcdef"
def naive_equal(a, b):
    if len(a) != len(b): return False
    for x, y in zip(a, b):
        if x != y: return False   # early return: the leak
        time.sleep(0.0005)        # amplify per-byte work so it is visible
    return True
def timed(guess, n=200):
    t = time.perf_counter()
    for _ in range(n): naive_equal(guess, SECRET)
    return (time.perf_counter() - t) / n
wrong  = b"X" + b"\x00"*18
prefix = b"S3CR3T" + b"\x00"*13
print(f"no-match prefix:   {timed(wrong)*1e3:6.2f} ms")
print(f"6-byte match:      {timed(prefix)*1e3:6.2f} ms")
PY
python3 timing.py

Expected output:

no-match prefix:     0.51 ms
6-byte match:        3.06 ms

The guess that matches six bytes takes measurably longer because the loop ran further before returning. An attacker who can time responses walks the secret byte by byte from exactly this signal.

Constant-Time Programming

The fix is code whose timing (and memory access pattern) does not depend on secret data. For comparison, that means always examining every byte and accumulating differences rather than branching.

LanguageConstant-time comparison
Pythonhmac.compare_digest(a, b)
Gosubtle.ConstantTimeCompare(a, b) == 1
JavaMessageDigest.isEqual(a, b)
CCRYPTO_memcmp (OpenSSL)

Constant-time discipline extends far beyond comparison: no secret-dependent branches, no secret-dependent array indices (which leak through the cache), and no secret-dependent loop bounds. Getting this right in the presence of aggressive compilers and speculative CPUs is genuinely hard, which leads to the real lesson.

Why You Use Vetted Libraries

You cannot easily verify constant-time behavior by reading source: the compiler may reintroduce a branch, and the CPU may leak through caches you never see. Vetted libraries (libsodium, BoringSSL, the OpenSSL primitives) have their constant-time properties tested, reviewed, and sometimes formally verified. This is the single strongest argument in the sibling Common Mistakes page against home-rolled crypto: even correct math leaks if the implementation is not constant-time.

Practical Guidance

  1. Compare secrets, tokens, and MACs only with a constant-time function; never use == or a byte loop that returns early.
  2. Keep secret data out of branch conditions, array indices, and loop bounds so timing and cache behavior do not depend on it.
  3. Prefer vetted cryptographic libraries whose constant-time properties are tested; assume your own code leaks until proven otherwise.
  4. Treat any observable difference in response time or behavior as a potential channel, and connect this to the sibling Padding Oracle page, which is a timing/behavior oracle in disguise.
  5. For high-assurance or hardware contexts, extend the threat model to power and EM channels and use hardware with documented countermeasures.