DEV Community

Cover image for Solving the CIA Kryptos Code (Part 1)
Isaac Lyman
Isaac Lyman

Posted on • Edited on • Originally published at isaaclyman.com

Solving the CIA Kryptos Code (Part 1)

You can find all the code for this series on GitHub.

On the campus of CIA headquarters in Virginia, United States, there's a 34-year-old sculpture called Kryptos. It somewhat resembles a heavy copper flag waving in the wind. Cut out of its surface are 1,736 uppercase ASCII characters split into four encrypted messages and a Vigenere cipher table.

The first three messages were decrypted in the 1990s, first by the NSA and CIA, who didn't publicize their success, and then by a private-sector computer scientist, who did. To this day, the fourth message hasn't been cracked.

This raises a few interesting points. First, the U.S. government paid a quarter of a million dollars to have secret messages installed on their property, then paid at least two different teams of codebreakers to figure out what the messages said. I can't explain why, but I find that funny. Second, some or several government agencies probably have solved the fourth cipher and just haven't bothered to let us know, partially because secret-keeping is the modus operandi of the NSA/CIA, and partially because it's free advertising—they can get a wave of public interest (and a handful of new recruits) every few years just by dropping another "clue." And third, we don't know for certain that the fourth message doesn't include the phrase "CIA SUX" or "HOMER SIMPSON FOR PRESIDENT". It probably doesn't, but it could.

Anyway, let's write some TypeScript code capable of decrypting and encrypting the first three messages, then make an attempt to solve the fourth one. (Keep your expectations nice and low, I don't have a Ph.D. in cryptanalysis.)

Getting started: Basic vigenere

The first Kryptos message uses a Vigenere cipher. A standard Vigenere is pretty straightforward: you assign the numbers 0-25 to the letters A-Z, such that A=0, B=1, C=2, and so on. You translate your source message into these numbers. Then you pick a decryption key; any sequence of letters will do. You also translate the decryption key into numbers. And finally, for each index of the source message, you go to the same index in the decryption key and add both numbers together, then translate back to a letter. When you have a number greater than 25, you loop back around to 0—that is, modulo 26.

For all you visual learners, here's how you'd translate "ILOVEWATER" with the key "HYDRATE":

Message I L O V E W A T E R
a=0 number (A) 8 11 14 21 4 22 0 19 4 17
Key H Y D R A T E H Y D
a=0 number (B) 7 24 3 17 0 19 4 7 24 3
Add A + B 15 35 17 38 4 41 4 26 28 20
Modulo 26 15 9 17 12 4 15 4 0 2 20
0=a letters P J R M E P E A C U

So the encrypted message is PJRMEPEACU. To decrypt it, you'd go the opposite direction:

Encrypted message P J R M E P E A C U
a=0 number (C) 15 9 17 12 4 15 4 0 2 20
Key H Y D R A T E H Y D
a=0 number (D) 7 24 3 17 0 19 4 7 24 3
Subtract C - D 8 -15 14 -5 4 -4 0 -7 -22 17
If negative, add 26 8 11 14 21 4 22 0 19 4 17
0=a letters I L O V E W A T E R

As you can see, the Vigenere cipher isn't particularly strong, but most people wouldn't be able to crack it without the key.

Here's how you'd encrypt and decrypt in TypeScript:

const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';

function vigEncrypt(message: string, key: string): string {
  const messageChars = message.toUpperCase().split('');


  // For each character in the message...
  let messageIx = 0;
  const encryptedChars = messageChars.map((messageChar) => {
    // Traditionally you'd remove spaces and symbols before encrypting to
    //  make it harder to crack, but you don't have to.
    if (!alphabet.includes(messageChar)) {
      return messageChar;
    }

    // Find the same index in the key, looping around if needed
    const keyIx = messageIx % key.length;
    messageIx++;
    // Get the character at that index
    const keyChar = key[keyIx];
    // Find its "a=0" number
    const keyNumber = alphabet.indexOf(keyChar);
    // Find the "a=0" number of the original message character as well
    const messageNumber = alphabet.indexOf(messageChar);
    // Add the numbers together, looping around if >26
    const encryptedNumber = (messageNumber + keyNumber) % 26;
    // Get the letter corresponding to that number
    const encryptedChar = alphabet[encryptedNumber];
    // That's our encrypted character!
    return encryptedChar;
  });

  // Join all the encrypted characters together into one message
  return encryptedChars.join('');
}

function vigDecrypt(encrypted: string, key: string): string {
  const encryptedChars = encrypted.toUpperCase().split('');

  // Now we simply reverse what we did above
  let encryptedIx = 0;
  const messageChars = encryptedChars.map((encryptedChar) => {
    if (!alphabet.includes(encryptedChar)) {
      return encryptedChar;
    }

    const keyIx = encryptedIx % key.length;
    encryptedIx++;
    const keyChar = key[keyIx];
    const keyNumber = alphabet.indexOf(keyChar);
    const encryptedNumber = alphabet.indexOf(encryptedChar);
    let messageNumber = encryptedNumber - keyNumber;
    // Special handling for negative numbers...
    if (messageNumber < 0) {
      messageNumber = alphabet.length + messageNumber;
    }

    const messageChar = alphabet[messageNumber];
    return messageChar;
  });

  // Join all the decrypted characters together into one message
  return messageChars.join('');
}

const sourceMessage = 'PROGRAMMINGISFUN';
const key = 'EXCEPTFORJAVA';

const encrypted = vigEncrypt(sourceMessage, key);
console.log(encrypted); // > TOQKGTRAZWGDSJRP

const decrypted = vigDecrypt(encrypted, key);
console.log(decrypted); // > PROGRAMMINGISFUN
Enter fullscreen mode Exit fullscreen mode

Nothing too crazy, right? But we're not there yet—Kryptos 1 has a trick up its sleeve.

Kryptos 1: Advanced vigenere

If you were sending an encrypted message and wanted to stump someone who's already familiar with Vigenere ciphers, what could you change to throw them off? You could pick a longer key or mess with the math, but Kryptos does something even more fun: it screws up the alphabet.

The Kryptos alphabet is KRYPTOSABCDEFGHIJLMNQUVWXZ. That is, you start with the word KRYPTOS, then go through the alphabet from A to Z, skipping any letters that were already present in KRYPTOS. This means there's effectively a second key: the key that displaces and rearranges the alphabet. We can swap out our alphabet in the code above to build a Kryptos 1 encoder/decoder.

The text of Kryptos 1 is:

EMUFPHZLRFAXYUSDJKZLDKRNSHGNFIVJ
YQTQUXQBQVYUVLLTREVJYQTMKYRDMFD
Enter fullscreen mode Exit fullscreen mode

And the key is "palimpsest". Let's plug this into our TypeScript.

const alphabet = 'KRYPTOSABCDEFGHIJLMNQUVWXZ';

function vigDecrypt(encrypted: string, key: string): string {
  const encryptedChars = encrypted.toUpperCase().split('');

  let encryptedIx = 0;
  const messageChars = encryptedChars.map((encryptedChar) => {
    if (!alphabet.includes(encryptedChar)) {
      return encryptedChar;
    }

    const keyIx = encryptedIx % key.length;
    encryptedIx++;
    const keyChar = key[keyIx];
    const keyNumber = alphabet.indexOf(keyChar);
    const encryptedNumber = alphabet.indexOf(encryptedChar);
    let messageNumber = encryptedNumber - keyNumber;

    if (messageNumber < 0) {
      messageNumber = alphabet.length + messageNumber;
    }

    const messageChar = alphabet[messageNumber];
    return messageChar;
  });

  return messageChars.join('');
}

const encrypted = `EMUFPHZLRFAXYUSDJKZLDKRNSHGNFIVJ
YQTQUXQBQVYUVLLTREVJYQTMKYRDMFD`;
const key = 'PALIMPSEST';

const decrypted = vigDecrypt(encrypted, key);
console.log(decrypted);
/* >
BETWEENSUBTLESHADINGANDTHEABSENC
EOFLIGHTLIESTHENUANCEOFIQLUSION
*/
Enter fullscreen mode Exit fullscreen mode

The message is "BETWEEN SUBTLE SHADING AND THE ABSENCE OF LIGHT LIES THE NUANCE OF IQLUSION". The last word is an intentional misspelling of "ILLUSION".

What if we want to generate the special Kryptos alphabet instead of hard-coding it? That's easy enough:

const alphabetKey = 'KRYPTOS';
const standardAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const alphabet = alphabetKey + standardAlphabet.split('').filter(ch => !alphabetKey.includes(ch)).join('');

function vigDecrypt(encrypted: string, key: string): string {
  const encryptedChars = encrypted.toUpperCase().split('');

  let encryptedIx = 0;
  const messageChars = encryptedChars.map((encryptedChar) => {
    if (!alphabet.includes(encryptedChar)) {
      return encryptedChar;
    }

    const keyIx = encryptedIx % key.length;
    encryptedIx++;
    const keyChar = key[keyIx];
    const keyNumber = alphabet.indexOf(keyChar);
    const encryptedNumber = alphabet.indexOf(encryptedChar);
    let messageNumber = encryptedNumber - keyNumber;

    if (messageNumber < 0) {
      messageNumber = alphabet.length + messageNumber;
    }

    const messageChar = alphabet[messageNumber];
    return messageChar;
  });

  return messageChars.join('');
}

const encrypted = `EMUFPHZLRFAXYUSDJKZLDKRNSHGNFIVJ
YQTQUXQBQVYUVLLTREVJYQTMKYRDMFD`;
const key = 'PALIMPSEST';

const decrypted = vigDecrypt(encrypted, key);
console.log(decrypted);
/* >
BETWEENSUBTLESHADINGANDTHEABSENC
EOFLIGHTLIESTHENUANCEOFIQLUSION
*/
Enter fullscreen mode Exit fullscreen mode

Only a couple of extra lines, and now we can play with different alphabet keys and cipher keys.

Kryptos 2: Vigenere (again)

Kryptos 2 is encrypted using the same method as Kryptos 1. The only difference is the key: instead of "palimpsest", it uses "abscissa".

The text of Kryptos 2 is:

VFPJUDEEHZWETZYVGWHKKQETGFQJNCE
GGWHKK?DQMCPFQZDQMMIAGPFXHQRLG
TIMVMZJANQLVKQEDAGDVFRPJUNGEUNA
QZGZLECGYUXUEENJTBJLBQCRTBJDFHRR
YIZETKZEMVDUFKSJHKFWHKUWQLSZFTI
HHDDDUVH?DWKBFUFPWNTDFIYCUQZERE
EVLDKFEZMOQQJLTTUGSYQPFEUNLAVIDX
FLGGTEZ?FKZBSFDQVGOGIPUFXHHDRKF
FHQNTGPUAECNUVPDJMQCLQUMUNEDFQ
ELZZVRRGKFFVOEEXBDMVPNFQXEZLGRE
DNQFMPNZGLFLPMRJQYALMGNUVPDXVKP
DQUMEBEDMHDAFMJGZNUPLGEWJLLAETG
Enter fullscreen mode Exit fullscreen mode

Let's plug it in!

const alphabetKey = 'KRYPTOS';
const standardAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const alphabet = alphabetKey + standardAlphabet.split('').filter(ch => !alphabetKey.includes(ch)).join('');

function vigDecrypt(encrypted: string, key: string): string {
  const encryptedChars = encrypted.toUpperCase().split('');

  let encryptedIx = 0;
  const messageChars = encryptedChars.map((encryptedChar) => {
    if (!alphabet.includes(encryptedChar)) {
      return encryptedChar;
    }

    const keyIx = encryptedIx % key.length;
    encryptedIx++;
    const keyChar = key[keyIx];
    const keyNumber = alphabet.indexOf(keyChar);
    const encryptedNumber = alphabet.indexOf(encryptedChar);
    let messageNumber = encryptedNumber - keyNumber;

    if (messageNumber < 0) {
      messageNumber = alphabet.length + messageNumber;
    }

    const messageChar = alphabet[messageNumber];
    return messageChar;
  });

  return messageChars.join('');
}

const encrypted = `VFPJUDEEHZWETZYVGWHKKQETGFQJNCE
GGWHKK?DQMCPFQZDQMMIAGPFXHQRLG
TIMVMZJANQLVKQEDAGDVFRPJUNGEUNA
QZGZLECGYUXUEENJTBJLBQCRTBJDFHRR
YIZETKZEMVDUFKSJHKFWHKUWQLSZFTI
HHDDDUVH?DWKBFUFPWNTDFIYCUQZERE
EVLDKFEZMOQQJLTTUGSYQPFEUNLAVIDX
FLGGTEZ?FKZBSFDQVGOGIPUFXHHDRKF
FHQNTGPUAECNUVPDJMQCLQUMUNEDFQ
ELZZVRRGKFFVOEEXBDMVPNFQXEZLGRE
DNQFMPNZGLFLPMRJQYALMGNUVPDXVKP
DQUMEBEDMHDAFMJGZNUPLGEWJLLAETG`;
const key = 'ABSCISSA';

const decrypted = vigDecrypt(encrypted, key);
console.log(decrypted);
/* >
ITWASTOTALLYINVISIBLEHOWSTHATPO
SSIBLE?THEYUSEDTHEEARTHSMAGNET
ICFIELDXTHEINFORMATIONWASGATHER
EDANDTRANSMITTEDUNDERGRUUNDTOANU
NKNOWNLOCATIONXDOESLANGLEYKNOWA
BOUTTHIS?THEYSHOULDITSBURIEDOUT
THERESOMEWHEREXWHOKNOWSTHEEXACTL
OCATION?ONLYWWTHISWASHISLASTMES
SAGEXTHIRTYEIGHTDEGREESFIFTYSE
VENMINUTESSIXPOINTFIVESECONDSNO
RTHSEVENTYSEVENDEGREESEIGHTMINU
TESFORTYFOURSECONDSWESTIDBYROWS
*/
Enter fullscreen mode Exit fullscreen mode

The message is:

IT WAS TOTALLY INVISIBLE HOWS THAT POSSIBLE?
THEY USED THE EARTHS MAGENTIC FIELD X
THE INFORMATION WAS GATHERED AND TRANSMITTED UNDERGRUUND
TO AN UNKNOWN LOCATION X
DOES LANGLEY KNOW ABOUT THIS?
THEY SHOULD ITS BURIED OUT THERE SOMEWHERE X
WHO KNOWS THE EXACT LOCATION?
ONLY WW THIS WAS HIS LAST MESSAGE X
THIRTY EIGHT DEGREES FIFTY SEVEN MINUTES
SIX POINT FIVE SECONDS NORTH
SEVENTY SEVEN DEGREES EIGHT MINUTES FOURTY FOUR SECONDS WEST
ID BY ROWS
Enter fullscreen mode Exit fullscreen mode

However, it's now known that the sculptor left out a letter for aesthetic reasons. The last snippet EWJLLAETG should be ESWJLLAETG, so the last line of the message is "X LAYER TWO", not "ID BY ROWS". Not that it makes the message any less cryptic.

Ready for part 2? It continues in the next post.

Acknowledgments

Thanks to the following, who made this series possible:

Top comments (0)