AES: Deep Dive
In 1997, the U.S. government launched a worldwide competition to find a replacement for the aging DES (Data Encryption Standard). Four years later, they chose Rijndael—a cipher designed by two Belgian cryptographers—and renamed it AES. Today, AES protects everything from your hard drive to your bank transactions. This is the complete story.
Origins: The AES Competition (1997-2001)
By the mid-1990s, DES (the previous encryption standard from 1977) was showing its age. With a 56-bit key, it was vulnerable to brute-force attacks (trying every possible key until one works)—demonstrated publicly when the Electronic Frontier Foundation cracked a DES key in 56 hours using a $250,000 custom machine.
NIST (National Institute of Standards and Technology) announced the AES selection process in January 1997, inviting cryptographers worldwide to submit candidates. The requirements were clear:
- Block size: 128 bits (fixed)
- Key sizes: 128, 192, and 256 bits
- Security: At least as secure as Triple-DES (a stopgap fix that ran DES three times)
- Performance: Efficient in both hardware and software
Fifteen algorithms entered the competition. By August 1999, NIST narrowed the field to five finalists:
| Algorithm | Designers | Origin |
|---|---|---|
| MARS | IBM | USA |
| RC6 | RSA Laboratories | USA |
| Rijndael | Joan Daemen, Vincent Rijmen | Belgium |
| Serpent | Anderson, Biham, Knudsen | UK/Israel/Norway |
| Twofish | Bruce Schneier et al. | USA |
On October 2, 2000, NIST announced the winner: Rijndael (pronounced "rain-doll"). The name combines the designers' surnames: Rijmen and Daemen.
Rijndael won because it offered the best combination of security, performance, and simplicity. On November 26, 2001, NIST published the AES standard as FIPS PUB 197.
The Designers
Joan Daemen and Vincent Rijmen developed Rijndael during their PhD research at KU Leuven (Katholieke Universiteit Leuven) in Belgium. They were the only two people involved—together they designed the cipher, wrote the reference implementation, and proved its security.
Many observers were surprised that the U.S. government chose a non-American algorithm. The decision demonstrated the competition's integrity and the strength of Rijndael's design.
AES at a Glance
| Property | Value |
|---|---|
| Block size | 128 bits (16 bytes) |
| Key sizes | 128, 192, or 256 bits |
| Rounds | 10 (AES-128), 12 (AES-192), 14 (AES-256) |
| Structure | Substitution-permutation network (replace bytes, then shuffle them) |
| Operations | SubBytes, ShiftRows, MixColumns, AddRoundKey |
Algorithm Structure
AES operates on a 4×4 matrix of bytes called the state. Each round transforms this state using four operations:
┌────┬────┬────┬────┐
│ s0 │ s4 │ s8 │ s12│
├────┼────┼────┼────┤
│ s1 │ s5 │ s9 │ s13│ ← The State (16 bytes)
├────┼────┼────┼────┤
│ s2 │ s6 │ s10│ s14│
├────┼────┼────┼────┤
│ s3 │ s7 │ s11│ s15│
└────┴────┴────┴────┘
The Four Operations
1. SubBytes (Substitution)
Each byte is replaced with another according to a lookup table called the S-box (substitution box). This provides confusion—making the relationship between the key and ciphertext as complex as possible.
The S-box is constructed using advanced mathematics (finite field arithmetic) that ensures no simple patterns exist. You don't need to understand the math to use AES—just know that each input byte maps to exactly one output byte, and this mapping was carefully designed to resist all known attack techniques.
2. ShiftRows (Permutation)
Each row of the state is cyclically shifted:
- Row 0: No shift
- Row 1: Shift left by 1 byte
- Row 2: Shift left by 2 bytes
- Row 3: Shift left by 3 bytes
Before: After:
┌────┬────┬────┬────┐ ┌────┬────┬────┬────┐
│ a0 │ a1 │ a2 │ a3 │ │ a0 │ a1 │ a2 │ a3 │ ← No shift
├────┼────┼────┼────┤ ├────┼────┼────┼────┤
│ b0 │ b1 │ b2 │ b3 │ │ b1 │ b2 │ b3 │ b0 │ ← Shift 1
├────┼────┼────┼────┤ ├────┼────┼────┼────┤
│ c0 │ c1 │ c2 │ c3 │ │ c2 │ c3 │ c0 │ c1 │ ← Shift 2
├────┼────┼────┼────┤ ├────┼────┼────┼────┤
│ d0 │ d1 │ d2 │ d3 │ │ d3 │ d0 │ d1 │ d2 │ ← Shift 3
└────┴────┴────┴────┘ └────┴────┴────┴────┘
This ensures that columns don't encrypt independently.
3. MixColumns (Diffusion)
Each column is transformed using matrix multiplication (don't worry—the computer does this). The fixed matrix is:
┌ ┐ ┌ ┐ ┌ ┐
│ b0│ │ 02 03 01 01 │ │ a0│
│ b1│ = │ 01 02 03 01 │ × │ a1│
│ b2│ │ 01 01 02 03 │ │ a2│
│ b3│ │ 03 01 01 02 │ │ a3│
└ ┘ └ ┘ └ ┘
This provides diffusion—spreading the influence of each input bit across multiple output bits. After MixColumns, each output byte depends on all four input bytes of that column.
Note: The final round omits MixColumns. This makes encryption and decryption more symmetric.
4. AddRoundKey (Key Mixing)
Each byte of the state is XORed with the corresponding byte of the round key. (XOR is a simple bit operation: compare two bits—if they're different, output 1; if they're the same, output 0.) The round keys are derived from the original key using the key schedule.
Round Structure
A complete AES encryption looks like this:
1. AddRoundKey (initial round key)
2. For rounds 1 to N-1:
- SubBytes
- ShiftRows
- MixColumns
- AddRoundKey
3. Final round (round N):
- SubBytes
- ShiftRows
- AddRoundKey (no MixColumns)
Where N = 10 for AES-128, N = 12 for AES-192, N = 14 for AES-256.
Key Schedule
AES derives round keys from the original key through a process called key expansion. For AES-128:
- The original 128-bit key provides the first 4 words (each word = 32 bits)
- The key schedule generates 40 additional words for a total of 44 words
- Each round uses 4 words (128 bits) as its round key
The expansion uses:
- RotWord: Rotate a word left by one byte
- SubWord: Apply the S-box to each byte of a word
- Rcon: Round constants (fixed values that change each round to add variation)
The key schedule is designed to:
- Ensure each round key is different
- Make related-key attacks difficult
- Allow efficient on-the-fly computation in constrained environments
Modes of Operation
AES is a block cipher—it encrypts exactly 128 bits (16 bytes) at a time. But what if your message is longer than 16 bytes? That's where modes of operation come in—they define how to chain multiple blocks together.
ECB (Electronic Codebook) — Don't Use
The simplest mode: encrypt each block independently with the same key.
Plaintext: │ Block 1 │ Block 2 │ Block 3 │
↓ ↓ ↓
┌───────┐ ┌───────┐ ┌───────┐
│ AES │ │ AES │ │ AES │
│ Key │ │ Key │ │ Key │
└───────┘ └───────┘ └───────┘
↓ ↓ ↓
Ciphertext: │ Block 1 │ Block 2 │ Block 3 │
The Problem: Identical plaintext blocks produce identical ciphertext blocks. This leaks patterns.
The famous "ECB Penguin" demonstrates this flaw: when an image of Tux (the Linux mascot) is encrypted with ECB mode, the penguin's outline remains visible because areas of the same color produce identical ciphertext.
Never use ECB for real encryption.
CBC (Cipher Block Chaining) — Legacy
Each plaintext block is XORed with the previous ciphertext block before encryption.
IV Ciphertext 1 Ciphertext 2
↓ ↓ ↓
┌───────┐ ┌───────┐ ┌───────┐
│ XOR │←P1 │ XOR │←P2 │ XOR │←P3
└───────┘ └───────┘ └───────┘
↓ ↓ ↓
┌───────┐ ┌───────┐ ┌───────┐
│ AES │ │ AES │ │ AES │
└───────┘ └───────┘ └───────┘
↓ ↓ ↓
Ciphertext 1 Ciphertext 2 Ciphertext 3
Properties:
- Requires an Initialization Vector (IV)—a random starting value that makes each encryption unique, even for the same message
- Identical plaintexts produce different ciphertexts (due to different IVs)
- Cannot be parallelized (each block depends on the previous)
- Requires padding for messages not a multiple of 128 bits
Vulnerabilities:
- Padding oracle attacks (if error messages reveal whether padding is correct, attackers can decrypt data byte by byte)
- IV reuse reveals if first plaintext blocks are identical
- Predictable IV enables chosen-plaintext attacks (BEAST)
- No authentication—an attacker can modify the ciphertext without detection
CTR (Counter Mode) — Stream Cipher Mode
Encrypts a counter value, then XORs the result with plaintext. This turns the block cipher into a stream cipher.
Nonce + 1 Nonce + 2 Nonce + 3
↓ ↓ ↓
┌───────┐ ┌───────┐ ┌───────┐
│ AES │ │ AES │ │ AES │
└───────┘ └───────┘ └───────┘
↓ ↓ ↓
┌───────┐ ┌───────┐ ┌───────┐
│ XOR │←P1 │ XOR │←P2 │ XOR │←P3
└───────┘ └───────┘ └───────┘
↓ ↓ ↓
Ciphertext 1 Ciphertext 2 Ciphertext 3
Properties:
- Parallelizable (huge performance advantage)
- Random access (can decrypt any block without processing previous blocks)
- No padding needed (can encrypt messages of any length)
- Requires unique nonce for each message with the same key
Critical Warning: If the same (key, nonce) pair is used twice, an attacker can XOR the ciphertexts to get the XOR of the plaintexts—completely breaking confidentiality. CTR nonce reuse is catastrophic.
GCM (Galois/Counter Mode) — Recommended
GCM combines CTR mode encryption with GHASH authentication. It's an AEAD (Authenticated Encryption with Associated Data) mode—meaning it both encrypts your data AND lets you verify it hasn't been tampered with.
┌─────────────────────────────────────────┐
│ GCM Structure │
├─────────────────────────────────────────┤
│ Counter Mode (CTR) for encryption │
│ + │
│ GHASH for authentication │
│ ↓ │
│ Ciphertext + Authentication Tag │
└─────────────────────────────────────────┘
Properties:
- Encrypt-then-authenticate: Encrypts first, then creates a verification tag from the ciphertext
- Authenticated: Produces a tag that verifies integrity and authenticity
- Associated Data: Can authenticate additional data (like headers) without encrypting it
- Parallelizable: Both encryption and authentication can be parallelized
- Hardware accelerated: Intel's PCLMULQDQ instruction accelerates GHASH
Tag sizes: 128 bits recommended; also allows 120, 112, 104, 96 bits (64 and 32 bits exist but are not recommended)
IV/Nonce: 96 bits recommended. Never reuse an IV with the same key. (The terms IV and nonce are often used interchangeably; "nonce" emphasizes uniqueness, "IV" emphasizes unpredictability—for GCM, uniqueness is the critical requirement.)
GCM is the default choice for TLS 1.2/1.3, IPsec, SSH, and most modern protocols.
CCM (Counter with CBC-MAC) — Alternative AEAD
CCM combines CTR mode encryption with CBC-MAC authentication (MAC = Message Authentication Code, a way to verify data hasn't been modified).
Properties:
- Authenticate-then-encrypt: Creates verification code from plaintext first, then encrypts everything
- Two passes over the data (slower than GCM)
- Simpler than GCM (uses only basic AES operations)
- Common in embedded systems and wireless protocols (Zigbee, Bluetooth)
Tag sizes: 4, 6, 8, 10, 12, 14, or 16 bytes
CCM is suitable when GCM's hardware acceleration isn't available.
XTS (XEX-based Tweaked-codebook with Ciphertext Stealing) — Disk Encryption
XTS is designed specifically for encrypting storage devices (hard drives, SSDs).
Properties:
- Uses two keys (one for encryption, one for generating unique values per sector)
- Each disk sector gets a unique modifier based on its location, so identical data in different locations encrypts differently
- No authentication (storage constraints don't allow expansion)
- Parallelizable
- Standardized in IEEE 1619-2007 and NIST SP 800-38E
Used by: BitLocker, VeraCrypt, FileVault, LUKS, and most full-disk encryption.
Limitation: Without authentication, XTS can't detect tampering—an attacker could flip bits and cause controlled corruption.
OFB (Output Feedback) — Rarely Used
Like CTR but chains the AES output instead of using a counter.
Properties:
- Stream cipher mode
- Cannot be parallelized
- Bit-flipping attacks possible
- Mostly obsolete—use CTR instead
CFB (Cipher Feedback) — Rarely Used
Similar to CBC but can operate on smaller units.
Properties:
- Self-synchronizing (recovers from bit errors)
- Cannot be parallelized
- Mostly obsolete
Mode Comparison
| Mode | Parallelizable | Authentication | Random Access | Typical Use |
|---|---|---|---|---|
| ECB | Yes | No | Yes | Never use |
| CBC | Decrypt only | No | Decrypt only | Legacy systems |
| CTR | Yes | No | Yes | When you add your own integrity check |
| GCM | Yes | Yes | Yes | Network protocols (TLS, IPsec) |
| CCM | No | Yes | No | Embedded/wireless |
| XTS | Yes | No | Yes | Disk encryption |
| OFB | No | No | No | Obsolete |
| CFB | No | No | No | Obsolete |
Recommendation: Use GCM for network/general use, XTS for disk encryption.
Security Analysis
Best Known Attacks
AES has withstood over 20 years of intensive cryptanalysis. The best known single-key attacks:
| Variant | Attack | Complexity | Comparison to Brute Force |
|---|---|---|---|
| AES-128 | Biclique (2015) | 2^126.0 | 4× faster |
| AES-192 | Biclique (2015) | 2^189.9 | ~4× faster |
| AES-256 | Biclique (2015) | 2^254.3 | ~3× faster |
These "attacks" are purely theoretical—they shave off a tiny fraction of the work but are still completely impossible. A 2^126 operation attack still requires:
- More energy than the sun produces in its lifetime
- Storage of ~10^16 terabytes (more than all the hard drives ever made)
- Billions of years on any foreseeable hardware
For practical purposes, AES is unbroken.
Related-Key Attacks
Related-key attacks on AES-192 and AES-256 exist, but they require the attacker to see encryptions made with specially crafted related keys (like Key1 and Key1-with-one-bit-flipped)—a scenario that doesn't occur in normal use because applications don't let attackers choose keys.
Side-Channel Attacks
The real threats to AES aren't mathematical—they're physical. Side-channel attacks exploit information leaked by the hardware running the encryption:
- Timing attacks: If some operations take longer than others, an attacker can deduce the key by measuring how long encryption takes
- Power analysis: Different operations use different amounts of electricity—measuring power consumption can reveal the key
- Electromagnetic emanations: Computers emit radio signals that vary based on what they're computing
Mitigations:
- Use hardware AES instructions (AES-NI) when available—they're designed to resist these attacks
- Ensure all operations take the same amount of time regardless of the data
- Use libraries specifically designed to resist side-channel attacks
Hardware Acceleration
Modern processors include dedicated AES instructions:
- Intel/AMD: AES-NI (since 2010)
- ARM: ARMv8 Cryptography Extensions
AES-NI provides special CPU instructions that perform AES operations in hardware rather than software. This means:
- 10+ GB/s encryption speed on modern CPUs
- Faster than most storage devices can read/write
- Built-in resistance to timing attacks (since operations always take the same time)
Test Vectors
NIST SP 800-38A Test Vector (AES-128-ECB)
From Appendix F.1.1:
Key: 2b7e151628aed2a6abf7158809cf4f3c
Plaintext: 6bc1bee22e409f96e93d7e117393172a
Ciphertext: 3ad77bb40d7a3660a89ecaf32466ef97
Verification
# Verify with OpenSSL
echo -n "6bc1bee22e409f96e93d7e117393172a" | xxd -r -p | \
openssl enc -aes-128-ecb -K "2b7e151628aed2a6abf7158809cf4f3c" -nopad | xxd -p
# Output: 3ad77bb40d7a3660a89ecaf32466ef97
Code Examples
OpenSSL CLI
# Generate a random key and IV
KEY=$(openssl rand -hex 32) # 256-bit key
IV=$(openssl rand -hex 16) # 128-bit IV for CBC
# Encrypt with AES-256-CBC
openssl enc -aes-256-cbc -K "$KEY" -iv "$IV" -in plaintext.txt -out ciphertext.bin
# Decrypt
openssl enc -d -aes-256-cbc -K "$KEY" -iv "$IV" -in ciphertext.bin -out decrypted.txt
Note: The
openssl enccommand doesn't support AEAD modes (GCM, CCM) directly. For authenticated encryption, use the library APIs shown below or theopenssl cmscommand.
Python
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
# Generate a random 256-bit key
key = AESGCM.generate_key(bit_length=256)
# Create cipher
aesgcm = AESGCM(key)
# Encrypt
nonce = os.urandom(12) # 96-bit nonce
plaintext = b"Hello, World!"
associated_data = b"header"
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
# Decrypt
decrypted = aesgcm.decrypt(nonce, ciphertext, associated_data)
print(decrypted.decode()) # Hello, World!
JavaScript (Node.js)
const crypto = require('crypto');
// AES-256-GCM encryption
function encrypt(plaintext, key) {
const iv = crypto.randomBytes(12); // 96-bit IV
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let ciphertext = cipher.update(plaintext, 'utf8', 'hex');
ciphertext += cipher.final('hex');
const tag = cipher.getAuthTag();
return { iv: iv.toString('hex'), ciphertext, tag: tag.toString('hex') };
}
// Decryption
function decrypt(encrypted, key) {
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(encrypted.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encrypted.tag, 'hex'));
let plaintext = decipher.update(encrypted.ciphertext, 'hex', 'utf8');
plaintext += decipher.final('utf8');
return plaintext;
}
// Usage
const key = crypto.randomBytes(32); // 256-bit key
const encrypted = encrypt('Hello, World!', key);
console.log(decrypt(encrypted, key)); // Hello, World!
Go
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
)
func main() {
// Generate random 256-bit key
key := make([]byte, 32)
rand.Read(key)
// Create AES cipher
block, _ := aes.NewCipher(key)
// Create GCM mode
gcm, _ := cipher.NewGCM(block)
// Generate nonce
nonce := make([]byte, gcm.NonceSize())
io.ReadFull(rand.Reader, nonce)
// Encrypt
plaintext := []byte("Hello, World!")
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
// Decrypt
decrypted, _ := gcm.Open(nil, nonce, ciphertext, nil)
fmt.Println(string(decrypted)) // Hello, World!
}
C (OpenSSL)
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
int aes_gcm_encrypt(unsigned char *plaintext, int plaintext_len,
unsigned char *key, unsigned char *iv,
unsigned char *ciphertext, unsigned char *tag) {
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
int len, ciphertext_len;
EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL);
EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv);
EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len);
ciphertext_len = len;
EVP_EncryptFinal_ex(ctx, ciphertext + len, &len);
ciphertext_len += len;
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag);
EVP_CIPHER_CTX_free(ctx);
return ciphertext_len;
}
int main() {
unsigned char key[32], iv[12], tag[16];
unsigned char plaintext[] = "Hello, World!";
unsigned char ciphertext[128];
RAND_bytes(key, 32);
RAND_bytes(iv, 12);
int len = aes_gcm_encrypt(plaintext, strlen((char*)plaintext),
key, iv, ciphertext, tag);
printf("Ciphertext length: %d bytes\n", len);
printf("Tag: ");
for (int i = 0; i < 16; i++) printf("%02x", tag[i]);
printf("\n");
return 0;
}
Compile:
gcc -o aes_gcm aes_gcm.c -lcrypto
Note: Production code should check return values from all EVP functions. On macOS with Homebrew, add -I/opt/homebrew/opt/openssl@3/include -L/opt/homebrew/opt/openssl@3/lib to the compile command.
Quick Reference
| Property | AES-128 | AES-192 | AES-256 |
|---|---|---|---|
| Key size | 128 bits | 192 bits | 256 bits |
| Block size | 128 bits | 128 bits | 128 bits |
| Rounds | 10 | 12 | 14 |
| Round keys | 11 | 13 | 15 |
| Best attack | 2^126.0 | 2^189.9 | 2^254.3 |
When to Use What
Use AES-128 when:
- Performance is critical
- You trust the algorithm (you should)
- Key management is strong
Use AES-256 when:
- Regulations require it (government, financial)
- Future-proofing against quantum computers (quantum computers would halve AES security, so AES-256 would still provide 128-bit security)
- "Defense in depth" mindset
Use GCM when:
- Encrypting network traffic
- You need authentication
- Hardware acceleration is available
Use XTS when:
- Full-disk encryption
- Storage encryption where space is constrained
Never use:
- ECB — Leaks patterns
- CBC without authentication — Attackers can manipulate ciphertext and learn secrets from error messages
- CTR without authentication — No integrity protection
Timeline
| Year | Event |
|---|---|
| 1997 | NIST announces AES competition |
| 1998 | Round 1: 15 candidates submitted |
| 1999 | Round 2: 5 finalists selected |
| 2000 | Rijndael selected as AES winner |
| 2001 | AES published as FIPS PUB 197 |
| 2003 | NSA approves AES for Top Secret (AES-192/256) |
| 2010 | Intel releases AES-NI instructions |
| 2010 | NIST approves XTS mode (SP 800-38E) |
| 2011 | Biclique attack published (theoretical) |
| 2015 | Improved biclique results (still theoretical) |
| Today | AES remains unbroken; billions of devices use it daily |
References
- NIST. (2001). FIPS PUB 197: Advanced Encryption Standard (AES)
- NIST. (2001). SP 800-38A: Recommendation for Block Cipher Modes of Operation
- NIST. (2010). SP 800-38E: The XTS-AES Mode for Confidentiality on Storage Devices
- Daemen, J., & Rijmen, V. (2002). The Design of Rijndael: AES — The Advanced Encryption Standard. Springer.
- Bogdanov, A., Khovratovich, D., & Rechberger, C. (2011). Biclique Cryptanalysis of the Full AES. ASIACRYPT 2011.
- AES on Wikipedia
- Block cipher mode of operation on Wikipedia