AES Encryption Explained — Block Cipher Modes, Padding, and Why ECB Is Insecure

AES (Advanced Encryption Standard, FIPS 197) is the symmetric cipher that protects everything from TLS traffic to stored passwords to EMV chip card transactions. It is a block cipher — it encrypts exactly 128 bits (16 bytes) at a time. But AES alone isn't enough: you need a mode of operation to handle data longer than one block. The mode you choose determines whether your encryption is secure or trivially breakable.

Hands-on: The AES / SM4 Encryption Tool lets you encrypt and decrypt with AES-128/192/256 in ECB, CBC, and GCM modes — all in your browser, no data uploaded.

1. AES Key Sizes and Rounds

AES supports three key lengths. All operate on the same 128-bit block, but longer keys mean more transformation rounds:

VariantKey SizeRoundsThroughput (AES-NI)Security
AES-128128 bits (16 bytes)10~1.5 GB/sSecure (NIST SP 800-131A)
AES-192192 bits (24 bytes)12~1.2 GB/sSecure but rarely used
AES-256256 bits (32 bytes)14~1.1 GB/sSecure — highest margin
AES-128 vs AES-256: Both are secure against all known practical attacks. AES-256 is recommended only if you need post-quantum security margin or compliance with certain standards (e.g., FIPS 140-2 Level 3+). For most applications, AES-128 is faster and equally secure.

2. Block Cipher Modes: The Critical Choice

AES encrypts 16 bytes at a time. Real messages are longer. The mode of operation defines how to chain multiple blocks together:

ECB (Electronic Codebook) — Do Not Use

Each 16-byte block is encrypted independently with the same key. Identical plaintext blocks produce identical ciphertext blocks.

PropertyECBCBCCTRGCM
ParallelizableYesEncrypt: No / Decrypt: YesYesYes
IV / Nonce requiredNoYes (unpredictable)Yes (unique)Yes (unique, 96-bit)
AuthenticationNoNoNoYes (GMAC)
Padding neededYesYesNo (stream)No (stream)
Pattern leakageYes — criticalNoNoNo
RecommendationNever useAcceptable with HMACGood (with MAC)Recommended
The ECB Penguin: The classic demonstration of ECB's insecurity encrypts an image of Tux (the Linux penguin) with AES-ECB. The ciphertext still shows the penguin silhouette because identical pixel blocks produce identical encrypted blocks. This is why openssl enc -aes-128-ecb should never be used for real data.

3. CBC Mode: The Traditional Workhorse

CBC (Cipher Block Chaining) XORs each plaintext block with the previous ciphertext block before encryption. This ensures identical plaintexts produce different ciphertexts as long as the IV is unique:

Encryption:  C₀ = AES(P₀ ⊕ IV)
             C₁ = AES(P₁ ⊕ C₀)
             C₂ = AES(P₂ ⊕ C₁)

Decryption:  P₀ = AES⁻¹(C₀) ⊕ IV
             P₁ = AES⁻¹(C₁) ⊕ C₀

PKCS7 Padding

AES processes 16-byte blocks. If your plaintext isn't a multiple of 16, you need padding. PKCS7 is the standard:

# Python PKCS7 padding
from cryptography.hazmat.primitives import padding

padder = padding.PKCS7(128).padder()
padded = padder.update(plaintext) + padder.finalize()

# Unpad
unpadder = padding.PKCS7(128).unpadder()
original = unpadder.update(padded) + unpadder.finalize()

If the plaintext is already 16 bytes, PKCS7 adds a full extra block of \x10\x10\x10...\x10 (16 bytes, each byte = 0x10). This padding is always present, so the unpadding logic is unambiguous.

Padding Oracle Attack: CBC without a MAC is vulnerable to the padding oracle attack (Vaudenay, 2002). An attacker who can distinguish "invalid padding" from "valid padding" errors can decrypt the entire message one byte at a time. This is why you must always authenticate CBC ciphertext with an HMAC (encrypt-then-MAC), or better, use GCM.

4. CTR Mode: Turning a Block Cipher into a Stream Cipher

CTR (Counter) mode doesn't encrypt the plaintext directly. Instead, it encrypts a counter nonce+counter, then XORs the result with the plaintext:

keystream_i = AES(nonce || counter_i)
ciphertext_i = plaintext_i ⊕ keystream_i

Because there's no padding, CTR mode handles arbitrary-length data. It's also fully parallelizable — both encryption and decryption can run in parallel across all blocks.

Nonce uniqueness is critical: If the same nonce is reused with the same key, XORing two ciphertexts cancels out the keystream: C₁ ⊕ C₂ = P₁ ⊕ P₂. The attacker learns the XOR of two plaintexts, which is easily analyzed. Never reuse a nonce.

5. GCM: Authenticated Encryption (The Default Choice)

GCM (Galois/Counter Mode) combines CTR mode for encryption with a Galois MAC for authentication. It provides both confidentiality and integrity in a single operation — you can detect if the ciphertext was modified:

# Python: AES-256-GCM encryption and decryption
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

key = AESGCM.generate_key(bit_length=256)       # 32-byte key
nonce = os.urandom(12)                           # 12-byte nonce (96-bit)

cipher = AESGCM(key)
ciphertext = cipher.encrypt(nonce, plaintext, associated_data)
# ciphertext includes the 16-byte GMAC tag appended

# Decryption will raise InvalidTag if tampered:
plaintext = cipher.decrypt(nonce, ciphertext, associated_data)

Why GCM Is the Standard

  1. Authenticated encryption: Detects tampering without a separate HMAC step.
  2. Hardware acceleration: AES-NI + PCLMULQDQ instructions make GCM faster than any software HMAC.
  3. TLS 1.3: GCM is the only AEAD mode in TLS 1.3 (along with ChaCha20-Poly1305).
  4. No padding: Stream-mode operation, so ciphertext is the same length as plaintext (plus 16-byte tag).

The Nonce Reuse Catastrophe

GCM is unforgiving about nonce reuse. If you reuse a nonce with the same key:

Use a 96-bit nonce. NIST SP 800-38D recommends a 96-bit nonce for GCM. With 96 bits, you can safely encrypt up to 2^32 messages with random nonces before collision risk. If you use a 64-bit random nonce, you're limited to about 2^32 messages total (birthday bound). For most applications, os.urandom(12) is correct.

6. Key Derivation: Never Use a Password Directly as an AES Key

AES keys must be uniformly random. Passwords are not random. Use a key derivation function:

# Python: derive AES-256 key from a password using PBKDF2
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
import os, base64

password = b"user_password"
salt = os.urandom(16)     # store the salt alongside ciphertext
kdf = PBKDF2HMAC(
    algorithm=hashes.SHA256(),
    length=32,             # 256-bit AES key
    salt=salt,
    iterations=600000,    # OWASP 2023 recommendation
)
key = kdf.derive(password)
# Use `key` for AES-256-GCM. Store salt + nonce + ciphertext + tag.
Key derivation is critical: Using a raw password as an AES key means the effective key space is limited to human-guessable passwords — a few billion possibilities instead of 2^256. Always use PBKDF2, scrypt, or Argon2. The PBKDF2 Calculator lets you experiment with iteration counts and derived key lengths.

7. JavaScript Implementation (AES-256-GCM)

// Browser: AES-256-GCM encrypt/decrypt using Web Crypto API
async function encrypt(plaintext, password) {
  // 1. Derive key with PBKDF2
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const baseKey = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(password),
    "PBKDF2", false, ["deriveKey"]
  );
  const key = await crypto.subtle.deriveKey(
    { name: "PBKDF2", salt, iterations: 600000, hash: "SHA-256" },
    baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]
  );

  // 2. Encrypt with AES-256-GCM
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = new Uint8Array(
    await crypto.subtle.encrypt(
      { name: "AES-GCM", iv }, key,
      new TextEncoder().encode(plaintext)
    )
  );
  // ciphertext already includes the 16-byte tag

  // 3. Package: salt + iv + ciphertext (all base64)
  return {
    salt: btoa(String.fromCharCode(...salt)),
    iv: btoa(String.fromCharCode(...iv)),
    data: btoa(String.fromCharCode(...ciphertext)),
  };
}
Test it yourself: The AES / SM4 Encryption Tool uses the same Web Crypto API. You can encrypt a message with AES-256-GCM and then verify the ciphertext matches by decrypting it back.

8. FAQ

What is the difference between AES-128, AES-192, and AES-256?

The number refers to the key length in bits. AES-128 uses 10 rounds, AES-192 uses 12 rounds, and AES-256 uses 14 rounds. All three operate on 128-bit blocks. AES-256 provides the highest security margin but is about 40% slower than AES-128. All three are considered secure by NIST.

Why is AES ECB mode insecure?

ECB (Electronic Codebook) encrypts each 16-byte block independently with the same key, so identical plaintext blocks produce identical ciphertext blocks. This leaks patterns — the famous ECB penguin image shows how the structure of the original image is visible in the ciphertext. Use CBC, CTR, or GCM instead.

What is the difference between an IV and a nonce?

An IV (Initialization Vector) is used in CBC mode and must be unpredictable (random). A nonce (Number Used Once) is used in CTR and GCM modes and only needs to be unique for a given key, not necessarily random — a counter works fine. Reusing a nonce with the same key in GCM is catastrophic and leaks the authentication key.

Should I use AES-GCM or AES-CBC?

Always prefer AES-GCM (Galois/Counter Mode) for new applications. GCM provides authenticated encryption — it guarantees both confidentiality and integrity. CBC only provides confidentiality and is vulnerable to padding oracle attacks if you don't add a separate MAC. GCM is also parallelizable and faster on modern CPUs with AES-NI.