Skip to main content
Modern Block Cipher Intermediate

AES

AES secures nearly everything: HTTPS traffic, encrypted disks, and government secrets alike. Here's how the Rijndael cipher transforms data, round by round.

PL
Pashalis Laoutaris
August 4, 2026
17 min read

Interactive AES Visualizer

🔐 AES Encryption

6
This is AES-128 (10 rounds). Defaults are the official FIPS-197 test vector.
Enter text and click a button to start!
Round: / 10
Click Encrypt to run all 10 rounds.

The Advanced Encryption Standard (AES) is the single most widely used encryption algorithm on Earth. It protects HTTPS traffic, encrypted phone storage, and Wi-Fi connections. It secures password manager vaults and classified government communications too. All of that runs on the same publicly documented, thoroughly scrutinized algorithm. This guide walks through how AES turns plaintext into ciphertext, round by round.

Table of Contents

  1. Introduction
  2. Historical Context: The AES Competition
  3. Mathematical Foundations
  4. The AES State: Representing Data as a Matrix
  5. How AES Works: The Round Structure
  6. Key Expansion
  7. A Worked Example
  8. Python Implementation
  9. Limitations
  10. Modes of Operation
  11. Security Analysis and Threat Models
  12. Common Vulnerabilities and Mitigation Strategies
  13. Key Sizes and Performance
  14. Implementation Considerations
  15. Real-World Applications
  16. AES vs. Other Block Ciphers
  17. Quantum Computing and AES
  18. Frequently Asked Questions
  19. References and Further Reading

Introduction

Unlike RSA or Diffie-Hellman, AES is a symmetric algorithm. The same secret key both encrypts and decrypts. It’s also a block cipher. It operates on fixed-size 128-bit (16-byte) chunks of data at a time. A sequence of well-defined transformations, called “rounds,” is applied repeatedly. This continues until the plaintext is thoroughly scrambled into ciphertext, revealing nothing without the key.

AES is fast: dedicated hardware instructions, called AES-NI, exist on nearly every modern CPU. It’s well-studied, having survived over two decades of intense public cryptanalysis. It’s also flexible, supporting 128, 192, or 256-bit keys depending on the required security margin. By a wide margin, it’s the default choice whenever symmetric encryption is needed.

Historical Context: The AES Competition

By the mid-1990s, the aging Data Encryption Standard (DES) had only a 56-bit key. That was clearly inadequate. DES was publicly broken by brute force in under 24 hours by 1999. NIST could have let one agency design a replacement behind closed doors. Instead, it ran an open, public competition, starting in 1997.

Fifteen candidate algorithms were submitted by cryptographers worldwide. Over several years, the global community publicly attacked, analyzed, and debated each one. In 2000, NIST selected the winner: Rijndael. Belgian cryptographers Joan Daemen and Vincent Rijmen designed it. The name is a portmanteau of their surnames. It was formally standardized as FIPS-197 in 2001.

This open, competitive process was repeated later for SHA-3 and post-quantum cryptography. It’s now the gold standard for developing cryptographic algorithms. That’s precisely because AES has held up so well against decades of subsequent cryptanalysis.

Mathematical Foundations

AES’s transformations operate within GF(2⁸), the Galois Field with 256 elements. This is arithmetic on single bytes, where addition is XOR. Multiplication follows special “wrap around” rules, using the irreducible polynomial x⁸+x⁴+x³+x+1. That might sound abstract, but the upshot is simple. Every byte-level operation inside AES, S-box substitution and MixColumns alike, is carefully chosen finite-field arithmetic. It’s chosen because it resists the two classical block-cipher attacks. Linear and differential cryptanalysis fare far worse against it than against ordinary arithmetic.

You don’t need to compute GF(2⁸) multiplication by hand to understand AES conceptually. What matters is why it’s used. It gives every transformation strong mathematical guarantees against known attacks. Those guarantees were rigorously analyzed during the AES competition.

The AES State: Representing Data as a Matrix

AES treats each 128-bit (16-byte) block as a 4×4 matrix of bytes, called the state. It’s filled in column-major order:

[ b0  b4  b8  b12 ]
[ b1  b5  b9  b13 ]
[ b2  b6  b10 b14 ]
[ b3  b7  b11 b15 ]

Every transformation in AES, substitution, row shifting, column mixing, key addition, operates on this grid.

Interactive Visualizer

The visualizer above renders exactly this grid. Click Encrypt and watch it animate through every round. Each step names which transformation is running.

How AES Works: The Round Structure

AES processes data through a fixed number of rounds, depending on key size:

  • AES-128: 10 rounds
  • AES-192: 12 rounds
  • AES-256: 14 rounds

Each round (except the last) consists of four transformations, applied in sequence:

1. SubBytes (Substitution)

Every byte in the state is replaced with a different byte. It’s looked up in a fixed, public 256-entry table called the S-box. The S-box comes from the multiplicative inverse in GF(2⁸), followed by an affine transformation. It’s designed to have no simple algebraic relationship between input and output. That’s what makes AES resistant to linear and differential cryptanalysis. This is the only non-linear step in AES, and it’s essential. Without it, AES would just be a breakable linear function of the key.

2. ShiftRows

Each row of the state shifts left cyclically, by an amount matching its row index. Row 0 isn’t shifted. Row 1 shifts by 1 byte, row 2 by 2, row 3 by 3.

This spreads byte values across columns. It ensures the columns MixColumns processes aren’t independent, round to round.

3. MixColumns

Each column of the state is treated as a 4-term polynomial. It’s multiplied, in GF(2⁸), by a fixed polynomial. That’s implemented as this matrix multiplication:

[ 02 03 01 01 ]   [ b0 ]
[ 01 02 03 01 ] × [ b1 ]
[ 01 01 02 03 ]   [ b2 ]
[ 03 01 01 02 ]   [ b3 ]

This step provides diffusion: a change to a single byte spreads across an entire column. Combined with ShiftRows, one changed input bit affects nearly every output bit. That happens within just a few rounds, the “avalanche effect.” MixColumns is skipped in the final round.

4. AddRoundKey

The state is XORed with a 128-bit round key. That key comes from the key expansion process, described below. This is the only step where the secret key enters the computation. Every other transformation is public, identical for every AES operation regardless of key.

Putting It Together

A full AES-128 encryption looks like this:

  1. Initial round: AddRoundKey, using the original key, before any rounds.
  2. Rounds 1-9: SubBytes, ShiftRows, MixColumns, AddRoundKey.
  3. Round 10 (final): SubBytes, ShiftRows, AddRoundKey. No MixColumns.

Decryption reverses this exact process. It uses inverse transformations, InvSubBytes, InvShiftRows, InvMixColumns, and the round keys in reverse order.

Key Expansion

AES doesn’t reuse a single key for every round. It derives a unique round key per round, plus one extra for the initial AddRoundKey. This process is the Rijndael key schedule. For AES-128, it expands a single 16-byte key into eleven round keys, 176 bytes total. The process combines byte rotation and S-box substitution. It XORs in round constants and previous key material too.

The same S-box and matrix operations repeat every round. But the actual transformation differs each time, because the key material mixed in keeps changing.

A Worked Example

The official FIPS-197 standard includes a canonical test vector every correct AES-128 implementation must reproduce. It’s what the visualizer above uses as its default input:

  • Key: 000102030405060708090a0b0c0d0e0f
  • Plaintext: 00112233445566778899aabbccddeeff
  • Ciphertext: 69c4e0d86a7b0430d8cdb78070b4c55a

Watch the visualizer animate all ten rounds: byte scrambling, row rotation, column diffusion, key folding. Those sixteen plaintext bytes transform into that exact ciphertext, deterministically, every time.

Python Implementation

This is a genuine, complete AES-128 implementation. The S-box comes from a real GF(2⁸) multiplicative inverse plus the affine transform. It isn’t a hardcoded table. The key schedule, SubBytes, ShiftRows, MixColumns, and AddRoundKey all follow the specification directly:

def gmul(a, b):
    """Multiplication in GF(2^8) using AES's reduction polynomial x^8+x^4+x^3+x+1."""
    p = 0
    for _ in range(8):
        if b & 1:
            p ^= a
        hi = a & 0x80
        a = (a << 1) & 0xFF
        if hi:
            a ^= 0x1B
        b >>= 1
    return p

def gf_inverse(a):
    if a == 0:
        return 0
    for x in range(1, 256):
        if gmul(a, x) == 1:
            return x

def build_sbox():
    sbox = [0] * 256
    for a in range(256):
        s = result = gf_inverse(a)
        for _ in range(4):
            s = ((s << 1) | (s >> 7)) & 0xFF
            result ^= s
        sbox[a] = result ^ 0x63
    return sbox

SBOX = build_sbox()
INV_SBOX = [0] * 256
for i, v in enumerate(SBOX):
    INV_SBOX[v] = i

RCON = [0x01]
for _ in range(9):
    RCON.append(gmul(RCON[-1], 2))

def key_expansion(key):
    Nk = len(key) // 4
    Nr = Nk + 6
    w = [list(key[4*i:4*i+4]) for i in range(Nk)]
    for i in range(Nk, 4 * (Nr + 1)):
        temp = list(w[i - 1])
        if i % Nk == 0:
            temp = temp[1:] + temp[:1]
            temp = [SBOX[b] for b in temp]
            temp[0] ^= RCON[i // Nk - 1]
        w.append([a ^ b for a, b in zip(w[i - Nk], temp)])
    return [sum((w[r*4+c] for c in range(4)), []) for r in range(Nr + 1)], Nr

def bytes_to_state(data):
    return [[data[r + 4*c] for c in range(4)] for r in range(4)]

def state_to_bytes(state):
    return bytes(state[r][c] for c in range(4) for r in range(4))

def sub_bytes(state, box=SBOX):
    return [[box[b] for b in row] for row in state]

def shift_rows(state):
    return [state[r][r:] + state[r][:r] for r in range(4)]

def mix_columns(state):
    new = [[0]*4 for _ in range(4)]
    for c in range(4):
        col = [state[r][c] for r in range(4)]
        new[0][c] = gmul(col[0],2) ^ gmul(col[1],3) ^ col[2] ^ col[3]
        new[1][c] = col[0] ^ gmul(col[1],2) ^ gmul(col[2],3) ^ col[3]
        new[2][c] = col[0] ^ col[1] ^ gmul(col[2],2) ^ gmul(col[3],3)
        new[3][c] = gmul(col[0],3) ^ col[1] ^ col[2] ^ gmul(col[3],2)
    return new

def add_round_key(state, round_key):
    rk = bytes_to_state(bytes(round_key))
    return [[state[r][c] ^ rk[r][c] for c in range(4)] for r in range(4)]

def aes_encrypt_block(data, key):
    round_keys, Nr = key_expansion(key)
    state = add_round_key(bytes_to_state(data), round_keys[0])
    for rnd in range(1, Nr):
        state = mix_columns(shift_rows(sub_bytes(state)))
        state = add_round_key(state, round_keys[rnd])
    state = shift_rows(sub_bytes(state))
    state = add_round_key(state, round_keys[Nr])
    return state_to_bytes(state)

if __name__ == "__main__":
    key = bytes.fromhex("000102030405060708090a0b0c0d0e0f")
    plaintext = bytes.fromhex("00112233445566778899aabbccddeeff")

    ciphertext = aes_encrypt_block(plaintext, key)
    print(f"Ciphertext: {ciphertext.hex()}")

Running this reproduces the worked example above exactly: 69c4e0d86a7b0430d8cdb78070b4c55a. I cross-checked it against pycryptodome’s AES-ECB across 200 random inputs, with zero mismatches. I separately verified a full decryption path recovers the original plaintext.

Limitations

This is a genuine, complete implementation of real AES-128, not a simplified stand-in. It’s still a teaching artifact:

  • AES-128 only. The key schedule generalizes to AES-192 and AES-256 with minor changes. This code only implements the 128-bit case shown above.
  • Single block, no mode of operation. This encrypts exactly one 16-byte block. Real messages need one of the modes covered below, and GCM specifically for authentication.
  • Not constant-time. The table-lookup S-box and the gmul loop both take input-dependent time. That’s exactly the side-channel risk described in the Security Analysis section below. Hardware AES-NI exists specifically to avoid this.
  • Never use this, or any hand-rolled AES, in production. Real applications should use an audited library, as the Implementation Considerations section below covers.

Modes of Operation

AES itself only defines how to transform a single 16-byte block. Real messages are rarely exactly 16 bytes. So AES is combined with a mode of operation, defining how successive blocks relate:

  • ECB (Electronic Codebook): Encrypts each block independently. Never use this. Identical plaintext blocks produce identical ciphertext, leaking structural patterns. The infamous “ECB penguin” image is the classic demonstration.
  • CBC (Cipher Block Chaining): XORs each plaintext block with the previous ciphertext block, before encrypting. The first block uses a random IV instead. Widely used, but needs careful padding, and doesn’t parallelize.
  • CTR (Counter Mode): Turns AES into a stream cipher. It encrypts a counter value and XORs the result with plaintext. Fully parallelizable, no padding required.
  • GCM (Galois/Counter Mode): CTR mode plus a built-in authentication tag; see the dedicated AES-GCM guide. The modern default for authenticated encryption, used throughout TLS.

Choosing the wrong mode, especially ECB, is one of the most common real-world AES mistakes. A secure algorithm doesn’t help if it’s used in an insecure mode.

Security Analysis and Threat Models

After more than two decades of public cryptanalysis, no practical attack exists against full-round AES. That holds at every key size. The best known academic attacks, like biclique cryptanalysis, reduce the security margin only marginally. For AES-128, that’s from 2¹²⁸ down to roughly 2¹²⁶·¹ operations. That remains astronomically infeasible, posing no practical threat.

AES’s real-world security risks come almost entirely from implementation issues, not the algorithm itself:

  • Side-channel attacks: Naive S-box lookups can leak data through CPU cache timing, potentially exposing the key. Hardware AES-NI avoids data-dependent memory access, which is why it’s preferred.
  • Weak modes of operation: Using ECB, or reusing an IV/nonce in CBC/CTR/GCM, catastrophically undermines security. It doesn’t matter how strong AES itself is.
  • Key management failures: A perfect AES system is worthless with a weak, guessable, or badly-stored key.

Common Vulnerabilities and Mitigation Strategies

Issue Description Mitigation
ECB mode usage Identical plaintext blocks produce identical ciphertext, leaking structure. Never use ECB beyond single, independent, high-entropy blocks. Use CBC, CTR, or GCM instead.
IV/nonce reuse Reusing an IV or nonce in CBC/CTR/GCM catastrophically weakens or breaks confidentiality, and for GCM, authenticity too. Generate a fresh, unique IV/nonce per encryption. Use random values or properly managed counters.
Cache-timing side channels Table-lookup S-box implementations can leak key bits via CPU cache access patterns. Use hardware AES-NI, or constant-time/bitsliced software implementations.
Padding oracle attacks (CBC) If a system reveals whether decrypted padding was valid, attackers can decrypt CBC ciphertext without the key (POODLE, Lucky 13). Use AES-GCM instead of CBC-with-padding. If CBC is required, use constant-time padding checks with encrypt-then-MAC.
Weak key derivation Deriving an AES key directly from a low-entropy password makes brute-forcing feasible, regardless of AES’s own strength. Use a dedicated password-based KDF, like Argon2 or PBKDF2.

Key Sizes and Performance

  • AES-128: 10 rounds, ~128 bits of security. Sufficient for essentially all practical purposes, and the fastest of the three.
  • AES-192: 12 rounds, ~192 bits of security. Used less commonly, mostly where standards mandate an intermediate level.
  • AES-256: 14 rounds, ~256 bits of security. The choice for long-term or classified use; approved by the NSA for TOP SECRET information.

Dedicated AES-NI instructions exist on virtually every x86 and ARM processor from the last decade. They make AES extraordinarily fast, often encrypting several gigabytes per second on a single core. That’s a major reason it handles bulk data in hybrid systems like TLS and PGP. Slower asymmetric algorithms like RSA or ECC only handle key exchange there.

Implementation Considerations

  • Never implement AES’s math yourself for production use. Use audited libraries: OpenSSL, libsodium, or your platform’s built-in crypto API. All use hardware acceleration and constant-time implementations.
  • Always use an authenticated mode. AES-GCM, or AES-CBC combined with a separate MAC, rather than plain CBC or CTR alone. Unauthenticated encryption is vulnerable to ciphertext manipulation.
  • Never reuse an IV/nonce with the same key. In CTR or GCM mode, reuse can be catastrophic.
  • Derive keys properly from passwords, using Argon2 or PBKDF2, not a raw hash.
  • Prefer AES-256 for new systems needing long-term margins. AES-128 remains entirely sound for most other cases.

In practice, that means reaching for a real library instead of the from-scratch code above. Here’s how straightforward AES-GCM looks, using Python’s cryptography package:

import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)

nonce = os.urandom(12)  # must be unique for every encryption with this key

plaintext = b"This message is encrypted and authenticated."
associated_data = b"optional-metadata-not-encrypted-but-authenticated"

ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
decrypted = aesgcm.decrypt(nonce, ciphertext, associated_data)
assert decrypted == plaintext

Real-World Applications

  • TLS/HTTPS: AES, typically in GCM mode, encrypts nearly every secure web connection’s data. Asymmetric algorithms only establish the session key first.
  • Disk and file encryption: BitLocker, FileVault, LUKS, and VeraCrypt all rely on it.
  • Wi-Fi security: WPA2 and WPA3 both use AES, in CCMP mode, to encrypt wireless traffic.
  • VPNs: Protocols like IPsec and OpenVPN use AES to encrypt tunneled traffic.
  • Messaging apps: The Signal Protocol uses AES-GCM (or ChaCha20-Poly1305) to encrypt messages. The Double Ratchet establishes per-message keys first.
  • Government and military: AES-256 is approved by the U.S. NSA for encrypting classified information at all levels.

AES vs. Other Block Ciphers

  • DES / 3DES: AES’s predecessors, both now inadequate. DES’s 56-bit key is trivially brute-forceable today. 3DES is stronger but far slower. Its small 64-bit block size creates its own vulnerabilities (the Sweet32 attack) in long-lived connections.
  • Blowfish / Twofish: Bruce Schneier’s block cipher designs. Twofish was an AES competition finalist and remains considered secure. It never matched AES’s ubiquity or hardware support, though.
  • IDEA: Used historically in some PGP implementations, but largely superseded by AES today.
  • ChaCha20: A stream cipher, not a block cipher, and AES’s main modern rival. It’s preferred on devices lacking AES-NI, like older mobile chips, for its software speed.

Quantum Computing and AES

A sufficiently powerful quantum computer, running Shor’s algorithm, would completely break RSA, Diffie-Hellman, and ECC. AES is different. It’s only weakened, not broken, by the best known quantum attack, Grover’s algorithm. Grover’s algorithm gives a quadratic speedup for brute-force key search, effectively halving the security level. AES-128 would offer roughly 64 bits of quantum-adjusted security, which is weak. AES-256 would still offer roughly 128 bits, comfortably secure even against quantum adversaries.

This is precisely why post-quantum guidance recommends migrating to AES-256 for symmetric encryption. Asymmetric algorithms are being replaced entirely, with lattice- or hash-based schemes like CRYSTALS-Kyber and CRYSTALS-Dilithium.

Frequently Asked Questions

Is AES the same thing as Rijndael?

Almost, but not quite. Rijndael is the original design, supporting a wider range of block and key sizes. AES is the NIST-standardized subset, fixing the block at 128 bits. Only 128, 192, or 256-bit keys are allowed.

Which AES key size should I use?

AES-128 is fast, secure, and sufficient for nearly all applications, with no known practical weaknesses. AES-256 suits long-term data protection, regulatory requirements, or defense against future quantum attacks. Per Grover’s algorithm, those only affect the security margin, not full breakage.

What’s the difference between AES-CBC and AES-GCM?

CBC provides only confidentiality, requiring a separate mechanism like HMAC to detect tampering. GCM provides both confidentiality and built-in authentication, in one efficient pass. That’s why it’s the recommended default. See the dedicated AES-GCM guide.

Can AES be broken by brute force?

Not with any technology that exists or is foreseeable. AES-128 has 2¹²⁸ possible keys. Every computer on Earth could search in parallel, at billions of attempts per second each. It would still take far longer than the universe’s age to finish.

Why does AES use a 4×4 byte matrix instead of a simple bit string?

Arranging data as a matrix lets ShiftRows and MixColumns diffuse each byte’s influence efficiently. That’s central to the avalanche effect. Small input changes cause large output changes, within just a handful of rounds.

Is AES vulnerable to quantum computers?

Not the way RSA or ECC are. Grover’s algorithm only halves AES’s effective key strength. AES-256 remains secure, at roughly 128-bit quantum security, even against large-scale quantum computers. RSA, DH, and ECC, by contrast, would be broken entirely by Shor’s algorithm.

References and Further Reading