🍱 Lunchbox Hands

security

XOR, ROT13 and Caesar: Why Toy Ciphers Still Matter

XOR cipher explained — why ROT13 and Caesar are toys, why XOR sits inside AES, HMAC and ChaCha20, and how a repeating-key XOR is broken in a few seconds.

Someone hands you a firmware blob full of unreadable strings, you XOR every byte with 0x5A on a hunch, and suddenly there are URLs in it. The same trick shows up in CTF challenges, game saves and the config section of a lot of malware. XOR is the weakest encryption anyone ships and simultaneously the single most important operation in real cryptography — AES, ChaCha20, HMAC, CBC mode and the one-time pad are all XOR at the point where the secret meets the data. Understanding why is the fastest route from “these are puzzle ciphers” to “here is why a repeated nonce destroys AES-GCM.”

Our XOR cipher tool and ROT13 & Caesar cipher tool run every example below in your browser.

Caesar and ROT13: addition mod 26

A Caesar cipher shifts each letter forward by a fixed amount — (c - 'A' + shift) mod 26, the alphabet as a ring you rotate. Suetonius describes Julius Caesar using a shift of three:

Attack at dawn  →  Dwwdfn dw gdzq

Decrypt with -shift. Digits, punctuation and spaces pass through untouched — itself a leak, since word boundaries survive — and the key space is 25 useful shifts, which is not a key space but a short list. Our Caesar tool decodes under all 26 at once and ranks them by chi-square against English letter frequencies plus a bonus per common English word found.

ROT13 is the shift of 13, and because 13 is half of 26 it is its own inverse: one button encodes and decodes, and every Unix box does it with no tool at all.

$ echo 'the butler did it' | tr 'A-Za-z' 'N-ZA-Mn-za-m'
gur ohgyre qvq vg

The shift is fixed and public, so the key space is zero bits — which was always the point. ROT13 hid Usenet punchlines behind one deliberate action, and no cipher does that job better, because the requirement is that decoding be trivial. ROT47 applies the same idea to the 94 printable ASCII characters ! (33) through ~ (126), rotated by 47, so digits and punctuation scramble too: Attack at dawn, 06:00! becomes pEE24< 2E 52H?[ _ei__P.

None of these is encryption — and neither is a Morse code translator, which is transcription with no key at all, the same family as the NATO spelling alphabet.

XOR: four properties, and they are all load-bearing

Exclusive-or on bits: the output is 1 exactly when the inputs differ. Four consequences follow, and modern cryptography leans on all four.

1. It is its own inverse. a ⊕ k ⊕ k = a, because k ⊕ k = 0. Encryption and decryption are the same code path with the same key — no inverse S-box, no decrypt routine, which is why our XOR tool has no encrypt/decrypt switch.

2. It is perfectly balanced. If k is a uniformly random bit, a ⊕ k is uniformly random regardless of what a was. No other common operation does this: AND is biased toward 0, OR toward 1, addition carries.

3. It has no carry. Bit i of the output depends only on bit i of the inputs — one gate per bit, trivially parallel, naturally constant-time. It never asks whether the byte is signed either, because signedness is a convention about reading bits, not storing them.

4. Given only the output, you learn nothing about either input. From c = a ⊕ b alone, every value of a is consistent with some b. What XOR does leak — the rest of this post — is the relationship between two things encrypted with the same key.

So XOR is glue, not strength. 'A' ⊕ 0x20 is 'a': ASCII case-flipping, not cryptography. XOR is where the secret gets applied; the strength lives in whatever generated it.

The one-time pad: the only XOR that is actually unbreakable

Take a key that is (a) truly random, (b) at least as long as the message and (c) never used twice, XOR it with the plaintext, and the result is provably unbreakable — not “unbroken so far” but mathematically incapable of being broken. Gilbert Vernam patented the teletype form in 1919 (US 1,310,719); Claude Shannon proved the property he called perfect secrecy in “Communication Theory of Secrecy Systems” (1949).

The proof is property 2 applied to every bit: for any ciphertext and any candidate plaintext of the same length, exactly one key maps one to the other. A 14-byte ciphertext is equally consistent with attack at dawn and retreat at ten — so there’s no work to do, and computation doesn’t help.

It is also nearly useless in practice, and two of the three reasons are quick. A pad from Math.random() is not a pad but a stream cipher with a tiny key, and Math.random’s state is recoverable from a handful of outputs; real browser entropy comes from crypto.getRandomValues, which our random number generator uses. And distribution is circular: to send an encrypted megabyte you must first deliver a megabyte of key securely, at which point you could have delivered the message. The third reason is the interesting one.

Key reuse: the two-time pad

Use the same pad on two messages and the secrecy evaporates in one line of algebra:

C1 = P1 ⊕ K
C2 = P2 ⊕ K
C1 ⊕ C2 = P1 ⊕ K ⊕ P2 ⊕ K = P1 ⊕ P2

The key cancels. An attacker who never sees K holds the XOR of two English sentences, which is not remotely random:

P1  attack at dawn
P2  retreat at ten
K   9e3f0a5c77b2d41e8a6503fc21bd
C1  ff4b7e3d14d9f47ffe45679d56d3
C2  ec5a7e2e12d3a03eeb11238844d3
C1 ⊕ C2 = 13110013060a5441155444151200

Look at bytes 3 and 14 of that last line: 00 and 00. A zero byte in P1 ⊕ P2 means the plaintexts agree there — both have t third and n last, which is also why the ciphertexts share a visible trailing d3. Before any real cryptanalysis, the structure is showing through.

Then comes crib dragging: guess a fragment of one plaintext, XOR it into P1 ⊕ P2, read out the corresponding fragment of the other. Guess right and you get English; guess wrong and you get noise, which is its own signal. XOR-ing attack at dawn into the value above returns retreat at ten exactly, and each hit extends the next.

Not a textbook hypothetical: under wartime pressure the Soviet Eighth Directorate duplicated roughly 35,000 pages of one-time pad material in 1942 and sent the copies to different missions. US cryptanalysts found the overlaps and read the traffic for years. That was VENONA — the cipher was never broken, only the “one-time” part.

Repeating-key XOR: Vigenère in binary, and how it dies

The obvious “fix” for a pad you can’t distribute is a short key, repeated: byte n of the data XORed with byte n mod keylen of the key. That is what our XOR tool does, and what almost every piece of XOR obfuscation in the wild does:

"Attack at dawn" ⊕ "KEY" (repeating)
= 0a312d2a26326b242d6b21383c2b

A 7-byte key is 256⁷ ≈ 7.2 × 10¹⁶ values, which looks respectable and does not matter, because you never enumerate it. This is Vigenère in binary; Kasiski broke the letter version in 1863. The modern recipe is Cryptopals set 1, challenge 6, in two steps.

Step 1 — find the key length. For each KEYSIZE from 2 to 40, chop the ciphertext into KEYSIZE-byte blocks, average the Hamming distance (differing bits) between blocks, and divide by KEYSIZE. Blocks that line up with the key’s period are both “English XOR the same key bytes,” so they differ in fewer bits than random data would. Cryptopals gives a test vector: the distance between this is a test and wokka wokka!!! is 37 bits.

A real run — 935 bytes of the opening of Moby-Dick, XORed with the 7-byte key ICEBERG, averaged over every block pair:

KEYSIZENormalized distance
282.634
142.635
72.639
212.640
352.643
29 (best non-multiple)2.763
38 (worst)2.799

This is the part tutorials skip: the true key length did not come first. The top five are exactly 7 and its multiples, spread across 0.009, while the best non-multiple sits 0.12 behind. The signal is strong; it just points at the set of multiples, not the smallest member. Take the top few, add their divisors, and let step 2 decide.

Step 2 — transpose and solve each column. Build seven columns: every 1st byte, every 2nd byte, and so on. Each was XORed with exactly one key byte, so each is an independent single-byte XOR — 255 useful keys, brute-forceable instantly, with English letter frequency picking the winner. Seven columns × 256 guesses = 1,792 trial decryptions, which returned ICEBERG and the full plaintext in milliseconds.

The work scales with the key length, not exponentially with it — a 40-byte key costs 40 column solves instead of 7. Known plaintext kills it faster still: if the file starts with {"version": or \x89PNG, XOR the known bytes against the ciphertext and the key falls out.

That leaves one honest use: a speed bump against casual tampering over game saves, string tables and config blobs. It costs nothing and stops strings turning up anything interesting; malware likes it for the same reasons, plus no imports that light up a scanner. The failure mode is calling it encryption in a design doc. If the threat model includes anyone with a spare afternoon, use a real cipher — our AES encrypt/decrypt tool does AES-256-GCM with a PBKDF2-derived key, and what “AES encrypt” actually does in your browser walks the layers.

The bridge: every stream cipher is XOR

A stream cipher is a cryptographically secure pseudorandom generator feeding an XOR. ChaCha20 emits a keystream from a key and a nonce; AES-CTR encrypts a counter block by block to emit one; both then XOR it into the plaintext, and AES-GCM is AES-CTR plus an authenticator. The generator carries all the security; the mixing step is the operation that just failed so badly above.

So the two-time pad failure transfers verbatim. Reuse a (key, nonce) pair in ChaCha20 or AES-GCM and you produce the same keystream twice, so C1 ⊕ C2 = P1 ⊕ P2 and every line of the key-reuse section applies unchanged. Nonce reuse doesn’t degrade GCM gradually; it removes it.

XOR is in the block ciphers too: AddRoundKey mixes each AES round key into the state, and CBC XORs each plaintext block with the previous ciphertext block. HMAC derives two keys from one by XOR-ing it with the bytes 0x36 and 0x5c — the ipad and opad in how HMAC works, reproducible with the HMAC generator.

Cipher → key space → time to break

CipherKey spaceTime to breakActually for
ROT13 / ROT47none — fixed, publicinstant, by eyespoiler hiding
Caesar25 shifts25 tries, one clickpuzzles, CTF warm-ups
Single-byte XOR255 keysmillisecondsstrings in binaries
Repeating-key XOR, n bytes256ⁿsecondsgame saves, configs
One-time pad, used once256^lennevernothing you will operate
One-time pad, used twiceirrelevantminutes, by handa case study
AES-256-GCM2²⁵⁶never, for the keyactual encryption

Note the two one-time pad rows: the only cipher here with a proof went from unbreakable to hand-solvable through an operational mistake, not a mathematical one. That ratio holds in modern systems — reused nonces, weak-password-derived keys and predictable randomness break far more deployments than cryptanalysis does. If a passphrase is in your chain anywhere, password entropy explained covers its worth.

What to do

  1. Use ROT13 for what it’s for. The ROT13 & Caesar tool does that, plus arbitrary shifts, ROT47, and the brute-force table when the shift is unknown.
  2. Treat XOR in a binary as a label, not a lock. Single-byte XOR is the first guess for obfuscated strings, a repeating ASCII key the second.
  3. Audit for nonce reuse before anything else. In any AES-GCM, AES-CTR or ChaCha20 code you own, prove the nonce cannot repeat under one key — the highest-severity bug in this family.
  4. Get your randomness from a CSPRNG. crypto.getRandomValues, never Math.random(), for keys, nonces, salts and tokens.

The XOR cipher tool takes text or hex input and a text or hex key, repeats the key across the data, and outputs hex, Base64 or UTF-8 — warning you when the key is longer than the data (only the leading bytes get used) or when the result isn’t valid UTF-8 and you should be reading hex. It runs in your browser, and “Use as input” XORs the result a second time so you can watch property 1 work.

The short version: the toys are toys, and the reason to learn them is that the operation at their center never left. It just got a much better key.