Part 4 — Symmetric encryption
Symmetric means one key, used for both encryption and decryption. It is fast, well understood, and does almost all of the actual encrypting in the world.
Chapter 12 — The idea, and what "perfect secrecy" costs
In one sentence
Encryption transforms a message using a key so that only someone with the key can recover it — and the only provably unbreakable scheme is also completely impractical.
The vocabulary
PLAINTEXT the original message "attack at dawn"
KEY the shared secret (128-256 random bits)
CIPHERTEXT the scrambled output 8f3a2b91c4...
CIPHER the algorithm AES, ChaCha20
plaintext + key --[ encrypt ]--> ciphertext
ciphertext + key --[ decrypt ]--> plaintext
Symmetric = the same key both ways. Asymmetric (Part 5) = two different keys.
Kerckhoffs's principle
The system must remain secure even if everything about it, except the key, is public knowledge.
Stated in 1883, and it is the single most important rule in this Part. All the secrecy lives in the key, never in the algorithm.
Why this matters practically:
"Our encryption is proprietary, nobody knows how it works."
-> This is a RED FLAG, not a feature.
Real algorithms (AES, ChaCha20) are published, standardised, and have been
attacked by thousands of researchers for decades without breaking.
THAT is why we trust them.
Your in-house cipher has been reviewed by you.
Never roll your own crypto. Not the algorithm, and preferably not the implementation either. This is not gatekeeping — it is that cryptographic bugs are silent. The code runs, the output looks random, and it is broken.
The one-time pad: perfect, and useless
There is an unbreakable cipher. XOR (§0.5) the message with a truly random key the same length as the message, and never reuse the key.
plaintext 01100001 01110100 01110100 "att"
key(random) 11010110 00101110 10011001
--------------------------- XOR
ciphertext 10110111 01011010 11101101
Claude Shannon proved in 1949 that this has perfect secrecy: the ciphertext gives an attacker literally zero information. Any plaintext of the same length is equally possible.
So why doesn't everyone use it?
1. The key must be as LONG as the message.
Encrypting 1 GB needs 1 GB of key -- which you must somehow
already have shared securely. If you can do that, you could
have just shared the message.
2. The key must be TRULY RANDOM. Not a PRNG. Real entropy.
3. The key must NEVER be reused. Not once.
4. It provides NO INTEGRITY. An attacker who knows the plaintext
can flip any bit they like:
ciphertext XOR ("attack" XOR "defend") = a valid encryption of "defend"
Why key reuse is catastrophic
This is worth seeing, because the same mistake recurs in modern systems (nonce reuse — Chapter 14).
C1 = P1 XOR K
C2 = P2 XOR K
Attacker computes:
C1 XOR C2 = (P1 XOR K) XOR (P2 XOR K) = P1 XOR P2
^^^^^^^^^
the key CANCELS OUT completely.
The attacker now has the XOR of two plaintexts, which is readily separated using language statistics. This broke the Soviet VENONA traffic in the 1940s, and it broke Microsoft's PPTP and WEP decades later.
The lesson that carries forward: a stream of key material must never be reused with different data. Modern ciphers keep this property; you break it by reusing a nonce.
What real ciphers do instead
Take a short key (128–256 bits) and stretch it into a long, unpredictable keystream — or scramble data in fixed-size blocks. You give up provable perfect secrecy for computational security: breaking it is possible in principle and infeasible in practice.
ONE-TIME PAD perfect secrecy impractical
AES / ChaCha20 computational a 256-bit key protects terabytes
security and fits in a config file
Practice (15 min)
mkdir -p ~/sec-lab/scratch && cd ~/sec-lab/scratch
python3 - <<'PY'
import os
def xor(a, b):
return bytes(x ^ y for x, y in zip(a, b))
# 1. One-time pad works
msg = b"attack at dawn"
key = os.urandom(len(msg))
ct = xor(msg, key)
print("plaintext :", msg)
print("ciphertext:", ct.hex())
print("decrypted :", xor(ct, key))
# 2. Reusing the key destroys it
p1, p2 = b"attack at dawn", b"retreat at dusk"
k = os.urandom(20)
c1, c2 = xor(p1, k), xor(p2, k)
print("\nC1 XOR C2 =", xor(c1, c2).hex())
print("P1 XOR P2 =", xor(p1, p2).hex())
print("identical -- the key cancelled out:", xor(c1, c2)[:14] == xor(p1, p2)[:14])
# 3. No integrity: flip 'attack' to 'defend' without the key
known, target = b"attack", b"defend"
forged = xor(ct[:6], xor(known, target)) + ct[6:]
print("\nforged decrypts to:", xor(forged, key))
PY
That third demo is the important one: the attacker changed the message without the key. Encryption alone does not give you integrity. Chapter 15 fixes this.
Common confusions
- "Longer key = proportionally more secure." 128-bit is already beyond brute force. 256-bit is margin (and post-quantum hedging, Chapter 23), not 2× the security.
- "Encryption means the data can't be changed." No. Encryption gives confidentiality only. Integrity needs a MAC or AEAD.
- "XOR is weak." XOR with a proper keystream is exactly what stream ciphers do. XOR with a short repeating key is the weak part.
Check yourself
- State Kerckhoffs's principle and why "proprietary encryption" is a warning sign.
- What makes the one-time pad impractical? Give three reasons.
- What happens if you XOR two ciphertexts encrypted with the same key?
- Does encryption protect against tampering?
(Answers: Appendix F.)
Further reading
- Book: Serious Cryptography (2nd ed.), Aumasson — Chapters 1–4. The best accessible treatment of everything in this Part.
- Video: Computerphile, "One Time Pad" and "XOR & the One Time Pad".
- Article: "Cryptographic Right Answers" (Latacora) — a short, opinionated list of what to use. Read it now and again after Part 5.