/** * Emulation of the Enigma machine. * * @author s2224834 * @copyright Crown Copyright 2019 * @license Apache-2.0 */ import OperationError from "../errors/OperationError.mjs"; import Utils from "../Utils.mjs"; /** * Provided default Enigma rotor set. * These are specified as a list of mappings from the letters A through Z in order, optionally * followed by < and a list of letters at which the rotor steps. */ export const ROTORS = [ {name: "I", value: "EKMFLGDQVZNTOWYHXUSPAIBRCJ= 65 && i <= 90) { return i - 65; } if (permissive) { // Allow case insensitivity if (i >= 97 && i <= 122) { return i - 97; } return -1; } throw new OperationError("a2i called on non-uppercase ASCII character"); } /** * Map a number in 0..25 to a letter. * * @param {number} i * @returns {char} */ export function i2a(i) { if (i >= 0 && i < 26) { return Utils.chr(i+65); } throw new OperationError("i2a called on value outside 0..25"); } /** * A rotor in the Enigma machine. */ export class Rotor { /** * Rotor constructor. * * @param {string} wiring - A 26 character string of the wiring order. * @param {string} steps - A 0..26 character string of stepping points. * @param {char} ringSetting - The ring setting. * @param {char} initialPosition - The initial position of the rotor. */ constructor(wiring, steps, ringSetting, initialPosition) { if (!/^[A-Z]{26}$/.test(wiring)) { throw new OperationError("Rotor wiring must be 26 unique uppercase letters"); } if (!/^[A-Z]{0,26}$/.test(steps)) { throw new OperationError("Rotor steps must be 0-26 unique uppercase letters"); } if (!/^[A-Z]$/.test(ringSetting)) { throw new OperationError("Rotor ring setting must be exactly one uppercase letter"); } if (!/^[A-Z]$/.test(initialPosition)) { throw new OperationError("Rotor initial position must be exactly one uppercase letter"); } this.map = new Array(26); this.revMap = new Array(26); const uniq = {}; for (let i=0; i { if (!/^[A-Z]{2}$/.test(pair)) { throw new OperationError(name + " must be a whitespace-separated list of uppercase letter pairs"); } const a = a2i(pair[0]), b = a2i(pair[1]); if (a === b) { // self-stecker return; } if (Object.prototype.hasOwnProperty.call(this.map, a)) { throw new OperationError(`${name} connects ${pair[0]} more than once`); } if (Object.prototype.hasOwnProperty.call(this.map, b)) { throw new OperationError(`${name} connects ${pair[1]} more than once`); } this.map[a] = b; this.map[b] = a; }); } /** * Transform a character through this object. * Returns other characters unchanged. * * @param {number} c - The character. * @returns {number} */ transform(c) { if (!Object.prototype.hasOwnProperty.call(this.map, c)) { return c; } return this.map[c]; } /** * Alias for transform, to allow interchangeable use with rotors. * * @param {number} c - The character. * @returns {number} */ revTransform(c) { return this.transform(c); } } /** * Reflector. PairMapBase but requires that all characters are accounted for. * * Includes a couple of optimisations on that basis. */ export class Reflector extends PairMapBase { /** * Reflector constructor. See PairMapBase. * Additional restriction: every character must be accounted for. */ constructor(pairs) { super(pairs, "Reflector"); const s = Object.keys(this.map).length; if (s !== 26) { throw new OperationError("Reflector must have exactly 13 pairs covering every letter"); } const optMap = new Array(26); for (const x of Object.keys(this.map)) { optMap[x] = this.map[x]; } this.map = optMap; } /** * Transform a character through this object. * * @param {number} c - The character. * @returns {number} */ transform(c) { return this.map[c]; } } /** * Plugboard. Unmodified PairMapBase. */ export class Plugboard extends PairMapBase { /** * Plugboard constructor. See PairMapbase. */ constructor(pairs) { super(pairs, "Plugboard"); } } /** * Base class for the Enigma machine itself. Holds rotors, a reflector, and a plugboard. */ export class EnigmaBase { /** * EnigmaBase constructor. * * @param {Object[]} rotors - List of Rotors. * @param {Object} reflector - A Reflector. * @param {Plugboard} plugboard - A Plugboard. */ constructor(rotors, reflector, plugboard) { this.rotors = rotors; this.rotorsRev = [].concat(rotors).reverse(); this.reflector = reflector; this.plugboard = plugboard; } /** * Step the rotors forward by one. * * This happens before the output character is generated. * * Note that rotor 4, if it's there, never steps. * * Why is all the logic in EnigmaBase and not a nice neat method on * Rotor that knows when it should advance the next item? * Because the double stepping anomaly is a thing. tl;dr if the left rotor * should step the next time the middle rotor steps, the middle rotor will * immediately step. */ step() { const r0 = this.rotors[0]; const r1 = this.rotors[1]; r0.step(); // The second test here is the double-stepping anomaly if (r0.steps.has(r0.pos) || r1.steps.has(Utils.mod(r1.pos + 1, 26))) { r1.step(); if (r1.steps.has(r1.pos)) { const r2 = this.rotors[2]; r2.step(); } } } /** * Encrypt (or decrypt) some data. * Takes an arbitrary string and runs the Engima machine on that data from * *its current state*, and outputs the result. Non-alphabetic characters * are returned unchanged. * * @param {string} input - Data to encrypt. * @returns {string} */ crypt(input) { let result = ""; for (const c of input) { let letter = a2i(c, true); if (letter === -1) { result += c; continue; } // First, step the rotors forward. this.step(); // Now, run through the plugboard. letter = this.plugboard.transform(letter); // Then through each wheel in sequence, through the reflector, and // backwards through the wheels again. for (const rotor of this.rotors) { letter = rotor.transform(letter); } letter = this.reflector.transform(letter); for (const rotor of this.rotorsRev) { letter = rotor.revTransform(letter); } // Finally, back through the plugboard. letter = this.plugboard.revTransform(letter); result += i2a(letter); } return result; } } /** * The Enigma machine itself. Holds 3-4 rotors, a reflector, and a plugboard. */ export class EnigmaMachine extends EnigmaBase { /** * EnigmaMachine constructor. * * @param {Object[]} rotors - List of Rotors. * @param {Object} reflector - A Reflector. * @param {Plugboard} plugboard - A Plugboard. */ constructor(rotors, reflector, plugboard) { super(rotors, reflector, plugboard); if (rotors.length !== 3 && rotors.length !== 4) { throw new OperationError("Enigma must have 3 or 4 rotors"); } } }