AES Without the Magic: A Practical Primer
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 hooked me enough to implement it myself was how unmagical it felt the first time I really looked inside. 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 I realised this wasn’t a spell to cargo-cult, but machinery I could understand and reason about.
My own entry point was embarrassingly small: as a teenager I managed to encrypt exactly one 128-bit block, with a half-broken implementation and no real sense of the surrounding theory. It wasn’t a serious system; it was just enough to see that the pieces actually moved, that the round function wasn’t magic ink but concrete operations. That single block ended up being the starting point for a much longer path: over time, turning a curiosity into something I could read, test and eventually explain without hand-waving.
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 not one tool, it’s 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.
None of these operations are mysterious on their own. The magic 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, linear mixing steps can be undone and everything can be implemented with predictable, constant-time operations on actual hardware.
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. A practical way to think about it is this: 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.
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 nearby tools. 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. 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, AES is not the only game in town. 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 implement without foot-guns. 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’s a principled “no” that prevents many subtle “well it almost decrypted” attacks.
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 peek.
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.