Posted on 7 mins read

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:

Meme of Charlie Day in slacks and a tie, looking over-caffeinated and obsessive, standing next to a corkboard covered with computer printouts and red twine

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!