Definition
and denote the ciphertext and plaintext blocks, respectively.
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:
The round functions execute identical transformations parameterized by different round keys (), 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 -bit input to an -bit output (typically 4-bit or 8-bit lookup tables). For example, given a 4-bit S-box mapping , 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:
Feistel Schemes
- Split the block into halves: .
- For round : 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 byte matrix) with key lengths of 128, 192, or 256 bits (corresponding to 10, 12, and 14 rounds, respectively).

Each standard round consists of four transformations:
- AddRoundKey: XORs a round key to .
- SubBytes: Replaces each byte according to S-box.
- ShiftRows: Cyclically shifts row to the left by bytes ().

- MixColumn: Multiplies each column by a fixed Maximum Distance Separable (MDS) matrix over .
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 into independent -bit blocks and encrypting each block separately with the exact same secret key :

ECB mode is insecure because identical plaintext blocks produce identical ciphertext blocks, leaking structural patterns.
Cipher Block Chaining Mode (CBC)
This initial value of ciphertext is random.

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.
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:
- Decrypt all the blocks as with unpadded CBC.
- 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 bytes, and suppose the message has full blocks plus one final partial block of length bytes (for example, bytes).
- Encrypt and normally using standard CBC chaining to compute the intermediate block:
- Construct a synthetic 16-byte plaintext block by concatenating the actual partial plaintext with the stolen tail bytes:
- Encrypt using standard CBC chained with :
- The last ciphertext block takes the unused head bytes from step 2:
- The output transmitted is .
This trick is more complicated but offers a few benefits:
- Plaintexts can be of any bit length not just bytes.
- Ciphertexts are exactly the same length as plaintexts.
- 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 () with the plaintext.

- Each input block to the cipher function is constructed by concatenating a Nonce () and an incrementing Counter ():
- Keystream Generation:
- Encryption:
- Decryption bitwise XOR is self-inverting (), decryption uses the exact same keystream and the forward encryption function :
CTR can be faster than any other mode.
Cryptanalysis & Attacks
Meet-in-the-Middle Attacks (MitM)

-
The attacker takes a known plaintext .For every possible 56-bit candidate key , compute the intermediate value:
Store these computed values in a lookup table (hash map) where the key is the 64-bit intermediate state and the value is the candidate key :
This phase requires encryptions and stores entries in memory.
-
The attacker takes the corresponding known ciphertext .For every possible 56-bit candidate key , compute the decrypted middle value:
Check if exists as a key in .If a match occurs (), the pair satisfies and becomes a candidate key pair.
-
Because the DES block size is 64 bits and the combined key space is 112 bits, there will be approximately false-positive key pairs that satisfy this equation for a single pair.To identify the correct , test the candidate pairs against 1 or 2 additional known plaintext-ciphertext pairs until only 1 pair remains.
Padding Oracle Attacks
This attack often happens to CBC padding cipher.

For example, if the attacker want to decrypt . Let .
- Pick a random block and vary its last byte until the padding oracle accepts the ciphertext as valid. Usually, in a valid ciphertext, , so you’ll find after trying around 128 values of .
- Find the value by setting to and searching for the that gives correct padding. When the oracle accepts the ciphertext as valid, it means you’ve found such that .
- Repeat steps 1 and 2 for all 16 bytes.