Cryptography

Side-Channel Attacks

When the algorithm is perfect but the machine running it whispers the key

A side-channel attack recovers secrets not by breaking the math but by measuring the physical side effects of running it — timing, power draw, cache state, electromagnetic emissions, even sound — and correlating those measurements with the key.

  • Attacksthe implementation, not the cipher
  • Channelstime · power · cache · EM · sound
  • Timing leak per byteoften sub-µs
  • DPA traces to break AES~10³
  • Primary defenseconstant-time code

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

The secret hides in the math; the leak hides in the machine

Modern ciphers are, for practical purposes, unbreakable. There is no known attack faster than brute force against AES-128, and brute-forcing a 128-bit key would take longer than the age of the universe on every computer ever built. So a thief does not attack the lock. The thief watches your hand on the dial.

That is the whole idea of a side-channel attack. The algorithm is a black box that takes a key and some data and produces ciphertext. But the box does not run in a vacuum — it runs on a real CPU, which takes time to execute, draws current from a power rail, evicts cache lines, radiates electromagnetic fields, and even makes faint sounds as capacitors flex. Every one of those physical effects can depend, however slightly, on the secret bits being processed. A side channel is any such observable that correlates with the secret. Measure it precisely enough, gather enough samples, and you reconstruct the key without ever touching the mathematics.

The canonical example is a password check. If your code compares the submitted password to the stored one byte by byte and bails out at the first mismatch, then a guess that gets the first byte right runs infinitesimally longer than one that gets it wrong. That timing difference is the side channel. It is tiny — a handful of nanoseconds — but it is real, repeatable, and it turns an exponential brute force into a linear one.

The mechanism: measure, model, correlate

Every side-channel attack follows the same three-stage skeleton, regardless of whether the channel is time, power, or cache:

  1. Acquire a leakage trace. Trigger the target operation and record an observable: the wall-clock latency, a power waveform sampled by an oscilloscope, or a cache-hit/miss vector. One trace is one measurement of the victim doing one operation.
  2. Build a leakage model. Hypothesize how the secret influences the observable. The classic model is the Hamming weight of an intermediate value: CMOS gates burn more power proportional to the number of bits that are 1 (or, in the Hamming-distance model, the number of bits that flip). For timing, the model is "a branch was or wasn't taken" or "this memory access hit or missed cache."
  3. Correlate hypotheses with reality. For each possible value of a small chunk of the key (a single byte, giving 256 guesses), predict the leakage under your model, then measure how well that prediction matches the recorded traces. The guess whose prediction correlates best with the real signal is almost certainly correct. Repeat per byte.

The reason this is devastating is the arithmetic of divide and conquer. A 128-bit AES key has 2128 possibilities — unsearchable. But AES processes the key one byte at a time, and a side channel lets you attack each byte independently: 16 bytes × 256 guesses each = 4,096 hypotheses to test, not 2128. You have collapsed an exponential search into a linear one. This is why a single exploitable leak, even a noisy one, is catastrophic: noise only forces you to average more traces, it does not restore the exponential.

The statistical tool that makes the "correlate" step work on noisy real-world traces is the Pearson correlation coefficient. Correlation Power Analysis (CPA), the workhorse of the field, computes, for each key guess k, the correlation between the modeled leakage H(plaintext, k) and the measured power at each time sample across all traces. The correct key produces a sharp correlation spike; wrong keys stay in the noise floor. With N traces, the signal-to-noise ratio improves as roughly √N, so doubling your trace budget cuts the noise by about 30%.

What the numbers actually say

  • Timing a string compare leaks at sub-microsecond granularity. A modern x86 core runs at ~3 GHz, so one extra byte-comparison loop iteration is roughly 0.3–1 ns of local difference. Over a LAN, jitter is microseconds; statistical filtering across ~104–106 samples still recovers the signal. Brumley and Boneh (2003) extracted an OpenSSL RSA key over a local network this way.
  • Differential Power Analysis can break AES with about 103 traces. Kocher, Jaffe, and Jun introduced DPA in 1999; on an unprotected smartcard, a few thousand power traces and a laptop are enough to recover a full DES or AES key. Side-channel competitions routinely report sub-1,000-trace breaks of naive implementations.
  • Flush+Reload resolves cache state in tens of nanoseconds. The latency gap between an L1 cache hit (~4 cycles, ~1.3 ns) and a main-memory miss (~200–300 cycles, ~70–100 ns) is enormous and trivially measurable with rdtsc. Yarom and Falkner (2014) used it to steal GnuPG RSA keys across VMs sharing a host.
  • Acoustic cryptanalysis works from across a room. Genkin, Shamir, and Tromer (2014) recovered a 4096-bit RSA key by recording the high-frequency coil whine of a laptop with a phone microphone held nearby — and from several meters away with a parabolic mic.
  • The brute-force baseline this all replaces: 2128 ≈ 3.4 × 1038 for AES-128. A side channel reduces a real-world key recovery to thousands of measurements that fit in seconds to minutes.

The major channels at a glance

ChannelWhat's measuredAccess neededTypical attackFamous result
TimingOperation latencyRemote (network) OKTime a branch / early returnBrumley–Boneh RSA, 2003
Power (SPA)Single power trace shapePhysical, oscilloscopeRead square-and-multiply by eyeKocher et al., 1999
Power (DPA / CPA)Statistics over many tracesPhysical, oscilloscopeCorrelate Hamming weightDPA on DES/AES smartcards
CacheHit/miss latency on shared cacheCo-resident process / VMFlush+Reload, Prime+ProbeYarom–Falkner, 2014; Spectre, 2018
ElectromagneticRadiated RF from switchingNear-field probeEM-DPA, TEMPESTScreen reconstruction (TEMPEST)
AcousticCoil whine / capacitor soundMicrophone in the roomFrequency analysis of CPU loadGenkin–Shamir–Tromer RSA, 2014

Two axes matter when triaging a channel: how close you must be (timing is remote-exploitable; power and EM usually need physical proximity) and how many traces you need (Simple Power Analysis reads one trace; Differential/Correlation methods need thousands but cut through noise that defeats SPA). Cache attacks are the dangerous middle ground — no physical access, just code running on the same machine, which is exactly the threat model of a shared cloud host or a browser tab.

The defense: constant-time code

If the channel is "the execution depends on the secret," the defense is to make the execution not depend on the secret. This is the constant-time discipline, and it has three hard rules:

  • No secret-dependent branches. An if (secret_bit) takes different paths and different time, and the branch predictor leaks it through cache too. Replace branches with arithmetic: compute both outcomes and select with a bitmask.
  • No secret-dependent memory addresses. A lookup like table[secret_byte] touches a cache line that depends on the secret — exactly what AES T-table cache attacks exploit. Either avoid the table (bit-slice the S-box) or scan the whole table every time.
  • No secret-dependent early exits. The string-compare bug. Always do all the work.

The core primitive is constant-time selection: turn a secret condition into an all-ones or all-zeros mask, then blend. mask = -(condition) gives 0xFFFF... or 0x0000..., and (a & mask) | (b & ~mask) picks a or b with no branch. Because the discipline is so easy to get wrong, hardened libraries provide blessed primitives: OpenSSL's CRYPTO_memcmp, Go's crypto/subtle, Python's hmac.compare_digest.

JavaScript: the bug and the fix

Here is the textbook timing leak and a constant-time replacement. The naive version is the one that ships in countless homegrown token checks.

// ✗ VULNERABLE: returns at the first mismatched byte.
// An attacker who controls `guess` can recover `secret` byte by byte
// by measuring which guesses take fractionally longer.
function naiveEquals(secret, guess) {
  if (secret.length !== guess.length) return false;
  for (let i = 0; i < secret.length; i++) {
    if (secret[i] !== guess[i]) return false;   // early return = the leak
  }
  return true;
}

// ✓ CONSTANT-TIME: always scans every byte, no early return,
// no secret-dependent branch. Time depends only on length.
function constantTimeEquals(a, b) {
  // Length is usually public (token size); compare it openly,
  // but still fold it into the accumulator so we never short-circuit.
  const len = Math.max(a.length, b.length);
  let diff = a.length ^ b.length;             // nonzero if lengths differ
  for (let i = 0; i < len; i++) {
    const x = i < a.length ? a.charCodeAt(i) : 0;
    const y = i < b.length ? b.charCodeAt(i) : 0;
    diff |= x ^ y;                            // OR-accumulate every difference
  }
  return diff === 0;                          // single branch, secret-independent
}

The accumulator diff is the trick: every byte XOR contributes to it, and only one comparison happens at the very end on a value that no longer reveals where the mismatch was. The loop runs the same number of iterations no matter what the inputs are. (In a real attack-resistant setting you would also avoid leaking the length through the loop bound; the comment above flags the subtlety.)

Python: simulating the attack

This script demonstrates why the early-return version is exploitable by actually recovering a secret one character at a time, purely from timing, and contrasts it with the standard-library defense.

import time, hmac, string

SECRET = "s3cr3t-token-9f"

def naive_equals(a: str, b: str) -> bool:
    if len(a) != len(b):
        return False
    for x, y in zip(a, b):
        if x != y:          # early return — the side channel
            return False
        # artificial work so the per-byte cost is measurable in a demo
        for _ in range(2000):
            pass
    return True

def time_guess(guess: str, samples: int = 5) -> float:
    best = float("inf")
    for _ in range(samples):
        t0 = time.perf_counter_ns()
        naive_equals(SECRET, guess)
        best = min(best, time.perf_counter_ns() - t0)   # min filters out jitter
    return best

def recover() -> str:
    alphabet = string.ascii_lowercase + string.digits + "-"
    known = ""
    while len(known) < len(SECRET):
        # The correct next char makes the compare run one byte longer.
        candidate = max(
            alphabet,
            key=lambda c: time_guess((known + c).ljust(len(SECRET), "\0")),
        )
        known += candidate
    return known

# ✓ The defense: a constant-time compare from the standard library.
def safe_equals(a: str, b: str) -> bool:
    return hmac.compare_digest(a, b)   # constant-time, no early return

if __name__ == "__main__":
    print("recovered:", recover())          # -> s3cr3t-token-9f
    print("safe match:", safe_equals(SECRET, SECRET))

The recover() loop never sees the secret. It only ever calls the comparison and watches the clock — yet it walks the secret out character by character because the correct character is always the one that makes the loop run one iteration longer. Replacing naive_equals with hmac.compare_digest flattens the timing and the attack collapses.

Variants worth knowing

Simple Power Analysis (SPA). Read the key off a single power trace by eye. RSA's square-and-multiply does a square for every bit and an extra multiply only for 1-bits, so a 1 looks visibly different from a 0 in the waveform. The fix is the constant-time "Montgomery ladder," which does the same operations regardless of the bit.

Differential / Correlation Power Analysis (DPA / CPA). Statistics over thousands of traces to beat noise. CPA correlates a Hamming-weight leakage model against the measured power; the key byte with the highest correlation wins. This is the standard attack taught with the ChipWhisperer hardware platform.

Cache attacks — Flush+Reload and Prime+Probe. Flush+Reload evicts a shared line with clflush, lets the victim run, then times re-access: a fast reload means the victim touched it. Prime+Probe fills a cache set, lets the victim run, then re-times its own lines to see which the victim evicted. Both spy on memory-access patterns with no special privileges.

Spectre and Meltdown (2018). Microarchitectural attacks that turn speculative or out-of-order execution into a side channel. Spectre coaxes the CPU into speculatively reading secret memory and leaking it through the cache before the speculation is rolled back; the architectural state is clean, but the cache footprint is not. They broke the assumption that process and privilege boundaries were also data-confidentiality boundaries.

Fault attacks (a close cousin). Instead of observing, the attacker injects a fault — a voltage glitch, a clock glitch, a laser pulse, or even Rowhammer bit flips — to make the device misbehave in a way that reveals the key (e.g., a single faulty RSA-CRT signature leaks the private factors). Often grouped with side channels because the defenses overlap.

EM and acoustic channels. Electromagnetic emanations (TEMPEST, EM-DPA) and acoustic emanations (coil whine) are non-contact channels that often beat power analysis because you do not need to splice into the power rail — a near-field probe or a microphone suffices.

Common bugs and edge cases

  • Comparing MACs or tokens with ==. The single most common real-world side channel. Always use hmac.compare_digest, crypto/subtle, or CRYPTO_memcmp.
  • Secret-dependent table lookups. AES S-box T-tables indexed by key-mixed bytes leak through the cache. Use a bit-sliced or AES-NI hardware implementation, or a full-table scan.
  • Trusting the compiler. An optimizer can re-introduce a branch or short-circuit you carefully removed, or hoist a "constant-time" select into a conditional move that is fast on one path. Constant-time guarantees need to be checked at the assembly level (tools: ctgrind, dudect, ct-verif).
  • Padding oracles. Returning a distinguishable error (or taking distinguishable time) for "bad padding" vs "bad MAC" is a side channel — Lucky Thirteen (2013) timed exactly this in TLS CBC.
  • Forgetting the length leak. A constant-time byte compare still leaks if the loop bound depends on a secret length; treat length as public or pad to a fixed size.
  • Assuming "remote means safe." Network jitter raises the trace count but does not close a timing channel; remote timing attacks are practical with enough samples and good statistics.
  • Blinding instead of constant-time, then skipping it under load. RSA blinding randomizes the input so timing carries no key information, but it has a cost and is sometimes disabled for performance — re-opening the channel.

Frequently asked questions

What is a side-channel attack in simple terms?

It is an attack that recovers a secret without breaking the algorithm itself. Instead of attacking the math, you measure a physical or behavioral side effect of the computation — how long it takes, how much power it draws, which cache lines it touches, or what it sounds like — and correlate that signal with the secret bits. The math stays unbroken; the implementation leaks.

Why does comparing two strings byte by byte leak the password?

A naive comparison returns the moment it finds the first mismatched byte. A guess that shares the first 3 bytes runs measurably longer than one that mismatches at byte 0. By timing thousands of requests an attacker recovers the secret one byte at a time, turning a 2^128 brute force into roughly 256 × 16 ≈ 4,096 guesses for a 16-byte token. The fix is a constant-time compare that always inspects every byte.

What is the difference between a timing attack and power analysis?

A timing attack measures how long the operation takes, which an attacker can often do remotely over a network. Power analysis measures the instantaneous current the chip draws, which usually requires physical access and an oscilloscope. Differential Power Analysis (DPA), introduced by Kocher, Jaffe, and Jun in 1999, can extract an AES or DES key from a smartcard using a few thousand power traces and basic statistics.

How does constant-time code defend against side channels?

Constant-time code guarantees that the sequence of instructions, memory accesses, and branches does not depend on secret data. No secret-dependent branches, no secret-dependent array indices, no early returns. The execution trace is identical regardless of the key, so timing and cache footprints carry no information. It usually costs a small constant-factor slowdown in exchange for closing the channel.

Is Spectre a side-channel attack?

Yes. Spectre (2018) tricks the CPU into speculatively executing code that touches memory based on secret data, then reads that secret back out through a cache-timing side channel — the speculative work is rolled back architecturally but leaves a footprint in the cache. The leak is the cache state, which Flush+Reload measures to within nanoseconds.

Can you side-channel a key over a network?

Yes, for timing channels. Brumley and Boneh showed in 2003 that RSA private keys could be extracted from an OpenSSL server over a local network using timing alone. Modern remote timing attacks defeat sub-microsecond differences by averaging huge sample counts and using statistical filtering, though network jitter raises the number of measurements needed by orders of magnitude.