Technology
Solving the CIA Kryptos code in TypeScript (Part 4)
You can find all the code for this series on GitHub.
Kryptos 4 is, I repeat, unsolved. We’re very unlikely to solve it today. Enthusiastic codebreakers have been putting together clues and proposing theories for decades. At least half of them are like this:
But hey, since we’ve got some code ready to go, we might as well try.
Setup
All the code we’ve written so far is available on GitHub. I won’t repeat it here. If you want to play along, you can clone the repo and use ts-node
to run the files.
Kryptos 4
The source text of Kryptos 4 is:
OBKR
UOXOGHULBSOLIFBBWFLRVQQPRNGKSSO
TWTQSJQSSEKZZWATJKLUDIAWINFBNYP
VTTMZFPKWGDKZXTJCDIGKUHUAUEKCAR
Thanks to a few clues from the sculptor, we know the decrypted message includes the phrases “BERLINCLOCK” and “EASTNORTHEAST” at certain positions. With that knowledge, we can write a function to rate possible solutions:
export function ratePossibleK4Solution(attempt: string): number {
attempt = attempt.toUpperCase();
const reversed = attempt.split('').reverse().join('');
let score = 0;
if (attempt.includes('EASTNORTHEAST') || reversed.includes('EASTNORTHEAST')) {
score++;
}
if (
attempt.slice(21).startsWith('EASTNORTHEAST') ||
reversed.slice(21).startsWith('EASTNORTHEAST')
) {
score++;
}
if (attempt.includes('BERLINCLOCK') || reversed.includes('BERLINCLOCK')) {
score++;
}
if (
attempt.slice(63).startsWith('BERLINCLOCK') ||
reversed.slice(63).startsWith('BERLINCLOCK')
) {
score++;
}
return score;
}
A score of 4 would be incredible, but even 2 would be exciting. As I’ve written code for Kryptos 1-3, I’ve made several little mistakes that sometimes resulted in a decrypted but offset message, like VEWATERILO
instead of ILOVEWATER
. So getting the hinted phrases to turn up at all could turn out to be a complete solution.
Attempt 1
Let’s try all the ciphers we’ve built so far:
- Vigenere with a standard alphabet
- Vigenere with the special KRYPTOS alphabet
- Keyed columnar tranposition
- Route transposition
- Routed keyed columnar transposition
const results = [];
// VIGENERE > PALIMPSEST
const vigStandard1 = vigDecrypt(encryptedK4, 'PALIMPSEST', standardAlphabet);
results.push({
description: 'Vigenere | PALIMPSEST | Standard alphabet',
score: ratePossibleK4Solution(vigStandard1),
decrypted: vigStandard1,
});
const vigReversed1 = vigEncrypt(encryptedK4, 'PALIMPSEST', standardAlphabet);
results.push({
description: 'Vigenere (reversed) | PALIMPSEST | Standard alphabet',
score: ratePossibleK4Solution(vigReversed1),
decrypted: vigReversed1,
});
const vigKryptos1 = vigDecrypt(
encryptedK4,
'PALIMPSEST',
generateAlphabet('KRYPTOS')
);
results.push({
description: 'Vigenere | PALIMPSEST | KRYPTOS alphabet',
score: ratePossibleK4Solution(vigKryptos1),
decrypted: vigKryptos1,
});
const vigReversedKryptos1 = vigEncrypt(
encryptedK4,
'PALIMPSEST',
generateAlphabet('KRYPTOS')
);
results.push({
description: 'Vigenere (reversed) | PALIMPSEST | KRYPTOS alphabet',
score: ratePossibleK4Solution(vigReversedKryptos1),
decrypted: vigReversedKryptos1,
});
// VIGENERE > ABSCISSA
const vigStandard2 = vigDecrypt(encryptedK4, 'ABSCISSA', standardAlphabet);
results.push({
description: 'Vigenere | ABSCISSA | Standard alphabet',
score: ratePossibleK4Solution(vigStandard2),
decrypted: vigStandard2,
});
const vigReversed2 = vigEncrypt(encryptedK4, 'ABSCISSA', standardAlphabet);
results.push({
description: 'Vigenere (reversed) | ABSCISSA | Standard alphabet',
score: ratePossibleK4Solution(vigReversed2),
decrypted: vigReversed2,
});
const vigKryptos2 = vigDecrypt(
encryptedK4,
'ABSCISSA',
generateAlphabet('KRYPTOS')
);
results.push({
description: 'Vigenere | ABSCISSA | KRYPTOS alphabet',
score: ratePossibleK4Solution(vigKryptos2),
decrypted: vigKryptos2,
});
const vigReversedKryptos2 = vigEncrypt(
encryptedK4,
'ABSCISSA',
generateAlphabet('KRYPTOS')
);
results.push({
description: 'Vigenere (reversed) | ABSCISSA | KRYPTOS alphabet',
score: ratePossibleK4Solution(vigReversedKryptos2),
decrypted: vigReversedKryptos2,
});
// COLUMNAR > KRYPTOS
const columnar1 = columnarDecrypt(encryptedK4, 'KRYPTOS');
results.push({
description: 'Keyed columnar | KRYPTOS',
score: ratePossibleK4Solution(columnar1),
decrypted: columnar1,
});
const columnarReversed1 = columnarEncrypt(encryptedK4, 'KRYPTOS');
results.push({
description: 'Keyed columnar (reversed) | KRYPTOS',
score: ratePossibleK4Solution(columnarReversed1),
decrypted: columnarReversed1,
});
// ROUTE > (86, 7)
const route1 = routeDecrypt(encryptedK4, 86, 7);
results.push({
description: 'Route | 86,7',
score: ratePossibleK4Solution(route1),
decrypted: route1,
});
const routeReversed1 = routeEncrypt(encryptedK4, 86, 7);
results.push({
description: 'Route (reversed) | 86,7',
score: ratePossibleK4Solution(routeReversed1),
decrypted: routeReversed1,
});
// ROUTED COLUMNAR > (86, KRYPTOS)
const routedColumnar1 = routedColumnarDecrypt(encryptedK4, 86, 'KRYPTOS');
results.push({
description: 'Routed columnar | 86,KRYPTOS',
score: ratePossibleK4Solution(routedColumnar1),
decrypted: routedColumnar1,
});
const routedColumnarReversed1 = routedColumnarEncrypt(encryptedK4, 86, 'KRYPTOS');
results.push({
description: 'Routed columnar (reversed) | 86,KRYPTOS',
score: ratePossibleK4Solution(routedColumnarReversed1),
decrypted: routedColumnarReversed1,
});
// LOG RESULTS
console.table(
results.map((result) => ({
...result,
decrypted: result.decrypted.slice(0, 15) + '...',
}))
);
That’s 14 swings and 14 misses:
┌─────────┬────────────────────────────────────────────────────────┬───────┬──────────────────────┐
│ (index) │ description │ score │ decrypted │
├─────────┼────────────────────────────────────────────────────────┼───────┼──────────────────────┤
│ 0 │ 'Vigenere | PALIMPSEST | Standard alphabet' │ 0 │ 'ZBZJIZFKOOFLQKC...' │
│ 1 │ 'Vigenere (reversed) | PALIMPSEST | Standard alphabet' │ 0 │ 'DBVZGDPSYAJLMAA...' │
│ 2 │ 'Vigenere | PALIMPSEST | KRYPTOS alphabet' │ 0 │ 'YRCFPYMQADMDLLG...' │
│ 3 │ 'Vigenere (reversed) | PALIMPSEST | KRYPTOS alphabet' │ 0 │ 'BILJGBTJNMXXZUW...' │
│ 4 │ 'Vigenere | ABSCISSA | Standard alphabet' │ 0 │ 'OASPMWFOGGCJTAW...' │
│ 5 │ 'Vigenere (reversed) | ABSCISSA | Standard alphabet' │ 0 │ 'OCCTCGPOGIMNJKG...' │
│ 6 │ 'Vigenere | ABSCISSA | KRYPTOS alphabet' │ 0 │ 'XKQMSZMXSSIBNKZ...' │
│ 7 │ 'Vigenere (reversed) | ABSCISSA | KRYPTOS alphabet' │ 0 │ 'FJSDDETFQVRKWFE...' │
│ 8 │ 'Keyed columnar | KRYPTOS' │ 0 │ 'OSDRMOIBSINZLAK...' │
│ 9 │ 'Keyed columnar (reversed) | KRYPTOS' │ 0 │ 'OOOFRTSAINZKIUO...' │
│ 10 │ 'Route | 86,7' │ 0 │ 'OLGSIMCKFSKWFIU...' │
│ 11 │ 'Route (reversed) | 86,7' │ 0 │ 'OKOKOFRTSAINZKI...' │
│ 12 │ 'Routed columnar | 86,KRYPTOS' │ 0 │ 'RDOTPJJCLBVQXQE...' │
│ 13 │ 'Routed columnar (reversed) | 86,KRYPTOS' │ 0 │ 'OKOKOFRTSAINZKI...' │
└─────────┴────────────────────────────────────────────────────────┴───────┴──────────────────────┘
Attempt 2
What if we try all three ciphers in sequence?
const results = [];
// K1-K3 forwards
(function () {
const vigKryptos1 = vigDecrypt(
encryptedK4,
'PALIMPSEST',
generateAlphabet('KRYPTOS')
);
const vigKryptos2 = vigDecrypt(
vigKryptos1,
'ABSCISSA',
generateAlphabet('KRYPTOS')
);
const routedColumnar = routedColumnarDecrypt(vigKryptos2, 86, 'KRYPTOS');
results.push({
description: 'K1-K3 ciphers',
score: ratePossibleK4Solution(routedColumnar),
decrypted: routedColumnar,
});
})();
// K3-K1 backwards
(function () {
const routedColumnar = routedColumnarEncrypt(encryptedK4, 86, 'KRYPTOS');
const vigKryptosReversed2 = vigEncrypt(
routedColumnar,
'ABSCISSA',
generateAlphabet('KRYPTOS')
);
const vigKryptosReversed1 = vigEncrypt(
vigKryptosReversed2,
'PALIMPSEST',
generateAlphabet('KRYPTOS')
);
results.push({
description: 'K3-K1 ciphers (reversed)',
score: ratePossibleK4Solution(vigKryptosReversed1),
decrypted: vigKryptosReversed1,
});
})();
// LOG RESULTS
console.table(
results.map((result) => ({
...result,
decrypted: result.decrypted.slice(0, 15) + '...',
}))
);
No luck!
┌─────────┬────────────────────────────┬───────┬──────────────────────┐
│ (index) │ description │ score │ decrypted │
├─────────┼────────────────────────────┼───────┼──────────────────────┤
│ 0 │ 'K1-K3 ciphers' │ 0 │ 'HKACCRSEVYDCGBO...' │
│ 1 │ 'K3-K1 ciphers (reversed)' │ 0 │ 'BMKUWQJOHRJTQUH...' │
└─────────┴────────────────────────────┴───────┴──────────────────────┘
Attempt 3
All right, last one. Let’s try each possible combination of two ciphers from (K1, K2, K3).
const results = [];
// Vigenere PALIMPSEST + Vigenere ABSCISSA
(function () {
const vigPalimpsest = vigDecrypt(encryptedK4, 'PALIMPSEST', generateAlphabet('KRYPTOS'));
const vigAbscissa = vigDecrypt(vigPalimpsest, 'ABSCISSA', generateAlphabet('KRYPTOS'));
results.push({
description: 'K1 + K2 ciphers',
score: ratePossibleK4Solution(vigAbscissa),
decrypted: vigAbscissa,
});
})();
(function () {
const vigAbscissa = vigEncrypt(encryptedK4, 'ABSCISSA', generateAlphabet('KRYPTOS'));
const vigPalimpsest = vigEncrypt(vigAbscissa, 'PALIMPSEST', generateAlphabet('KRYPTOS'));
results.push({
description: 'K2 + K1 ciphers (reversed)',
score: ratePossibleK4Solution(vigPalimpsest),
decrypted: vigPalimpsest,
});
})();
// Vigenere PALIMPSEST + routed columnar
(function () {
const vigKryptos = vigDecrypt(encryptedK4, 'PALIMPSEST', generateAlphabet('KRYPTOS'));
const routedColumnar = routedColumnarDecrypt(vigKryptos, 86, 'KRYPTOS');
results.push({
description: 'K1 + K3 ciphers',
score: ratePossibleK4Solution(routedColumnar),
decrypted: routedColumnar,
});
})();
(function () {
const routedColumnar = routedColumnarEncrypt(encryptedK4, 86, 'KRYPTOS');
const vigKryptos = vigEncrypt(routedColumnar, 'PALIMPSEST', generateAlphabet('KRYPTOS'));
results.push({
description: 'K3 + K1 ciphers (reversed)',
score: ratePossibleK4Solution(vigKryptos),
decrypted: vigKryptos,
});
})();
// Vigenere ABSCISSA + routed columnar
(function () {
const vigKryptos = vigDecrypt(encryptedK4, 'ABSCISSA', generateAlphabet('KRYPTOS'));
const routedColumnar = routedColumnarDecrypt(vigKryptos, 86, 'KRYPTOS');
results.push({
description: 'K2 + K3 ciphers',
score: ratePossibleK4Solution(routedColumnar),
decrypted: routedColumnar,
});
})();
(function () {
const routedColumnar = routedColumnarEncrypt(encryptedK4, 86, 'KRYPTOS');
const vigKryptos = vigEncrypt(routedColumnar, 'ABSCISSA', generateAlphabet('KRYPTOS'));
results.push({
description: 'K3 + K2 ciphers (reversed)',
score: ratePossibleK4Solution(vigKryptos),
decrypted: vigKryptos,
});
})();
// LOG RESULTS
console.table(
results.map((result) => ({
...result,
decrypted: result.decrypted.slice(0, 15) + '...',
}))
);
And as you might have predicted: no cigar.
┌─────────┬──────────────────────────────┬───────┬──────────────────────┐
│ (index) │ description │ score │ decrypted │
├─────────┼──────────────────────────────┼───────┼──────────────────────┤
│ 0 │ 'K1 + K2 ciphers' │ 0 │ 'UNPPHVFGKYFRYEA...' │
│ 1 │ 'K2 + K1 ciphers (reversed)' │ 0 │ 'IWWZYHDWKKTAHRP...' │
│ 2 │ 'K1 + K3 ciphers' │ 0 │ 'UAGXMCGLYLJLQHQ...' │
│ 3 │ 'K3 + K1 ciphers (reversed)' │ 0 │ 'RDQFBHDXANDUOIB...' │
│ 4 │ 'K2 + K3 ciphers' │ 0 │ 'QPZIQBCPENJFLHV...' │
│ 5 │ 'K3 + K2 ciphers (reversed)' │ 0 │ 'OECSOLDQBWGWPSV...' │
└─────────┴──────────────────────────────┴───────┴──────────────────────┘
Bummer! There’s still an infinity of untried possibilities, though: different keys, different vigenere alphabets, different orders of operations, different ciphers altogether. Someday a lucky codebreaker will manage to brute-force it. Go ahead and clone the repo if you want to give it a try.
Best of luck!