Solving the CIA Kryptos Code (Part 3)
Tags: cryptography, cryptanalysis, typescript
Published: 2024-01-26 22:54:00

---
title: Solving the CIA Kryptos Code (Part 3)
published: true
description: 
tags: cryptography, cryptanalysis, typescript
series: Solving the CIA Kryptos code
canonical_url: https://isaaclyman.com/blog/posts/kryptos-3/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8uqzn666fv3ue94f2kie.jpg
published_at: 2024-01-26 22:54 +0000
---

_You can find all the code for this series [on GitHub](https://github.com/isaaclyman/kryptos-typescript)._

Kryptos 3 is going to require more code than anything else so far. It's encoded using both the types of transposition we covered in Part 2, plus a twist.

1. It's written out with a rectangle size of 86 and a block size of 7.
2. The columns are stacked.
3. A keyed columnar cipher is applied with the key 'KRYPTOS'. (KRYPTOS is seven characters long, which is why our block size is 7.)
4. The route transposition is completed: each line is read top to bottom, from the leftmost characters to the rightmost characters.
5. The encrypted message is reversed. (That's the twist.)

We'll need to combine both pieces of transposition code to make this happen.

## Updating the route transposition code

You'll notice the columnar transposition is (rudely) inserted right in the middle of the route transposition! We need to break the route transposition into steps: one step to arrange the message in stacked columns, then another step to read it out (and vice versa).

```ts
function chunk<T>(array: T[], chunkSize: number): T[][] {
  const result: T[][] = [];
  for (let i = 0; i < array.length; i += chunkSize) {
      const chunk = array.slice(i, i + chunkSize);
      result.push(chunk);
  }
  return result;
}

function routeEncryptArrange(message: string, rectangleSize: number, blockSize: number): string[] {
  const messageChars = message.split('');
  const messageLines = chunk(messageChars, rectangleSize);
  const chunkedLines = messageLines.map(line => chunk(line, blockSize));

  const allChunks = chunkedLines.flat();
  const finalChunk = allChunks.pop();
  const mainChunkSizes = allChunks.map(chunk => chunk.length);
  const oddChunkSize = mainChunkSizes.find(chunkSize => chunkSize !== blockSize);

  if (
    typeof oddChunkSize === 'number' &&
    Array.isArray(finalChunk) &&
    finalChunk.length !== oddChunkSize &&
    finalChunk.length !== blockSize
  ) {
    if (finalChunk.length < oddChunkSize) {
      finalChunk.push(...'Q'.repeat(oddChunkSize - finalChunk.length));
    } else {
      finalChunk.push(...'Q'.repeat(blockSize - finalChunk.length));
    }
  }

  const stackedLines: string[][] = [];
  
  const blocksPerLine = Math.ceil(rectangleSize / blockSize);
  for (let blockIx = 0; blockIx < blocksPerLine; blockIx++) {
    const numberOfRows = chunkedLines.length;
    for (let rowIx = 0; rowIx < numberOfRows; rowIx++) {
      const encryptedRowIx = rowIx + (blockIx * numberOfRows);
      stackedLines[encryptedRowIx] = chunkedLines[rowIx][blockIx];
    }
  }

  return stackedLines.filter(line => Array.isArray(line)).map(line => line.join(''));
}

function routeEncryptReadOut(stackedLines: string[], blockSize: number): string {
  const encryptedChars: string[] = [];
  for (let charIx = 0; charIx < blockSize; charIx++) {
    for (let rowIx = 0; rowIx < stackedLines.length; rowIx++) {
      if (typeof stackedLines[rowIx] !== 'string') {
        continue;
      }

      const char = stackedLines[rowIx][charIx];
      if (typeof char === 'string') {
        encryptedChars.push(char);
      }
    }
  }
  
  return encryptedChars.join('');
}

function routeDecryptReadIn(encrypted: string, rectangleSize: number, blockSize: number): string[] {
  const numberOfRows = Math.ceil(encrypted.length / rectangleSize);
  const chunksPerRow = Math.ceil(rectangleSize / blockSize);
  const lastRowLength = encrypted.length % rectangleSize;
  const lastRowChunks = Math.ceil(lastRowLength / blockSize);
  const totalChunks = (chunksPerRow * (numberOfRows - 1)) + lastRowChunks;

  const lastChunkSize = lastRowLength % blockSize;
  const oddChunkSize = rectangleSize % blockSize;
  
  let numberOfShortVLines: number;
  if (oddChunkSize !== blockSize) {
    numberOfShortVLines = blockSize - oddChunkSize;
  } else if (lastChunkSize !== blockSize) {
    numberOfShortVLines = blockSize - lastChunkSize;
  } else {
    numberOfShortVLines = 0;
  }
  
  const numberOfLongVLines = blockSize - numberOfShortVLines;

  const longVLineLength = totalChunks;
  const shortVLineLength = totalChunks -
    ((oddChunkSize !== blockSize ? numberOfRows - 1 : 0) +
     (lastChunkSize !== blockSize ? 1 : 0));

  const stackedLines: string[][] = [];
  let encryptedIx = 0;

  for (let vLineIx = 0; vLineIx < blockSize; vLineIx++) {
    const currentVLineLength = vLineIx < numberOfLongVLines ?
      longVLineLength :
      shortVLineLength;

    for (let rowIx = 0; rowIx < currentVLineLength; rowIx++) {
      stackedLines[rowIx] ??= [];

      stackedLines[rowIx][vLineIx] = encrypted[encryptedIx++];
    }
  }

  return stackedLines.filter(line => Array.isArray(line)).map(line => line.join(''));
}

function routeDecryptUnarrange(stackedLines: string[], rectangleSize: number, blockSize: number): string {
  const messageLength = stackedLines.join('').length;
  const numberOfRows = Math.ceil(messageLength / rectangleSize);
  const blocksPerRow = Math.ceil(rectangleSize / blockSize);
  const lastRowLength = messageLength % rectangleSize;
  const lastRowBlocks = Math.ceil(lastRowLength / blockSize);

  const messageLines: string[][] = [];
  let stackedLineIx = 0;

  for (let columnIx = 0; columnIx < blocksPerRow; columnIx++) {
    const rowsInBlock = columnIx < lastRowBlocks ? numberOfRows : numberOfRows - 1;
    for (let rowIx = 0; rowIx < rowsInBlock; rowIx++) {
      messageLines[rowIx] ??= [];
      messageLines[rowIx].push(stackedLines[stackedLineIx++]);
    }
  }

  return messageLines.flat().join('');
}

const rectangleSize = 15;
const blockSize = 4

const arrangedToEncrypt = routeEncryptArrange(
  'THEFITNESSGRAMPACERTESTISAMULTISTAGEAEROBICCAPACITYTEST',
  rectangleSize,
  blockSize
);
const encrypted = routeEncryptReadOut(
  arrangedToEncrypt,
  blockSize
);
console.log(encrypted);
// > TAIPITGTSIRSAUCHCSATEEYSSOTMLCEETCNSATGABQPTAFRAIETEERMI

const arrangedToDecrypt = routeDecryptReadIn(
  encrypted,
  rectangleSize,
  blockSize
);
const decrypted = routeDecryptUnarrange(
  arrangedToDecrypt,
  rectangleSize,
  blockSize
);
console.log(decrypted);
// > THEFITNESSGRAMPACERTESTISAMULTISTAGEAEROBICCAPACITYTESTQ
```

## Updating the columnar transposition code

Now we need to modify the columnar transposition code so it can take an array of prearranged strings instead of a joined, unspaced string. You may have noticed how the columnar transposition and the route transposition have the same final step: reading out lines of characters from top to bottom, left to right. We can omit that step from the columnar transposition, since the second step of the route transposition will handle it.

```ts
function chunk<T>(array: T[], chunkSize: number): T[][] {
  const result: T[][] = [];
  for (let i = 0; i < array.length; i += chunkSize) {
      const chunk = array.slice(i, i + chunkSize);
      result.push(chunk);
  }
  return result;
}

function columnarEncrypt(stackedLines: string[], key: string): string[] {
  const keyChars = key.split('');
  const sortedKey = keyChars.toSorted();
  const positionNumbers = keyChars.map(char => sortedKey.indexOf(char));

  const encryptedRows: (string | null)[][] = Array(stackedLines.length)
    .fill(null)
    .map(_ => Array(key.length).fill(null).map(_ => null));

  for (const orderedColumnIx in keyChars) {
    const messageColumnIx = positionNumbers.indexOf(Number(orderedColumnIx));
    
    for (const rowIx in stackedLines) {
      encryptedRows[rowIx][orderedColumnIx] = stackedLines[rowIx][messageColumnIx] ?? ' ';
    }
  }

  return encryptedRows.map(row => row.join(''));
}

function columnarDecrypt(stackedLines: string[], key: string): string[] {
  const keyChars = key.split('');
  const sortedKey = keyChars.toSorted();
  const positionNumbers = keyChars.map(char => sortedKey.indexOf(char));

  const encryptedRows: (string | null)[][] = Array(stackedLines.length)
    .fill(null)
    .map(_ => Array(key.length).fill(null).map(_ => null));

  for (const messageColumnIx in keyChars) {
    const orderedColumnIx = positionNumbers.indexOf(Number(messageColumnIx));
    
    for (const rowIx in stackedLines) {
      encryptedRows[rowIx][orderedColumnIx] = stackedLines[rowIx][messageColumnIx] ?? ' ';
    }
  }

  return encryptedRows.map(row => row.join(''));
}

const key = 'HYDRATE';
const message = [
  'ILOVEWA',
  'TER'
]

const encrypted = columnarEncrypt(message, key);
console.log(encrypted);
// > ["EOAIVWL", " R T  E"]

const decrypted = columnarDecrypt(encrypted, key);
console.log(decrypted);
// > ["ILOVEWA", "TER    "]
```

Why are there spaces in this version? Because position is important. R has to be in the second column after encryption, not the first, or the decryption process will break.

## Putting it all together

It would be great if we could just call these functions in order to encrypt and decrypt Kryptos 3. However, there's one more obstacle. Since we're dealing with shuffled-up columns after the `columnarEncrypt` method is called, the longest vertical lines won't always be the leftmost ones. We need to inspect the key to know which lines are long and which ones are short. Check out `routeDecryptReadIn`:

```ts
function chunk<T>(array: T[], chunkSize: number): T[][] {
  const result: T[][] = [];
  for (let i = 0; i < array.length; i += chunkSize) {
      const chunk = array.slice(i, i + chunkSize);
      result.push(chunk);
  }
  return result;
}

function columnarEncrypt(stackedLines: string[], key: string): string[] {
  const keyChars = key.split('');
  const sortedKey = keyChars.toSorted();
  const positionNumbers = keyChars.map(char => sortedKey.indexOf(char));

  const encryptedRows: (string | null)[][] = Array(stackedLines.length)
    .fill(null)
    .map(_ => Array(key.length).fill(null).map(_ => null));

  for (const orderedColumnIx in keyChars) {
    const messageColumnIx = positionNumbers.indexOf(Number(orderedColumnIx));
    
    for (const rowIx in stackedLines) {
      encryptedRows[rowIx][orderedColumnIx] = stackedLines[rowIx][messageColumnIx] ?? ' ';
    }
  }

  return encryptedRows.map(row => row.join(''));
}

function columnarDecrypt(stackedLines: string[], key: string): string[] {
  const keyChars = key.split('');
  const sortedKey = keyChars.toSorted();
  const positionNumbers = keyChars.map(char => sortedKey.indexOf(char));

  const encryptedRows: (string | null)[][] = Array(stackedLines.length)
    .fill(null)
    .map(_ => Array(key.length).fill(null).map(_ => null));

  for (const messageColumnIx in keyChars) {
    const orderedColumnIx = positionNumbers.indexOf(Number(messageColumnIx));
    
    for (const rowIx in stackedLines) {
      encryptedRows[rowIx][orderedColumnIx] = stackedLines[rowIx][messageColumnIx] ?? ' ';
    }
  }

  return encryptedRows.map(row => row.join(''));
}

function routeEncryptArrange(message: string, rectangleSize: number, blockSize: number): string[] {
  const messageChars = message.split('');
  const messageLines = chunk(messageChars, rectangleSize);
  const chunkedLines = messageLines.map(line => chunk(line, blockSize));

  const allChunks = chunkedLines.flat();
  const finalChunk = allChunks.pop();
  const mainChunkSizes = allChunks.map(chunk => chunk.length);
  const oddChunkSize = mainChunkSizes.find(chunkSize => chunkSize !== blockSize);

  if (
    typeof oddChunkSize === 'number' &&
    Array.isArray(finalChunk) &&
    finalChunk.length !== oddChunkSize &&
    finalChunk.length !== blockSize
  ) {
    if (finalChunk.length < oddChunkSize) {
      finalChunk.push(...'Q'.repeat(oddChunkSize - finalChunk.length));
    } else {
      finalChunk.push(...'Q'.repeat(blockSize - finalChunk.length));
    }
  }

  const stackedLines: string[][] = [];
  
  const blocksPerLine = Math.ceil(rectangleSize / blockSize);
  for (let blockIx = 0; blockIx < blocksPerLine; blockIx++) {
    const numberOfRows = chunkedLines.length;
    for (let rowIx = 0; rowIx < numberOfRows; rowIx++) {
      const encryptedRowIx = rowIx + (blockIx * numberOfRows);
      stackedLines[encryptedRowIx] = chunkedLines[rowIx][blockIx];
    }
  }

  return stackedLines.filter(line => Array.isArray(line)).map(line => line.join(''));
}

function routeEncryptReadOut(stackedLines: string[], blockSize: number): string {
  const encryptedChars: string[] = [];
  for (let charIx = 0; charIx < blockSize; charIx++) {
    for (let rowIx = 0; rowIx < stackedLines.length; rowIx++) {
      if (typeof stackedLines[rowIx] !== 'string') {
        continue;
      }

      const char = stackedLines[rowIx][charIx];
      if (typeof char === 'string' && char !== ' ') {
        encryptedChars.push(char);
      }
    }
  }
  
  return encryptedChars.join('');
}

function routeDecryptReadIn(encrypted: string, rectangleSize: number, key: string): string[] {
  const numberOfRows = Math.ceil(encrypted.length / rectangleSize);
  const blockSize = key.length;
  const chunksPerRow = Math.ceil(rectangleSize / blockSize);
  const lastRowLength = encrypted.length % rectangleSize;
  const lastRowChunks = Math.ceil(lastRowLength / blockSize);
  const totalChunks = (chunksPerRow * (numberOfRows - 1)) + lastRowChunks;

  const lastChunkSize = (lastRowLength % blockSize) || blockSize;
  const oddChunkSize = rectangleSize % blockSize;
  
  let numberOfShortVLines: number;
  if (oddChunkSize !== blockSize) {
    numberOfShortVLines = blockSize - oddChunkSize;
  } else if (lastChunkSize !== blockSize) {
    numberOfShortVLines = blockSize - lastChunkSize;
  } else {
    numberOfShortVLines = 0;
  }
  
  const numberOfLongVLines = blockSize - numberOfShortVLines;

  const longVLineLength = totalChunks;
  const shortVLineLength = totalChunks -
    ((oddChunkSize !== blockSize ? numberOfRows - 1 : 0) +
     (lastChunkSize !== blockSize ? 1 : 0));

  const stackedLines: string[][] = [];
  let encryptedIx = 0;

  // New code to inspect the key so we can detect long vs.
  //  short lines.
  const keyChars = key.split('');
  const sortedKey = keyChars.toSorted();
  const positionNumbers = keyChars.map(char => sortedKey.indexOf(char));

  for (let vLineIx = 0; vLineIx < blockSize; vLineIx++) {
    const transposedVLineIx = positionNumbers.indexOf(vLineIx);
    const currentVLineLength = transposedVLineIx < numberOfLongVLines ?
      longVLineLength :
      shortVLineLength;

    // Updated to add spaces in the empty spots on a short line
    for (let rowIx = 0; rowIx < longVLineLength; rowIx++) {
      stackedLines[rowIx] ??= [];

      stackedLines[rowIx][vLineIx] = rowIx < currentVLineLength ? encrypted[encryptedIx++] : ' ';
    }
  }

  return stackedLines.filter(line => Array.isArray(line)).map(line => line.join(''));
}

function routeDecryptUnarrange(stackedLines: string[], rectangleSize: number, blockSize: number): string {
  // Remove any empty space, we don't need it anymore
  stackedLines = stackedLines.map(line => line.replace(/ /g, ''));
  const messageLength = stackedLines.join('').length;
  const numberOfRows = Math.ceil(messageLength / rectangleSize);
  const blocksPerRow = Math.ceil(rectangleSize / blockSize);
  const lastRowLength = messageLength % rectangleSize;
  const lastRowBlocks = Math.ceil(lastRowLength / blockSize);

  const messageLines: string[][] = [];
  let stackedLineIx = 0;

  // For each column of blocks...
  for (let columnIx = 0; columnIx < blocksPerRow; columnIx++) {
    const rowsInBlock = columnIx < lastRowBlocks ? numberOfRows : numberOfRows - 1;
    // For each row in the column...
    for (let rowIx = 0; rowIx < rowsInBlock; rowIx++) {
      messageLines[rowIx] ??= [];
      messageLines[rowIx].push(stackedLines[stackedLineIx++]);
    }
  }

  return messageLines.flat().join('');
}

const message = 'THEREAREMOREHYDROGENATOMSINAWATERMOLECULETHANSTARSINTHESOLARSYSTEM';
const key = 'PLANETS';
const rectangleSize = 15;

const arrangedToEncrypt = routeEncryptArrange(message, rectangleSize, key.length);
console.log(arrangedToEncrypt);
/* >
[
  "THEREAR", "ROGENAT", "TERMOLE", "STARSIN",
  "SYSTEMQ", "EMOREHY", "OMSINAW", "CULETHA",
  "THESOLA", "D", "A", "N",
  "R"
]
*/

const columnarEncrypted = columnarEncrypt(arrangedToEncrypt, key);
console.log(columnarEncrypted);
/* >
[
  "EEHRTRA", "GNOERTA", "ROEMTEL", "ASTRSNI",
  "SEYTSQM", "OEMREYH", "SNMIOWA", "LTUECAH",
  "EOHSTAL", "    D  ", "    A  ", "    N  ",
  "    R  "
]
*/

const encrypted = routeEncryptReadOut(columnarEncrypted, key.length);
console.log(encrypted);
/* >
EGRASOSLEENOSEENTOHOETYMMUHREMRTRIESTRTSSEOCTDANRRTENQYWAAAALIMHAHL
*/

const arrangedToDecrypt = routeDecryptReadIn(encrypted, rectangleSize, key);
console.log(arrangedToDecrypt);
/* >
[
  "EEHRTRA", "GNOERTA", "ROEMTEL", "ASTRSNI",
  "SEYTSQM", "OEMREYH", "SNMIOWA", "LTUECAH",
  "EOHSTAL", "    D  ", "    A  ", "    N  ",
  "    R  "
]
*/

const columnarDecrypted = columnarDecrypt(arrangedToDecrypt, key);
console.log(columnarDecrypted);
/* >
[
  "THEREAR", "ROGENAT", "TERMOLE", "STARSIN",
  "SYSTEMQ", "EMOREHY", "OMSINAW", "CULETHA",
  "THESOLA", "D      ", "A      ", "N      ",
  "R      "
]
*/

const decrypted = routeDecryptUnarrange(columnarDecrypted, rectangleSize, key.length);
console.log(decrypted);

/* >
THEREAREMOREHYDROGENATOMSINAWATERMOLECULETHANSTARSINTHESOLARSYSTEMQ
*/
```

Seems stable! There's a padding Q at the end, but that's expected.

Let's see if it can handle Kryptos 3.

## Solving Kryptos 3

The source text of Kryptos 3 is:

```
ENDYAHROHNLSRHEOCPTEOIBIDYSHNAIA
CHTNREYULDSLLSLLNOHSNOSMRWXMNE
TPRNGATIHNRARPESLNNELEBLPIIACAE
WMTWNDITEENRAHCTENEUDRETNHAEOE
TFOLSEDTIWENHAEIOYTEYQHEENCTAYCR
EIFTBRSPAMHHEWENATAMATEGYEERLB
TEEFOASFIOTUETUAEOTOARMAEERTNRTI
BSEDDNIAAHTTMSTEWPIEROAGRIEWFEB
AECTDDHILCEIHSITEGOEAOSDDRYDLORIT
RKLMLEHAGTDHARDPNEOHMGFMFEUHE
ECDMRIPFEIMEHNLSSTTRTVDOHW?
```

Let's plug it in! Don't forget that it's reversed.

```ts
function chunk<T>(array: T[], chunkSize: number): T[][] {
  const result: T[][] = [];
  for (let i = 0; i < array.length; i += chunkSize) {
      const chunk = array.slice(i, i + chunkSize);
      result.push(chunk);
  }
  return result;
}

function columnarEncrypt(stackedLines: string[], key: string): string[] {
  const keyChars = key.split('');
  const sortedKey = keyChars.toSorted();
  const positionNumbers = keyChars.map(char => sortedKey.indexOf(char));

  const encryptedRows: (string | null)[][] = Array(stackedLines.length)
    .fill(null)
    .map(_ => Array(key.length).fill(null).map(_ => null));

  for (const orderedColumnIx in keyChars) {
    const messageColumnIx = positionNumbers.indexOf(Number(orderedColumnIx));
    
    for (const rowIx in stackedLines) {
      encryptedRows[rowIx][orderedColumnIx] = stackedLines[rowIx][messageColumnIx] ?? ' ';
    }
  }

  return encryptedRows.map(row => row.join(''));
}

function columnarDecrypt(stackedLines: string[], key: string): string[] {
  const keyChars = key.split('');
  const sortedKey = keyChars.toSorted();
  const positionNumbers = keyChars.map(char => sortedKey.indexOf(char));

  const encryptedRows: (string | null)[][] = Array(stackedLines.length)
    .fill(null)
    .map(_ => Array(key.length).fill(null).map(_ => null));

  for (const messageColumnIx in keyChars) {
    const orderedColumnIx = positionNumbers.indexOf(Number(messageColumnIx));
    
    for (const rowIx in stackedLines) {
      encryptedRows[rowIx][orderedColumnIx] = stackedLines[rowIx][messageColumnIx] ?? ' ';
    }
  }

  return encryptedRows.map(row => row.join(''));
}

function routeEncryptArrange(message: string, rectangleSize: number, blockSize: number): string[] {
  const messageChars = message.split('');
  const messageLines = chunk(messageChars, rectangleSize);
  const chunkedLines = messageLines.map(line => chunk(line, blockSize));

  const allChunks = chunkedLines.flat();
  const finalChunk = allChunks.pop();
  const mainChunkSizes = allChunks.map(chunk => chunk.length);
  const oddChunkSize = mainChunkSizes.find(chunkSize => chunkSize !== blockSize);

  if (
    typeof oddChunkSize === 'number' &&
    Array.isArray(finalChunk) &&
    finalChunk.length !== oddChunkSize &&
    finalChunk.length !== blockSize
  ) {
    if (finalChunk.length < oddChunkSize) {
      finalChunk.push(...'Q'.repeat(oddChunkSize - finalChunk.length));
    } else {
      finalChunk.push(...'Q'.repeat(blockSize - finalChunk.length));
    }
  }

  const stackedLines: string[][] = [];
  
  const blocksPerLine = Math.ceil(rectangleSize / blockSize);
  for (let blockIx = 0; blockIx < blocksPerLine; blockIx++) {
    const numberOfRows = chunkedLines.length;
    for (let rowIx = 0; rowIx < numberOfRows; rowIx++) {
      const encryptedRowIx = rowIx + (blockIx * numberOfRows);
      stackedLines[encryptedRowIx] = chunkedLines[rowIx][blockIx];
    }
  }

  return stackedLines.filter(line => Array.isArray(line)).map(line => line.join(''));
}

function routeEncryptReadOut(stackedLines: string[], blockSize: number): string {
  const encryptedChars: string[] = [];
  for (let charIx = 0; charIx < blockSize; charIx++) {
    for (let rowIx = 0; rowIx < stackedLines.length; rowIx++) {
      if (typeof stackedLines[rowIx] !== 'string') {
        continue;
      }

      const char = stackedLines[rowIx][charIx];
      if (typeof char === 'string' && char !== ' ') {
        encryptedChars.push(char);
      }
    }
  }
  
  return encryptedChars.join('');
}

function routeDecryptReadIn(encrypted: string, rectangleSize: number, key: string): string[] {
  const numberOfRows = Math.ceil(encrypted.length / rectangleSize);
  const blockSize = key.length;
  const chunksPerRow = Math.ceil(rectangleSize / blockSize);
  const lastRowLength = encrypted.length % rectangleSize;
  const lastRowChunks = Math.ceil(lastRowLength / blockSize);
  const totalChunks = (chunksPerRow * (numberOfRows - 1)) + lastRowChunks;

  const lastChunkSize = (lastRowLength % blockSize) || blockSize;
  const oddChunkSize = rectangleSize % blockSize;
  
  let numberOfShortVLines: number;
  if (oddChunkSize !== blockSize) {
    numberOfShortVLines = blockSize - oddChunkSize;
  } else if (lastChunkSize !== blockSize) {
    numberOfShortVLines = blockSize - lastChunkSize;
  } else {
    numberOfShortVLines = 0;
  }
  
  const numberOfLongVLines = blockSize - numberOfShortVLines;

  const longVLineLength = totalChunks;
  const shortVLineLength = totalChunks -
    ((oddChunkSize !== blockSize ? numberOfRows - 1 : 0) +
     (lastChunkSize !== blockSize ? 1 : 0));

  const stackedLines: string[][] = [];
  let encryptedIx = 0;

  // New code to inspect the key so we can detect long vs.
  //  short lines.
  const keyChars = key.split('');
  const sortedKey = keyChars.toSorted();
  const positionNumbers = keyChars.map(char => sortedKey.indexOf(char));

  for (let vLineIx = 0; vLineIx < blockSize; vLineIx++) {
    const transposedVLineIx = positionNumbers.indexOf(vLineIx);
    const currentVLineLength = transposedVLineIx < numberOfLongVLines ?
      longVLineLength :
      shortVLineLength;

    // Updated to add spaces in the empty spots on a short line
    for (let rowIx = 0; rowIx < longVLineLength; rowIx++) {
      stackedLines[rowIx] ??= [];

      stackedLines[rowIx][vLineIx] = rowIx < currentVLineLength ? encrypted[encryptedIx++] : ' ';
    }
  }

  return stackedLines.filter(line => Array.isArray(line)).map(line => line.join(''));
}

function routeDecryptUnarrange(stackedLines: string[], rectangleSize: number, blockSize: number): string {
  // Remove any empty space, we don't need it anymore
  stackedLines = stackedLines.map(line => line.replace(/ /g, ''));
  const messageLength = stackedLines.join('').length;
  const numberOfRows = Math.ceil(messageLength / rectangleSize);
  const blocksPerRow = Math.ceil(rectangleSize / blockSize);
  const lastRowLength = messageLength % rectangleSize;
  const lastRowBlocks = Math.ceil(lastRowLength / blockSize);

  const messageLines: string[][] = [];
  let stackedLineIx = 0;

  // For each column of blocks...
  for (let columnIx = 0; columnIx < blocksPerRow; columnIx++) {
    const rowsInBlock = columnIx < lastRowBlocks ? numberOfRows : numberOfRows - 1;
    // For each row in the column...
    for (let rowIx = 0; rowIx < rowsInBlock; rowIx++) {
      messageLines[rowIx] ??= [];
      messageLines[rowIx].push(stackedLines[stackedLineIx++]);
    }
  }

  return messageLines.flat().join('');
}

const encrypted = 'ENDYAHROHNLSRHEOCPTEOIBIDYSHNAIACHTNREYULDSLLSLLNOHSNOSMRWXMNETPRNGATIHNRARPESLNNELEBLPIIACAEWMTWNDITEENRAHCTENEUDRETNHAEOETFOLSEDTIWENHAEIOYTEYQHEENCTAYCREIFTBRSPAMHHEWENATAMATEGYEERLBTEEFOASFIOTUETUAEOTOARMAEERTNRTIBSEDDNIAAHTTMSTEWPIEROAGRIEWFEBAECTDDHILCEIHSITEGOEAOSDDRYDLORITRKLMLEHAGTDHARDPNEOHMGFMFEUHEECDMRIPFEIMEHNLSSTTRTVDOHW?';
const key = 'KRYPTOS';
const rectangleSize = 86;

const unreversed = encrypted.split('').reverse().join('');
const arrangedToDecrypt = routeDecryptReadIn(unreversed, rectangleSize, key);
const columnarDecrypted = columnarDecrypt(arrangedToDecrypt, key);
const decrypted = routeDecryptUnarrange(columnarDecrypted, rectangleSize, key.length);
console.log(decrypted);

/* >
?SLOWLYDESPARATLYSLOWLYTHEREMAINSOFPASSAGEDEBRISTHATENCUMBEREDTHELOWERPARTOFTHEDOORWAYWASREMOVEDWITHTREMBLINGHANDSIMADEATINYBREACHINTHEUPPERLEFTHANDCORNERANDTHENWIDENINGTHEHOLEALITTLEIINSERTEDTHECANDLEANDPEEREDINTHEHOTAIRESCAPINGFROMTHECHAMBERCAUSEDTHEFLAMETOFLICKERBUTPRESENTLYDETAILSOFTHEROOMWITHINEMERGEDFROMTHEMISTXCANYOUSEEANYTHINGQ
*/
```

The message is:

```
? SLOWLY DESPARATLY SLOWLY THE REMAINS OF PASSAGE DEBRIS
THAT ENCUMBERED THE LOWER PART OF THE DOORWAY
WAS REMOVED WITH TREMBLING HANDS
I MADE A TINY BREACH IN THE UPPER LEFT HAND CORNER
AND THEN WIDENING THE HOLE A LITTLE
INSERTED THE CANDLE AND PEERED IN
THE HOT AIR ESCAPING FROM THE CHAMBER
CAUSED THE FLAME TO FLICKER
BUT PRESENTLY DETAILS OF THE ROOM WITHIN
EMERGED FROM THE MIST X
CAN YOU SEE ANYTHING Q
```

Presumably, the question mark should go at the end of the passage, not the beginning.

There you go! Kryptos 3, the message that stumped codebreakers for years, decoded in under 200 lines of TypeScript.

In the next post, we'll throw a few things at Kryptos 4 and see how it holds up.

## Acknowledgments

Thanks to the following, who made this series possible:

- The NSA: [The CIA Kryptos Sculpture (Declassified)](https://www.nsa.gov/portals/75/documents/news-features/declassified-documents/cia-kryptos-sculpture/KRYPTOS_Summary.pdf)
- UC San Diego: [The Kryptos Sculpture Solutions](https://mathweb.ucsd.edu/~crypto/Projects/KarlWang/index2.html)
- Wikipedia: [Kryptos](https://en.wikipedia.org/wiki/Kryptos)