The cipher behind the padlock
Every time your browser shows a padlock, your data is most likely encrypted with AES: HTTPS, Wi-Fi (WPA2 and WPA3), WhatsApp messages, BitLocker and FileVault disks, and VPNs all use it.
In 1997 the old standard, DES, was in trouble: its 56-bit key could be broken by brute force, and in 1998 a machine built for under $250,000 did it in 56 hours. The US standards agency NIST held an open competition for a successor. In 2000 it picked Rijndael, by the Belgian cryptographers Joan Daemen and Vincent Rijmen. It became AES, published as the standard FIPS-197.
AES is a symmetric block cipher: the same secret key encrypts and decrypts, and it works on fixed-size blocks.
| AES-128 | AES-192 | AES-256 | |
|---|---|---|---|
| Block size | 128 bits | 128 bits | 128 bits |
| Key size | 128 bits | 192 bits | 256 bits |
| Rounds | 10 | 12 | 14 |
The state
The 16 bytes of a block are written into a 4 × 4 grid called the state, column by column: bytes 0–3 form the first column, bytes 4–7 the second, and so on. Every step changes this grid.
One round, four steps
Claude Shannon said a good cipher needs confusion (the relation between key and ciphertext is complicated) and diffusion (every input bit affects many output bits). Each AES step provides one of them:
| Step | What it does | Purpose |
|---|---|---|
| SubBytes | Replaces every byte using the S-box, a fixed table of 256 entries | Confusion: the only non-linear step |
| ShiftRows | Rotates row r by r places to the left | Diffusion between columns |
| MixColumns | Multiplies each column by a fixed 4 × 4 matrix in GF(2⁸) | Diffusion within a column |
| AddRoundKey | XORs the state with this round’s key | Brings in the secret key |
The whole of AES-128:
- Round 0: AddRoundKey with the key itself.
- Rounds 1–9: SubBytes → ShiftRows → MixColumns → AddRoundKey.
- Round 10: SubBytes → ShiftRows → AddRoundKey (no MixColumns).
Worked example: round 1 of the FIPS-197 example
The standard encrypts the plaintext 32 43 f6 a8 88 5a 30 8d 31 31 98 a2 e0 37 07 34 with the key 2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c. Each line below is the state written column by column:
| Step | State (column 0 · column 1 · column 2 · column 3) |
|---|---|
| After round 0 | 19 3d e3 be · a0 f4 e2 2b · 9a c6 8d 2a · e9 f8 48 08 |
| SubBytes | d4 27 11 ae · e0 bf 98 f1 · b8 b4 5d e5 · 1e 41 52 30 |
| ShiftRows | d4 bf 5d 30 · e0 b4 52 ae · b8 41 11 f1 · 1e 27 98 e5 |
| MixColumns | 04 66 81 e5 · e0 cb 19 9a · 48 f8 d3 7a · 28 06 26 4c |
| Round key 1 | a0 fa fe 17 · 88 54 2c b1 · 23 a3 39 39 · 2a 6c 76 05 |
| AddRoundKey | a4 9c 7f f2 · 68 9f 35 2b · 6b 5b ea 43 · 02 6a 50 49 |
After all 10 rounds the ciphertext is 39 25 84 1d 02 dc 09 fb dc 11 85 97 19 6a 0b 32.
SubBytes
The S-box is a 16 × 16 table. The first hex digit of a byte picks the row and the second picks the column: 19 is row 1, column 9, which holds d4. Behind the table is maths: take the byte’s multiplicative inverse in GF(2⁸), then apply a fixed bit-mixing (affine) transform.
ShiftRows
Drawn as rows, the state before and after ShiftRows:
| Row | Before | After | Rotation |
|---|---|---|---|
| 0 | d4 e0 b8 1e | d4 e0 b8 1e | none |
| 1 | 27 bf b4 41 | bf b4 41 27 | 1 left |
| 2 | 11 98 5d 52 | 5d 52 11 98 | 2 left |
| 3 | ae f1 e5 30 | 30 ae f1 e5 | 3 left |
MixColumns
Each column is multiplied by this matrix:
| 02 | 03 | 01 | 01 |
| 01 | 02 | 03 | 01 |
| 01 | 01 | 02 | 03 |
| 03 | 01 | 01 | 02 |
The arithmetic is in GF(2⁸), the same mod-2 polynomial arithmetic as a CRC: addition is XOR, and multiplying by 02 means shifting left one bit and XORing 1b if a bit falls off the top. Multiplying by 03 is ·02 XOR the byte itself. For column 0 = (d4, bf, 5d, 30):
- 02 · d4 = b3 (d4 = 1101 0100; shifted it is 1010 1000 with a 1 falling off, and a8 ⊕ 1b = b3)
- 03 · bf = 02 · bf ⊕ bf = 65 ⊕ bf = da
- top byte = b3 ⊕ da ⊕ 5d ⊕ 30 = 04
AddRoundKey and the key schedule
AES-128 needs 11 round keys, one for round 0 and one for each round, made from the key by the key schedule. A round key is four 4-byte words. The first word of round key 1 is made like this:
- Take the last word of the key,
09 cf 4f 3c, and rotate it one byte:cf 4f 3c 09(RotWord). - Put each byte through the S-box:
8a 84 eb 01(SubWord). - XOR the round constant into the first byte: 8a ⊕ 01 = 8b (Rcon = 01, 02, 04, 08, …).
- XOR with the first word of the key,
2b 7e 15 16: the result isa0 fa fe 17.
Each of the other three words is the previous word XOR the word four places back.
Decryption
Decryption runs the inverse steps in reverse order: InvShiftRows (rotate right), InvSubBytes (the inverse S-box), AddRoundKey (XOR is its own inverse) and InvMixColumns (multiply by the inverse matrix, with entries 0e, 0b, 0d, 09), using the round keys from last to first.
The avalanche effect
Change one bit of the plaintext and, after just two rounds, about half of the 128 bits of the state are different. The two ciphertexts look completely unrelated. Press Flip one bit in the model to watch the difference spread: SubBytes changes a whole byte, MixColumns spreads it over a column, and ShiftRows carries it into the other columns.
Modes of operation
AES encrypts exactly 16 bytes. A mode of operation decides how longer messages are handled:
- ECB encrypts every block on its own. Equal blocks give equal ciphertext, so patterns leak (the famous “ECB penguin” picture). Never use it.
- CBC XORs each block with the previous ciphertext block before encrypting.
- CTR encrypts a counter and XORs the result with the message, turning AES into a stream cipher.
- GCM is CTR plus an authentication tag that detects tampering. It is what HTTPS uses most.
AES vs DES
| DES (1977) | AES (2001) | |
|---|---|---|
| Block size | 64 bits | 128 bits |
| Key size | 56 bits | 128, 192 or 256 bits |
| Rounds | 16 | 10, 12 or 14 |
| Structure | Feistel network (half the block per round) | Substitution–permutation network (the whole block per round) |
| Status | Broken by brute force (1998) | Secure, the global standard |
Code
AES-128 from scratch, following the steps above. It prints the ciphertext from the standard:
def xtime(b): # multiply by 02 in GF(2^8)
return ((b << 1) ^ 0x1B) & 0xFF if b & 0x80 else b << 1
def gmul(a, b): # multiply two bytes in GF(2^8)
r = 0
while b:
if b & 1:
r ^= a
a, b = xtime(a), b >> 1
return r
def make_sbox():
sbox = []
for x in range(256):
inv = next((y for y in range(1, 256) if gmul(x, y) == 1), 0) # inverse, 0 → 0
s = inv
for k in range(1, 5): # affine transform
s ^= ((inv << k) | (inv >> (8 - k))) & 0xFF
sbox.append(s ^ 0x63)
return sbox
SBOX = make_sbox()
RCON = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36]
def expand_key(key): # 16 bytes → 11 round keys of 16 bytes
w = [list(key[i:i + 4]) for i in range(0, 16, 4)]
for i in range(4, 44):
t = w[i - 1][:]
if i % 4 == 0:
t = [SBOX[b] for b in t[1:] + t[:1]] # RotWord, SubWord
t[0] ^= RCON[i // 4 - 1]
w.append([a ^ b for a, b in zip(w[i - 4], t)])
return [sum(w[4 * r:4 * r + 4], []) for r in range(11)]
def shift_rows(s): # byte s[r + 4c] is row r, column c
return [s[r + 4 * ((c + r) % 4)] for c in range(4) for r in range(4)]
def mix_columns(s):
out = []
for c in range(4):
a = s[4 * c:4 * c + 4]
out += [gmul(a[0], 2) ^ gmul(a[1], 3) ^ a[2] ^ a[3],
a[0] ^ gmul(a[1], 2) ^ gmul(a[2], 3) ^ a[3],
a[0] ^ a[1] ^ gmul(a[2], 2) ^ gmul(a[3], 3),
gmul(a[0], 3) ^ a[1] ^ a[2] ^ gmul(a[3], 2)]
return out
def aes128_encrypt(block, key):
keys = expand_key(key)
s = [b ^ k for b, k in zip(block, keys[0])] # round 0
for rnd in range(1, 11):
s = shift_rows([SBOX[b] for b in s]) # SubBytes, ShiftRows
if rnd < 10:
s = mix_columns(s)
s = [b ^ k for b, k in zip(s, keys[rnd])] # AddRoundKey
return bytes(s)
pt = bytes.fromhex("3243f6a8885a308d313198a2e0370734")
key = bytes.fromhex("2b7e151628aed2a6abf7158809cf4f3c")
print(aes128_encrypt(pt, key).hex()) # 3925841d02dc09fbdc118597196a0b32
Writing your own AES is great for learning but dangerous in real software: hand-made code leaks secrets through timing and is easy to misuse. Real programs use a library and an authenticated mode such as GCM:
# pip install cryptography
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=128)
nonce = os.urandom(12) # never reuse a nonce with the same key
sealed = AESGCM(key).encrypt(nonce, b"attack at dawn", None)
print(AESGCM(key).decrypt(nonce, sealed, None)) # b'attack at dawn'
Common mistakes
- Filling the state row by row. AES fills it column by column, so the second input byte is row 1, column 0.
- Rotating rows to the right in ShiftRows. Encryption rotates left; decryption rotates right.
- Using ordinary multiplication in MixColumns. 02 · d4 is not 1a8: the result must stay a byte, so it is reduced to b3 in GF(2⁸).
- Doing MixColumns in the last round. Round 10 (AES-128) skips it.
- Thinking a bigger key means a bigger block. AES-256 still encrypts 128-bit blocks; it just uses 14 rounds.
Complexity at a glance
| Case / operation | Time | Why |
|---|---|---|
| Encrypting one 16-byte block | O(1) | Always 10, 12 or 14 rounds of 16 byte operations. |
| Encrypting n bytes | O(n) | Modern CPUs have AES instructions (AES-NI), reaching several GB/s. |
| Brute force on a 128-bit key | 2¹²⁸ ≈ 3.4 × 10³⁸ tries | Far beyond any computer that could ever be built. |
| Best known attack on full AES-128 | about 2¹²⁶ | Only 4× faster than brute force, so AES is still unbroken. |
Quick check
Test yourself — pick an answer to see if you got it.
1. What is the block size of AES?
The block is always 128 bits (16 bytes). Only the key size changes, which changes the number of rounds.
2. How many rounds does AES-256 use?
AES-128 uses 10 rounds, AES-192 uses 12 and AES-256 uses 14.
3. Which AES step is the only non-linear one?
The S-box (a multiplicative inverse in GF(2⁸) plus an affine map) is non-linear. The other three steps are linear, so without SubBytes AES could be solved like a system of equations.
4. In ShiftRows, by how many places does row 2 rotate?
Row r rotates r places to the left. Row 0 stays, row 1 moves 1, row 2 moves 2 and row 3 moves 3.
5. Which step is left out of the last round?
Round 10 of AES-128 has no MixColumns. It would add no security there, and leaving it out makes decryption mirror encryption.
6. What is 57 ⊕ 83 in hexadecimal (as in AddRoundKey)?
0101 0111 ⊕ 1000 0011 = 1101 0100, which is d4. XOR gives 1 wherever the bits differ.