August 27, 2026
Breaking Classical Cryptography: OverTheWire Krypton - Complete Walkthrough (Part 2)
The key was always there. You just needed to look at the right letters.

By Abdelrhman mohamed
12 min read
Welcome back.
If you haven't read Part 1 yet, stop here and go read it. This part picks up exactly where we left off, and everything that follows builds directly on what we covered before Base64, ROT13, Caesar, and the Vigenère cipher broken through frequency analysis and trigram detection.
Quick recap: We went through Krypton Levels 0 through 3. Level 3 was the most interesting Vigenère cipher, multiple intercepted messages, frequency analysis, and an iterative substitution approach that revealed the plaintext character by character. The password we took into Level 4 was BRUTE.
Note: If you tried ARUTE (what the iterative tr substitution showed) and it didn't work, that's because one letter in the chain was still off. The correct password out of Level 3 is "BRUTE". The "vignere_decoder.py" script from Part 1 would have given you the clean answer, which brings us perfectly to this level.
Today we go deeper.
Level 4 → Level 5 is where the scripts from Part 1 stop being helpers and start being the entire attack. This time we have the key length handed to us on a silver platter. The question is: now what do we do with it?
Let's go.
A Note About the Scripts
Before we start, something important to say for anyone who found this article without reading Part 1.
The four scripts we developed and published in Part 1 are the engine behind this entire series. We'll be using them again in this part, and in every part after this. You don't need to rewrite them. You don't need to change them. They were built to be reusable across all Krypton levels.
Here's what each one does (a quick refresher):
freq_analysis.py - counts how often each character or character group appears in a file. Pass groupsize 1 for single letters, 3 for trigrams, etc.
keyLength.py - Kasiski Examination. Finds repeated sequences in the ciphertext and calculates the most probable key length from the distances between those repetitions.
vignere_shift.py - given a key length and a column index (shift), extracts every Nth character starting at that position. Turns a Vigenère column into a simple Caesar cipher.
vignere_decoder.py - given a key and a ciphertext file, reverses the Vigenère formula and recovers the plaintext.
All scripts are available on GitHub, Grab them once. Use them forever.
Level 4 → Level 5: Vigenère with a Known Key Length
What's Different This Time?
Remember Level 3? We had to figure out the key length ourselves. We used trigram analysis, saw that JDS appeared way too many times to be random, concluded that JDS = THE, and from there deduced the key was 3 characters long.
Level 4 skips that step for you. The level description says it straight:
"For this exercise, the key length is 6."
The page even gives you a worked example showing how Vigenère encryption works numerically. Read it carefully. It explains exactly what we're undoing.
Here's the example from the page broken down:
Plaintext: P R O C E E D M E E T I N G A S A G R E E D
Key: G O L D G O L D G O L D G O L D G O L D G O
P indices: 15 17 14 2 4 4 3 12 4 4 19 8 13 6 0 18 0 6 17 4 4 3
K indices: 6 14 11 3 6 14 11 3 6 14 11 3 6 14 11 3 6 14 11 3 6 14
C = P+K: 21 5 25 5 10 18 14 15 10 18 4 11 19 20 11 21 6 20 2 7 10 17
Ciphertext: V F Z F K S O P K S E L T U L V G U C H K RPlaintext: P R O C E E D M E E T I N G A S A G R E E D
Key: G O L D G O L D G O L D G O L D G O L D G O
P indices: 15 17 14 2 4 4 3 12 4 4 19 8 13 6 0 18 0 6 17 4 4 3
K indices: 6 14 11 3 6 14 11 3 6 14 11 3 6 14 11 3 6 14 11 3 6 14
C = P+K: 21 5 25 5 10 18 14 15 10 18 4 11 19 20 11 21 6 20 2 7 10 17
Ciphertext: V F Z F K S O P K S E L T U L V G U C H K RThis is Vigenère by the numbers. Every letter gets shifted by the corresponding key letter, and the key repeats every 6 characters. Our job: run it in reverse.
Two intercepted messages (found1 and found2), both in American English, both encrypted with the same 6 character key. This is almost too good.
The Challenge Setup
SSH as krypton4 with password BRUTE:
ssh krypton4@krypton.labs.overthewire.org -p 2231
cd /krypton/krypton4
lsssh krypton4@krypton.labs.overthewire.org -p 2231
cd /krypton/krypton4
ls
Files:
HINTa hint from the level designersREADMElevel descriptionfound1,found2two intercepted messages, same keykrypton5the encrypted password for the next level
cat krypton5
HCIKV RJOXcat krypton5
HCIKV RJOXTen characters. Encrypted with a 6-character Vigenère key. We need the key to unlock it.
cat found1cat found1
A massive wall of ciphertext. This is our material for the attack.
Setting Up the Workspace
Create a temp directory:
mktemp -dmktemp -dOutput: /tmp/tmp.ABxMoQjeEu
cd /tmp/tmp.ABxMoQjeEucd /tmp/tmp.ABxMoQjeEu
From your attack machine, transfer the scripts:
scp -P 2231 freq_analysis.py vignere_decoder.py vignere_shift.py \
krypton4@krypton.labs.overthewire.org:/tmp/tmp.ABxMoQjeEuscp -P 2231 freq_analysis.py vignere_decoder.py vignere_shift.py \
krypton4@krypton.labs.overthewire.org:/tmp/tmp.ABxMoQjeEu
Verify they landed:
lsls
Good. Everything is in place.
The Core Idea: Breaking the Problem Into Six Smaller Problems
Here's the fundamental insight behind breaking Vigenère when you know the key length.
The Vigenère cipher with key length 6 is essentially six separate Caesar ciphers running in parallel.
- Every character at position 0, 6, 12, 18… was encrypted by key[0]
- Every character at position 1, 7, 13, 19… was encrypted by key[1]
- Every character at position 2, 8, 14, 20… was encrypted by key[2]
- …and so on until key[5]
If you pull out all the characters from position 0, 6, 12, 18… and look at them as a group, they form a Caesar cipher with a single fixed shift. Finding the most frequent character in that group and mapping it to E (the most common English letter) gives you key[0].
Do the same for each of the other 5 groups and you have the complete key.
That's exactly what vignere_shift.py and freq_analysis.py do together. vignere_shift.py does the extraction. freq_analysis.py does the counting. You do the subtraction to find the key byte.
Let's execute this.
Step 1: Extract Column 0 and Find key[0]
python3 vignere_shift.py /krypton/krypton4/found1 6 0 > found1-shift0python3 vignere_shift.py /krypton/krypton4/found1 6 0 > found1-shift0This extracts every character at positions 0, 6, 12, 18… from found1 and saves it to found1-shift0.
Now analyze the frequency of that column:
python3 freq_analysis.py found1-shift0 1python3 freq_analysis.py found1-shift0 1
Output (top letters):
J: 37
S: 24
Y: 22
T: 20
W: 17
F: 17
M: 16
...J: 37
S: 24
Y: 22
T: 20
W: 17
F: 17
M: 16
...J is the most frequent character in column 0, appearing 37 times.
In English, E is the most frequent letter. So in column 0, E was encrypted as J.
Using the Vigenère formula: C = (P + K) mod 26
So: K = (C - P) mod 26 = (J - E) mod 26
Let's calculate:
- J = index 9 (A=0, B=1, …, J=9)
- E = index 4
- K = (9–4) mod 26 = 5
- Index 5 = F
key[0] = F
The alphabet index reference (A=0 through Z=25) that you can see in the Kate editor is just a visual aid for doing this mental math quickly. When your most frequent letter is J (9), and you subtract E's position (4), you get 5, which is F. Simple arithmetic, but it's easy to make mistakes without a reference.
Step 2: Extract Column 1 and Find key[1]
python3 vignere_shift.py /krypton/krypton4/found1 6 1 > found1-shift1
python3 freq_analysis.py found1-shift1 1python3 vignere_shift.py /krypton/krypton4/found1 6 1 > found1-shift1
python3 freq_analysis.py found1-shift1 1
Output:
V: 35
K: 31
Y: 19
F: 19
Z: 18
...V: 35
K: 31
Y: 19
F: 19
Z: 18
...V is the most frequent character in column 1, appearing 35 times.
K = (V - E) mod 26 = (21 - 4) mod 26 = 17
Index 17 = RK = (V - E) mod 26 = (21 - 4) mod 26 = 17
Index 17 = Rkey[1] = R
Kate editor now shows the key building up: FR…
Steps 3 through 6: Completing the Key
Repeat the same process for columns 2, 3, 4, and 5:
python3 vignere_shift.py /krypton/krypton4/found1 6 2 > found1-shift2
python3 freq_analysis.py found1-shift2 1
# Most frequent → I (index 8) → K = (8-4) = 4 = E → key[2] = E
python3 vignere_shift.py /krypton/krypton4/found1 6 3 > found1-shift3
python3 freq_analysis.py found1-shift3 1
# Most frequent → O (index 14) → K = (14-4) = 10 = K → key[3] = K
python3 vignere_shift.py /krypton/krypton4/found1 6 4 > found1-shift4
python3 freq_analysis.py found1-shift4 1
# Most frequent → I (index 8) → K = (8-4) = 4 = E → key[4] = E
python3 vignere_shift.py /krypton/krypton4/found1 6 5 > found1-shift5
python3 freq_analysis.py found1-shift5 1
# Most frequent → C (index 2) → K = (2-4+26) mod 26 = 24 = Y → key[5] = Ypython3 vignere_shift.py /krypton/krypton4/found1 6 2 > found1-shift2
python3 freq_analysis.py found1-shift2 1
# Most frequent → I (index 8) → K = (8-4) = 4 = E → key[2] = E
python3 vignere_shift.py /krypton/krypton4/found1 6 3 > found1-shift3
python3 freq_analysis.py found1-shift3 1
# Most frequent → O (index 14) → K = (14-4) = 10 = K → key[3] = K
python3 vignere_shift.py /krypton/krypton4/found1 6 4 > found1-shift4
python3 freq_analysis.py found1-shift4 1
# Most frequent → I (index 8) → K = (8-4) = 4 = E → key[4] = E
python3 vignere_shift.py /krypton/krypton4/found1 6 5 > found1-shift5
python3 freq_analysis.py found1-shift5 1
# Most frequent → C (index 2) → K = (2-4+26) mod 26 = 24 = Y → key[5] = YQuick tip on negative results:_ When your calculation gives a negative number, just add 26. For example, column 5: C (index 2) − E (index 4) = −2. Add 26 → 24. Index 24 = Y. The modular arithmetic handles the wrap-around of the alphabet._
Column Shift Most Frequent Index KeyByte key[n]
Cipher Letter Calculation
0 0 J 9 9 − 4 = 5 F
1 1 V 21 21 − 4 = 17 R
2 2 I 8 8 − 4 = 4 E
3 3 O 14 14 − 4 = 10 K
4 4 I 8 8 − 4 = 4 E
5 5 C 2 (2−4+26) mod 26 = 24 YColumn Shift Most Frequent Index KeyByte key[n]
Cipher Letter Calculation
0 0 J 9 9 − 4 = 5 F
1 1 V 21 21 − 4 = 17 R
2 2 I 8 8 − 4 = 4 E
3 3 O 14 14 − 4 = 10 K
4 4 I 8 8 − 4 = 4 E
5 5 C 2 (2−4+26) mod 26 = 24 Y
The key is: FREKEY
Step 3: Decrypt the Password
Now we plug the key into vignere_decoder.py and point it at krypton5:
python3 vignere_decoder.py /krypton/krypton4/krypton5 FREKEYpython3 vignere_decoder.py /krypton/krypton4/krypton5 FREKEY
Output:
Decoding file '/krypton/krypton4/krypton5' with key 'FREKEY':
CLEARTEXTDecoding file '/krypton/krypton4/krypton5' with key 'FREKEY':
CLEARTEXTPassword: CLEARTEXT
Clean. Precise. No guessing.
Bonus: What Were the Messages Actually Saying?
This is the part I love about Krypton. The "found" files are never random garbage. They're real text. Let's decrypt found1 and found2 and see what they actually say.
python3 vignere_decoder.py /krypton/krypton4/found1 FREKEYpython3 vignere_decoder.py /krypton/krypton4/found1 FREKEY
The decrypted text from found1 (formatted for readability):
The soldier with the green whiskers led them through the streets of the Emerald City until they reached the room where the Guardian of the Gates lived. This officer unlocked their spectacles to put them back in his great box, and then he politely opened the gate for our friends. "Which road leads to the Wicked Witch of the West?" asked Dorothy.
"There is no road," answered the Guardian of the Gates. "No one ever wishes to go that way."
"How then are we to find her?" inquired the girl.
"That will be easy," replied the man, "for when she knows you are in the country of the Winkies she will find you and make you all her slaves."
"Perhaps not," said the Scarecrow, "for we mean to destroy her."
"Oh, that is different," said the Guardian of the Gates. "No one has ever destroyed her before, so naturally I thought she would make slaves of you, as she has of the rest. But take care, for she is wicked and fierce, and may not allow you to destroy her. Keep to the west, where the sun sets, and you cannot fail to find her."
They thanked him and bade him goodbye and turned toward the west, walking over fields of soft grass dotted here and there with daisies and buttercups.
Dorothy still wore the pretty silk dress she had put on in the palace, but now, to her surprise, she found it was no longer green but pure white. The ribbon around Toto's neck had also lost its green color and was as white as Dorothy's dress.
The Emerald City was soon left far behind as they advanced. The ground became rougher and hillier, for there were no farms nor houses in this country of the West, and the ground was untilled.
In the afternoon the sun shone hot in their faces, for there were not trees to offer them shade, so that before night Dorothy and Toto and the Lion were tired and lay down upon the grass and fell asleep, with the Woodman and the Scarecrow keeping watch.
This is a passage from "The Wonderful Wizard of Oz" by L. Frank Baum.
Specifically, this is the scene where Dorothy and her companions leave the Emerald City and head west to find the Wicked Witch of the West. The Guardian of the Gates warns them that she's dangerous and no one has ever defeated her. Dorothy's dress turns white as they leave the green city behind.
A classic piece of American literature. Hidden behind a Vigenère cipher. And now fully readable.
Now let's decrypt found2:
python3 vignere_decoder.py /krypton/krypton4/found2 FREKEYpython3 vignere_decoder.py /krypton/krypton4/found2 FREKEY
Another passage from the same book. This one picks up later in the journey, where Dorothy and her friends are camping in the forest:
They were obliged to camp out that night under a large tree in the forest, for there were no houses near. The tree made a good, thick covering to protect them from the dew, and the Tin Woodman chopped a great pile of wood with his axe…
The story continues with Dorothy and Toto eating the last of their bread, the Lion going into the forest, the Scarecrow collecting nuts, and then, the travelers encounter a massive ditch that seems to cut off their journey entirely. The Cowardly Lion offers to jump it with each of them on his back, one at a time.
Same story. Same key. Two intercepted messages. Both now fully decrypted.
This is exactly the scenario the level is simulating: an attacker intercepts multiple messages from the same source, all encrypted with the same key. The more ciphertext you have, the stronger your frequency analysis becomes. The plaintexts being in the same language gives you the statistical foundation to recover everything.
Why This Approach Works - The Math Behind Column Isolation
Let's make sure we fully understand what we did, because this technique is foundational.
In Vigenère with key length N:
- Position i is encrypted by
key[i mod N] - Position i+N is also encrypted by
key[i mod N](same key byte, one cycle later) - Position i+2N is also encrypted by
key[i mod N]
So if we extract all positions where index mod N == 0, we have a sequence that was ALL encrypted by key[0]. That's a Caesar cipher. One shift. One unknown.
In English, E is the most frequent letter at ~12%. In a Caesar-encrypted version of English, E becomes (E + key_byte) mod 26. So the most frequent character in our extracted column should be at position (4 + key_byte) mod 26.
To recover the key byte: key_byte = (most_frequent_index - 4) mod 26
This works because:
- We have enough ciphertext (found1 and found2 are long)
- The underlying language has a predictable frequency distribution
- The column extraction isolates a pure Caesar cipher
The longer your ciphertext, the more reliably the frequency distribution matches the expected English pattern. A column of 5 characters is noise. A column of 50+ characters starts to show the E → most_frequent pattern clearly.
The Complete Attack Chain - Level 4 at a Glance
1. Know the key length: 6 (given by the level)
2. For each column i (0 to 5):
vignere_shift.py found1 6 i > found1-shiftN
freq_analysis.py found1-shiftN 1
key[i] = (most_frequent_index - 4) mod 26
3. Key recovered: F R E K E Y
4. vignere_decoder.py krypton5 FREKEY → CLEARTEXT
Password: CLEARTEXT1. Know the key length: 6 (given by the level)
2. For each column i (0 to 5):
vignere_shift.py found1 6 i > found1-shiftN
freq_analysis.py found1-shiftN 1
key[i] = (most_frequent_index - 4) mod 26
3. Key recovered: F R E K E Y
4. vignere_decoder.py krypton5 FREKEY → CLEARTEXT
Password: CLEARTEXTQuick Reference — Running Totals Across All Levels
Level 0: echo <base64> | base64 -d → KRYPTONISGREAT
Level 1: cat krypton2 | tr 'A-Z' 'N-ZA-M' → ROTTEN
Level 2: encrypt ABCDEF... → cipher alphabet, shift=12
cat krypton3 | tr '[M-ZA-L]' '[A-Z]' → CAESARISEASY
Level 3: freq_analysis found1 3 → JDS=THE, key_len=3
Iterative tr substitution → BRUTE
Level 4: vignere_shift found1 6 N → 6 columns
freq_analysis per column → FREKEY
vignere_decoder krypton5 FREKEY → CLEARTEXTLevel 0: echo <base64> | base64 -d → KRYPTONISGREAT
Level 1: cat krypton2 | tr 'A-Z' 'N-ZA-M' → ROTTEN
Level 2: encrypt ABCDEF... → cipher alphabet, shift=12
cat krypton3 | tr '[M-ZA-L]' '[A-Z]' → CAESARISEASY
Level 3: freq_analysis found1 3 → JDS=THE, key_len=3
Iterative tr substitution → BRUTE
Level 4: vignere_shift found1 6 N → 6 columns
freq_analysis per column → FREKEY
vignere_decoder krypton5 FREKEY → CLEARTEXTThe Scripts Are Yours to Keep
From this point forward, every level in this series uses the same toolkit from Part 1. No new tools. No rewrites. Just different inputs, different key lengths, and increasingly clever attacks.
The four scripts freq_analysis.py, keyLength.py, vignere_shift.py, vignere_decoder.py are available on GitHub with the signature header from Part 1. Grab them once, put them in your toolkit, and they'll serve you across all remaining Krypton levels.
What changes in the upcoming parts:
- Level 5 → Level 6: Vigenère again, but with an unknown key length this time.
keyLength.pybecomes essential. - Level 6 → Level 7: We move away from classical ciphers entirely. Stream ciphers. A completely different kind of problem.
Those are separate articles. But the mindset stays the same every cipher leaks. Your job is to find the leak.
Closing
Level 4 was elegant in its simplicity. The hard part (finding key length) was already done for us. The technique (column isolation + Caesar frequency analysis) was clean and mechanical. The scripts did the heavy lifting. The result was clean.
But notice what we gained here that wasn't obvious in Level 3: by going column-by-column with vignere_shift.py, we didn't just break the cipher,
we recovered the actual key. FREKEY. We could now decrypt any message ever encrypted with this key. Not just the one in front of us. All of them.
That's the difference between "I solved this puzzle" and "I broke this system."
All automation scripts are on GitHub.
Stay tuned for Level 5. And as always take notes.