AES Algorithm for beginners
Source: Dev.to
Overview
AES is a symmetric‑key encryption algorithm that is considered extremely secure, very easy to implement, and used in the real world.
This guide teaches you how to implement its 128‑bit CTR mode variant in C.
The procedure:
- Randomly generate a 128‑bit cipher key.
- Secretly share it with your friend (e.g., via Diffie‑Hellman – not covered here).
- Encrypt your message with the key.
- Send the encrypted message.
- Your friend decrypts it with the same key.
Test data is available at YouTube.
Implementing AES128
The core function encrypts a single 16‑byte block using a 16‑byte key:
void AES128( uint8_t* block, uint8_t* cipherKey) { }
It is a pipeline of several helper functions that operate on the block sequentially.

The 11 “Apply Key” steps each use a different round key, derived from the main cipher key via getNthRoundKey.
The second half of the guide defines higher‑level helpers that repeatedly call AES128:
uint8_t* encrypt(uint8_t* plainText, uint64_t lengthInBytes, uint8_t* cipherKey) { }
uint8_t* decrypt(uint8_t* encryptedText, uint64_t lengthInBytes, uint8_t* cipherKey) { }
The Block
Blocks are 4 × 4 matrices of bytes (0‑255). In C they are stored as a flat array uint8_t[16] (stack) or uint8_t* (heap). Example block: {0,1,2,…,15}.

Note: The block is stored column‑major (the second element is below the first).
Step 1 – Print a block
void printBlock(uint8_t* block) { }
Step 2 – Set an element
void setElement(uint8_t* block, int nthRow, int nthCol, uint8_t element) { }
Rows and columns are flat arrays of four bytes. The first element of a row is the leftmost; the first element of a column is the topmost.
Step 3 – Extract a row / column
uint8_t* getRow(uint8_t* block, int nthRow) {
// malloc storage for a new row
// copy values from block to the new storage
// return the newly allocated storage
}
uint8_t* getCol(uint8_t* block, int nthCol) {
// same as above for a column
}
Step 4 – Print a row / column
void printRow(uint8_t* row) { }
void printCol(uint8_t* col) { }
Important: Functions that allocate memory (
getRow,getCol) must have their returned pointers freed after use.
Step 5 – Rotate operations

void rotateLeftRow(uint8_t* row) { }
void rotateUpCol(uint8_t* col) { }
// Operate in‑place; do not allocate new memory.
Step 6 – Write a row / column back into a block
void setRow(uint8_t* block, uint8_t* row, int nthRow) {
// copy row into the block (no allocations)
}
void setCol(uint8_t* block, uint8_t* col, int nthCol) {
// copy column into the block (no allocations)
}
These primitives form the basis of the AES‑128 pipeline; verify them before proceeding.
subBytes Function
The S‑box constants (index 0 → 255) are:
{99,124,119,123,242,107,111,197,48,1,103,43,254,215,171,118,
202,130,201,125,250,89,71,240,173,212,162,175,156,164,114,192,
183,253,147,38,54,63,247,204,52,165,229,241,113,216,49,21,
4,199,35,195,24,150,5,154,7,18,128,226,235,39,178,117,
9,131,44,26,27,110,90,160,82,59,214,179,41,227,47,132,
83,209,0,237,32,252,177,91,106,203,190,57,74,76,88,207,
208,239,170,251,67,77,51,133,69,249,2,127,80,60,159,168,
81,163,64,143,146,157,56,245,188,182,218,33,16,255,243,210,
205,12,19,236,95,151,68,23,196,167,126,61,100,93,25,115,
96,129,79,220,34,42,144,136,70,238,184,20,222,94,11,219,
224,50,58,10,73,6,36,92,194,211,172,98,145,149,228,121,
231,200,55,109,141,213,78,169,108,86,244,234,101,122,174,8,
186,120,37,46,28,166,180,198,232,221,116,31,75,189,139,138,
112,62,181,102,72,3,246,14,97,53,87,185,134,193,29,158,
225,248,152,17,105,217,142,148,155,30,135,233,206,85,40,223,
140,161,137,13,191,230,66,104,65,153,45,15,176,84,187,22}
Step 7 – S‑box accessor
uint8_t sbox(uint8_t i) { }
Step 8 – Apply S‑box to a state
void subBytes(uint8_t* state) {
// replace each byte with sbox(byte)
// operate in‑place, no allocations
}
shiftRows Function

Step 9 – Implement shiftRows
void shiftRows(uint8_t* state) {
// For each row:
// 1. getRow()
// 2. rotateLeftRow() the appropriate number of positions
// 3. setRow()
// Free any allocated rows.
}
mixColumns Function
First we need a Galois‑field multiplication helper. Pre‑computed tables are provided.
Tables
Table for b = 1 – values 0‑255 in order.
Table for b = 2 – see original content.
Table for b = 3 – see original content.
Step 10 – gfmul
uint8_t gfmul(uint8_t a, uint8_t b) {
// a ∈ [0,255], b ∈ {1,2,3}
// return the corresponding entry from the appropriate table
}
Step 11 – mixColumns

void mixColumns(uint8_t* state) {
// 1. Allocate a temporary 4×4 block for the output.
// 2. For each output element (row r, col c):
// - fetch row r of the multiplication matrix,
// - fetch column c of the input state,
// - compute Σ gfmul(matrix_elem, state_elem) (xor the results),
// - store in the temporary block.
// 3. Copy the temporary block back into `state`.
// 4. Free the temporary block.
}
useRoundKey Function
Step 12 – XOR state with round key
void useRoundKey(uint8_t* state, uint8_t* roundKey) {
// for i = 0..15: state[i] ^= roundKey[i];
}
Key Expansion
The 11 round keys are 4 × 4 blocks derived from the original cipher key.

Columns are denoted w0, w1, …, w43.
Round key 0 is a direct copy of the cipher key.
Step 13 – Get a column of a round key
uint8_t* getNthColOfRoundKey(int nthCol, uint8_t* cipherKey) {
uint8_t rcon[] = {1,2,4,8,16,32,64,128,27,54};
if (/* nthCol is 0‑3 */) {
// return the corresponding column from cipherKey using getCol()
} else if (/* nthCol ≡ 1 (mod 4) */) {
// allocate new column
// col = getNthColOfRoundKey(nthCol-1) XOR getNthColOfRoundKey(nthCol-4)
// return col
} else {
// allocate new column
// tmp = getNthColOfRoundKey(nthCol-1)
// rotateUpCol(tmp)
// col = tmp XOR getNthColOfRoundKey(nthCol-4)
// col[0] ^= rcon[nthCol/4 - 1]; // apply rcon
// return col
}
}
Step 14 – Get an entire round key
uint8_t* getNthRoundKey(int n, uint8_t* cipherKey) {
// allocate 16‑byte block
// for each column i = 0..3:
// copy getNthColOfRoundKey(4*n + i) into the block
// return the block
}
Step 15 – Complete AES128
void AES128(uint8_t* block, uint8_t* cipherKey) {
// 1. Initial AddRoundKey (round 0)
// 2. For rounds 1‑9:
// subBytes()
// shiftRows()
// mixColumns()
// useRoundKey() with roundKey = getNthRoundKey(round, cipherKey)
// 3. Final round (10):
// subBytes()
// shiftRows()
// useRoundKey() with roundKey = getNthRoundKey(10, cipherKey)
// Free any allocated round keys.
}
Turning Cipher Key into a Keystream (CTR Mode)

Encryption steps:
- Generate a random 64‑bit nonce.
- Split the plaintext into 16‑byte blocks.
- For each block
i, build a 16‑byte keystream:- first 8 bytes = nonce
- last 8 bytes =
i(as 8‑byte little‑endian) - run
AES128on this keystream with the cipher key.
- XOR the keystream with the plaintext block.
- Append the nonce to the ciphertext.
Decryption mirrors the process, extracting the nonce from the last 8 bytes.
Step 16 – Extract a byte from a 64‑bit number
uint8_t getNthByte(uint64_t number, int n) {
// returns the n‑th least‑significant byte (0‑based)
return (number >> (n * 8)) & 0xFF;
}
Step 17 – Generate a keystream for a block
uint8_t* getKeystream(uint8_t* cipherKey, uint64_t nthBlock, uint8_t* nonce) {
// allocate 16‑byte array `keystream`
// copy nonce (8 bytes) into keystream[0..7]
// convert nthBlock to 8‑byte array and copy into keystream[8..15]
// AES128(keystream, cipherKey);
// return keystream;
}
Step 18 – Encrypt a message
uint8_t* encrypt(uint8_t* plainText, uint64_t lengthInBytes, uint8_t* cipherKey) {
// 1. allocate 8‑byte nonce and fill with random data
// 2. allocate output buffer of size lengthInBytes + 8 (nonce)
// 3. for each 16‑byte block:
// keystream = getKeystream(cipherKey, blockIndex, nonce);
// xor plaintext block with keystream → ciphertext block
// free keystream
// 4. copy nonce to the last 8 bytes of the output
// 5. return the output buffer
}
Step 19 – Decrypt a message
uint8_t* decrypt(uint8_t* encryptedText, uint64_t lengthInBytes, uint8_t* cipherKey) {
// 1. allocate 8‑byte nonce
// 2. copy the last 8 bytes of encryptedText into nonce
// 3. allocate output buffer of size lengthInBytes - 8
// 4. for each 16‑byte block:
// keystream = getKeystream(cipherKey, blockIndex, nonce);
// xor ciphertext block with keystream → plaintext block
// free keystream
// 5. return the plaintext buffer
}
Step 20 – (Optional) File‑based utility
Adapt the program to read a plaintext file, write the encrypted output (including the nonce), and vice‑versa for decryption.