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!