A local web tool, built with pure Python (standard library only, no pip installs) that shows how AES-128 encrypts a single 16-byte block step by step. Every intermediate state is drawn as a 4×4 grid of boxes (one box = one byte in hex). Click any byte to see the exact math behind it — the S-box lookup, the GF(2⁸) arithmetic, or the XOR — not just the answer.
The project is meant as a learning and demonstration tool: AES is implemented by hand so that all of its internal steps, and the reasoning behind them, are visible.
- Pure Python backend, no dependencies. Standard library only
(
http.server,json,webbrowser); runs withpython main.py, which starts a local server and opens your browser. - AES implemented by hand (no
pycryptodome,cryptography, etc.) — seeaes.py. - A round map, not just a step counter. All 40 steps are laid out as a
clickable timeline grouped by round (
R0 · INITIAL,R1…R9,R10 · FINAL), so you always know where you are in the whole pipeline and can jump straight to any step. - Click a byte, see the math — not just an animation of the answer
appearing:
- SubBytes — click a byte to see it highlighted in the actual S-box table: which row (high nibble) and column (low nibble) it looked up.
- ShiftRows — click a byte to see exactly which column it moved to, and which column the byte now sitting there came from.
- MixColumns — click a byte in the output grid to see the full GF(2⁸) arithmetic that produced it, term by term.
- AddRoundKey — click a byte to see the bit-by-bit binary XOR that produced the result.
- 4×4 state grid with changed bytes, round-key bytes, and the currently-inspected byte each in their own color (with a legend).
- Animated transitions between before/after state, with a speed slider from fast down to slow motion.
- Bit view — click a byte to also show its 8 bits (MSB first).
- Plaintext input as hex or text — enter 32 hex characters, or a text
string like
Text Test(UTF-8, zero-padded to 16 bytes). The fields are pre-filled with the FIPS-197 test vector. - Light and dark themes, following your system preference by default with a manual override.
- Resolved plaintext (text and hex) and the final ciphertext are shown once you run it.
The core idea, unchanged from the original design: aes.py does not
encrypt silently — encrypt_steps() returns a list of steps, computed
entirely in Python. A tiny stdlib HTTP server (server.py) exposes that as
a JSON endpoint, and the browser only renders the list it gets back — it
never computes cryptography itself. This is also why the S-box lookup and
GF(2⁸) breakdowns shown in the UI are trustworthy: they're read straight off
the server's computed steps, not recomputed in JavaScript.
A step looks like this:
Step = {
"name": "SubBytes", # SubBytes | ShiftRows | MixColumns | AddRoundKey
"round": 3, # 0..10
"state_before": [[..4..], ...], # 4x4 matrix before
"state_after": [[..4..], ...], # 4x4 matrix after
"round_key": [[..4..], ...] | None, # only set for AddRoundKey
"changed": [(row, col), ...], # changed cells (for highlighting)
"explanation": "short text", # shown in the UI
"mix_terms": terms | None, # only set for MixColumns: the GF(2^8)
# terms behind every output byte
}encrypt_steps(plaintext, key) returns (steps, ciphertext) and produces
exactly 40 steps for AES-128:
1 Round 0: AddRoundKey
36 Rounds 1-9: each SubBytes -> ShiftRows -> MixColumns -> AddRoundKey (9 x 4)
3 Round 10: SubBytes -> ShiftRows -> AddRoundKey (no MixColumns!)
aes_visualizer/
├── sbox.py # fixed AES S-box (FIPS-197) + Rcon (via GF(2^8) doubling)
├── aes.py # AES-128 logic -> encrypt_steps(plaintext, key) -> (steps, ciphertext)
├── server.py # stdlib HTTP server: serves web/ + POST /api/encrypt
├── main.py # entry point: starts the server, opens your browser
└── web/
├── index.html # page structure
├── styles.css # design system (dark/light, no external fonts)
└── app.js # renders steps, animates, builds the S-box / GF(2^8) / XOR detail views
README.md
specifications.txt # Python version + requirements (no pip packages)
Dockerfile # container image (plain python:3.12-slim, no Tk/X11 needed)
docker-compose.yml # start with "docker compose up --build"
DOCKER.md # running the visualizer in a container
run-docker.sh # build + run + open the browser
.dockerignore
.gitignore
-
The state is filled column-wise:
state[row][col] = bytes[col*4 + row]. -
SubBytes — each byte is replaced through the fixed S-box (
out = SBOX[in]). -
ShiftRows — row
ris cyclically rotated left byrpositions (row 0 stays, row 1 by 1, row 2 by 2, row 3 by 3). -
MixColumns — each column is multiplied by the fixed matrix in GF(2⁸):
| 2 3 1 1 | |s0| |r0| | 1 2 3 1 | × |s1| = |r1| | 1 1 2 3 | |s2| |r2| | 3 1 1 2 | |s3| |r3|The GF(2⁸) multiplication uses
xtime(doubling) with the reduction polynomial0x11B. Click a byte in the visualizer's output grid during a MixColumns step to see this worked out for real, byte-for-byte. -
AddRoundKey — the state is XORed with the round key, byte by byte.
-
KeyExpansion — the 16-byte key is expanded into 11 round keys (44 words):
W[0..3]is the key itself; fori = 4..43,W[i] = W[i-4] XOR temp, wheretemp = W[i-1]and, wheni % 4 == 0, additionallytemp = SubWord(RotWord(temp)) XOR Rcon[i/4].
- Python 3.10 or newer (tested with Python 3.10, 3.11 and 3.12).
- Standard library only — no packages need to be installed via
pip. - Any current desktop browser (Chrome, Firefox, Safari, Edge) — no plugins, no build step, no npm.
A compact overview is in specifications.txt.
cd aes_visualizer
python main.pyThis prints the local URL (default http://127.0.0.1:8000/) and opens it in
your default browser. Stop the server with Ctrl+C.
If port 8000 is already in use, override it:
AES_VIZ_PORT=8001 python main.pyaes.py contains a self-test against the FIPS-197 test vector:
cd aes_visualizer
python aes.pyExpected output (shortened):
OK - FIPS-197 Appendix B test vector passed.
OK - Ciphertext: 39 25 84 1d 02 dc 09 fb dc 11 85 97 19 6a 0b 32
OK - generated 40 steps.
The visualizer can also run in a container — see DOCKER.md.
Short version:
docker build -t aes-visualizer .
docker run --rm -p 8000:8000 aes-visualizer… or simply ./run-docker.sh, or docker compose up --build. Then open
http://localhost:8000/ — no X11 forwarding required.
The input fields are pre-filled with this official test vector:
Plaintext: 32 43 f6 a8 88 5a 30 8d 31 31 98 a2 e0 37 07 34
Key: 2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c
Ciphertext: 39 25 84 1d 02 dc 09 fb dc 11 85 97 19 6a 0b 32
- Enter the key as 32 hex characters. For the plaintext, pick Hex or
Text (e.g.
Text Test) and type it, then click Run. The resolved plaintext (text and hex) is shown below the inputs. - Use the round map to see the whole pipeline at a glance, and click any tick to jump straight to that step. Or page through with Next ► / ◄ Back, or play automatically with ▶ Play — the speed slider ranges from fast down to slow motion.
- Click any byte in the state grid to see the operation's actual math for that byte (S-box lookup, row rotation, GF(2⁸) terms, or XOR bits), and to show/hide its 8 bits.
- On the last step, the finished ciphertext is shown.