The Enigma Machine exercise
The basics
An Enigma machine has three different configurable components:
- A plugboard, that swaps pairs of letters
- The Rotors, that rotate with each key press
- A reflector, that swaps pairs of letters
Each letter corresponds to a number, from 1 to 26.
In escense, the enigma machine is a simple substitution cipher, but the configurable components make it a lot more complex.
Inside, the enigma machine will understand the letters as numbers, and will perform the following operations:
- The plugboard will swap the numbers
- The rotors will rotate
- The rotors will substitute the numbers
- The reflector will swap the numbers
- The rotors will substitute the numbers again
- The plugboard will swap the numbers again
Mapping the letters to numbers
const lettersToNumbers = new Map<string, number>([['A', 1],['B', 2],['C', 3],['D', 4],['E', 5],['F', 6],['G', 7],['H', 8],['I', 9],['J', 10],['K', 11],['L', 12],['M', 13],['N', 14],['O', 15],['P', 16],['Q', 17],['R', 18],['S', 19],['T', 20],['U', 21],['V', 22],['W', 23],['X', 24],['Y', 25],['Z', 26],]);
Mapping the numbers to letters
const numbersToLetters = new Map<number, string>([[1, 'A'],[2, 'B'],[3, 'C'],[4, 'D'],[5, 'E'],[6, 'F'],[7, 'G'],[8, 'H'],[9, 'I'],[10, 'J'],[11, 'K'],[12, 'L'],[13, 'M'],[14, 'N'],[15, 'O'],[16, 'P'],[17, 'Q'],[18, 'R'],[19, 'S'],[20, 'T'],[21, 'U'],[22, 'V'],[23, 'W'],[24, 'X'],[25, 'Y'],[26, 'Z'],]);
The plugboard
When the plugboard is configured, it will swap pairs of letters. For example, if the plugboard is configured to swap A and B, and C and D, the letter A will be swapped by B, and the letter B will be swapped by A. The same goes for C and D.
type PlugboardConfig = [string, string][];class PlugBoard {dictionary: Map<string, string>;constructor(config: PlugboardConfig) {const fullConfig = config.reduce((acc, [key, value]) => {acc.push([key, value]);acc.push([value, key]);return acc;}, [] as PlugboardConfig);this.dictionary = new Map(fullConfig);}// don't get confused by the getter, it's just a way to make the dictionary read only.// this is will be called as plugboard.config (without the parenthesis)get config() {return this.dictionary;}swap(letter: string) {return this.dictionary.get(letter) || letter;}}
Testing the Plugboard
When we pass a configuration, the plugboard should build a map with the swapped letters.
const plugboard = new PlugBoard([['A', 'B'],['C', 'D'],]);console.log(plugboard.config); // Map(4) {// 'A' => 'B',// 'B' => 'A',// 'C' => 'D',// 'D' => 'C'// }
The Enigma Machine class
The Enigma Machine class puts all the components together and keeps track of the state of the rotors.
we will implement a encrypt method that will take the letter to encrypt and return the encrypted letter.
class EnigmaMachine {plugboard: PlugBoard;constructor(plugboard?: PlugBoard) {this.plugboard = plugboard || new PlugBoard([]);}encrypt(letter: string) {let encriptedLetter = this.plugboard.swap(letter);return encriptedLetter;}}
Testing the Enigma Machine with only the plugboard
const plugboard = new PlugBoard([['A', 'B'],['C', 'D'],]);const enigmaMachine = new EnigmaMachine(plugboard);console.log(enigmaMachine.encrypt('A')); // Bconsole.log(enigmaMachine.encrypt('B')); // Aconsole.log(enigmaMachine.encrypt('C')); // Dconsole.log(enigmaMachine.encrypt('D')); // C
The Reflector
The reflector will swap numbers based in a configuration that defines the order of the letters to swap. We will represent this configuration as a two strings of 26 characters, where the position of the letter in the string represents the number of the letter, and the value of the string represents the number of the letter to swap with.
class Reflector {regularAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';reflectorConfig = 'EJMZALYXVBWFCRQUONTSPIKHGD';// index will be a number between 1 and 26forward(index: number) {// we need to substract 1 because the indexes// in the enigma machine are 1 based, and in JS are 0 basedconst inputLetter = this.regularAlphabet[index - 1];// and we need to add 1 here for the same reasonconst result = this.reflectorConfig.indexOf(inputLetter) + 1;return result;}}
Testing the Reflector
const reflector = new Reflector();console.log(reflector.forward(1)); // 5 which is Econsole.log(reflector.forward(5)); // 1 which is Aconsole.log(reflector.forward(26)); // 4 which is Dconsole.log(reflector.forward(4)); // 26 which is Z
Adding the reflector to the Enigma Machine
class EnigmaMachine {plugboard: PlugBoard;reflector: Reflector;constructor(plugboard?: PlugBoard) {this.plugboard = plugboard || new PlugBoard([]);this.reflector = new Reflector();}encrypt(letter: string) {let encriptedLetter = this.plugboard.swap(letter);// get the number that corresponds to the letterconst number = lettersToNumbers.get(encriptedLetter) as number;// pass the number through the reflectorconst reflectedNumber = this.reflector.forward(number);// get the letter that corresponds to the reflected numberconst reflectedLetter = numbersToLetters.get(reflectedNumber) as string;// swap the reflected letterencriptedLetter = this.plugboard.swap(reflectedLetter);return encryptedLetter;}}
Testing the Enigma Machine with the reflector only
const enigmaMachine = new EnigmaMachine();console.log(enigmaMachine.encrypt('A')); // Econsole.log(enigmaMachine.encrypt('E')); // Aconsole.log(enigmaMachine.encrypt('Z')); // Dconsole.log(enigmaMachine.encrypt('D')); // Z
Testing the Enigma Machine with the plugboard and the reflector
const plugboard = new PlugBoard([['A', 'B'],['C', 'D'],]);const enigmaMachine = new EnigmaMachine(plugboard);console.log(enigmaMachine.encrypt('A')); // Econsole.log(enigmaMachine.encrypt('B')); // Jconsole.log(enigmaMachine.encrypt('C')); // Mconsole.log(enigmaMachine.encrypt('D')); // Zconsole.log(enigmaMachine.encrypt('E')); // Aconsole.log(enigmaMachine.encrypt('J')); // Bconsole.log(enigmaMachine.encrypt('M')); // Cconsole.log(enigmaMachine.encrypt('Z')); // D
The Rotors
Let's start the rotors without the rotation. We will implement the forward and backward methods that will take a number and return the number that corresponds to the letter after passing through the rotor.
class Rotor {regularAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';rotorConfig: string;constructor(config: string) {this.rotorConfig = config;}forward(index: number) {const inputLetter = this.regularAlphabet[index - 1];const result = this.rotorConfig.indexOf(inputLetter) + 1;return result;}backward(index: number) {const inputLetter = this.rotorConfig[index - 1];const result = this.regularAlphabet.indexOf(inputLetter) + 1;return result;}}
Testing the Rotor
const rotor = new Rotor('EKMFLGDQVZNTOWYHXUSPAIBRCJ');console.log(rotor.forward(1)); // 21 which is Uconsole.log(rotor.forward(5)); // 1 which is Aconsole.log(rotor.backward(21)); // 1 which is Aconsole.log(rotor.backward(1)); // 5 which is E
Adding one rotor to the Enigma Machine
class EnigmaMachine {plugboard: PlugBoard;reflector: Reflector;rotor1: Rotor;constructor(plugboard?: PlugBoard, rotor1?: Rotor) {this.plugboard = plugboard || new PlugBoard([]);this.reflector = new Reflector();this.rotor1 = rotor1 ||new Rotor('EKMFLGDQVZNTOWYHXUSPAIBRCJ');}encrypt(letter: string) {let encriptedLetter = this.plugboard.swap(letter);// get the number that corresponds to the letterconst number = lettersToNumbers.get(encriptedLetter) as number;// pass the number through the rotorconst rotorNumber = this.rotor1.forward(number);// pass the number through the reflectorconst reflectedNumber = this.reflector.forward(rotorNumber);// pass the number backward through the rotorconst reflectedRotorNumber = this.rotor1.backward(reflectedNumber);// get the letter that corresponds to the reflected numberconst reflectedRotorLetter = numbersToLetters.get(reflectedRotorNumber) as string;// swap the reflected letterencriptedLetter = this.plugboard.swap(reflectedRotorLetter);return encryptedLetter;}}
Testing the Enigma Machine with the plugboard, the reflector and the rotor
const plugboard = new PlugBoard([['A', 'B'],['C', 'D'],]);const rotor1 = new Rotor('EKMFLGDQVZNTOWYHXUSPAIBRCJ');const enigmaMachine = new EnigmaMachine(plugboard, rotor1);console.log(enigmaMachine.encrypt('A')); // Hconsole.log(enigmaMachine.encrypt('B')); // Mconsole.log(enigmaMachine.encrypt('M')); // Bconsole.log(enigmaMachine.encrypt('H')); // A
Adding the rotation to the rotor
We will add a method to the rotor that will rotate the rotor one position. We will also add a method to the Enigma Machine that will rotate the rotor when a key is pressed.
class Rotor {regularAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';rotorConfig: string;position: number;constructor(config: string, position: number) {this.rotorConfig = config;this.position = position;}forward(index: number) {const inputLetter = this.regularAlphabet[index - 1];const result = this.rotorConfig.indexOf(inputLetter) + 1;return result;}backward(index: number) {const inputLetter = this.rotorConfig[index - 1];const result = this.regularAlphabet.indexOf(inputLetter) + 1;return result;}rotate() {this.position = this.position === 26 ? 1 : this.position + 1;const alphabet = this.regularAlphabet.split('');for (let i = 0; i < position; i++) {const letter = alphabet.shift();if (letter) {alphabet.push(letter);}}this.regularAlphabet = alphabet.join('');const rotorConfig = this.rotorConfig.split('');for (let i = 0; i < position; i++) {const letter = rotorConfig.shift();if (letter) {rotorConfig.push(letter);}}this.rotorConfig = rotorConfig.join('');}get offset() {return this.position;}}### Testing the rotor rotation```tsconst rotor = new Rotor('EKMFLGDQVZNTOWYHXUSPAIBRCJ', 1);console.log(rotor.offset); // 1rotor.rotate();console.log(rotor.offset); // 2rotor.rotate();console.log(rotor.offset); // 3
Adding the rotation to the Enigma Machine
class EnigmaMachine {plugboard: PlugBoard;reflector: Reflector;rotor1: Rotor;constructor(plugboard?: PlugBoard, rotor1?: Rotor) {this.plugboard = plugboard || new PlugBoard([]);this.reflector = new Reflector();this.rotor1 = rotor1 ||new Rotor('EKMFLGDQVZNTOWYHXUSPAIBRCJ', 1);}encrypt(letter: string) {let encriptedLetter = this.plugboard.swap(letter);// get the number that corresponds to the letterconst number = lettersToNumbers.get(encriptedLetter) as number;// pass the number through the rotorconst rotorNumber = this.rotor1.forward(number);// pass the number through the reflectorconst reflectedNumber = this.reflector.forward(rotorNumber);// pass the number backward through the rotorconst reflectedRotorNumber = this.rotor1.backward(reflectedNumber);// get the letter that corresponds to the reflected numberconst reflectedRotorLetter = numbersToLetters.get(reflectedRotorNumber) as string;// swap the reflected letterencriptedLetter = this.plugboard.swap(reflectedRotorLetter);// rotate the rotorthis.rotor1.rotate();return encryptedLetter;}}
Testing the Enigma Machine with the plugboard, the reflector and the rotor rotation
const plugboard = new PlugBoard([['A', 'B'],['C', 'D'],]);const rotor1 = new Rotor('EKMFLGDQVZNTOWYHXUSPAIBRCJ', 1);const enigmaMachine = new EnigmaMachine(plugboard, rotor1);// Each time we press a key, the rotor will rotate, giving a different resultconsole.log(enigmaMachine.encrypt('A')); // Hconsole.log(enigmaMachine.encrypt('A')); // Kconsole.log(enigmaMachine.encrypt('A')); // Dconsole.log(enigmaMachine.encrypt('A')); // Wconst enigmaMachine2 = new EnigmaMachine(plugboard, rotor1);// The second enigma machine has the same configuration as the first one,// so if we press the result of the first machine, we will get the inputconsole.log(enigmaMachine2.encrypt('H')); // Aconsole.log(enigmaMachine2.encrypt('K')); // Aconsole.log(enigmaMachine2.encrypt('D')); // Aconsole.log(enigmaMachine2.encrypt('W')); // A
Putting All the rotors
class EnigmaMachine {plugboard: PlugBoard;reflector: Reflector;rotor1: Rotor;rotor2: Rotor;rotor3: Rotor;constructor(plugboard?: PlugBoard, rotor1?: Rotor, rotor2?: Rotor, rotor3?: Rotor) {this.plugboard = plugboard || new PlugBoard([]);this.reflector = new Reflector();this.rotor1 = rotor1 ||new Rotor('EKMFLGDQVZNTOWYHXUSPAIBRCJ', 1);this.rotor2 = rotor2 ||new Rotor('AJDKSIRUXBLHWTMCQGZNPYFVOE', 1);this.rotor3 = rotor3 ||new Rotor('BDFHJLCPRTXVZNYEIWGAKMUSQO', 1);}encrypt(letter: string) {let encriptedLetter = this.plugboard.swap(letter);// get the number that corresponds to the letterconst number = lettersToNumbers.get(encriptedLetter) as number;// pass the number through the rotor 1const rotor1Number = this.rotor1.forward(number);// pass the number through the rotor 2const rotor2Number = this.rotor2.forward(rotor1Number);// pass the number through the rotor 1const rotor3Number = this.rotor3.forward(rotor3Number);// pass the number through the reflectorconst reflectedNumber = this.reflector.forward(rotor3Number);// pass the number backward through the rotor 1const reflectedRotor3Number = this.rotor3.backward(reflectedNumber);// pass the number backward through the rotor 2const reflectedRotor2Number = this.rotor2.backward(reflectedRotor3Number);// pass the number backward through the rotor 3const reflectedRotor1Number = this.rotor1.backward(reflectedRotor2Number);// get the letter that corresponds to the reflected numberconst reflectedRotorLetter = numbersToLetters.get(reflectedRotor3Number) as string;// swap the reflected letterencriptedLetter = this.plugboard.swap(reflectedRotorLetter);// rotate the rotors// the rotor 1 rotates every time a key is pressedthis.rotor1.rotate();// the rotor 2 rotates every time the rotor 1 completes a full rotationif (this.rotor1.offset === 1) {this.rotor2.rotate();}// the rotor 3 rotates every time the rotor 2 completes a full rotationif (this.rotor2.offset === 1) {this.rotor3.rotate();}return encryptedLetter;}}
Testing the Enigma Machine with all the rotors
const plugboard = new PlugBoard([['A', 'B'],['C', 'D'],]);const rotor1 = new Rotor('EKMFLGDQVZNTOWYHXUSPAIBRCJ', 1);const rotor2 = new Rotor('AJDKSIRUXBLHWTMCQGZNPYFVOE', 1);const rotor3 = new Rotor('BDFHJLCPRTXVZNYEIWGAKMUSQO', 1);const enigmaMachine = new EnigmaMachine(plugboard, rotor1, rotor2, rotor3);// Each time we press a key, the rotors will rotate, giving a different resultconsole.log(enigmaMachine.encrypt('A')); // Uconsole.log(enigmaMachine.encrypt('A')); // Lconsole.log(enigmaMachine.encrypt('A')); // Qconsole.log(enigmaMachine.encrypt('A')); // Vconst enigmaMachine2 = new EnigmaMachine(plugboard, rotor1, rotor2, rotor3);// The second enigma machine has the same configuration as the first one,// so if we press the result of the first machine, we will get the inputconsole.log(enigmaMachine2.encrypt('U')); // Aconsole.log(enigmaMachine2.encrypt('L')); // Aconsole.log(enigmaMachine2.encrypt('Q')); // Aconsole.log(enigmaMachine2.encrypt('V')); // A