Definition

CC and PP denote the ciphertext and plaintext blocks, respectively.

C=E(K,P),P=D(K,C)C = \mathbf{E}(K, P), \quad P = \mathbf{D}(K, C)

Block sizes are typically 64 bits (e.g., DES) or 128 bits (e.g., AES).

Construction

Rounds

A typical block cipher iterates a sequence of rounds:

C=R3(R2(R1(P)),P=iR3(iR2(iR1(C))C = \mathbf{R_3}(\mathbf{R_2}(\mathbf{R_1}(P)), \quad P = \mathbf{iR_3}(\mathbf{iR_2}(\mathbf{iR_1}(C))

The round functions execute identical transformations parameterized by different round keys (KiK_i), which breaks symmetry to resist slide attacks.

Substitution-Permutation Networks (SPNs)

An SPN is an iterated cipher architecture that maps an input block through alternating rounds of substitution and permutation layers.

Substitution Layer (S-box) provides non-linear confusion by mapping an mm-bit input to an nn-bit output (typically 4-bit or 8-bit lookup tables). For example, given a 4-bit S-box mapping S=[3,8,F,1,A,6,5,B,E,D,4,2,7,0,9,C]\text{S} = [3, 8, \text{F}, 1, \text{A}, 6, 5, \text{B}, \text{E}, \text{D}, 4, 2, 7, 0, 9, \text{C}], input 0000 maps to 3 (0011), and input 0101 (5) maps to 6 (0110).

Permutation provides linear diffusion by dispersing the output bits across the input of subsequent S-boxes. In modern byte-oriented ciphers, this is implemented as a matrix multiplication over a finite field:

[abcd]=M[abcd]over GF(28)\begin{bmatrix} a' \\ b' \\ c' \\ d' \end{bmatrix} = \mathbf{M} \cdot \begin{bmatrix} a \\ b \\ c \\ d \end{bmatrix} \quad \text{over } \text{GF}(2^8)

Feistel Schemes

  1. Split the block into halves: P=L0R0P = L_0 \mathbin{\Vert} R_0.
  2. For round i=1ni = 1 \dots n: Li=Ri1L_i = R_{i-1} R_i = L_{i-1} \oplus \mathbf{F}(R_{i-1}, K_i) 3. The final ciphertext is $C = R_n \mathbin{\Vert} L_n$.

The Advanced Encryption Standard (AES)

AES Internals

AES operates on a 128-bit block (structured as a 4×44 \times 4 byte matrix) with key lengths of 128, 192, or 256 bits (corresponding to 10, 12, and 14 rounds, respectively).

aes-internal-state-2d

Each standard round consists of four transformations:

  1. AddRoundKey: XORs a round key KiK_i to ss.
  2. SubBytes: Replaces each byte according to S-box.
  3. ShiftRows: Cyclically shifts row ii to the left by ii bytes (i{0,1,2,3}i \in \{0, 1, 2, 3\}).
    shiftrows
  4. MixColumn: Multiplies each column by a fixed Maximum Distance Separable (MDS) matrix over GF(28)\text{GF}(2^8). [s0s1s2s3]=M[s0s1s2s3]over GF(28)\begin{bmatrix} s_0'\\ s_1'\\ s_2'\\ s_3' \end{bmatrix} = M\cdot \begin{bmatrix} s_0\\ s_1\\ s_2\\ s_3 \end{bmatrix} \quad \text{over } \text{GF}(2^8)

Notice that the final round omits MixColumns to ensure the decryption algorithm maintains structural symmetry with the encryption algorithm.

AES in Action

Following is an example in Python:

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from os import urandom

BLOCK_SIZE = 16
KEY_SIZE = 16

# Pick a random 16-byte key using Python's crypto PRNG.
k = urandom(KEY_SIZE)
print(f"k = {k.hex()}")

# Create an instance of AES-128.
aes = Cipher(algorithms.AES(k), modes.ECB())
aes_ecb_encryptor = aes.encryptor()

# Set plaintext p to the all-zero string.
p = bytes([0x00] * BLOCK_SIZE)

# Encrypt plaintext p to ciphertext c.
c = aes_ecb_encryptor.update(p) + aes_ecb_encryptor.finalize()
print(f"enc({p.hex()}) = {c.hex()}")

# Decrypt ciphertext c to plaintext p.
aes_ecb_decryptor = aes.decryptor()
p = aes_ecb_decryptor.update(c) + aes_ecb_decryptor.finalize()
print(f"dec({c.hex()}) = {p.hex()}")

And the Go version:

package main

import (
	"crypto/aes"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
)

const (
	BlockSize = 16
	KeySize   = 16
)

func main() {
	// Pick a random 16-byte key using Go's crypto PRNG.
	k := make([]byte, KeySize)
	if _, err := io.ReadFull(rand.Reader, k); err != nil {
		panic(err)
	}
	fmt.Printf("k = %s\n", hex.EncodeToString(k))

	// Create an instance of AES-128 (cipher.Block provides raw single-block ECB primitives).
	block, err := aes.NewCipher(k)
	if err != nil {
		panic(err)
	}

	// Set plaintext p to the all-zero byte slice.
	p := make([]byte, BlockSize)

	// Encrypt plaintext p to ciphertext c.
	c := make([]byte, BlockSize)
	block.Encrypt(c, p)
	fmt.Printf("enc(%s) = %s\n", hex.EncodeToString(p), hex.EncodeToString(c))

	// Decrypt ciphertext c to plaintext decryptedP.
	decryptedP := make([]byte, BlockSize)
	block.Decrypt(decryptedP, c)
	fmt.Printf("dec(%s) = %s\n", hex.EncodeToString(c), hex.EncodeToString(decryptedP))
}

Modes of Operation

Electronic Codebook Mode (ECB)

ECB encrypts multi-block data by splitting the plaintext PP into independent bb-bit blocks (P1,P2,,Pm)(P_1, P_2, \dots, P_m) and encrypting each block separately with the exact same secret key KK:

Ci=E(K,Pi)for each block i{1,,m}C_i = \mathbf{E}(K,P_i) \quad \text{for each block } i \in \{1, \dots, m\}

ecb-mode

ECB mode is insecure because identical plaintext blocks produce identical ciphertext blocks, leaking structural patterns.

Cipher Block Chaining Mode (CBC)

Ci=E(K,PiCi1)C_i=\mathbf{E}(K,P_i\oplus C_{i-1})

This initial value of ciphertext is random.

cbc-mode

In CBC mode, decryption needs to know the IV used to encrypt, so the IV is sent along with the ciphertext.

With CBC, decryption can be much faster than encryption due to parallelism.

Pi=D(K,Ci)Ci1P_i = \mathbf{D}(K, C_i)\oplus C_{i-1}

Message Encryption

Message Padding

We use padding to expand a message to fill a complete block by adding extra bytes to the plaintext. Here are the rules for padding 16-byte blocks:

  • 1 byte remaining: append 0x0F repeated 15 times.
  • 2 bytes remaining: append 0x0E repeated 14 times.
  • 15 bytes remaining: append 0x01 once.
  • 0 bytes remaining (exact multiple): append a full block of 0x10 (16 in decimal) repeated 16 times.

The decryption steps are as following:

  1. Decrypt all the blocks as with unpadded CBC.
  2. Make sure that the last bytes of the last block conform to the padding rule: that they finish with at least one 01 byte, at least two 02 bytes, or at least three 03 bytes, and so on. If the padding isn’t valid—for example, if the last bytes are 01 02 03—the message is rejected. Otherwise, decryption strips the padding bytes and returns the plaintext bytes left.
Ciphertext Stealing

CTS “steals” ciphertext bytes from the second-to-last block to fill out the last block, then swaps the order of the final blocks so the total ciphertext length matches the exact length of the original plaintext.

Let the block size be B=16B = 16 bytes, and suppose the message has MM full blocks plus one final partial block P3P_3 of length L<16L < 16 bytes (for example, L=4L = 4 bytes).

  1. Encrypt and normally using standard CBC chaining to compute the intermediate block:
Cprev=EK(P2C1)C'_{\text{prev}} = E_K(P_2 \oplus C_1)
  1. Construct a synthetic 16-byte plaintext block P3P^*_3 by concatenating the actual partial plaintext P3P_3 with the stolen tail bytes:
P3=P3Cprev[L15]P^*_3 = P_3 \mathbin{\Vert} C'_{\text{prev}}[L \dots 15]
  1. Encrypt P3P^*_3 using standard CBC chained with CprevC'_{\text{prev}}:
C2=EK(P3Cprev)C_2 = E_K(P^*_3 \oplus C'_{\text{prev}})
  1. The last ciphertext block C3C_3 takes the unused head bytes from step 2:
C3=Cprev[0L1]C_3 = C'_{\text{prev}}[0 \dots L-1]
  1. The output transmitted is C1C2C3C_1 \mathbin{\Vert} C_2 \mathbin{\Vert} C_3.

This trick is more complicated but offers a few benefits:

  1. Plaintexts can be of any bit length not just bytes.
  2. Ciphertexts are exactly the same length as plaintexts.
  3. It’s not vulnerable to padding oracle attacks.

Counter Mode (CTR)

CTR encrypts a sequential series of counter blocks to produce a pseudorandom keystream, which is then XORed (\oplus) with the plaintext.

ctr-mode

  1. Each input block to the cipher function EKE_K is constructed by concatenating a Nonce (NN) and an incrementing Counter (CtrCtr):
Blocki=N(Ctr+i1)\text{Block}_i = N \mathbin{\Vert} (Ctr + i - 1)
  1. Keystream Generation:
Si=EK(N(Ctr+i1))S_i = E_K(N \mathbin{\Vert} (Ctr + i - 1))
  1. Encryption:
Ci=PiSi=PiEK(N(Ctr+i1))C_i = P_i \oplus S_i = P_i \oplus E_K(N \mathbin{\Vert} (Ctr + i - 1))
  1. Decryption
    bitwise XOR is self-inverting ((PS)S=P(P \oplus S) \oplus S = P), decryption uses the exact same keystream and the forward encryption function EKE_K:
Pi=CiSi=CiEK(N(Ctr+i1))P_i = C_i \oplus S_i = C_i \oplus E_K(N \mathbin{\Vert} (Ctr + i - 1))

CTR can be faster than any other mode.

Cryptanalysis & Attacks

Meet-in-the-Middle Attacks (MitM)

meet-in-the-middle-attacks

  1. The attacker takes a known plaintext PP.For every possible 56-bit candidate key k{0,1}56k \in \{0, 1\}^{56}, compute the intermediate value:

    Mk=E(k,P)M_k = E(k, P)

    Store these computed values in a lookup table (hash map) where the key is the 64-bit intermediate state MkM_k and the value is the candidate key kk:

    Table[Mk]=k\text{Table}[M_k] = k

    This phase requires 2562^{56} encryptions and stores 2562^{56} entries in memory.

  2. The attacker takes the corresponding known ciphertext CC.For every possible 56-bit candidate key k{0,1}56k' \in \{0, 1\}^{56}, compute the decrypted middle value:

    Mk=D(k,C)M'_{k'} = D(k', C)

    Check if MkM'_{k'} exists as a key in Table\text{Table}.If a match occurs (Mk==MkM'_{k'} == M_k), the pair (K1=Table[Mk],K2=k)(K_1 = \text{Table}[M'_{k'}], K_2 = k') satisfies E(K1,P)=D(K2,C)E(K_1, P) = D(K_2, C) and becomes a candidate key pair.

  3. Because the DES block size is 64 bits and the combined key space is 112 bits, there will be approximately 211264=2482^{112 - 64} = 2^{48} false-positive key pairs that satisfy this equation for a single (P,C)(P, C) pair.To identify the correct (K1,K2)(K_1, K_2), test the candidate pairs against 1 or 2 additional known plaintext-ciphertext pairs (P2,C2)(P_2, C_2) until only 1 pair remains.

Padding Oracle Attacks

This attack often happens to CBC padding cipher.

padding-orable-attacks

For example, if the attacker want to decrypt C2C_2. Let X=D(K,C2)X=\mathbf{D}(K, C_2).

  1. Pick a random block C1C_1 and vary its last byte until the padding oracle accepts the ciphertext as valid. Usually, in a valid ciphertext, C1[15]X[15]=01C_1[15] \oplus X[15] = 01, so you’ll find X[15]X[15] after trying around 128 values of C1[15]C_1[15].
  2. Find the value X[14]X[14] by setting C1[15]C_1[15] to X[15]02X[15] \oplus 02 and searching for the C1[14]C_1[14] that gives correct padding. When the oracle accepts the ciphertext as valid, it means you’ve found C1[14]C_1[14] such that C1[14]X[14]=02C_1[14] \oplus X[14] = 02.
  3. Repeat steps 1 and 2 for all 16 bytes.