close

What is AES and how it works?

What is AES and how it works?

In my day‑to‑day work, AES occasionally shows up as a requirement or as a behind‑the‑scenes feature of a technology. Believe it or not, we use it many times each day without realizing it: when you connect to secure Wi‑Fi, visit an HTTPS site, message someone via a mobile app, or interact with your banking app, AES is encrypting the data.

Until recently, I treated AES as a black box - put input data, get scrambled output. I’ve always wondered what happens inside that box, and this article answers that curiosity while giving me a chance to share what I’ve learned.

This article explains how AES works and describes in detail what each operation does. It does not cover the underlying mathematics or the reasoning behind each step. If you’re interested in those details, you’ll need to consult additional resources.

Please do not use the information in this article to build your own production-grade AES implementation. Instead, rely on established libraries that leverage hardware acceleration where possible. The accompanying repository contains sample code that implements AES purely for educational purposes.

AES (Advanced Encryption Standard) is a symmetric encryption algorithm, meaning one key is used for both encrypting and decrypting data. Given plaintext and a key, AES transforms the data into ciphertext that cannot be read without the key, allowing it to be stored or transmitted securely over networks.

The standard replaced DES (Data Encryption Standard) in the early 2000s.

The AES algorithm relies heavily on the exclusive‑OR (XOR, ADD, ⊕) operation because it is fast and computationally inexpensive. Below is a quick reference truth table to refresh your memory.

xyx ⊕ y
000
011
101
110

Before we dive in, let’s define the terms that appear throughout this article. I’ll introduce additional concepts as they arise later on.

  • Nibble - Four bits (half a byte).
  • Byte - Eight bits.
  • Word - 32 bits, or four bytes.
  • State - A 4×4 matrix of 16 bytes (128‑bit block) that AES manipulates during encryption.
  • Cipher() - The core AES routine: it encrypts a 128‑bit plaintext block with provided round keys.
  • Plaintext / Plain block - The data segment that is fed into AES for encryption.
  • Ciphertext / Cipher block - The scrambled output produced by AES.
  • Message encryption - Encrypting an entire message, which may be any length—shorter or longer than 128 bits. I’ll discuss techniques for handling multi‑block messages toward the end of this article.

AES accepts keys of 128, 192, or 256 bits. To encrypt plaintext, it processes the data through a series of rounds. Each round uses a unique key derived from the original key - a process called Key Expansion (or Round Key Generation). We’ll begin by exploring this step first.

Key Expansion Operation

When AES starts an encryption or decryption session, it first expands the user‑supplied key into a sequence of round keys.

The number of rounds, and therefore the number of round keys, depends on the key size:

Key sizeRounds
128 bits10
192 bits12
256 bits14

Each round key, is a 128‑bit block. A longer key yields more rounds and stronger encryption.

The Round Key  is the 128‑bit word that Cipher() consumes in each round.
The Generated Key is the intermediate block of words produced during the expansion; it has the same number of words as the original key.

Both keys and state matrices are organized into columns of four bytes (one word per column). The first four bytes form the first word, the next four bytes form the second word, and so on.

Organizing bytes in a 4x4 matrix, `b` denotes a byte, `w` specifies a word.

Figure 1. Organizing bytes in a 4x4 matrix, b denotes a byte, w specifies a word.

The key‑expansion routine builds each round key from the previous one by applying a small set of deterministic transformations.

Below is the process for producing the first word in a new 16-byte block .

  1. Rotate the last word of the previous block (w3) left by one byte, this operation is called Rotate Word.
  2. Substitute each byte of the rotated word using the AES S‑box (SubBytes). This masks the original values, more details below.
  3. Add a round constant. Every round key has its own constant; it is XORed with the substituted word.
  4. Add that result with the first word of the previous block (w0).The outcome is the new first word (w4) of the current block.
Deriving first word of the next generated key.

Figure 2. Deriving first word of the next generated key

In the diagram above we use the Sample128-bitkey string as the 128‑bit input key. Its hexadecimal representation is[53 61 6d 70 6c 65 31 32 38 2d 62 69 74 6b 65 79 ] .

To derive next word, we simply XOR the preceding word(w4) of the current block with the word in the same position from the previous key block (w1).

Deriving second word of a generated key.

Figure 3: Deriving second word of a generated key

Below is pseudocode illustrating how the remaining words are derived.

w4 = SubBytes(RotateWord(w3)) ⊕ round_constant[1] ⊕ w0
w5 = w4 ⊕ w1
w6 = w5 ⊕ w2
w7 = w6 ⊕ w3

w8 = SubBytes(RotateWord(w7)) ⊕ round_constant[2] ⊕ w4
w9 = w8 ⊕ w5
w10 = w9 ⊕ w6
w11 = w10 ⊕ w7

w12 = SubBytes(RotateWord(w11)) ⊕ round_constant[3] ⊕ w8
w13 = w12 ⊕ w9
w14 = w13 ⊕ w10
w15 = w14 ⊕ w11
. . .

The RotateWord and XOR operations are straightforward; let’s examine the SubBytes and AddRoundConstant steps in a bit more detail—trust me, they’re not complicated.

Substitute Bytes

The Substitute Bytes (or SubBytes) operation substitutes each byte with a value from the AES S‑Box. The S‑Box is a 16 × 16 lookup table (256 entries), where each cell holds a substitution value.

To find the replacement for a given byte, split it into two nibbles. For example, 0x3E becomes high nibble 0x3 and low nibble 0xE. The high nibble selects the row index (row = 3, or the fourth row), and the low nibble selects the column index (column = 14, or the 15th column). Looking up that cell returns the substitution value 0xB2.

Substitute-Byte Operation Example.

Figure 4: Substitute-Byte Operation Example

Below is a view of the complete S‑Box table. For additional details, see the Wikipedia entry on the Rijndael S‑Box

123456789abcdef
0637c777bf26b6fc53001672bfed7ab
1ca82c97dfa5947f0add4a2af9ca472
2b7fd9326363ff7cc34a5e5f171d831
304c723c31896059a071280e2eb27b2
409832c1a1b6e5aa0523bd6b329e32f
553d100ed20fcb15b6acbbe394a4c58
6d0efaafb434d338545f9027f503c9f
751a3408f929d38f5bcb6da2110fff3
8cd0c13ec5f974417c4a77e3d645d19
960814fdc222a908846eeb814de5e0b
ae0323a0a4906245cc2d3ac629195e4
be7c8376d8dd54ea96c56f4ea657aae
cba78252e1ca6b4c6e8dd741f4bbd8b
d703eb5664803f60e613557b986c11d
ee1f8981169d98e949b1e87e9ce5528
f8ca1890dbfe6426841992d0fb054bb

Round Constants

A list of 10 round constants is built into AES. Each constant contains one byte that isn’t zero; all other bytes are 00. When a round constant is added to the word, only the first byte in the word changes.

RoundConstant
101 00 00 00
202 00 00 00
304 00 00 00
408 00 00 00
510 00 00 00
620 00 00 00
740 00 00 00
880 00 00 00
91b 00 00 00
1036 00 00 00

When you expand a 128‑bit key, all ten round constants are used, one for each round.
For a 192‑bit key we only need the first eight constants. Eight more keys (each six words long) are generated from the original key, but the round key is 4 word each, so we end up with 12 round keys.
With a 256‑bit key, the first seven constants suffice. Here we generated seven keys (each 8 words long), resulting in 14 round keys.
The diagram below summarizes how key expansion works for 128‑, 192‑, and 256‑bit keys, hopefully it makes the process clearer.

Key Expansion for 128-bit, 192-bit and 256-bit keys.

Figure 5 – Key Expansion for 128‑bit, 192‑bit, and 256‑bit Keys

  • Gray rectangles show the original key words.
  • The orange rectangle marks the first word of a newly generated key. It is produced by rotating the previous word, substituting its bytes, adding a round constant, and adding the corresponding word from the preceding key.
  • The teal‑ish rectangles illustrate the simple XOR between a word in the current block and the same‑position word from the prior block.
  • For 256‑bit keys an extra step is applied: every fifth word of each generated key undergoes a byte substitution (highlighted in green).

Note: A round key (rk) consists of four words. These four words are supplied to each round of the Cipher() operation, which we will discuss next.

Cipher Operation

The Cipher() operation encrypts data one 128‑bit block (16 bytes) at a time; this block is called the state. It applies a sequence of transformation functions to the state, using the round keys produced by KeyExpansion.

In most cipher modes, the Cipher’s input is simply a 128‑bit chunk extracted from the plaintext. The first step is to add round key 0 (rk₀) to the state, i.e. XOR the state with the original key.

AES Cipher Algorithm.

Figure 6 – AES Cipher Algorithm

In every round, except the final one, a fixed sequence of transformations is applied to the state:

  1. Substitute Bytes
  2. Shift Rows
  3. Mix Columns
  4. Add Round Key

For the last round we drop Mix Columns and perform only:

  1. Substitute Bytes
  2. Shift Rows
  3. Add Round Key (XOR with the final round key)

The Substitute Bytes operation was described earlier; below I’ll briefly explain the remaining functions.

Shift Rows

The ShiftRows() function circularly rotates each of the state’s rows to the left:

  • Row 0 remains unchanged.
  • Row 1 shifts one byte to the left.
  • Row 2 shifts two bytes to the left.
  • Row 3 shifts three bytes to the left.
Shift Rows.

Figure 7: Shift Rows

Mix Columns

The MixColumns() operation processes each column of the state by multiplying it with a fixed 4×4 matrix (in Galois‑field arithmetic).

Column Multiplication with a Fixed Matrix.

Figure 8: Column Multiplication with a Fixed Matrix

The multiplication used in MixColumns() differs from the ordinary dot product you learn in linear algebra. For each output byte, you take each byte in the input column and multiply it by a corresponding byte in the corresponding row of the 4×4 matrix. Then you XOR all four products to obtain that output byte. I hope the pseudocode below makes this clearer:

s'[0] = gfMul(s[0], 0x02) ⊕ gfMul(s[1], 0x03) ⊕ gfMul(s[2], 0x01) ⊕ gfMul(s[3], 0x01)
s'[1] = gfMul(s[0], 0x01) ⊕ gfMul(s[1], 0x02) ⊕ gfMul(s[2], 0x03) ⊕ gfMul(s[3], 0x01)
s'[2] = gfMul(s[0], 0x01) ⊕ gfMul(s[1], 0x01) ⊕ gfMul(s[2], 0x02) ⊕ gfMul(s[3], 0x03)
s'[3] = gfMul(s[0], 0x03) ⊕ gfMul(s[1], 0x01) ⊕ gfMul(s[2], 0x01) ⊕ gfMul(s[3], 0x02)

Both the multiplication and addition are GF(2⁸) operations, not ordinary integer arithmetic.
Addition in this field is just XOR, exactly what we’ve been using so far. Multiplication (gfMul()) involves polynomial arithmetic; for AES the product of any two bytes is always reduced modulo the irreducible polynomial x⁸+x⁴+x³+x+1, which corresponds to the hexadecimal value 0x11B.

The MixColumns step is, by far, the most intricate part of the algorithm.

Add Round Key

The AddRoundKey() operation simply XORs each byte of the state with the corresponding byte of the round key, producing a new state.

Add Round Key Operation

Figure 9: Add Round Key Operation

That’s essentially everything involved in the Cipher() operation.

With the ciphertext in hand, decryption is performed by the complementary InvCipher() operation using the same key that was used for encryption.

The Inverse Cipher Operation

The InvCipher() function is the inverse of Cipher(); it applies the same round keys but processes them in reverse order.

AES Inverse Cipher() Algorithm.

Figure 10: AES InvCipher Algorithm

As the diagram shows, every step of InvCipher() is performed in the reverse order of Cipher(). The AddRoundKey operation remains unchanged; we simply XOR with the round key as before. For the other three transformations we use their inverses:

OperationInverse
SubBytesInvSubBytes (uses an inverse S‑Box)
ShiftRowsInvShiftRows (shifts in the opposite direction)
MixColumnsInvMixColumns (uses a different matrix)

The inverse S‑Box table can be found here. Similarly, the inverse MixColumns matrix is shown below.

Inverse MixColumn Matrix.

Figure 11: Inverse MixColumns Matrix Multiplication

The InvShiftRows() transformation simply reverses what ShiftRows() does: it shifts each row to the right.

  • Row 0 stays unchanged.
  • Row 1 shifts one byte to the right.
  • Row 2 shifts two bytes to the right.
  • Row 3 shifts three bytes to the right.
Inverse Shift Rows Operation.

Figure 12: The Inverse ShiftRows Operation

In Figure 13 I try to consolidate everything we’ve covered so far, giving you an overview of what happens inside AES. Each colored rectangle represents a distinct transformation. In the inverse cipher diagram, notice that all rectangles are mirrored horizontally, reflecting the reversed order of operations in InvCipher().

Rounds for Cipher() and InvCipher().

Figure 13: Rounds for Cipher() and InvCipher()

What remains to be explained is how larger messages are encrypted. I’ll touch on this in the next section, but first note that the material below deals with cipher modes, extensions that use AES as their building block and are not part of the original AES standard itself.

Encrypting long messages

A long message can be split into 16‑byte (128‑bit) blocks; each block can be encrypted independently, and the resulting ciphertext blocks are concatenated, this is how ECB mode works (we’ll discuss it later).

When the plaintext length isn’t a multiple of 16 bytes, two common solutions are:

  1. Pad the final partial block so that its length becomes exactly 16 bytes.
  2. Use a cipher mode that does not require padding.

The most widely used padding scheme is PKCS#7, which consist of the following steps:

  1. Determine how many bytes are needed to fill the last block.
  2. Append that number of bytes, each byte having the value equal to the number of padding bytes.

For example, if four bytes are required to complete a block, you append 0x04, 0x04, 0x04 , 0x04. If the message already ends on a full block (no padding needed), PKCS#7 still appends an extra block containing sixteen bytes of value 0x10.

PKCS#7 Padding Example.

Figure 14: PKCS#7 Padding Example

Other padding schemes are available, such as:

  • Zero padding – fill the remaining bytes with 0x00. The downside is that if the original data ends in 0x00, it becomes ambiguous. For pure ASCII text this usually isn’t a problem.
  • ISO 10126 / ANSI X9.23 – pad with random bytes, leaving only the last byte to indicate the total number of padding bytes (including itself).

Once the plaintext has been padded, you can encrypt the whole message using a cipher mode of your choice. In the next section I’ll give a quick overview of three common modes to illustrate how they work; many more exist (see this page for more details).

NOTE: Cipher modes are not part of the AES standard itself; they merely employ AES as their underlying block cipher.

ECB - Electronic Codebook is the simplest mode. Encryption consist in:

  1. Pad the plaintext and split it into 16‑byte blocks.
  2. Encrypt each block independently with the same key using Cipher().
  3. Concatenate the ciphertext blocks to form the final message.

During decryption, the process reverses: split the ciphertext into 16‑byte blocks, apply InvCipher() with the same key to each block, concatenate the results, and finally strip off the padding.

ECB Cipher Mode

Figure 15: ECB Cipher Mode (Encryption & Decryption)

ECB is considered insecure because identical plaintext blocks generate identical ciphertext blocks, exposing statistical patterns. The linked Wikipedia article displays a classic example: an image encrypted with ECB still reveals visible structures from the original.

CBC - Cipher Block Chaining. In this mode every plaintext block is XORed with the preceding ciphertext block before it is fed to Cipher() operation.

For the very first block there is no previous ciphertext, so an IV (Initialization Vector) is used instead. The IV is a 16‑byte block of random or pseudorandom data; it does not have to be secret like the key.

Because each ciphertext block depends on the one that came before it, CBC encryption cannot be parallelized. As with ECB, the message must be padded to a multiple of 16 bytes.

CBC Cipher Mode

Figure 16: CBC Cipher Mode (Encryption & Decryption)

Because each ciphertext block contains all the data needed to recover its plaintext, CBC decryption can be performed in parallel. If the IV is missing or corrupted, only the first plaintext block will be wrong; the remaining blocks decrypt correctly.
Since the IV is not secret, it is usually prefixed to the ciphertext. During decryption the routine reads the first 16 bytes as the IV and then processes the rest of the data.

CTR - Counter Mode. In CTR mode each plaintext block is processed with a different input to the block cipher: The input can simply be an IV that’s incremented for every block, or it can consist of two parts:a random nonce (the first half) and an incrementing counter (the second half). Then encrypt the input with the Cipher() operation. Lastly, XOR the resulting keystream block with the plaintext block to produce ciphertext.
Because every counter value is independent, CTR encryption and decryption can be fully parallelized.

CRT Cipher Mode

Figure 17: CTR Cipher Mode (Encryption & Decryption)

A key point: CTR decryption does not require InvCipher(). To recover plaintext you reuse the same nonce/counter values, encrypt them in exactly the same way as during encryption, and XOR the result with the ciphertext block.

As noted earlier, many other modes exist. For instance, GCM (Galois/Counter Mode) extends CTR by adding authentication tags and extra processing to provide both confidentiality and integrity.

A few words about key generation, you can come up with your own key, or use a Key Derivation Function (KDF).
The most common KDF for passwords is PBKDF2. It takes a human‑readable password or passphrase, optionally coupled with a salt (a random byte string), and applies many iterations of a hash function to produce a cryptographically strong key.
Because the same password and salt always produce the same key, both the password/salt pair and the derived key must be treated as secrets and stored securely.

Summary

AES is essentially a black‑box block cipher: you feed it a key and a 128‑bit plaintext block, and it returns an unintelligible 128‑bit ciphertext block.

Inside that box, the key is first expanded into several round keys; the Cipher() routine then applies a sequence of transformations using each round key to produce the ciphertext. The inverse process, InvCipher(), reverses those steps to recover the original plaintext.

That’s all there is to AES, encrypting and decrypting fixed‑size blocks of 128 bits.

To handle longer messages we discussed padding schemes, three block‑cipher modes (ECB, CBC, CTR), and that you can generate a secure key with PBKDF2.

I hope you found this article as enjoyable to read as it was for me to write. As always, use approved standard libraries for encryption and store your keys securely.