UNPKG

cyberchef

Version:

The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.

370 lines (349 loc) 11.4 kB
/** * 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<R"}, {name: "II", value: "AJDKSIRUXBLHWTMCQGZNPYFVOE<F"}, {name: "III", value: "BDFHJLCPRTXVZNYEIWGAKMUSQO<W"}, {name: "IV", value: "ESOVPZJAYQUIRHXLNFTGKDCMWB<K"}, {name: "V", value: "VZBRGITYUPSDNHLXAWMJQOFECK<A"}, {name: "VI", value: "JPGVOUMFYQBENHZRDKASXLICTW<AN"}, {name: "VII", value: "NZJHGRCXMYSWBOUFAIVLPEKQDT<AN"}, {name: "VIII", value: "FKQHTLXOCBJSPDZRAMEWNIUYGV<AN"}, ]; export const ROTORS_FOURTH = [ {name: "Beta", value: "LEYJVCNIXWPBQMDRTAKZGFUHOS"}, {name: "Gamma", value: "FSOKANUERHMBTIYCWLQPZXVGJD"}, ]; /** * Provided default Enigma reflector set. * These are specified as 13 space-separated transposed pairs covering every letter. */ export const REFLECTORS = [ {name: "B", value: "AY BR CU DH EQ FS GL IP JX KN MO TZ VW"}, {name: "C", value: "AF BV CP DJ EI GO HY KR LZ MX NW TQ SU"}, {name: "B Thin", value: "AE BN CK DQ FU GY HW IJ LO MP RX SZ TV"}, {name: "C Thin", value: "AR BD CO EJ FN GT HK IV LM PW QZ SX UY"}, ]; export const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); /** * Map a letter to a number in 0..25. * * @param {char} c * @param {boolean} permissive - Case insensitive; don't throw errors on other chars. * @returns {number} */ export function a2i(c, permissive=false) { const i = Utils.ord(c); if (i >= 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<LETTERS.length; i++) { const a = a2i(LETTERS[i]); const b = a2i(wiring[i]); this.map[a] = b; this.revMap[b] = a; uniq[b] = true; } if (Object.keys(uniq).length !== LETTERS.length) { throw new OperationError("Rotor wiring must have each letter exactly once"); } const rs = a2i(ringSetting); this.steps = new Set(); for (const x of steps) { this.steps.add(Utils.mod(a2i(x) - rs, 26)); } if (this.steps.size !== steps.length) { // This isn't strictly fatal, but it's probably a mistake throw new OperationError("Rotor steps must be unique"); } this.pos = Utils.mod(a2i(initialPosition) - rs, 26); } /** * Step the rotor forward by one. */ step() { this.pos = Utils.mod(this.pos + 1, 26); return this.pos; } /** * Transform a character through this rotor forwards. * * @param {number} c - The character. * @returns {number} */ transform(c) { return Utils.mod(this.map[Utils.mod(c + this.pos, 26)] - this.pos, 26); } /** * Transform a character through this rotor backwards. * * @param {number} c - The character. * @returns {number} */ revTransform(c) { return Utils.mod(this.revMap[Utils.mod(c + this.pos, 26)] - this.pos, 26); } } /** * Base class for plugboard and reflector (since these do effectively the same * thing). */ class PairMapBase { /** * PairMapBase constructor. * * @param {string} pairs - A whitespace separated string of letter pairs to swap. * @param {string} [name='PairMapBase'] - For errors, the name of this object. */ constructor(pairs, name="PairMapBase") { // I've chosen to make whitespace significant here to make a) code and // b) inputs easier to read this.pairs = pairs; this.map = {}; if (pairs === "") { return; } pairs.split(/\s+/).forEach(pair => { 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"); } } }