Cryptography

Block Cipher Modes (CBC, CTR, GCM)

A cipher only encrypts 16 bytes — modes are how you encrypt a gigabyte

Block cipher modes turn a fixed-size cipher like AES into a tool for encrypting arbitrary-length data: ECB encrypts each block independently and leaks patterns, CBC chains blocks with an IV, CTR turns the cipher into a parallel keystream, and GCM adds authentication.

  • AES block size128 bits (16 bytes)
  • ECB securityBroken — never use
  • CTR / GCM nonceUnique per message
  • GCM tag size128 bits
  • Default choice todayAES-GCM or ChaCha20-Poly1305

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.

Why a block cipher needs a mode

A block cipher like AES is a deterministic function on a single fixed-size block. AES takes a 128-bit (16-byte) block and a key, and maps it to a 128-bit ciphertext block — and that's all it does. It is a giant, key-dependent permutation over the 2128 possible blocks. It has no concept of "a file," "a message," or "a stream." Feed it 17 bytes and it doesn't know what to do.

A mode of operation is the protocol that wraps the raw block cipher so it can encrypt arbitrary-length data. The mode decides how to split the message into blocks, what to feed the cipher for each block, and how to glue the outputs back together. Crucially, the mode — not AES — is where almost all real-world cryptographic failures live. AES itself has never been broken; padding oracles, the ECB penguin, BEAST, and nonce-reuse forgeries are all mode failures.

Two facts drive every mode design:

  1. Determinism leaks. If encrypting the same plaintext always gives the same ciphertext, an observer learns which messages repeat. Good modes inject randomness — an IV or nonce — so the same message encrypts differently each time.
  2. Confidentiality is not integrity. Hiding the contents of a message doesn't stop an attacker from changing it. Modern modes bolt on authentication so tampering is detected.

ECB: the mode you must never use

Electronic Codebook is the naive answer: chop the plaintext into 16-byte blocks and encrypt each one independently with the same key. It is trivially parallel and needs no IV. It is also broken, because it satisfies neither fact above.

C[i] = E(K, P[i])      // each block encrypted in isolation

Because the cipher is deterministic, P[i] == P[j] forces C[i] == C[j]. Any structure in the plaintext — repeated headers, runs of zeros, the uniform background of an image — survives encryption as repeated ciphertext. The canonical demo is the ECB penguin: encrypt a bitmap of Tux the penguin with AES-ECB and you can still see the penguin, because every block of the white background encrypts to the same ciphertext block. ECB hides byte values but leaks the shape of the data.

ECB is the baseline every other mode improves on. The rule is absolute: never use ECB to encrypt anything longer than a single block.

CBC: chaining with an IV

Cipher Block Chaining fixes ECB's determinism by feeding each ciphertext block forward. Before encrypting block i, you XOR it with the previous ciphertext block; the very first block is XORed with a random initialization vector.

C[0] = E(K, P[0] XOR IV)
C[i] = E(K, P[i] XOR C[i-1])      // chaining

P[0] = D(K, C[0]) XOR IV
P[i] = D(K, C[i]) XOR C[i-1]      // decryption

The IV makes identical messages encrypt to different ciphertexts, killing the ECB pattern leak. But CBC has structural costs. Encryption is strictly sequential — block i can't start until block i−1 is done — so it can't use the CPU's pipelining or AES-NI parallelism on the encrypt path. (Decryption does parallelize, since each P[i] depends only on C[i] and C[i-1], both already known.)

CBC needs the plaintext padded to a whole number of blocks, usually with PKCS#7. That padding is the seed of the padding-oracle attack: if a server leaks whether decryption produced valid padding, an attacker decrypts the ciphertext byte by byte without the key. CBC also requires an unpredictable IV — TLS 1.0 reused the last ciphertext block of the previous record as the next IV, which let attackers predict it and mount BEAST. CBC provides confidentiality only; you must add a separate MAC (encrypt-then-MAC) to get integrity.

CTR: a block cipher as a keystream

Counter mode stops feeding plaintext into the cipher at all. Instead it encrypts a sequence of counters — a nonce concatenated with a block index — to produce a pseudorandom keystream, then XORs that keystream with the plaintext. The block cipher is used purely as a random-looking number generator.

keystream[i] = E(K, nonce || i)
C[i] = P[i] XOR keystream[i]
P[i] = C[i] XOR keystream[i]      // identical operation, symmetric

This has three big consequences. First, every block's keystream depends only on its index, so the whole thing is fully parallelizable and seekable — you can decrypt byte 1,000,000 without touching the first 999,999. Second, CTR turns AES into a stream cipher, so no padding is needed and there's no padding oracle. Third, encryption and decryption are the same operation (XOR), so you only ever need the cipher's encrypt function.

The price: CTR is brittle about its nonce. Reuse a (key, nonce) pair and you reuse the keystream; XOR the two ciphertexts and the keystream cancels, exposing P1 XOR P2. Like CBC, plain CTR is unauthenticated — bit-flipping the ciphertext flips the exact same bit in the recovered plaintext, invisibly. CTR is rarely used bare; it's the engine inside GCM.

GCM: CTR plus authentication

Galois/Counter Mode is CTR for confidentiality plus a GHASH authentication tag for integrity — a single primitive that gives you authenticated encryption with associated data (AEAD). You encrypt with CTR, then compute a 128-bit tag over the ciphertext and any associated data (headers, packet numbers — data you want authenticated but not encrypted) using carry-less multiplication in the Galois field GF(2128).

H        = E(K, 0^128)            // hash subkey
C[i]     = P[i] XOR E(K, J0 + i)  // CTR encryption
tag      = GHASH(H, AAD, C) XOR E(K, J0)
// decryption recomputes the tag and rejects if it doesn't match

The tag is the whole point. With GCM, any single flipped ciphertext bit changes the recomputed tag, the comparison fails, and decryption returns an error instead of garbage plaintext — closing the silent-tampering hole that plagues CBC and bare CTR. Because the confidentiality engine is CTR, GCM keeps CTR's parallelism and needs no padding (so no padding oracle). The GHASH step is cheap on modern CPUs thanks to the PCLMULQDQ carry-less-multiply instruction.

GCM inherits CTR's nonce fragility and makes it worse: reuse a (key, nonce) pair and you not only leak P1 XOR P2, you leak the hash subkey H, which lets an attacker forge valid tags for messages they never saw. The recommended nonce is a 96-bit random or counter value, kept unique for the lifetime of the key. GCM is the default record-layer mode in TLS 1.3.

ECB vs CBC vs CTR vs GCM

ECBCBCCTRGCM
ConfidentialityBroken (leaks patterns)Good (with IV)Good (with nonce)Good
Built-in integrityNoNoNoYes (128-bit tag)
RandomizationNoneRandom unpredictable IVUnique nonceUnique nonce
Encryption parallel?YesNo (sequential)YesYes
Decryption parallel?YesYesYesYes
Padding needed?YesYes (PKCS#7)NoNo
Random access / seekableYesNoYesYes
Worst failure modeThe ECB penguinPadding oracle, BEASTNonce reuse → keystream reuseNonce reuse → key recovery + forgery
Uses cipher decrypt op?YesYesNo (encrypt only)No (encrypt only)
Real-world useNever (textbook only)Legacy TLS, disk encryption layersEngine inside GCM, disk encryptionTLS 1.3, IPsec, SSH, S3 SSE

The trend is clear: the industry moved from unauthenticated modes (CBC, CTR) to AEAD modes (GCM, ChaCha20-Poly1305) precisely because separating confidentiality from integrity caused decades of bugs. If you are choosing a mode today and have no special constraint, choose GCM or ChaCha20-Poly1305.

What the numbers actually say

  • Block size is 16 bytes for AES. A 1 MB file is 65,536 AES blocks. In ECB, every 16-byte run that repeats anywhere in those 65,536 blocks produces a visibly identical ciphertext block — that's how the penguin's outline survives.
  • GCM costs one extra 128-bit multiply per block. On CPUs with AES-NI + PCLMULQDQ, AES-GCM runs around 1–2 cycles per byte — roughly 2–4 GB/s on a single modern core, only modestly slower than raw CTR.
  • The GCM nonce birthday bound. With random 96-bit nonces under one key, NIST SP 800-38D limits you to about 232 messages before the collision probability becomes unacceptable — which is why long-lived keys use a counter, not pure randomness, for the nonce.
  • A 16-byte tag costs 16 bytes. GCM expands every message by exactly the tag length (typically 16 bytes) plus the 12-byte nonce you must transmit — negligible for files, but it matters for tiny packets.
  • CBC's sequential encrypt path can be 3–8× slower than CTR/GCM on hardware that can pipeline AES rounds, because the data dependency between blocks defeats instruction-level parallelism.

JavaScript implementation

In production you never roll your own — you call a vetted library. Here's AES-GCM and AES-CBC using the Web Crypto API, which ships in every browser and Node 16+. Note how GCM handles the tag and authentication transparently:

// AES-256-GCM — authenticated encryption, the modern default.
async function gcmEncrypt(keyBytes, plaintext, aad) {
  const key = await crypto.subtle.importKey(
    'raw', keyBytes, 'AES-GCM', false, ['encrypt']);
  const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit nonce, UNIQUE per message
  const ct = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv, additionalData: aad ?? new Uint8Array() },
    key, plaintext);
  // ct already includes the 16-byte auth tag appended at the end.
  return { iv, ct: new Uint8Array(ct) };
}

async function gcmDecrypt(keyBytes, iv, ct, aad) {
  const key = await crypto.subtle.importKey(
    'raw', keyBytes, 'AES-GCM', false, ['decrypt']);
  // Throws OperationError if the tag fails — tampering is rejected automatically.
  const pt = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv, additionalData: aad ?? new Uint8Array() },
    key, ct);
  return new Uint8Array(pt);
}

// CBC for comparison — note it needs a 16-byte IV and gives NO integrity.
async function cbcEncrypt(keyBytes, plaintext) {
  const key = await crypto.subtle.importKey(
    'raw', keyBytes, 'AES-CBC', false, ['encrypt']);
  const iv = crypto.getRandomValues(new Uint8Array(16));
  const ct = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, key, plaintext);
  // WARNING: you must add your own MAC (encrypt-then-MAC) for integrity.
  return { iv, ct: new Uint8Array(ct) };
}

The asymmetry is the lesson: GCM's decrypt throws on tampering, so integrity is free. CBC's does not — a flipped bit silently corrupts the plaintext, and you're responsible for a separate MAC.

Python implementation

Python's cryptography library exposes high-level AEAD wrappers. This also shows the nonce-reuse trap concretely — and the ECB pattern leak that motivates everything else:

import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

# --- AES-GCM: the modern default ---
def gcm_encrypt(key, plaintext, aad=b""):
    nonce = os.urandom(12)                 # 96-bit nonce — MUST be unique per key
    ct = AESGCM(key).encrypt(nonce, plaintext, aad)  # tag appended to ct
    return nonce, ct

def gcm_decrypt(key, nonce, ct, aad=b""):
    # Raises InvalidTag on any tampering or wrong AAD.
    return AESGCM(key).decrypt(nonce, ct, aad)

# --- Why ECB is broken: identical blocks -> identical ciphertext ---
def ecb_leaks(key):
    enc = Cipher(algorithms.AES(key), modes.ECB()).encryptor()
    block = b"A" * 16
    out = enc.update(block * 3) + enc.finalize()
    # All three 16-byte ciphertext blocks are byte-for-byte identical:
    assert out[0:16] == out[16:32] == out[32:48]
    return out

# --- CTR: a keystream you must never reuse ---
def ctr_keystream_reuse_is_fatal(key, nonce):
    def ctr(pt):
        enc = Cipher(algorithms.AES(key), modes.CTR(nonce)).encryptor()
        return enc.update(pt) + enc.finalize()
    c1, c2 = ctr(b"attack at dawn!!"), ctr(b"retreat at dusk!")
    # XOR of ciphertexts == XOR of plaintexts: the keystream cancels out.
    leaked = bytes(a ^ b for a, b in zip(c1, c2))
    return leaked  # == b"attack..." XOR b"retreat..."  -> plaintext relationship exposed

The ecb_leaks assertion is the penguin in miniature: three identical input blocks become three identical output blocks. And ctr_keystream_reuse_is_fatal shows why a nonce is sacred — reuse it and the keystream subtracts itself out.

Variants worth knowing

CCM (Counter with CBC-MAC). The other NIST AEAD mode: CTR for confidentiality, CBC-MAC for integrity. Two passes over the data (slower than GCM's one), but it needs no carry-less-multiply hardware, so it's common in constrained IoT and Wi-Fi (WPA2 uses AES-CCMP).

ChaCha20-Poly1305. Not a block-cipher mode at all — ChaCha20 is a native stream cipher and Poly1305 is its MAC. It's the go-to AEAD on devices without AES hardware (most phones, older ARM), where constant-time software AES is slow. TLS 1.3 negotiates it as a co-equal alternative to AES-GCM.

XTS. The disk-encryption mode (BitLocker, FileVault, LUKS). It's tweakable: each disk sector gets a "tweak" derived from its position, so identical sectors encrypt differently without storing a per-sector IV. It deliberately omits authentication because there's nowhere on a fixed-size sector to put a tag.

SIV / AES-GCM-SIV (nonce-misuse-resistant). Built to survive accidental nonce reuse: the IV is derived from the message itself, so reusing a nonce on different messages leaks only whether the messages were identical — not the key. The cost is two passes over the data. Worth it when nonce uniqueness is hard to guarantee (e.g. distributed systems with no shared counter).

CFB and OFB. Older self-synchronizing / synchronous stream constructions from a block cipher. Largely historical now; CTR superseded both because it's seekable and parallel.

Common bugs and edge cases

  • Reusing a nonce in CTR or GCM. The single most catastrophic mistake. In CTR it leaks plaintext XOR; in GCM it additionally leaks the GHASH key and enables tag forgery. Use a counter or a CSPRNG, and rotate keys before the birthday bound.
  • Using ECB "because it's simpler." It's never acceptable for multi-block data. If a library defaults to ECB (some old ones do), that's a bug, not a feature.
  • Treating CBC/CTR as if they provide integrity. They don't. Encrypt-then-MAC, or just use an AEAD mode and stop thinking about it.
  • MAC-then-encrypt or encrypt-and-MAC ordering. Only encrypt-then-MAC is generically secure. The other orderings caused real attacks (e.g. Lucky 13 against TLS's MAC-then-encrypt CBC).
  • Predictable IVs in CBC. The IV must be unpredictable, not just unique — predictable IVs enabled BEAST. A fresh CSPRNG value per message is correct.
  • Leaking a padding error. Distinct error messages or response times for "bad padding" vs "bad MAC" reopen the padding oracle. Verify the MAC in constant time before touching the padding, or use a mode with no padding.
  • Forgetting the tag is part of the ciphertext. GCM output is ciphertext + tag; drop or truncate the tag and decryption can't authenticate. Most libraries append it automatically — don't split it off and lose it.

Frequently asked questions

Why does ECB mode leak the patterns in an image?

ECB encrypts every 16-byte block independently with the same key, so identical plaintext blocks always produce identical ciphertext blocks. Large uniform regions — like the background of an image — map to repeating ciphertext, leaving the outline of the original clearly visible. This is the famous ECB penguin.

What is an IV and why must it be unpredictable in CBC?

The initialization vector is a random block XORed into the first plaintext block so that encrypting the same message twice gives different ciphertext. In CBC the IV must be unpredictable: if an attacker can guess the next IV (as in TLS 1.0's chained IVs), they can mount the BEAST attack. CTR and GCM only require the nonce to be unique, not unpredictable.

Why is CTR mode parallelizable but CBC is not?

CTR encrypts a counter — nonce plus block index — to produce a keystream, then XORs it with the plaintext. Every block's keystream depends only on its index, so all blocks can be computed at once. CBC feeds each ciphertext block into the next as input, so block N cannot start until block N-1 finishes. Encryption is strictly sequential; only CBC decryption parallelizes.

What does GCM add over plain CTR mode?

GCM is CTR mode for confidentiality plus a GHASH authentication tag for integrity. The tag is computed over the ciphertext and any associated data using multiplication in GF(2^128). Without it, an attacker who flips a ciphertext bit flips the matching plaintext bit silently; with it, any tampering fails the tag check and decryption aborts.

What happens if you reuse a nonce in CTR or GCM?

Reusing a nonce with the same key is catastrophic. Two messages encrypted under the same keystream let an attacker XOR the ciphertexts to cancel the keystream and recover plaintext relationships. In GCM, nonce reuse is worse: it leaks the GHASH authentication key H, letting an attacker forge valid tags for arbitrary messages.

Why is padding-oracle a CBC problem and not a CTR problem?

CBC needs block-aligned input, so it uses padding (typically PKCS#7). If a server reveals whether decryption hit a padding error — through an error message or timing difference — an attacker can decrypt ciphertext one byte at a time without the key. CTR and GCM are stream-like and need no padding, so they have no padding oracle. This is why authenticated modes are now preferred.