Skip to content
Mario Ciavarella
Back to overview

Oct 25, 202412 min read

Inside AES: A Practical Primer

If you read “encrypted at rest” or “secure channel” in a spec, AES is usually the thing actually moving the bits. It hides under labels like disk encryption, token stores, TLS offload and “secure cookies”, quietly encrypting data even when you never call it directly.

Open the AES Lab

Cryptography · Engineering

Contents

If you read “encrypted at rest” or “secure channel” in a spec, there’s a good chance AES is the thing actually moving the bits around. It’s the cipher hiding under labels like “disk encryption”, “token encryption”, “TLS offload”, “KMS key”, “secure cookies”. You almost never call it directly, yet it silently sits on the hot path of your data.

What pushed me to implement it myself was how ordinary it looked the first time I opened it up. On paper AES is sold as “a modern block cipher over finite fields”; in a debugger it’s a 4×4 grid of bytes, a handful of table lookups, some shifts and XORs, repeated with absurd discipline. Watching a tidy pattern dissolve into structured noise round after round was the moment it turned into machinery I could take apart.

My own entry point was one 128-bit block: as a teenager I got exactly that far, with no sense of the theory around it. It was enough to see the pieces move — that the round function was a sequence of concrete operations. That single block turned into a much longer path: a curiosity became something I could read, test and eventually explain.


Where AES quietly lives

Once you start looking for it, AES shows up everywhere. The full-disk encryption you enable with a toggle on laptops and phones is usually “AES-XTS”. Transparent encryption in databases, “column encryption” in ORMs and secrets stores in cloud consoles are often “AES-GCM with keys in KMS”. On the wire, when a browser and a server negotiate a TLS connection, most modern cipher suites boil down to “do a fancy key exchange, then speak AES-GCM for the actual traffic”.

From the outside, these features have friendly names and pretty dashboards. Underneath, they rely on the same primitive: a fast, symmetric cipher that takes a chunk of data, a key and some parameters and turns that chunk into something indistinguishable from random noise for anyone who doesn’t know the key.


How AES fits among other cryptographic tools

Cryptography is a toolbox. AES occupies a very specific drawer: shared-secret encryption of bulk data.

On one side of it you have public-key algorithms like RSA or elliptic-curve schemes. They are slow, expensive and wonderful at one thing: agreeing on secrets and proving who you are. They power key exchanges, certificate chains and signatures on software releases. You don’t use them to encrypt gigabytes; you use them to establish keys and trust.

On the other side you have hash functions. They take arbitrary input and spit out fixed-size fingerprints. You use them to detect changes, build Merkle trees, store passwords (through dedicated schemes like PBKDF2, scrypt or Argon2) and derive fresh keys from old ones. They don’t encrypt anything; they deliberately throw information away.

AES sits in the middle. Given that you already have a shared key, it answers a narrower question: how do we transform blocks of data so that, without that key, they look like random noise but can still be recovered perfectly by someone who has the key? If you want the formal story, the AES standard is published as FIPS 197. The rest of this article is the informal one.


AES as a block cipher

AES is a block cipher: it transforms fixed-size blocks of data into blocks of the same size, under the control of a key. In AES the block size is 16 bytes. Internally, the cipher sees that block as a 4×4 matrix of bytes. You choose a key of 128, 192 or 256 bits; the matrix stays the same size regardless.

The work happens in rounds. Each round takes the 4×4 matrix and runs it through a small pipeline of reversible steps: substitute each byte using a carefully constructed lookup table, rotate the rows so bytes move into new columns, mix the columns together using simple arithmetic on bytes, then blend in a “round key” derived from your original key with XOR. With a 128-bit key you do this ten times; with 256 bits, fourteen times.

Two details the usual picture leaves out, both of them visible in the microscope further down. A round key is XORed into the block once before the first round, so nothing ever enters the pipeline unmasked. And the final round leaves out the column mixing — not to save work: MixColumns is linear and anyone can undo it, so applying it after the last key addition would protect nothing, while leaving it out gives decryption the same shape as encryption.

None of these operations are mysterious on their own. The strength is in how they interact. After a handful of rounds, flipping a single bit in the input usually flips about half the bits at the output in a way that looks chaotic. This is the avalanche effect: tiny changes in plaintext or key explode into large, unpredictable changes in ciphertext.

Mathematically, the internal arithmetic lives in a small universe called GF(2^8), a finite field where values are bytes, addition is XOR and multiplication is “normal” multiplication reduced modulo a fixed polynomial. That choice gives AES what it needs: every non-zero byte has an inverse, the linear mixing steps can be undone, and the round function reduces to operations a processor is good at.

What it does not give you is constant time. The natural way to implement the substitution step is a 256-byte lookup table, and a lookup whose index depends on the key leaves a trace in the cache that has been turned into key-recovery attacks more than once. Implementations that do run in constant time get there through dedicated instructions — AES-NI on x86, the crypto extensions on ARM — or by bitslicing the cipher so that no table is consulted at all. Constant time comes from the implementation; the standard says nothing about it.

If you hold onto that picture — a 4×4 grid of bytes pushed through the same pipeline of simple steps — you have enough of AES to follow what happens next.


From blocks to actual messages

Almost no real-world message is exactly 16 bytes long. Logs, JWT payloads, database rows, video segments and API bodies are longer, messier and rarely aligned to neat boundaries. To deal with that, AES is wrapped in a mode of operation that decides how to process sequences of blocks and how they relate to each other.

The simplest possible mode encrypts each block independently. It is usually called ECB and is mostly useful as a teaching tool and a smell in audits. Because identical blocks of plaintext produce identical blocks of ciphertext, any structure in your input bleeds straight through. Encrypted images still show their outlines; repeated records in a file light up as stripes. More useful modes deliberately link blocks together or synthesize a keystream.

In CBC, each plaintext block is XORed with the previous ciphertext block before going through AES. The very first block uses a special value called an Initialization Vector (IV). This chaining means that even if two blocks of plaintext are equal, their ciphertexts will differ as long as their histories differ. CBC is conceptually close to “encrypting in batches”.

In CTR, AES stops encrypting your data directly. Instead it encrypts a sequence of counters combined with a nonce. The outputs of AES form a keystream, which you XOR with your data to encrypt and decrypt. That looks and feels like a stream cipher: you can jump to the middle if you know the offset, parallelise encryption and decryption and treat the data as a continuous flow.

GCM builds on CTR and adds integrity. Besides encrypting with a counter, it computes a tag over the ciphertext and any associated metadata using a polynomial function over a larger field. When you decrypt, you recompute the tag; if it doesn’t match, you throw the whole thing away. GCM is one of the standard AEAD modes: authenticated encryption with associated data. AES is the engine; modes are the ways you couple that engine to real workloads — batch processing, streaming, authenticated channels — and most accidents happen in that coupling.

The AES Lab is where that single teenage block ended up. It runs on AES.js, the public implementation I kept rewriting: every table, every round and every mode written out in plain JavaScript, with no WebCrypto underneath. It is table-driven, so it leaks through timing exactly as the previous section described — the price of being legible, and the reason it guards nothing. Encrypt something in it and switch the mode: the round-by-round view shows the pattern dissolving exactly as described here.


How AES compares to “just hashing” or “just encrypting”

Developers run into AES in situations where “we need to secure this” is the starting point, not the spec. That’s where it helps to contrast it with the tools sitting next to it in the drawer.

Encrypting passwords with AES is almost always the wrong move. The problem there is not someone eavesdropping on your database; it’s someone stealing a copy and trying billions of guesses per second. AES is designed to be fast, and that speed works for the attacker. Password hashing schemes are designed to be annoyingly slow and resource-hungry, so each guess is expensive. They are built on top of hash functions and have their own standards, like RFC 9106 for Argon2.

On the transport side there are alternatives. Stream ciphers like ChaCha20 paired with Poly1305 as a MAC can be more attractive on devices without hardware AES instructions. On x86 servers and recent ARM cores, AES-GCM wins on raw throughput; on some embedded chips, ChaCha20-Poly1305 is easier to get right — which, given what the previous section said about lookup tables, is not only a question of speed.

And when you see long public keys, certificates and signatures, AES is not involved at all. That’s asymmetric territory: algorithms designed to work in one direction (validate a signature, check a certificate) for many clients and in the other direction (sign, issue) for a much smaller set of servers or authorities.

The important bit is to recognise when you’re in “encrypt bulk data with a secret key” territory — that’s where AES belongs — and when you’re in “prove identity”, “store passwords” or “prevent replay” territory, which call for different tools.


The small details that make or break it

In theory, “use AES-256” sounds like a decisive security choice. In practice, most breakages have nothing to do with the number in the name and everything to do with the details around it. Padding is one of those details.

Some modes want the message length to be a multiple of 16 bytes, so the last block is padded according to a convention such as PKCS#7. Get the rules slightly wrong or mix raw bytes and “characters” carelessly and you create opportunities for attackers to probe how your code responds to malformed ciphertexts. Initialization vectors and nonces are another. They exist to make sure that encrypting the same message twice with the same key doesn’t produce the same ciphertext. In CBC the IV needs to be unpredictable; in CTR and GCM the main requirement is that the nonce never repeats with the same key. These values are usually stored or transmitted alongside the ciphertext and are not secrets in themselves, but reusing them or generating them poorly can completely undo the guarantees of the mode.

Then there is integrity. Plain encryption gives you confidentiality, but it doesn’t stop an attacker from flipping bits in transit and looking at how the application reacts. That’s why AEAD modes like GCM insist on a tag: if the tag doesn’t match on decryption, the whole message is rejected without telling you which part was wrong. It is an unconditional no, which is what closes off the attacks that live in “almost decrypted”.

Finally, keys themselves need a life outside the code. They have to be created from good randomness, scoped to particular uses, renewed, expired and stored somewhere better than a config file in a repo. A beautiful AES implementation is not worth much if the keys are sprinkled through logs, pasted into tickets or hard-coded into mobile apps. If you want a sense of the standards that try to codify all this behaviour, the NIST series starting with SP 800-38A is a good place to start.


Reading AES in the wild

Most of the time you won’t be writing AES by hand; you’ll be reading someone else’s choices. When you see it mentioned in a config, a library or a design document, a few questions are usually enough to tell whether you should relax or dig deeper. What role is AES playing here: long-term storage, short-lived tokens, streaming traffic? Which mode is actually in use and does it provide both encryption and integrity or only one of the two? Where do the IVs or nonces come from and are they clearly stored alongside the ciphertext or reconstructed in some clever way? Are there separate keys for different purposes or a single “SECRET_KEY” doing everything from encryption to HMACs and JWT signing? What do error messages look like when decryption fails — do they leak hints about whether padding, tags or keys were wrong?

You don’t need to derive the S-Box formula to have useful opinions about these things. A solid mental picture of AES as a 16-byte block cipher, wrapped in different modes, glued to the rest of the system by padding, IVs, tags and key management, is already enough to spot when “we use AES” is just marketing and when it actually means something.

Comments (0)

0 / 2000 characters

No comments yet

Nothing here so far. Yours would be the first.