AES Modes of Operation: Security vs Disaster Is Just One Option
1. Why Should You Care?
Hereโs a penguin image encrypted with AES:
Original ECB Encrypted CBC Encrypted
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ ๐ง๐ง๐ง โ โ ๐ง๐ง๐ง โ โ โโโโโโโโโโ โ
โ ๐ง๐ง๐ง โ โ โ ๐ง๐ง๐ง โ โ โโโโโโโโโโ โ
โ ๐ง๐ง๐ง โ โ ๐ง๐ง๐ง โ โ โโโโโโโโโโ โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
Penguin still visible! Completely randomAfter ECB mode encryption, you can still see itโs a penguin. Why? Because identical plaintext blocks produce identical ciphertext blocks. Same-color regions in the image have identical ciphertext, leaking the outline.
The AES algorithm is exactly the same, but the mode choice determines security.
2. Definition
A mode of operation defines how to use a block cipher to encrypt data longer than one block.
AES can only encrypt 16-byte blocks. If your data is longer (almost always), you need a way to:
- Split data into blocks
- Decide how blocks relate to each other
- Handle the last incomplete block (padding)
Different modes solve these problems differently, with different security properties.
3. ECB: Never Use This
How It Works
Plaintext: Pโ Pโ Pโ Pโ
โ โ โ โ
โผ โผ โผ โผ
โโโโโโโโโโโโโโโโ
Key โโโโบ โE โโE โโE โโE โ
โโโโโโโโโโโโโโโโ
โ โ โ โ
โผ โผ โผ โผ
Ciphertext: Cโ Cโ Cโ Cโ
Each block encrypted independentlyWhy Itโs Insecure
If Pโ = Pโ, then Cโ = Cโ
Attackers can:
- Detect repeated patterns
- Rearrange blocks
- Replace blocks without decryptingThe Famous ECB Penguin
This example is so famous that โECB Penguinโ became a cryptography meme. Any image with repeated patterns leaks structural information when ECB-encrypted.
When ECB Is Acceptable
Almost never. Only exceptions:
- Encrypting a single block (16 bytes) of random data
- Key wrapping algorithms (specially designed)
Even in these cases, better options exist.
4. CBC: Classic but Needs Care
How It Works
Plaintext: Pโ Pโ Pโ Pโ
โ โ โ โ
IV โโโโโโโโบโ โ โ โ
โ โ โ โ
โผ โ โ โ
โโโโโ โ โ
Key โโโโบ โE โโผ โ โ
โโโโโ โ โ
โ โ โ โ
โ โ โ โ
โผ โผ โ โ
Cโ โโโโโโโโโฌโโโโ โ
โE โโผ โ
โโโโโ โ
โ โ โ
โ โ โ
โผ โผ โ
Cโ โโโโโโโโโโโฌโโโ โ
โE โ โผ
โโโโ โ
โ โ
โ โ
โผ โผ
Cโ โโโโโโโโโโโโโฌโโโ
โE โ
โโโโ
โ
โผ
Cโ
Each block's encryption depends on previous ciphertext blockThe Critical Role of IV
Same plaintext + same key:
IV = "random1234567890" โ Ciphertext A
IV = "different7654321" โ Ciphertext B (completely different!)
IV ensures same plaintext doesn't produce same ciphertextCBC Advantages
- Same plaintext produces different ciphertext (if IV differs)
- One-bit error in ciphertext affects only two plaintext blocks
- Well-studied, widely used
CBC Problems
- IV must be random and unpredictable
# Wrong
iv = b"0" * 16 # Fixed IV
iv = str(counter).zfill(16).encode() # Predictable IV
# Correct
iv = os.urandom(16) # Random IV- Padding Oracle Attacks
If the server leaks "whether padding is correct" during decryption:
Attackers can recover plaintext byte-by-byte
This is how POODLE and Lucky 13 attacks work- No Integrity Protection
Attackers can modify ciphertext
Decryption produces garbage, but you might not know
Must add HMAC separately to verify integrity- Cannot Parallelize Encryption
Each block depends on previous block
Encryption must be sequential
(Decryption can be parallel)5. CTR: Turning Block Cipher into Stream Cipher
How It Works
Nonce || Counter: N|0 N|1 N|2 N|3
โ โ โ โ
โผ โผ โผ โผ
โโโโ โโโโ โโโโ โโโโ
Key โโโโโโโโโโโบ โE โ โE โ โE โ โE โ
โโโโ โโโโ โโโโ โโโโ
โ โ โ โ
Keystream: Kโ Kโ Kโ Kโ
โ โ โ โ
Plaintext: Pโ โ Pโ โ Pโ โ Pโ
โ โ โ โ
โผ โผ โผ โผ
Ciphertext: Cโ Cโ Cโ CโCTR Advantages
- Can parallelize encryption and decryption
- No padding needed
- Encryption and decryption use same operation
- Random access (compute block N without previous blocks)
CTR Problem
Nonce must NEVER be reused!
If same key + nonce encrypt two messages:
Cโ = Pโ โ K
Cโ = Pโ โ K
Cโ โ Cโ = Pโ โ Pโ
Attacker gets XOR of two plaintexts
If one plaintext is known, the other is recovered6. GCM: The Modern Default
What Is GCM
GCM (Galois/Counter Mode) is an authenticated encryption mode that provides:
- Confidentiality (encryption)
- Integrity (authentication)
- Additional Authenticated Data (AAD)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AES-GCM = AES-CTR Encryption + GHASH Authentication โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโHow It Works
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AES-CTR Encryption โ
โ Nonce โ Counter โ AES โ Keystream โ
โ โ โ
โ Plaintext โ
โ โ โ
โ Ciphertext โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ GHASH Authentication โ
โ AAD + Ciphertext + Lengths โ
โ โ โ
โ Authentication Tag โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโWhy GCM Is the Modern Choice
- Built-in Integrity Check
Automatically verifies authentication tag during decryption
If data was tampered, decryption fails
No separate HMAC needed- Additional Authenticated Data (AAD)
Can authenticate data that doesn't need encryption (like headers)
AAD is not encrypted but included in tag computation
Tampering AAD causes authentication failure- High Performance
CTR mode can be parallel
GHASH can be hardware-accelerated
Modern CPUs have AES-NI and PCLMULQDQ instructionsGCM Considerations
- Nonce Must Be Unique
Like CTR, nonce reuse is catastrophic
Common approaches:
- Counter (if you can guarantee no repeats)
- Random 96 bits (collision probability extremely low but not zero)- Tag Length
Recommended: 128 bits (16 bytes)
Acceptable: 96 bits for some applications
Shorter tag = weaker authentication- Data Volume Limits
Single key + nonce combination:
Maximum 2ยณโน - 256 bits (~64GB) per encryption
Beyond this limit, change key or nonce7. Mode Comparison Table
| Feature | ECB | CBC | CTR | GCM |
|---|---|---|---|---|
| Confidentiality | โ ๏ธ | โ | โ | โ |
| Integrity | โ | โ | โ | โ |
| Parallel Encrypt | โ | โ | โ | โ |
| Parallel Decrypt | โ | โ | โ | โ |
| Padding Needed | โ | โ | โ | โ |
| IV/Nonce Needed | โ | โ | โ | โ |
| IV/Nonce Reuse Impact | N/A | Pattern Leak | Total Break | Total Break |
| Recommended | โ | โ ๏ธ | โ ๏ธ | โ |
8. Practical Code Examples
AES-GCM (Recommended)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
def encrypt_aes_gcm(plaintext: bytes, key: bytes, aad: bytes = b"") -> tuple:
"""
Encrypt using AES-GCM
Returns (nonce, ciphertext_with_tag)
"""
aesgcm = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
return nonce, ciphertext
def decrypt_aes_gcm(nonce: bytes, ciphertext: bytes, key: bytes, aad: bytes = b"") -> bytes:
"""
Decrypt using AES-GCM
Raises exception if authentication fails
"""
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce, ciphertext, aad)
# Usage example
key = AESGCM.generate_key(bit_length=256)
message = b"Secret message"
header = b"public header" # AAD
nonce, ciphertext = encrypt_aes_gcm(message, key, header)
plaintext = decrypt_aes_gcm(nonce, ciphertext, key, header)
print(f"Original: {message}")
print(f"Decrypted: {plaintext}")AES-CBC + HMAC (Legacy but Still Acceptable)
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import hashes, hmac, padding
import os
def encrypt_aes_cbc_hmac(plaintext: bytes, enc_key: bytes, mac_key: bytes) -> tuple:
"""
AES-CBC encryption + HMAC authentication
Encrypt-then-MAC approach
"""
# Padding
padder = padding.PKCS7(128).padder()
padded = padder.update(plaintext) + padder.finalize()
# Encrypt
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(enc_key), modes.CBC(iv))
encryptor = cipher.encryptor()
ciphertext = encryptor.update(padded) + encryptor.finalize()
# Compute MAC (including IV)
h = hmac.HMAC(mac_key, hashes.SHA256())
h.update(iv + ciphertext)
tag = h.finalize()
return iv, ciphertext, tag
def decrypt_aes_cbc_hmac(iv: bytes, ciphertext: bytes, tag: bytes,
enc_key: bytes, mac_key: bytes) -> bytes:
"""
Verify HMAC then decrypt
"""
# Verify MAC first
h = hmac.HMAC(mac_key, hashes.SHA256())
h.update(iv + ciphertext)
h.verify(tag) # Raises exception if verification fails
# Decrypt
cipher = Cipher(algorithms.AES(enc_key), modes.CBC(iv))
decryptor = cipher.decryptor()
padded = decryptor.update(ciphertext) + decryptor.finalize()
# Remove padding
unpadder = padding.PKCS7(128).unpadder()
plaintext = unpadder.update(padded) + unpadder.finalize()
return plaintext9. Common Mistakes
Mistake 1: Using ECB Mode
# Wrong
cipher = Cipher(algorithms.AES(key), modes.ECB())
# Correct
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce))Mistake 2: Reusing IV/Nonce
# Wrong
nonce = b"fixed_nonce_123" # Fixed nonce
for message in messages:
encrypt(message, key, nonce)
# Correct
for message in messages:
nonce = os.urandom(12) # Generate new each time
encrypt(message, key, nonce)Mistake 3: CBC Without Authentication
# Wrong
ciphertext = aes_cbc_encrypt(plaintext, key, iv)
# No MAC, attacker can tamper
# Correct
ciphertext = aes_cbc_encrypt(plaintext, key, iv)
tag = hmac(mac_key, iv + ciphertext)
# Verify tag before decryptionMistake 4: MAC-then-Encrypt vs Encrypt-then-MAC
# Wrong (MAC-then-Encrypt)
tag = hmac(plaintext)
ciphertext = encrypt(plaintext + tag)
# Cannot verify integrity before decryption
# Correct (Encrypt-then-MAC)
ciphertext = encrypt(plaintext)
tag = hmac(ciphertext)
# Can verify integrity before decryption10. Summary
Three things to remember:
Never use ECB. It leaks plaintext patterns. If you see ECB in code, itโs a bug.
Prefer GCM. It provides encryption and authentication, and is the modern default. If you must use CBC, always add HMAC (Encrypt-then-MAC).
IV/Nonce must be unique. CBCโs IV needs to be unpredictable. CTR and GCMโs nonce just needs to be unique, but reuse leads to total compromise.
11. Whatโs Next
Weโve understood AES modes of operation. But how is symmetric encryption used in real systems?
In the next article, weโll explore: Symmetric encryption in real systemsโthe symmetric encryption phase in HTTPS, file encryption, and database encryption best practices.
