Introduction

Welcome to the
Crypto Workbook

This workbook guides you through the CryptoPals challenge series and a live classroom CTF. You will implement attacks, not just read about them — from XOR ciphers to breaking CBC-mode encryption in a live network exercise.

Google Colab — chapters 1–7
Linux terminal — all chapters + CTF
How to use this workbook

Each chapter shows which environment to use. Colab is great for learning the crypto algorithms (no setup, runs in a browser). The Linux terminal is required for the CTF server/client, Wireshark, and network exercises. Most coding chapters work in both — platform tabs show you the right version for each.

What is CryptoPals?

CryptoPals (cryptopals.com) is a set of 64 cryptographic programming challenges written by ex-Matasano Security engineers. Unlike textbooks, it teaches crypto by making you break things: implement a cipher, then attack it. This workbook covers Set 1 (XOR, frequency analysis, ECB) and Set 2 (PKCS#7, CBC mode, bitflipping attacks), then connects those skills directly to the live Classroom CTF.

Environment overview

EnvironmentBest forSetup needed
Google ColabAlgorithm implementation, learning crypto concepts, Set 1 & Set 2 challengesGoogle account only — runs in browser
Linux terminalCTF server/client, Wireshark/tcpdump, socket programming, full pipelinePython 3 + pycryptodome + Wireshark

Core vocabulary

TermPlain-English meaning
plaintextThe original readable message before encryption
ciphertextThe scrambled output after encryption
keyThe secret value used to encrypt or decrypt
XOR (⊕)Bitwise exclusive-or — the foundation of stream ciphers
block cipherEncrypts fixed-size chunks (AES = 16 bytes per block)
mode of operationHow a block cipher handles data longer than one block (ECB, CBC, CTR…)
IVInitialization vector — random bytes that seed CBC mode
PKCS#7A padding scheme that pads plaintext to the next block boundary
Environment setup

Linux quickstart
& auto-setup script

Run the script below once on any Debian/Ubuntu Linux machine and your entire CryptoPals + CTF environment will be ready: Python dependencies, Wireshark, tcpdump, netcat, tmux, and a project folder structure.

Linux terminal only
Before you run

This script uses sudo for system packages. Run it on your own machine or a dedicated lab VM — not a shared production server. Tested on Ubuntu 22.04 and Debian 12.

One-shot setup script

Copy the entire block below into a file called setup_crypto_lab.sh, then run it. Every step is annotated so you know exactly what is being installed and why.

bash — setup_crypto_lab.sh
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────
# CryptoPals + CTF Lab — Auto-setup script
# Tested on Ubuntu 22.04 / Debian 12
# Run with: bash setup_crypto_lab.sh
# ─────────────────────────────────────────────────────────────────
set -e   # exit immediately if any command fails

echo "[1/7] Updating apt package lists..."
sudo apt-get update -q

echo "[2/7] Installing system tools..."
sudo apt-get install -y \
  python3 python3-pip \          # Python runtime + pip
  wireshark \                    # GUI packet capture (for PCAP analysis)
  tshark \                       # CLI version of Wireshark (scriptable)
  tcpdump \                      # Raw packet capture to .pcap files
  netcat-openbsd \               # nc — test TCP connections, simple clients
  tmux \                         # Split one terminal into multiple panes
  net-tools \                    # ifconfig, netstat (older but useful)
  curl wget                      # Download challenge input files

echo "[3/7] Allowing Wireshark capture without sudo..."
# Add current user to the 'wireshark' group so you don't need sudo for captures
sudo usermod -aG wireshark "$USER"

echo "[4/7] Installing Python crypto libraries..."
pip3 install --user \
  pycryptodome \                 # AES, RSA, SHA — used in chapters 6-10
  requests \                     # Download challenge files over HTTP
  scapy                          # Advanced packet crafting (optional, CTF bonus)

echo "[5/7] Creating project folder structure..."
mkdir -p ~/cryptopals/{set1,set2,ctf,pcaps,utils}
# set1/  → challenges 1-8   (XOR, frequency, ECB)
# set2/  → challenges 9-16  (PKCS7, CBC, bitflip)
# ctf/   → server.py, client.py, attacker.py
# pcaps/ → captured .pcap files from Wireshark/tcpdump
# utils/ → shared helpers (xor, padding, scoring)

echo "[6/7] Writing shared utility module..."
cat > ~/cryptopals/utils/crypto_helpers.py << 'PYEOF'
# ── crypto_helpers.py ──────────────────────────────────────────
# Import this in every challenge script:
#   import sys, os; sys.path.insert(0, os.path.expanduser("~/cryptopals/utils"))
#   from crypto_helpers import *

def xor_bytes(a: bytes, b: bytes) -> bytes:
    return bytes(x ^ y for x, y in zip(a, b))

def score_english(text: bytes) -> float:
    freq = {32:13,101:12.7,116:9.1,97:8.2,111:7.5,105:7,110:6.7,
            115:6.3,104:6.1,114:6,100:4.3,108:4,99:2.8,117:2.8}
    lower = text.lower()
    score = sum(lower.count(b) * w for b, w in freq.items())
    printable = sum(1 for b in text if 32 <= b < 127)
    score += printable * 0.5
    return score

def break_single_xor(ct: bytes):
    best = (-1, 0, b"")
    for k in range(256):
        plain = bytes(b ^ k for b in ct)
        s = score_english(plain)
        if s > best[0]:
            best = (s, k, plain)
    return best[1], best[2]

def pkcs7_pad(data: bytes, bs: int = 16) -> bytes:
    n = bs - (len(data) % bs)
    return data + bytes([n] * n)

def pkcs7_unpad(data: bytes) -> bytes:
    n = data[-1]
    if n == 0 or n > 16 or data[-n:] != bytes([n]*n):
        raise ValueError("Bad padding")
    return data[:-n]

def hamming_distance(a: bytes, b: bytes) -> int:
    return sum(bin(x ^ y).count("1") for x, y in zip(a, b))
PYEOF

echo "[7/7] Downloading Set 1 challenge data files..."
# Challenge 6 (break repeating-key XOR)
curl -s https://cryptopals.com/static/challenge-data/6.txt \
     -o ~/cryptopals/set1/challenge6.txt
# Challenge 7 (AES-128-ECB decrypt)
curl -s https://cryptopals.com/static/challenge-data/7.txt \
     -o ~/cryptopals/set1/challenge7.txt
# Challenge 8 (detect ECB mode)
curl -s https://cryptopals.com/static/challenge-data/8.txt \
     -o ~/cryptopals/set1/challenge8.txt
# Challenge 10 (CBC decryption)
curl -s https://cryptopals.com/static/challenge-data/10.txt \
     -o ~/cryptopals/set2/challenge10.txt

echo ""
echo "✓ Setup complete! Log out and back in for Wireshark group to take effect."
echo "  Project folder: ~/cryptopals/"
echo "  Start a tmux session: tmux new -s crypto"

Running the script

bash — terminal
# Step 1: Create the script file
nano setup_crypto_lab.sh
# Paste the script above, then Ctrl+O to save, Ctrl+X to exit

# Step 2: Make it executable
chmod +x setup_crypto_lab.sh

# Step 3: Run it
bash setup_crypto_lab.sh

# Step 4: Log out and back in (needed for Wireshark group permissions)
# Or apply group without logging out:
newgrp wireshark

Tmux cheatsheet — working with split terminals

Tmux lets you run the server in one pane and the client in another — essential for the CTF chapters.

bash — tmux commands
tmux new -s crypto          # create a new session named "crypto"
Ctrl+B then %              # split vertically (left/right panes)
Ctrl+B then "              # split horizontally (top/bottom panes)
Ctrl+B then ← → ↑ ↓       # move between panes
Ctrl+B then z              # zoom current pane to full screen (toggle)
Ctrl+B then d              # detach session (keeps it running)
tmux attach -t crypto      # re-attach to the session later
exit                       # close a pane

Useful network commands

bash — networking
# Find your LAN IP address (share with teammates)
ip a | grep "inet " | grep -v "127.0.0.1"
# or: hostname -I

# Check if a port is in use
ss -tlnp | grep 9999

# Test connectivity to a teammate's server
nc -zv 192.168.1.42 9999

# Kill a process holding a port
fuser -k 9999/tcp

# Capture all CTF traffic and save to file
sudo tcpdump -i eth0 -w ~/cryptopals/pcaps/capture.pcap port 9999

# Read a pcap in hex+ASCII (no Wireshark needed)
tcpdump -r ~/cryptopals/pcaps/capture.pcap -XX | head -60
CryptoPals — Set 1, Challenge 3

Single-byte XOR
cipher

A single-byte XOR cipher XORs every byte of a message against the same one-byte key. Only 256 possible keys exist — try all of them, score each result against English letter frequencies, and the highest scorer is your plaintext.

Google Colab supported
Linux terminal supported

How XOR encryption works

Key property

plaintext ⊕ key = ciphertext
ciphertext ⊕ key = plaintext
XOR is its own inverse — applying it twice with the same key cancels out.

Colab — getting started

Go to colab.research.google.com → New notebook. Create one cell per section below. No installs needed for this chapter — it's pure Python.

python — Colab Cell 1: setup & helpers
# ── Cell 1: Shared helpers (run this first in every Set 1 notebook) ──
# No pip installs needed — pure Python standard library

def score_english(text: bytes) -> float:
    """Score bytes on how English-like they look.
    Higher = more likely to be English plaintext."""
    freq = {
        32: 13.0,   # space — most frequent character in English text
        101: 12.7,  # e
        116: 9.1,   # t
        97:  8.2,   # a
        111: 7.5,   # o
        105: 7.0,   # i
        110: 6.7,   # n
        115: 6.3,   # s
        104: 6.1,   # h
        114: 6.0,   # r
    }
    lower = text.lower()
    score = 0.0
    for byte_val, weight in freq.items():
        score += lower.count(byte_val) * weight
    # bonus for printable ASCII (penalises random-looking bytes)
    score += sum(0.5 for b in text if 32 <= b < 127)
    return score

def break_single_xor(ciphertext: bytes):
    """Try all 256 single-byte keys. Return (key, plaintext) with best score."""
    best_score = -1
    best_key   = 0
    best_plain = b""
    for key in range(256):
        candidate = bytes(b ^ key for b in ciphertext)
        s = score_english(candidate)
        if s > best_score:
            best_score = s
            best_key   = key
            best_plain = candidate
    return best_key, best_plain

print("✓ Helpers loaded")
python — Colab Cell 2: Challenge 3 solution
# ── Cell 2: CryptoPals Set 1 Challenge 3 ──
# The hex string below has been XOR-encrypted with a single byte.
# Find the key and recover the plaintext.

hex_string = "1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736"

# Step 1: decode hex → raw bytes
ciphertext = bytes.fromhex(hex_string)
print(f"Ciphertext length: {len(ciphertext)} bytes")
print(f"Ciphertext (hex):  {ciphertext.hex()}")

# Step 2: brute-force all 256 keys
key, plaintext = break_single_xor(ciphertext)

# Step 3: display result
print(f"\n✓ Key found:  0x{key:02x} = {key} = chr('{chr(key)}')")
print(f"✓ Plaintext:  {plaintext.decode(errors='replace')}")
Linux terminal — running the script

Save the script to ~/cryptopals/set1/challenge3.py then run python3 challenge3.py. The shared helpers from the setup script are imported from ~/cryptopals/utils/.

bash — create and run
cd ~/cryptopals/set1
nano challenge3.py     # paste the script below, save with Ctrl+O Ctrl+X
python3 challenge3.py
python — ~/cryptopals/set1/challenge3.py
#!/usr/bin/env python3
"""CryptoPals Set 1 Challenge 3 — Single-byte XOR cipher."""
import sys, os
sys.path.insert(0, os.path.expanduser("~/cryptopals/utils"))
from crypto_helpers import break_single_xor

hex_string = "1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736"
ciphertext = bytes.fromhex(hex_string)

key, plaintext = break_single_xor(ciphertext)
print(f"Key:       0x{key:02x} ({key})")
print(f"Plaintext: {plaintext.decode(errors='replace')}")

English letter frequencies

Interactive demo

Enter ciphertext above and click Crack ↓
CryptoPals Set 1, Challenge 3
Find the key, decrypt the message
Easy
The hex string 1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736 has been XORed against a single byte. Find the key and recover the plaintext.
Decode the hex string with bytes.fromhex() first. Then pass the result into break_single_xor(). The plaintext is a recognizable English sentence. You'll know it immediately when you see it.
Why is single-byte XOR trivially breakable even without knowing the key?
With a keyspace of only 256 values, brute force is trivial. The scoring function (letter frequency) distinguishes the correct decryption from the 255 wrong ones.
CryptoPals — Set 1, Challenge 5

Repeating-key XOR

Repeating-key XOR (Vigenère-like) cycles through a multi-byte key. Each plaintext byte is XORed against the corresponding key byte modulo the key length. Much harder to brute-force — but not unbreakable, as Chapter 4 shows.

Google Colab supported
Linux terminal supported
Formula

ciphertext[i] = plaintext[i] ⊕ key[i mod len(key)]

For key "ICE": byte 0 uses 'I', byte 1 uses 'C', byte 2 uses 'E', byte 3 uses 'I' again…

Colab — continue in the same notebook

Add new cells below your Cell 1 helpers from Challenge 3. The score_english helper is reused in later challenges.

python — Colab Cell: repeating-key XOR
# ── CryptoPals Set 1 Challenge 5 ──
# Implement repeating-key XOR and verify the output matches the expected hex.

def repeating_key_xor(data: bytes, key: bytes) -> bytes:
    """XOR each byte of data with the corresponding key byte (wrapping)."""
    return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))

# Challenge 5 test vector
plaintext = (
    b"Burning 'em, if you ain't quick and nimble\n"
    b"I go crazy when I hear a cymbal"
)
key = b"ICE"
ct  = repeating_key_xor(plaintext, key)

expected = (
    "0b3637272a2b2e63622c2e69692a23693a2a3c6324202d623d63343c2a26"
    "22632b204d621b69613b621c484f13"   # (first 60 chars shown)
)

print("Output (hex):", ct.hex())
print("Matches expected:", ct.hex().startswith(expected[:30]))
bash — create and run
cd ~/cryptopals/set1
nano challenge5.py
python3 challenge5.py
python — ~/cryptopals/set1/challenge5.py
#!/usr/bin/env python3
"""CryptoPals Set 1 Challenge 5 — Repeating-key XOR."""

def repeating_key_xor(data: bytes, key: bytes) -> bytes:
    return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))

plaintext = (
    b"Burning 'em, if you ain't quick and nimble\n"
    b"I go crazy when I hear a cymbal"
)
ct = repeating_key_xor(plaintext, b"ICE")
print(ct.hex())
CryptoPals Set 1, Challenge 5
Implement and verify repeating-key XOR
Easy
Implement repeating_key_xor(data, key). Verify your output matches the expected hex for the "Burning 'em" stanza encrypted with key ICE.
Use enumerate(data) to get both the index i and byte value b. Key index is i % len(key). Call .hex() on the result to get a hex string for comparison.
With a repeating key of length 6, which bytes of the ciphertext share the same key byte?
Position i uses key[i % 6]. So 0, 6, 12, 18 … all use key[0] — these form a "column" you can attack independently with frequency analysis once you know the key length.
CryptoPals — Set 1, Challenge 6

Breaking repeating-key XOR

The hardest challenge in Set 1. Use Hamming distance to guess the key length, then reduce the multi-byte key problem into several single-byte XOR problems you already know how to solve.

Google Colab supported
Linux terminal supported
Key insight — why Hamming distance works

When KEYSIZE equals the true key length, XORing two consecutive KEYSIZE-byte blocks cancels the key: (P1 ⊕ K) ⊕ (P2 ⊕ K) = P1 ⊕ P2. The Hamming distance of two English plaintext blocks is much lower than random bytes — so the correct key size produces the lowest normalized distance.

Colab — download challenge file in-notebook

Colab can fetch the challenge input directly over the internet — no manual file download needed.

python — Colab Cell 1: download data
# ── Download the challenge 6 ciphertext directly from CryptoPals ──
import urllib.request, base64

url  = "https://cryptopals.com/static/challenge-data/6.txt"
data = urllib.request.urlopen(url).read()
ciphertext = base64.b64decode(data)
print(f"Downloaded {len(ciphertext)} bytes of ciphertext")
python — Colab Cell 2: full break pipeline
# ── Full repeating-key XOR breaker ──
# Assumes score_english(), break_single_xor(), repeating_key_xor()
# are already defined from the Cell 1 helpers above.

def hamming_distance(a: bytes, b: bytes) -> int:
    """Count differing bits between two byte strings."""
    return sum(bin(x ^ y).count('1') for x, y in zip(a, b))

# Sanity check — should equal 37
assert hamming_distance(b"this is a test", b"wokka wokka!!!") == 37
print("✓ Hamming distance correct")

def guess_keysize(ct: bytes, lo=2, hi=40):
    """Return top-3 key length guesses by normalized Hamming distance."""
    scores = []
    for k in range(lo, hi + 1):
        blocks = [ct[i*k:(i+1)*k] for i in range(4)]
        # average over 3 block-pair comparisons for accuracy
        dist = (
            hamming_distance(blocks[0], blocks[1]) +
            hamming_distance(blocks[1], blocks[2]) +
            hamming_distance(blocks[2], blocks[3])
        ) / (3 * k)
        scores.append((dist, k))
    scores.sort()
    return [k for _, k in scores[:3]]

def break_repeating_xor(ct: bytes):
    """Full pipeline: guess key length → transpose → per-column solve."""
    best = None
    for k in guess_keysize(ct):
        # Slice into k columns: column i = bytes at positions i, i+k, i+2k …
        columns = [ct[i::k] for i in range(k)]
        key = bytes(break_single_xor(col)[0] for col in columns)
        plain = repeating_key_xor(ct, key)
        s = score_english(plain)
        if best is None or s > best[0]:
            best = (s, key, plain)
    return best[1], best[2]

# Run it on the downloaded ciphertext
key, plaintext = break_repeating_xor(ciphertext)
print(f"\n✓ Key: {key.decode(errors='replace')}")
print(f"\nFirst 200 chars of plaintext:\n{plaintext[:200].decode(errors='replace')}")
Linux — file already downloaded by setup script

The setup script downloaded challenge6.txt to ~/cryptopals/set1/. The script below reads it directly from disk.

bash
cd ~/cryptopals/set1
nano challenge6.py
python3 challenge6.py
python — ~/cryptopals/set1/challenge6.py
#!/usr/bin/env python3
"""CryptoPals Set 1 Challenge 6 — Break repeating-key XOR."""
import sys, os, base64
sys.path.insert(0, os.path.expanduser("~/cryptopals/utils"))
from crypto_helpers import score_english, break_single_xor, hamming_distance

def repeating_key_xor(data, key):
    return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))

def guess_keysize(ct, lo=2, hi=40):
    scores = []
    for k in range(lo, hi + 1):
        bl = [ct[i*k:(i+1)*k] for i in range(4)]
        d  = (hamming_distance(bl[0],bl[1])+hamming_distance(bl[1],bl[2])+hamming_distance(bl[2],bl[3]))/(3*k)
        scores.append((d, k))
    return [k for _,k in sorted(scores)[:3]]

# Read file downloaded by setup script
ct = base64.b64decode(open("challenge6.txt").read())

best = None
for k in guess_keysize(ct):
    cols  = [ct[i::k] for i in range(k)]
    key   = bytes(break_single_xor(c)[0] for c in cols)
    plain = repeating_key_xor(ct, key)
    s     = score_english(plain)
    if best is None or s > best[0]:
        best = (s, key, plain)

key, plaintext = best[1], best[2]
print(f"Key: {key.decode()}")
print(plaintext[:300].decode(errors='replace'))
CryptoPals Set 1, Challenge 6
Break repeating-key XOR on a real file
Medium
Run the full pipeline on the challenge 6 data. The key is a short English word — you'll recognise it. The plaintext is a famous piece of song lyrics.
If the top key-length guess gives garbage, try the 2nd or 3rd candidate. Average the Hamming distance over multiple block pairs (3 or more) for accuracy. The key length is between 2 and 40.
CryptoPals — Set 1, Challenge 8

ECB mode detection

AES-ECB encrypts each 16-byte block independently. Identical plaintext blocks produce identical ciphertext blocks — a fatal flaw. Detect ECB by looking for repeated 16-byte blocks in any ciphertext.

Google Colab supported
Linux terminal supported
ECB vs CBC — the key difference

ECB: each 16-byte block encrypted independently — same plaintext block always → same ciphertext block.
CBC: each block XORed with the previous ciphertext block before encrypting — identical plaintext blocks → different ciphertext.

Colab — install pycryptodome first

This chapter uses AES — run the install cell once at the top of your notebook before importing.

python — Colab Cell 1: install
# Run this cell once per Colab session.
# pycryptodome provides the AES cipher used from Chapter 5 onward.
!pip install pycryptodome -q
print("✓ pycryptodome ready")
python — Colab Cell 2: ECB detector + challenge 8
# ── CryptoPals Set 1 Challenge 8 — Detect AES-ECB ──
import urllib.request

def detect_ecb(ct: bytes, bs: int = 16) -> bool:
    """Return True if any 16-byte block is repeated — ECB signature."""
    blocks = [ct[i:i+bs] for i in range(0, len(ct), bs)]
    return len(blocks) != len(set(blocks))  # set removes duplicates

# Download the 60 ciphertexts
url   = "https://cryptopals.com/static/challenge-data/8.txt"
lines = urllib.request.urlopen(url).read().decode().strip().splitlines()

print(f"Scanning {len(lines)} ciphertexts...\n")
for i, line in enumerate(lines):
    ct = bytes.fromhex(line)
    if detect_ecb(ct):
        blocks = [ct[j:j+16] for j in range(0,len(ct),16)]
        dupes  = [b for b in set(blocks) if blocks.count(b) > 1]
        print(f"✓ Line {i}: ECB detected!")
        print(f"  Repeated block(s): {[d.hex() for d in dupes]}")
Linux — challenge file already on disk

The setup script downloaded challenge8.txt to ~/cryptopals/set1/.

bash
cd ~/cryptopals/set1
nano challenge8.py
python3 challenge8.py
python — ~/cryptopals/set1/challenge8.py
#!/usr/bin/env python3
"""CryptoPals Set 1 Challenge 8 — Detect AES-ECB."""

def detect_ecb(ct: bytes, bs=16) -> bool:
    blocks = [ct[i:i+bs] for i in range(0, len(ct), bs)]
    return len(blocks) != len(set(blocks))

lines = open("challenge8.txt").read().strip().splitlines()
for i, line in enumerate(lines):
    ct = bytes.fromhex(line)
    if detect_ecb(ct):
        blocks = [ct[j:j+16] for j in range(0,len(ct),16)]
        dupes  = [b for b in set(blocks) if blocks.count(b)>1]
        print(f"Line {i}: ECB detected! Repeated: {[d.hex() for d in dupes]}")
Never use ECB mode

Many libraries default to ECB — always specify the mode explicitly. Use CBC with a random IV, CTR, or an AEAD mode like AES-GCM for any real system.

An intercepted ciphertext contains a repeated 16-byte block. What does this immediately reveal?
In ECB: identical plaintext → identical ciphertext. A repeated ciphertext block leaks that the corresponding plaintext blocks are the same — significant information even without the key.
CryptoPals — Set 2, Challenge 9

PKCS#7 padding

Block ciphers require input to be a multiple of the block size. PKCS#7 is the standard: append N bytes each with value N to reach the next 16-byte boundary. If already aligned, add a full block of 0x10 bytes.

Google Colab supported
Linux terminal supported
Message lengthBytes to addPadding bytes appended
20 bytes120c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c 0c
13 bytes303 03 03
16 bytes1610 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10
python — Colab Cell: PKCS#7 pad & unpad
# ── CryptoPals Set 2 Challenge 9 — PKCS#7 padding ──

def pkcs7_pad(data: bytes, bs: int = 16) -> bytes:
    """Pad data to a multiple of bs bytes using PKCS#7."""
    n = bs - (len(data) % bs)   # n is always 1..bs, never 0
    return data + bytes([n] * n)

def pkcs7_unpad(data: bytes) -> bytes:
    """Strip PKCS#7 padding. Raises ValueError on invalid padding."""
    n = data[-1]
    if n == 0 or n > 16:
        raise ValueError("Invalid padding length")
    if data[-n:] != bytes([n] * n):
        raise ValueError("Padding bytes are inconsistent")
    return data[:-n]

# Challenge 9 example — pad "YELLOW SUBMARINE" to 20-byte block
result = pkcs7_pad(b"YELLOW SUBMARINE", 20)
print(f"Padded:   {result}")
print(f"Hex:      {result.hex()}")
print(f"Unpadded: {pkcs7_unpad(result)}")

# Show padding for various lengths
for length in [1, 13, 16, 20, 31, 32]:
    padded = pkcs7_pad(b"A" * length)
    n      = padded[-1]
    print(f"len={length:2d}  pad_bytes={n:2d}  total={len(padded)}")
bash
cd ~/cryptopals/set2
nano challenge9.py
python3 challenge9.py
python — ~/cryptopals/set2/challenge9.py
#!/usr/bin/env python3
"""CryptoPals Set 2 Challenge 9 — Implement PKCS#7 padding."""
import sys, os
sys.path.insert(0, os.path.expanduser("~/cryptopals/utils"))
from crypto_helpers import pkcs7_pad, pkcs7_unpad

result = pkcs7_pad(b"YELLOW SUBMARINE", 20)
print(f"Padded: {result}")
for l in [1,13,16,20,31,32]:
    p = pkcs7_pad(b"A"*l)
    print(f"len={l:2d}  pad={p[-1]:2d}  total={len(p)}")
Padding oracle attacks

If a server raises different errors for valid vs invalid padding — or takes different amounts of time — an attacker can decrypt any ciphertext byte by byte without knowing the key. This is covered in CryptoPals Set 3, Challenge 17.

CryptoPals — Set 2, Challenge 10

CBC mode & the IV

CBC fixes ECB's fatal flaw by XORing each plaintext block with the previous ciphertext block before encrypting. The first block uses a random Initialization Vector. Identical plaintexts now produce completely different ciphertexts.

Google Colab supported
Linux terminal supported
CBC Encryption — block chain
IV (random)
Plaintext[0]
AES_enc(key)
Ciphertext[0]
Ciphertext[0]
Plaintext[1]
AES_enc(key)
Ciphertext[1]
Colab — requires pycryptodome

Make sure you ran !pip install pycryptodome -q at the top of your notebook. We use AES.MODE_ECB as the raw block cipher — implementing the CBC chaining ourselves is the whole point of this challenge.

python — Colab Cell: CBC from scratch
# ── CryptoPals Set 2 Challenge 10 — Implement CBC mode ──
# Do NOT use AES.MODE_CBC — implement the chaining manually.
import urllib.request, base64
from Crypto.Cipher import AES

def xor_bytes(a: bytes, b: bytes) -> bytes:
    return bytes(x ^ y for x, y in zip(a, b))

def cbc_encrypt(plaintext: bytes, key: bytes, iv: bytes) -> bytes:
    aes  = AES.new(key, AES.MODE_ECB)   # raw AES block cipher
    prev = iv
    out  = b""
    for i in range(0, len(plaintext), 16):
        block  = plaintext[i:i+16]
        block  = xor_bytes(block, prev)  # XOR with previous ciphertext (or IV)
        enc    = aes.encrypt(block)
        out   += enc
        prev   = enc                       # chain!
    return out

def cbc_decrypt(ciphertext: bytes, key: bytes, iv: bytes) -> bytes:
    aes  = AES.new(key, AES.MODE_ECB)
    prev = iv
    out  = b""
    for i in range(0, len(ciphertext), 16):
        block  = ciphertext[i:i+16]
        dec    = aes.decrypt(block)
        out   += xor_bytes(dec, prev)   # XOR with previous ciphertext
        prev   = block                    # chain!
    return out

# Download and decrypt Challenge 10
raw  = urllib.request.urlopen("https://cryptopals.com/static/challenge-data/10.txt").read()
ct   = base64.b64decode(raw)
key  = b"YELLOW SUBMARINE"
iv   = bytes(16)                        # 16 zero bytes

plaintext = cbc_decrypt(ct, key, iv)
# Strip PKCS#7 padding
n = plaintext[-1]
plaintext = plaintext[:-n]

print(plaintext[:300].decode(errors='replace'))
bash
cd ~/cryptopals/set2
nano challenge10.py
python3 challenge10.py
python — ~/cryptopals/set2/challenge10.py
#!/usr/bin/env python3
"""CryptoPals Set 2 Challenge 10 — Implement CBC mode."""
import sys, os, base64
sys.path.insert(0, os.path.expanduser("~/cryptopals/utils"))
from crypto_helpers import pkcs7_unpad
from Crypto.Cipher import AES

def xor_bytes(a,b): return bytes(x^y for x,y in zip(a,b))

def cbc_decrypt(ct, key, iv):
    aes=AES.new(key,AES.MODE_ECB); prev=iv; out=b""
    for i in range(0,len(ct),16):
        b=ct[i:i+16]; out+=xor_bytes(aes.decrypt(b),prev); prev=b
    return out

ct = base64.b64decode(open("challenge10.txt").read())
pt = pkcs7_unpad(cbc_decrypt(ct, b"YELLOW SUBMARINE", bytes(16)))
print(pt[:300].decode(errors='replace'))
CryptoPals — Set 2, Challenge 16

CBC bitflipping attack

CBC is semantically secure, but modifying a ciphertext byte flips the corresponding bit in the next plaintext block. An attacker who can submit and re-submit ciphertexts can inject arbitrary content — without knowing the key.

Google Colab supported
Linux terminal supported
CBC decryption formula

plaintext[i] = AES_dec(ciphertext[i]) ⊕ ciphertext[i-1]

Flip a bit in ciphertext[i-1] → that exact bit flips in plaintext[i]. Block i-1 decrypts to garbage, but you don't care about that block.

python — Colab Cell: full bitflipping exploit
# ── CryptoPals Set 2 Challenge 16 — CBC Bitflipping ──
# Goal: make is_admin() return True without knowing the key.
# The server blocks ';' and '=' from user input.
import os
from Crypto.Cipher import AES

KEY = os.urandom(16)   # random key each run
IV  = os.urandom(16)

def xor_bytes(a,b): return bytes(x^y for x,y in zip(a,b))

def cbc_enc(pt, key, iv):
    from Crypto.Util.Padding import pad
    aes=AES.new(key,AES.MODE_ECB); prev=iv; out=b""
    for b in [pt[i:i+16] for i in range(0,len(pt),16)]:
        enc=aes.encrypt(xor_bytes(b,prev)); out+=enc; prev=enc
    return out

def cbc_dec(ct, key, iv):
    aes=AES.new(key,AES.MODE_ECB); prev=iv; out=b""
    for b in [ct[i:i+16] for i in range(0,len(ct),16)]:
        out+=xor_bytes(aes.decrypt(b),prev); prev=b
    return out

# Prefix = 32 bytes (2 full 16-byte AES blocks)
PREFIX = b"comment1=cooking%20MCs;userdata="
SUFFIX = b";comment2=%20like%20a%20pound%20of%20bacon"

def encrypt_userdata(userdata: str) -> bytes:
    # Strip attacker-controlled chars that would give admin directly
    clean = userdata.replace(";","").replace("=","")
    pt = PREFIX + clean.encode() + SUFFIX
    # pad to block boundary
    n = 16 - (len(pt) % 16)
    pt += bytes([n]*n)
    return cbc_enc(pt, KEY, IV)

def is_admin(ct: bytes) -> bool:
    pt = cbc_dec(ct, KEY, IV)
    return b";admin=true;" in pt

# ── THE EXPLOIT ───────────────────────────────────────────
# Prefix is 32 bytes = 2 blocks. Our input starts at block index 2.
# We submit 16 'A' bytes as block 2 (our controllable block).
# After encryption, we modify bytes in ciphertext block 1 (index 16-31)
# to flip specific bits in plaintext block 2 when it decrypts.
# 
# We WANT block 2 plaintext to start with: ;admin=true;
# Currently it decrypts to: AAAAAAAAAAAAAAAA
# Delta = ord('A') ^ ord(target_char)
# ─────────────────────────────────────────────────────────

ct     = bytearray(encrypt_userdata("AAAAAAAAAAAAAAAA"))
target = b";admin=true;"
filler = b"A" * len(target)

for j in range(len(target)):
    # Modify ciphertext block 1 at offset j
    # This flips the corresponding bit in decrypted block 2
    ct[16 + j] ^= filler[j] ^ target[j]

print("is_admin():", is_admin(bytes(ct)))  # → True
print("Decrypted block 2:", cbc_dec(bytes(ct),KEY,IV)[32:48])
bash
cd ~/cryptopals/set2
nano challenge16.py
python3 challenge16.py
python — ~/cryptopals/set2/challenge16.py
#!/usr/bin/env python3
"""CryptoPals Set 2 Challenge 16 — CBC Bitflipping Attack."""
import os
from Crypto.Cipher import AES

KEY=os.urandom(16); IV=os.urandom(16)
def xb(a,b): return bytes(x^y for x,y in zip(a,b))
def enc(pt,k,iv):
    aes=AES.new(k,AES.MODE_ECB);prev=iv;out=b""
    for b in [pt[i:i+16] for i in range(0,len(pt),16)]:
        e=aes.encrypt(xb(b,prev));out+=e;prev=e
    return out
def dec(ct,k,iv):
    aes=AES.new(k,AES.MODE_ECB);prev=iv;out=b""
    for b in [ct[i:i+16] for i in range(0,len(ct),16)]:
        out+=xb(aes.decrypt(b),prev);prev=b
    return out

PREFIX=b"comment1=cooking%20MCs;userdata="
SUFFIX=b";comment2=%20like%20a%20pound%20of%20bacon"

def make_ct(ud):
    pt=PREFIX+ud.replace(";","").replace("=","").encode()+SUFFIX
    n=16-(len(pt)%16);pt+=bytes([n]*n)
    return enc(pt,KEY,IV)
def is_admin(ct): return b";admin=true;" in dec(ct,KEY,IV)

ct=bytearray(make_ct("AAAAAAAAAAAAAAAA"))
target=b";admin=true;"
for j in range(len(target)): ct[16+j]^=ord('A')^target[j]
print("is_admin:",is_admin(bytes(ct)))
The fix: authenticated encryption

Use AES-GCM (an AEAD mode) — any modification to the ciphertext is detected before decryption. CBC alone provides confidentiality but not integrity.

CryptoPals Set 2, Challenge 16
Inject ;admin=true; via bitflipping
Hard
Build the server functions, implement the exploit, and confirm is_admin() returns True without the server stripping your semicolons.
The prefix is exactly 32 bytes (2 blocks). Submit 16 'A' bytes. After getting the ciphertext, loop over the target string ;admin=true; — for each byte at position j, XOR ct[16 + j] with ord('A') ^ target[j]. This cancels the 'A' and injects the target character into block 2's decryption.
Classroom CTF — Phase 1

Set up your
encrypted channel

Your team sets up an AES-CBC encrypted socket channel. Other teams will capture your traffic and attempt to break it. This chapter walks through the full Linux setup: server, client, traffic capture, and common deliberate weaknesses your instructor may ask you to introduce.

Linux terminal — required for this chapter
Why not Colab for the CTF?

Google Colab does not expose arbitrary TCP ports to the internet, and its network interface is not sniffable by classmates on the same LAN. The CTF requires a real Linux machine (VM, lab PC, or cloud instance) on a shared network.

Project file layout

bash — folder structure
~/cryptopals/ctf/
  server.py       # your team's encrypted server
  client.py       # your team's encrypted client
  attacker.py     # tools for breaking opponents (Chapter 10)
~/cryptopals/pcaps/
  capture.pcap    # traffic you capture from opponents

Server — complete implementation

python — ~/cryptopals/ctf/server.py
#!/usr/bin/env python3
"""
CTF AES-CBC Encrypted Chat Server
─────────────────────────────────
Run:  python3 server.py
Share your KEY (hex) with your teammate via a secure out-of-band channel
(e.g. whisper it in person — do NOT send it over the network!).

Port 9999 must be reachable from your teammate's machine.
If behind a firewall: sudo ufw allow 9999/tcp
"""
import socket, os, sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

# ── Configuration ──────────────────────────────────────────────────
PORT      = 9999
KEY       = os.urandom(16)    # 128-bit random key — share this secretly!
BLOCK     = 16
# ───────────────────────────────────────────────────────────────────

def encrypt_msg(plaintext: str) -> bytes:
    """Encrypt a string. Prepend the IV so the receiver can decrypt."""
    iv  = os.urandom(BLOCK)              # fresh random IV every message
    aes = AES.new(KEY, AES.MODE_CBC, iv)
    ct  = aes.encrypt(pad(plaintext.encode(), BLOCK))
    return iv + ct                        # IV || ciphertext

def decrypt_msg(data: bytes) -> str:
    """Decrypt IV || ciphertext received from client."""
    iv, ct = data[:BLOCK], data[BLOCK:]
    aes    = AES.new(KEY, AES.MODE_CBC, iv)
    return unpad(aes.decrypt(ct), BLOCK).decode()

def main():
    print(f"[SERVER] Started on port {PORT}")
    print(f"[SERVER] KEY (hex) = {KEY.hex()}")
    print(f"[SERVER] Share this key with your teammate IN PERSON.\n")

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        srv.bind(("", PORT))
        srv.listen(1)
        print(f"[SERVER] Waiting for connection...")
        conn, addr = srv.accept()
        print(f"[SERVER] Connected: {addr}\n")

        with conn:
            while True:
                data = conn.recv(4096)
                if not data:
                    print("[SERVER] Connection closed.")
                    break
                try:
                    msg = decrypt_msg(data)
                    print(f"[CLIENT → SERVER] {msg}")
                    reply_text = input("[SERVER reply] ")
                    conn.sendall(encrypt_msg(reply_text))
                except Exception as e:
                    print(f"[SERVER] Decrypt error: {e}")
                    conn.sendall(encrypt_msg("ERROR: decryption failed"))

if __name__ == "__main__":
    main()

Client — complete implementation

python — ~/cryptopals/ctf/client.py
#!/usr/bin/env python3
"""
CTF AES-CBC Encrypted Chat Client
──────────────────────────────────
Run:  python3 client.py
Enter the server IP and the shared KEY (hex) when prompted.
"""
import socket, os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

BLOCK = 16

KEY_HEX = input("Enter server KEY (hex): ").strip()
SERVER  = input("Enter server IP:        ").strip()
PORT    = 9999
KEY     = bytes.fromhex(KEY_HEX)

def encrypt_msg(plaintext: str) -> bytes:
    iv  = os.urandom(BLOCK)
    aes = AES.new(KEY, AES.MODE_CBC, iv)
    return iv + aes.encrypt(pad(plaintext.encode(), BLOCK))

def decrypt_msg(data: bytes) -> str:
    iv, ct = data[:BLOCK], data[BLOCK:]
    aes    = AES.new(KEY, AES.MODE_CBC, iv)
    return unpad(aes.decrypt(ct), BLOCK).decode()

print(f"\n[CLIENT] Connecting to {SERVER}:{PORT}...")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((SERVER, PORT))
    print("[CLIENT] Connected. Type messages below.\n")
    while True:
        msg = input("[YOU] ")
        if msg.lower() in ("quit", "exit"): break
        s.sendall(encrypt_msg(msg))
        reply = s.recv(4096)
        print(f"[SERVER] {decrypt_msg(reply)}\n")

Running the CTF channel in tmux

bash — step by step
# ── Terminal 1 (or left tmux pane): Start the server ──
cd ~/cryptopals/ctf
python3 server.py
# Note the KEY (hex) printed — share it with your teammate in person.
# Note your IP: hostname -I

# ── Terminal 2 (or right tmux pane): Start the client ──
cd ~/cryptopals/ctf
python3 client.py
# Enter the KEY and server IP when prompted

# ── Terminal 3 (optional): Capture your own traffic to verify encryption ──
sudo tcpdump -i lo -w ~/cryptopals/pcaps/my_traffic.pcap port 9999
# Ctrl+C to stop, then inspect:
tcpdump -r ~/cryptopals/pcaps/my_traffic.pcap -XX | head -40
# You should see raw ciphertext — no plaintext visible

Capturing opponent traffic (LAN)

bash — sniff opponent channel
# Find the network interface connected to the lab LAN
ip a                          # look for eth0, ens33, wlan0 etc.

# Sniff all TCP on port 9999 across the LAN (replace eth0 with yours)
sudo tcpdump -i eth0 port 9999 -w ~/cryptopals/pcaps/opponents.pcap

# Let it run while opponents communicate, then Ctrl+C

# Quick analysis — show hex payload of each packet
tcpdump -r ~/cryptopals/pcaps/opponents.pcap -XX

# Or open in Wireshark for GUI analysis:
wireshark ~/cryptopals/pcaps/opponents.pcap &

Deliberate weaknesses (instructor-assigned)

Your instructor may ask your team to introduce one of these flaws. Attackers who find and exploit it score bonus points. Only make these changes if explicitly instructed.

WeaknessChange to makeWhat it enables
ECB modeChange AES.MODE_CBC to AES.MODE_ECB and remove the IVBlock repetition detection, structural leaks
Reused IVReplace os.urandom(16) with bytes(16) (all zeros)XOR of two ciphertexts cancels the IV — known-plaintext attack
Hardcoded keySet KEY = bytes(16) (16 zero bytes)Attacker who guesses common weak keys can decrypt everything
No padding checkRemove the unpad() call in decryptEnables padding oracle attack
Classroom CTF — Phase 2

Break your opponents

You have a PCAP file from another team. Work through the attack checklist — each step applies a technique from earlier chapters. The attacker script below combines all tools into one file.

Linux terminal — required for this chapter

Attack checklist

  1. Open the PCAP and extract raw payloadsUse tshark (CLI) or Wireshark (GUI). Export each direction's raw bytes.
  2. Check for ECB modeSplit each payload into 16-byte blocks. Any repeats? → ECB confirmed.
  3. Check IV handlingIs the first 16 bytes of every message always the same? → IV reuse vulnerability.
  4. Reused IV attackIf IV is constant, XOR two ciphertexts to cancel the IV. Known-plaintext recovers the other plaintext.
  5. CBC bitflip probeFlip bytes in a captured ciphertext, replay it, observe server response. Different behavior = bitflip vulnerability.
  6. Padding oracle probeSend modified ciphertexts to the server. Different errors for valid vs invalid padding = padding oracle.

Full attacker script

python — ~/cryptopals/ctf/attacker.py
#!/usr/bin/env python3
"""
CTF Attacker Toolkit
─────────────────────────────────────────────────────────────────
Usage:
  1. Capture opponent traffic:
       sudo tcpdump -i eth0 port 9999 -w opponents.pcap
  2. Extract raw TCP payloads with tshark:
       tshark -r opponents.pcap -T fields -e data -Y tcp.payload > payloads.txt
  3. Run this script:
       python3 attacker.py payloads.txt
─────────────────────────────────────────────────────────────────
import sys, os

# ── 1. DETECT ECB MODE ────────────────────────────────────────────
def detect_ecb(data: bytes, bs=16) -> bool:
    """True if any 16-byte block repeats — ECB signature."""
    blocks = [data[i:i+bs] for i in range(0, len(data), bs)]
    return len(blocks) != len(set(blocks))

# ── 2. CHECK IV REUSE ─────────────────────────────────────────────
def check_iv_reuse(payloads: list[bytes]) -> bool:
    """True if all messages share the same first 16 bytes (IV)."""
    ivs = [p[:16] for p in payloads if len(p) >= 16]
    return len(set(ivs)) == 1

# ── 3. REUSED IV ATTACK ───────────────────────────────────────────
def reused_iv_attack(
    ct1: bytes, ct2: bytes, known_pt1: bytes
) -> bytes:
    """
    If the IV is reused:
      ct1 = AES_enc(pt1 XOR IV)
      ct2 = AES_enc(pt2 XOR IV)
    XOR of blocks:  ct1_b0 XOR ct2_b0 = pt1_b0 XOR pt2_b0
    Therefore:      pt2_b0 = ct1_b0 XOR ct2_b0 XOR pt1_b0
    """
    def xb(a,b): return bytes(x^y for x,y in zip(a,b))
    # Strip IV prefix if present (first 16 bytes)
    c1, c2 = ct1[16:], ct2[16:]
    return xb(xb(c1[:16], c2[:16]), known_pt1[:16])

# ── 4. MAIN ANALYSIS LOOP ─────────────────────────────────────────
def main():
    if len(sys.argv) < 2:
        print("Usage: python3 attacker.py payloads.txt")
        print("       (each line = hex-encoded TCP payload from tshark)")
        sys.exit(1)

    lines    = open(sys.argv[1]).read().strip().splitlines()
    payloads = []
    for line in lines:
        line = line.strip()
        if line:
            try:
                payloads.append(bytes.fromhex(line))
            except ValueError:
                pass   # skip non-hex lines

    print(f"\n[*] Loaded {len(payloads)} payloads\n")
    print("═" * 50)

    # ── ECB detection ──
    print("\n[1] ECB Mode Detection")
    ecb_found = False
    for i, p in enumerate(payloads):
        if detect_ecb(p):
            print(f"    ✓ Payload {i}: ECB mode detected! (repeated 16-byte block)")
            ecb_found = True
    if not ecb_found:
        print("    – No ECB detected. Likely CBC or CTR.")

    # ── IV reuse check ──
    print("\n[2] IV Reuse Check")
    if check_iv_reuse(payloads):
        print(f"    ✓ ALL {len(payloads)} payloads share the same IV: {payloads[0][:16].hex()}")
        print("    ✓ IV REUSE CONFIRMED — known-plaintext attack possible!")

        # Attempt reused-IV attack if we have ≥2 payloads and know pt1
        if len(payloads) >= 2:
            print("\n[3] Reused-IV Attack (requires known plaintext of msg 1)")
            # Common CTF scenario: first message is a known greeting
            known_guesses = [
                b"Hello, this is a",
                b"comment1=cooking",
                b"GET / HTTP/1.1\r\n",
                b"user=admin&pass=p",
            ]
            for guess in known_guesses:
                recovered = reused_iv_attack(payloads[0], payloads[1], guess)
                printable  = all(32 <= b < 127 for b in recovered)
                if printable:
                    print(f"    Possible pt2 block 0: {recovered.decode(errors='replace')!r}")
    else:
        print("    – IVs differ between messages (correct behaviour).")

    # ── Payload summary ──
    print("\n[*] Raw payload hex dump (first 5):")
    for i, p in enumerate(payloads[:5]):
        print(f"    [{i}] len={len(p):4d}  {p[:32].hex()}…")

    print("\n[*] Done. See Chapter 10 of the workbook for next steps.\n")

if __name__ == "__main__":
    main()

Extracting payloads from a PCAP with tshark

bash — tshark payload extraction
# Extract raw TCP data bytes, one hex string per line
tshark -r ~/cryptopals/pcaps/opponents.pcap \
       -T fields \
       -e data \
       -Y "tcp.payload and tcp.dstport==9999" \
       > ~/cryptopals/pcaps/payloads.txt

# Run the attacker
python3 ~/cryptopals/ctf/attacker.py ~/cryptopals/pcaps/payloads.txt

Scoring your break

AchievementPointsTechnique
Prove ECB mode was used1 ptRepeated-block detection
Recover any plaintext message3 ptsReused IV, known plaintext
Recover the secret key5 ptsPadding oracle, bitflip + key recovery
Send a valid impersonation message5 ptsKey recovery or bitflipping
Clean, documented attack script2 ptsCode quality
Classroom CTF — Phase 3

Scoring & debrief

The CTF ends with a structured debrief — the most valuable part. Teams present attacks and defenses, explaining what failed and why. Track your CryptoPals progress below and review the discussion questions before presenting.

CryptoPals progress tracker

Click a cell to mark a challenge complete. Progress is saved in your browser.

Set 1 (Challenges 1–8)
Set 2 (Challenges 9–16)
Challenges completed
0 / 16
Percentage done
0%

Debrief discussion questions

  1. What mode did your opponents use?ECB, CBC, or something else? How did you determine it from the ciphertext alone?
  2. What implementation mistakes were most common?IV reuse? Hardcoded keys? No authentication? Which were easiest to exploit?
  3. Why is CBC still dangerous without a MAC?The bitflipping attack showed this. What real-world protocol vulnerabilities mirror this flaw? (BEAST, POODLE, Lucky13)
  4. What would it take to make the channel truly secure?Key exchange (Diffie-Hellman), authentication (HMAC), integrity (AES-GCM). Why does TLS bundle all three?
  5. Confidentiality vs integrity — what's the difference?Can you have one without the other? Give a concrete example from this exercise.

What comes next

SetTopicsDifficulty
Set 3CTR mode, MT19937 Mersenne Twister, padding oracle⬛⬛⬜⬜
Set 4Stream cipher attacks, MD4/SHA-1 length extension⬛⬛⬛⬜
Set 5Diffie-Hellman, SRP, MITM key fixation⬛⬛⬛⬜
Set 6RSA, DSA, nonce reuse, Bleichenbacher's attack⬛⬛⬛⬛
Recommended next platforms

cryptohack.org — best continuation of CryptoPals-style work, browser-based. PicoCTF — beginner to intermediate, free. HackTheBox — closer to real-world pentesting.