DEV Community

Cover image for Building Own Block Cipher: Part 3 - AES
Dmytro Huz
Dmytro Huz

Posted on • Originally published at dmytrohuz.substack.com

Building Own Block Cipher: Part 3 - AES

In the previous article, we built DES from scratch — not because it’s still relevant, but because it forces you to understand how block ciphers are assembled from specifications.

DES is awkward, bit-heavy, and full of historical baggage.

That’s exactly why it’s a good learning tool.

AES continues that journey — but with very different design goals and very different failure modes.

Where DES teaches structure,

AES teaches discipline.


Before touching code, let’s be honest about one thing:

AES is not interesting because it’s new.

It’s interesting because it’s everywhere.

Right now, AES is used in:

  • HTTPS (TLS)
  • Disk encryption (BitLocker, FileVault, LUKS)
  • Password managers
  • Cloud storage
  • Mobile devices
  • VPNs
  • Hardware security modules

If data is encrypted in 2025, there’s a very high chance AES is involved.

And yet, despite being studied, standardized, and deployed for more than 20 years, AES is still frequently implemented incorrectly.

Not because AES is broken.

But because people misunderstand what AES actually is.


AES Is Not Broken — Implementations Are

There are no practical cryptanalytic breaks of AES.

What does exist is a long history of failures caused by:

  • using AES-ECB (still happening)
  • reusing nonces in CTR or GCM
  • broken key expansion
  • incorrect byte/row/column layout
  • treating AES as “encryption” instead of a primitive

So the goal of this article is not to “learn AES”.

It’s to remove the illusion that AES is magic.


Scope of This Article (Read This First)

This article is an overview of how AES works and how it is implemented, based on a real, working implementation.

👉 The full implementation with detailed comments is here:

https://github.com/DmytroHuzz/build_own_block_cipher/blob/main/aes/aes.py

What you’ll find on GitHub:

  • full AES-128 implementation
  • key expansion
  • block encryption
  • CTR mode
  • tests against known vectors

What you’ll find here:

  • the mental model
  • the algorithmic structure
  • why the code is written the way it is

Think of this article as the assembly instructions

and the GitHub file as the assembled furniture.


Reading the AES Specification Like IKEA Instructions

AES is defined in FIPS-197: https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.197.pdf.

The document is:

  • precise
  • clean
  • unforgiving

It is also extremely easy to think you understand while silently misreading it.

So I approached the spec the same way I approach IKEA manuals:

  • don’t assume understanding
  • follow the order literally
  • assemble exactly what is written
  • only then ask why it works

Before touching individual transformations, we need to understand the whole machine.


Step 0: The Big Picture — What AES Actually Does

Before key expansion, S-boxes, or finite fields, answer this:

What does AES look like as a complete algorithm?

AES in One Sentence

AES repeatedly transforms a 4×4 byte matrix (the state) using round-specific keys, until the original plaintext is no longer recognizable.

That’s it.

Everything else is detail.


The Three Moving Parts of AES

AES consists of exactly three conceptual components:

1. The State

  • 16 bytes (128 bits)
  • arranged as a 4×4 matrix
  • transformed in place

2. The Round Keys

  • derived from the original key
  • one per round
  • injected using XOR

3. The Round Function

  • a fixed sequence of transformations
  • applied repeatedly

No branching.

No randomness.

No conditions.

AES is completely deterministic.


AES as a Loop (Not a Black Box)

For AES-128, the algorithm looks like this:

state = plaintext_block_as_state
round_keys = expand_key(key)

state ^= round_keys[0]

for round = 1..9:
    SubBytes(state)
    ShiftRows(state)
    MixColumns(state)
    state ^= round_keys[round]

SubBytes(state)
ShiftRows(state)
state ^= round_keys[10]

ciphertext = state_as_bytes
Enter fullscreen mode Exit fullscreen mode

That’s the entire cipher.

If you understand this loop, every AES implementation becomes readable.


Why AES Starts with AddRoundKey

AES begins with AddRoundKey, before any substitutions or mixing.

This is deliberate.

It ensures:

  • the key affects the cipher immediately
  • no “raw plaintext” enters a round
  • every transformation is key-dependent

This is one of those design choices that looks boring — and turns out to be essential.


Why the Final Round Is Different

The final round does not include MixColumns.

Why?

Because MixColumns exists to:

  • spread changes across columns
  • increase diffusion between rounds

After the final round, there is no next round.

Removing MixColumns:

  • simplifies decryption
  • keeps symmetry clean
  • avoids unnecessary diffusion

AES isn’t symmetric by accident.

It’s symmetric because it was engineered to be implemented correctly.


AES Is Not “Encryption” Yet

At this point we have:

  • a block algorithm
  • deterministic output
  • no randomness

That means:

AES alone does not encrypt messages.

It encrypts one 16-byte block.

This is why modes of operation exist — something we’ll come back to later, especially since the implementation includes CTR mode.


Step 1: Key Expansion (Before Anything Else)

Before AES can encrypt anything, it expands the key.

This is not optional.

This is not a helper.

This is half the cipher.

Why Key Expansion Matters

AES does not reuse the same key every round.

Instead:

  • the original key is expanded into round keys
  • each round uses a different key
  • each round key depends on all previous ones

This prevents:

  • simple patterns
  • slide attacks
  • related-key attacks

What Key Expansion Actually Does

High-level process:

  1. Split the key into 4-byte words
  2. For every new word:
    • rotate
    • apply SubBytes
    • XOR with round constant (Rcon)
    • XOR with the word 4 positions back

The implementation mirrors the spec line-by-line — and that’s exactly what you want here.

This is not a place to be clever.

This is a place to be correct.


AddRoundKey: The Most Honest Operation in AES

Now that we have round keys, AES begins with AddRoundKey.

It’s simply:

XOR the state with the round key

Why XOR?

  • reversible
  • fast
  • symmetric
  • no information loss

AddRoundKey is how key material enters the cipher.

Everything else rearranges or mixes data —

this is where the key actually does something.


The AES State (Where Most Bugs Are Born)

AES operates on 16 bytes, arranged into a 4×4 matrix.

And this is where implementations silently fail.

The state is column-major, not row-major.

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

Your block_to_state and state_to_bytes functions make this explicit.

If this mapping is wrong:

  • AES still runs
  • output still looks random
  • tests almost pass

This is why AES bugs are dangerous.


SubBytes: The Only Non-Linear Step

SubBytes replaces each byte using a fixed lookup table (S-box).

Important fact:

This is the only non-linear operation in AES.

Everything else is linear algebra and XOR.

You can derive the S-box mathematically.

You don’t need to to implement AES.

Using the fixed table is correct and intentional.


ShiftRows: Small Function, Big Consequences

Each row is rotated left:

  • row 0 → 0
  • row 1 → 1
  • row 2 → 2
  • row 3 → 3

This step:

  • breaks column alignment
  • ensures diffusion across rounds

If your state layout is wrong, this is where everything collapses.


MixColumns: Algebra Without Fear

MixColumns treats each column independently.

It mixes bytes using fixed coefficients in GF(2⁸).

In practice:

  • multiplication by 2 → xtime
  • multiplication by 3 → xtime(x) XOR x
  • addition → XOR
  • reduction → fixed polynomial

The implementation strips this down to what actually matters.

No matrices.

No abstractions.

Just mechanics.


AES Rounds (Putting It Together)

AES-128:

  • initial AddRoundKey
  • 9 full rounds:
    • SubBytes
    • ShiftRows
    • MixColumns
    • AddRoundKey
  • final round (no MixColumns)

At this point:

  • nothing is hidden
  • nothing is magical
  • everything is mechanical

That’s exactly what you want in cryptography.


CTR Mode: Turning AES into Real Encryption

Important clarification:

AES itself is not encryption.

It’s a block cipher.

To encrypt real data, we need a mode of operation.

Your implementation includes CTR (Counter) mode — deliberately.

How CTR Mode Works

CTR turns AES into a stream cipher:

  1. Combine nonce + counter
  2. Encrypt with AES
  3. XOR with plaintext
  4. Increment counter
  5. Repeat

Key properties:

  • no padding
  • encryption == decryption
  • parallelizable
  • fast

Critical rule:

Never reuse the same key + nonce pair.

CTR is secure only if nonces are unique.


What AES Actually Teaches

DES taught structure.

AES teaches discipline.

Most AES failures are not cryptographic.

They are:

  • layout mistakes
  • key handling errors
  • mode misuse
  • overconfidence

Implementing AES once forces you to:

  • respect specifications
  • respect data layout
  • respect boring correctness

Summary

If you’ve made it this far —

you’re no longer “using AES”.

You’re implementing it.

And that changes how you read every crypto API forever.

Top comments (0)