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.
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:
| Variant | Key Size | Rounds | Throughput (AES-NI) | Security |
|---|---|---|---|---|
| AES-128 | 128 bits (16 bytes) | 10 | ~1.5 GB/s | Secure (NIST SP 800-131A) |
| AES-192 | 192 bits (24 bytes) | 12 | ~1.2 GB/s | Secure but rarely used |
| AES-256 | 256 bits (32 bytes) | 14 | ~1.1 GB/s | Secure — highest margin |
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.
| Property | ECB | CBC | CTR | GCM |
|---|---|---|---|---|
| Parallelizable | Yes | Encrypt: No / Decrypt: Yes | Yes | Yes |
| IV / Nonce required | No | Yes (unpredictable) | Yes (unique) | Yes (unique, 96-bit) |
| Authentication | No | No | No | Yes (GMAC) |
| Padding needed | Yes | Yes | No (stream) | No (stream) |
| Pattern leakage | Yes — critical | No | No | No |
| Recommendation | Never use | Acceptable with HMAC | Good (with MAC) | Recommended |
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.
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.
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
- Authenticated encryption: Detects tampering without a separate HMAC step.
- Hardware acceleration: AES-NI + PCLMULQDQ instructions make GCM faster than any software HMAC.
- TLS 1.3: GCM is the only AEAD mode in TLS 1.3 (along with ChaCha20-Poly1305).
- 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:
- The CTR keystream repeats, leaking
P₁ ⊕ P₂(same as CTR mode) - Worse: The GHASH authentication key
Hcan be recovered, allowing forgery of arbitrary messages
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.
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)),
};
}
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.