From 088864fd9c45105e8e311e5f64adda751614652a Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 3 Jan 2019 16:36:56 +0000 Subject: [PATCH 01/47] Add Enigma operation --- .gitignore | 1 + src/core/config/Categories.json | 3 +- src/core/lib/Enigma.mjs | 349 ++++++++++++++++++++ src/core/operations/Enigma.mjs | 200 ++++++++++++ tests/operations/index.mjs | 1 + tests/operations/tests/Enigma.mjs | 518 ++++++++++++++++++++++++++++++ 6 files changed, 1071 insertions(+), 1 deletion(-) create mode 100644 src/core/lib/Enigma.mjs create mode 100644 src/core/operations/Enigma.mjs create mode 100644 tests/operations/tests/Enigma.mjs diff --git a/.gitignore b/.gitignore index 3ca816f6..edbcf679 100755 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ docs/* !docs/*.conf.json !docs/*.ico .vscode +.*.swp src/core/config/modules/* src/core/config/OperationConfig.json src/core/operations/index.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 686c9842..3e0d108e 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -102,7 +102,8 @@ "JWT Decode", "Citrix CTX1 Encode", "Citrix CTX1 Decode", - "Pseudo-Random Number Generator" + "Pseudo-Random Number Generator", + "Enigma" ] }, { diff --git a/src/core/lib/Enigma.mjs b/src/core/lib/Enigma.mjs new file mode 100644 index 00000000..845ce413 --- /dev/null +++ b/src/core/lib/Enigma.mjs @@ -0,0 +1,349 @@ +/** + * Emulation of the Enigma machine. + * + * @author s2224834 + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import OperationError from "../errors/OperationError"; +import Utils from "../Utils"; + +/** + * 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 = {}; + this.revMap = {}; + 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) { + throw new OperationError(`${name}: cannot connect ${pair[0]} to itself`); + } + if (this.map.hasOwnProperty(a)) { + throw new OperationError(`${name} connects ${pair[0]} more than once`); + } + if (this.map.hasOwnProperty(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 (!this.map.hasOwnProperty(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. + */ +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"); + } + } +} + +/** + * 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"); + } + } +} diff --git a/src/core/operations/Enigma.mjs b/src/core/operations/Enigma.mjs new file mode 100644 index 00000000..d43d780d --- /dev/null +++ b/src/core/operations/Enigma.mjs @@ -0,0 +1,200 @@ +/** + * Emulation of the Enigma machine. + * + * @author s2224834 + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import * as Enigma from "../lib/Enigma"; + +/** + * Enigma operation + */ +class EnigmaOp extends Operation { + /** + * Enigma constructor + */ + constructor() { + super(); + + this.name = "Enigma"; + this.module = "Default"; + this.description = "Encipher/decipher with the WW2 Enigma machine.

The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. AB CD EF connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.
This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses the thin reflectors and the beta or gamma rotor in the 4th slot)."; + this.infoURL = "https://wikipedia.org/wiki/Enigma_machine"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "1st (right-hand) rotor", + type: "editableOption", + value: Enigma.ROTORS, + // Default config is the rotors I-III *left to right* + defaultIndex: 2 + }, + { + name: "1st rotor ring setting", + type: "option", + value: Enigma.LETTERS + }, + { + name: "1st rotor initial value", + type: "option", + value: Enigma.LETTERS + }, + { + name: "2nd rotor", + type: "editableOption", + value: Enigma.ROTORS, + defaultIndex: 1 + }, + { + name: "2nd rotor ring setting", + type: "option", + value: Enigma.LETTERS + }, + { + name: "2nd rotor initial value", + type: "option", + value: Enigma.LETTERS + }, + { + name: "3rd rotor", + type: "editableOption", + value: Enigma.ROTORS, + defaultIndex: 0 + }, + { + name: "3rd rotor ring setting", + type: "option", + value: Enigma.LETTERS + }, + { + name: "3rd rotor initial value", + type: "option", + value: Enigma.LETTERS + }, + { + name: "4th rotor", + type: "editableOption", + value: Enigma.ROTORS_OPTIONAL, + defaultIndex: 10 + }, + { + name: "4th rotor initial value", + type: "option", + value: Enigma.LETTERS + }, + { + name: "Reflector", + type: "editableOption", + value: Enigma.REFLECTORS + }, + { + name: "Plugboard", + type: "string", + value: "" + }, + { + name: "Strict output", + hint: "Remove non-alphabet letters and group output", + type: "boolean", + value: true + }, + ]; + } + + /** + * Helper - for ease of use rotors are specified as a single string; this + * method breaks the spec string into wiring and steps parts. + * + * @param {string} rotor - Rotor specification string. + * @param {number} i - For error messages, the number of this rotor. + * @returns {string[]} + */ + parseRotorStr(rotor, i) { + if (rotor === "") { + throw new OperationError(`Rotor ${i} must be provided.`); + } + if (!rotor.includes("<")) { + return [rotor, ""]; + } + return rotor.split("<", 2); + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [ + rotor1str, rotor1ring, rotor1pos, + rotor2str, rotor2ring, rotor2pos, + rotor3str, rotor3ring, rotor3pos, + rotor4str, rotor4pos, + reflectorstr, plugboardstr, + removeOther + ] = args; + const rotors = []; + const [rotor1wiring, rotor1steps] = this.parseRotorStr(rotor1str, 1); + rotors.push(new Enigma.Rotor(rotor1wiring, rotor1steps, rotor1ring, rotor1pos)); + const [rotor2wiring, rotor2steps] = this.parseRotorStr(rotor2str, 2); + rotors.push(new Enigma.Rotor(rotor2wiring, rotor2steps, rotor2ring, rotor2pos)); + const [rotor3wiring, rotor3steps] = this.parseRotorStr(rotor3str, 3); + rotors.push(new Enigma.Rotor(rotor3wiring, rotor3steps, rotor3ring, rotor3pos)); + if (rotor4str !== "") { + // Fourth rotor doesn't have a ring setting - A is equivalent to no setting + const [rotor4wiring, rotor4steps] = this.parseRotorStr(rotor4str, 4); + rotors.push(new Enigma.Rotor(rotor4wiring, rotor4steps, "A", rotor4pos)); + } + const reflector = new Enigma.Reflector(reflectorstr); + const plugboard = new Enigma.Plugboard(plugboardstr); + if (removeOther) { + input = input.replace(/[^A-Za-z]/g, ""); + } + const enigma = new Enigma.EnigmaMachine(rotors, reflector, plugboard); + let result = enigma.crypt(input); + if (removeOther) { + // Five character cipher groups is traditional + result = result.replace(/([A-Z]{5})(?!$)/g, "$1 "); + } + return result; + } + + /** + * Highlight Enigma + * This is only possible if we're passing through non-alphabet characters. + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlight(pos, args) { + if (args[13] === false) { + return pos; + } + } + + /** + * Highlight Enigma in reverse + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlightReverse(pos, args) { + if (args[13] === false) { + return pos; + } + } + +} + +export default EnigmaOp; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index da9d41be..119f3ac4 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -82,6 +82,7 @@ import "./tests/TranslateDateTimeFormat"; import "./tests/Magic"; import "./tests/ParseTLV"; import "./tests/Media"; +import "./tests/Enigma"; // Cannot test operations that use the File type yet //import "./tests/SplitColourChannels"; diff --git a/tests/operations/tests/Enigma.mjs b/tests/operations/tests/Enigma.mjs new file mode 100644 index 00000000..a1acaf33 --- /dev/null +++ b/tests/operations/tests/Enigma.mjs @@ -0,0 +1,518 @@ +/** + * Enigma machine tests. + * @author s2224834 + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import TestRegister from "../TestRegister"; + +TestRegister.addTests([ + { + // Simplest test: A single keypress in the default position on a basic + // Enigma. + name: "Enigma: basic wiring", + input: "G", + expectedOutput: "P", + recipeConfig: [ + { + "op": "Enigma", + "args": [ + // Note: start on Z because it steps when the key is pressed + "BDFHJLCPRTXVZNYEIWGAKMUSQO Date: Thu, 3 Jan 2019 16:40:29 +0000 Subject: [PATCH 02/47] Add Typex operation WIP --- src/core/config/Categories.json | 3 +- src/core/operations/Typex.mjs | 396 ++++++++++++++++++++++++++++++++ 2 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/Typex.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 3e0d108e..f3b73921 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -103,7 +103,8 @@ "Citrix CTX1 Encode", "Citrix CTX1 Decode", "Pseudo-Random Number Generator", - "Enigma" + "Enigma", + "Typex" ] }, { diff --git a/src/core/operations/Typex.mjs b/src/core/operations/Typex.mjs new file mode 100644 index 00000000..89114863 --- /dev/null +++ b/src/core/operations/Typex.mjs @@ -0,0 +1,396 @@ +/** + * Emulation of the Typex machine. + * + * @author s2224834 + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import Utils from "../Utils"; +import * as Enigma from "../lib/Enigma"; + +const ROTORS = [ + {name: "1", value: "QWECYJIBFKMLTVZPOHUDGNRSXA Date: Thu, 3 Jan 2019 17:51:20 +0000 Subject: [PATCH 03/47] Enigma: fix 4th rotor ringstellung --- src/core/operations/Enigma.mjs | 33 +++++++------- tests/operations/tests/Enigma.mjs | 73 +++++++++++++++++++------------ 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/core/operations/Enigma.mjs b/src/core/operations/Enigma.mjs index d43d780d..38456ebf 100644 --- a/src/core/operations/Enigma.mjs +++ b/src/core/operations/Enigma.mjs @@ -82,6 +82,11 @@ class EnigmaOp extends Operation { value: Enigma.ROTORS_OPTIONAL, defaultIndex: 10 }, + { + name: "4th rotor ring setting", + type: "option", + value: Enigma.LETTERS + }, { name: "4th rotor initial value", type: "option", @@ -130,25 +135,17 @@ class EnigmaOp extends Operation { * @returns {string} */ run(input, args) { - const [ - rotor1str, rotor1ring, rotor1pos, - rotor2str, rotor2ring, rotor2pos, - rotor3str, rotor3ring, rotor3pos, - rotor4str, rotor4pos, - reflectorstr, plugboardstr, - removeOther - ] = args; + const reflectorstr = args[12]; + const plugboardstr = args[13]; + const removeOther = args[14]; const rotors = []; - const [rotor1wiring, rotor1steps] = this.parseRotorStr(rotor1str, 1); - rotors.push(new Enigma.Rotor(rotor1wiring, rotor1steps, rotor1ring, rotor1pos)); - const [rotor2wiring, rotor2steps] = this.parseRotorStr(rotor2str, 2); - rotors.push(new Enigma.Rotor(rotor2wiring, rotor2steps, rotor2ring, rotor2pos)); - const [rotor3wiring, rotor3steps] = this.parseRotorStr(rotor3str, 3); - rotors.push(new Enigma.Rotor(rotor3wiring, rotor3steps, rotor3ring, rotor3pos)); - if (rotor4str !== "") { - // Fourth rotor doesn't have a ring setting - A is equivalent to no setting - const [rotor4wiring, rotor4steps] = this.parseRotorStr(rotor4str, 4); - rotors.push(new Enigma.Rotor(rotor4wiring, rotor4steps, "A", rotor4pos)); + for (let i=0; i<4; i++) { + if (i === 3 && args[i*3] === "") { + // No fourth rotor + break; + } + const [rotorwiring, rotorsteps] = this.parseRotorStr(args[i*3], 1); + rotors.push(new Enigma.Rotor(rotorwiring, rotorsteps, args[i*3 + 1], args[i*3 + 2])); } const reflector = new Enigma.Reflector(reflectorstr); const plugboard = new Enigma.Plugboard(plugboardstr); diff --git a/tests/operations/tests/Enigma.mjs b/tests/operations/tests/Enigma.mjs index a1acaf33..f8776b42 100644 --- a/tests/operations/tests/Enigma.mjs +++ b/tests/operations/tests/Enigma.mjs @@ -21,7 +21,7 @@ TestRegister.addTests([ "BDFHJLCPRTXVZNYEIWGAKMUSQO Date: Thu, 3 Jan 2019 18:48:50 +0000 Subject: [PATCH 04/47] Typex: move machine implementation to lib/ --- src/core/lib/Typex.mjs | 183 ++++++++++++++++++++++++++++++++ src/core/operations/Typex.mjs | 193 ++-------------------------------- 2 files changed, 193 insertions(+), 183 deletions(-) create mode 100644 src/core/lib/Typex.mjs diff --git a/src/core/lib/Typex.mjs b/src/core/lib/Typex.mjs new file mode 100644 index 00000000..a99f3b6e --- /dev/null +++ b/src/core/lib/Typex.mjs @@ -0,0 +1,183 @@ +/** + * Emulation of the Typex machine. + * + * @author s2224834 + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import OperationError from "../errors/OperationError"; +import * as Enigma from "../lib/Enigma"; +import Utils from "../Utils"; + +export const ROTORS = [ + {name: "1", value: "QWECYJIBFKMLTVZPOHUDGNRSXA Date: Thu, 3 Jan 2019 18:51:39 +0000 Subject: [PATCH 05/47] Enigma: make sure op class is called Enigma --- src/core/operations/Enigma.mjs | 40 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/core/operations/Enigma.mjs b/src/core/operations/Enigma.mjs index 38456ebf..c45f59ae 100644 --- a/src/core/operations/Enigma.mjs +++ b/src/core/operations/Enigma.mjs @@ -8,12 +8,12 @@ import Operation from "../Operation"; import OperationError from "../errors/OperationError"; -import * as Enigma from "../lib/Enigma"; +import {ROTORS, LETTERS, ROTORS_OPTIONAL, REFLECTORS, Rotor, Reflector, Plugboard, EnigmaMachine} from "../lib/Enigma"; /** * Enigma operation */ -class EnigmaOp extends Operation { +class Enigma extends Operation { /** * Enigma constructor */ @@ -30,72 +30,72 @@ class EnigmaOp extends Operation { { name: "1st (right-hand) rotor", type: "editableOption", - value: Enigma.ROTORS, + value: ROTORS, // Default config is the rotors I-III *left to right* defaultIndex: 2 }, { name: "1st rotor ring setting", type: "option", - value: Enigma.LETTERS + value: LETTERS }, { name: "1st rotor initial value", type: "option", - value: Enigma.LETTERS + value: LETTERS }, { name: "2nd rotor", type: "editableOption", - value: Enigma.ROTORS, + value: ROTORS, defaultIndex: 1 }, { name: "2nd rotor ring setting", type: "option", - value: Enigma.LETTERS + value: LETTERS }, { name: "2nd rotor initial value", type: "option", - value: Enigma.LETTERS + value: LETTERS }, { name: "3rd rotor", type: "editableOption", - value: Enigma.ROTORS, + value: ROTORS, defaultIndex: 0 }, { name: "3rd rotor ring setting", type: "option", - value: Enigma.LETTERS + value: LETTERS }, { name: "3rd rotor initial value", type: "option", - value: Enigma.LETTERS + value: LETTERS }, { name: "4th rotor", type: "editableOption", - value: Enigma.ROTORS_OPTIONAL, + value: ROTORS_OPTIONAL, defaultIndex: 10 }, { name: "4th rotor ring setting", type: "option", - value: Enigma.LETTERS + value: LETTERS }, { name: "4th rotor initial value", type: "option", - value: Enigma.LETTERS + value: LETTERS }, { name: "Reflector", type: "editableOption", - value: Enigma.REFLECTORS + value: REFLECTORS }, { name: "Plugboard", @@ -145,14 +145,14 @@ class EnigmaOp extends Operation { break; } const [rotorwiring, rotorsteps] = this.parseRotorStr(args[i*3], 1); - rotors.push(new Enigma.Rotor(rotorwiring, rotorsteps, args[i*3 + 1], args[i*3 + 2])); + rotors.push(new Rotor(rotorwiring, rotorsteps, args[i*3 + 1], args[i*3 + 2])); } - const reflector = new Enigma.Reflector(reflectorstr); - const plugboard = new Enigma.Plugboard(plugboardstr); + const reflector = new Reflector(reflectorstr); + const plugboard = new Plugboard(plugboardstr); if (removeOther) { input = input.replace(/[^A-Za-z]/g, ""); } - const enigma = new Enigma.EnigmaMachine(rotors, reflector, plugboard); + const enigma = new EnigmaMachine(rotors, reflector, plugboard); let result = enigma.crypt(input); if (removeOther) { // Five character cipher groups is traditional @@ -194,4 +194,4 @@ class EnigmaOp extends Operation { } -export default EnigmaOp; +export default Enigma; From 1b1a3c261dc62c0e62e6c71a6b38b5fe4cacbe36 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Fri, 4 Jan 2019 13:21:15 +0000 Subject: [PATCH 06/47] Typex: random rotors --- src/core/lib/Typex.mjs | 25 ++++++++++++++++--------- src/core/operations/Typex.mjs | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/core/lib/Typex.mjs b/src/core/lib/Typex.mjs index a99f3b6e..df6e646b 100644 --- a/src/core/lib/Typex.mjs +++ b/src/core/lib/Typex.mjs @@ -9,19 +9,26 @@ import OperationError from "../errors/OperationError"; import * as Enigma from "../lib/Enigma"; import Utils from "../Utils"; +/** + * A set of example Typex rotors. No Typex rotor wirings are publicly available, so these are + * randomised. + */ export const ROTORS = [ - {name: "1", value: "QWECYJIBFKMLTVZPOHUDGNRSXA Date: Fri, 4 Jan 2019 13:33:31 +0000 Subject: [PATCH 07/47] Add Bombe operation Still needs some work, but functional --- src/core/config/Categories.json | 3 +- src/core/lib/Bombe.mjs | 472 +++++++++++++++++++++++++++++++ src/core/lib/Enigma.mjs | 25 +- src/core/operations/Bombe.mjs | 118 ++++++++ tests/operations/index.mjs | 1 + tests/operations/tests/Bombe.mjs | 67 +++++ 6 files changed, 682 insertions(+), 4 deletions(-) create mode 100644 src/core/lib/Bombe.mjs create mode 100644 src/core/operations/Bombe.mjs create mode 100644 tests/operations/tests/Bombe.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 3e0d108e..5a40846c 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -103,7 +103,8 @@ "Citrix CTX1 Encode", "Citrix CTX1 Decode", "Pseudo-Random Number Generator", - "Enigma" + "Enigma", + "Bombe" ] }, { diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs new file mode 100644 index 00000000..70a2d2cb --- /dev/null +++ b/src/core/lib/Bombe.mjs @@ -0,0 +1,472 @@ +/** + * Emulation of the Bombe machine. + * + * @author s2224834 + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import OperationError from "../errors/OperationError"; +import Utils from "../Utils"; +import {Rotor, a2i, i2a} from "./Enigma"; + +/** + * Convenience/optimisation subclass of Rotor + * + * This allows creating multiple Rotors which share backing maps, to avoid repeatedly parsing the + * rotor spec strings and duplicating the maps in memory. + */ +class CopyRotor extends Rotor { + /** + * Return a copy of this Rotor. + */ + copy() { + const clone = { + map: this.map, + revMap: this.revMap, + pos: this.pos, + step: this.step, + transform: this.transform, + revTransform: this.revTransform, + }; + return clone; + } +} + +/** + * Node in the menu graph + * + * A node represents a cipher/plaintext letter. + */ +class Node { + /** + * Node constructor. + * @param {number} letter - The plain/ciphertext letter this node represents (as a number). + */ + constructor(letter) { + this.letter = letter; + this.edges = new Set(); + this.visited = false; + } +} + +/** + * Edge in the menu graph + * + * An edge represents an Enigma machine transformation between two letters. + */ +class Edge { + /** + * Edge constructor - an Enigma machine mapping between letters + * @param {number} pos - The rotor position, relative to the beginning of the crib, at this edge + * @param {number} node1 - Letter at one end (as a number) + * @param {number} node2 - Letter at the other end + */ + constructor(pos, node1, node2) { + this.pos = pos; + this.node1 = node1; + this.node2 = node2; + node1.edges.add(this); + node2.edges.add(this); + this.visited = false; + } + + /** + * Given the node at one end of this edge, return the other end. + * @param node {number} - The node we have + * @returns {number} + */ + getOther(node) { + if (this.node1 === node) { + return this.node2; + } + return this.node1; + } +} + +/** + * Scrambler. + * + * This is effectively just an Enigma machine, but it only operates on one character at a time and + * the stepping mechanism is different. + */ +class Scrambler { + /** Scrambler constructor. + * @param {Object[]} rotors - List of rotors in this scrambler + * @param {Object} reflector - This scrambler's reflector + * @param {number} pos - Position offset from start of crib + * @param {number} end1 - Letter in menu this scrambler is attached to + * @param {number} end2 - Other letter in menu this scrambler is attached to + */ + constructor(rotors, reflector, pos, end1, end2) { + this.reflector = reflector; + this.rotors = rotors; + this.rotorsRev = [].concat(rotors).reverse(); + this.initialPos = pos; + this.rotors[0].pos += pos; + this.end1 = end1; + this.end2 = end2; + } + + /** + * Step the rotors forward. + * + * All nodes in the Bombe step in sync. + * @param {number} n - How many rotors to step + */ + step(n) { + // The Bombe steps the slowest rotor on an actual Enigma first. + for (let i=this.rotors.length - 1; i>=this.rotors.length-n; i--) { + this.rotors[i].step(); + } + } + + /** + * Run a letter through the scrambler. + * @param {number} i - The letter to transform (as a number) + * @returns {number} + */ + transform(i) { + let letter = i; + 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); + } + return letter; + } + + /** + * Given one letter in the menu this scrambler maps to, return the other. + * @param end {number} - The node we have + * @returns {number} + */ + getOtherEnd(end) { + if (this.end1 === end) { + return this.end2; + } + return this.end1; + } + + /** + * Read the position this scrambler is set to. + * Note that because of Enigma's stepping, you need to set an actual Enigma to the previous + * position in order to get it to make a certain set of electrical connections when a button + * is pressed - this function *does* take this into account. + * However, as with the rest of the Bombe, it does not take stepping into account - the middle + * and slow rotors are treated as static. + * @return {string} + */ + getPos() { + let result = ""; + for (let i=0; i 25) { + // A crib longer than this will definitely cause the middle rotor to step somewhere + // A shorter crib is preferable to reduce this chance, of course + throw new OperationError("Crib is too long"); + } + for (let i=0; i nConnections) { + mostConnected = oMostConnected; + nConnections = oNConnections; + } + } + return [loops, nNodes, mostConnected, nConnections, edges]; + } + + /** + * Build a menu from the ciphertext and crib. + * A menu is just a graph where letters in either the ciphertext or crib (Enigma is symmetric, + * so there's no difference mathematically) are nodes and states of the Enigma machine itself + * are the edges. + * Additionally, we want a single connected graph, and of the subgraphs available, we want the + * one with the most loops (since these generate feedback cycles which efficiently close off + * disallowed states). + * Finally, we want to identify the most connected node in that graph (as it's the best choice + * of measurement point). + * @returns [Object, Object[]] - the most connected node, and the list of edges in the subgraph + */ + makeMenu() { + // First, we make a graph of all of the mappings given by the crib + // Make all nodes first + const nodes = new Map(); + for (const c of this.ciphertext + this.crib) { + if (!nodes.has(c)) { + const node = new Node(c); + nodes.set(c, node); + } + } + // Then all edges + for (let i=0; i { + let result = b[0] - a[0]; + if (result === 0) { + result = b[1] - a[1]; + } + return result; + }); + this.nLoops = graphs[0][0]; + return [graphs[0][2], graphs[0][4]]; + } + + /** + * Implement Welchman's diagonal board: If A steckers to B, that implies B steckers to A, and + * so forth. This function just gets the paired wire. + * @param {number[2]} i - Bombe state wire + * @returns {number[2]} + */ + getDiagonal(i) { + return [i[1], i[0]]; + } + + /** + * Bombe electrical simulation. Energise a wire. For all connected wires (both via the diagonal + * board and via the scramblers), energise them too, recursively. + * @param {number[2]} i - Bombe state wire + */ + energise(i) { + const idx = 26*i[0] + i[1]; + if (this.wires[idx]) { + return; + } + this.energiseCount ++; + this.wires[idx] = true; + this.energise(this.getDiagonal(i)); + + for (const scrambler of this.scramblers[i[0]]) { + const out = scrambler.transform(i[1]); + const other = scrambler.getOtherEnd(i[0]); + this.energise([other, out]); + } + } + + /** + * Having set up the Bombe, do the actual attack run. This tries every possible rotor setting + * and attempts to logically invalidate them. If it can't, it's added to the list of candidate + * solutions. + * @returns {string[][2]} - list of pairs of candidate rotor setting, and calculated stecker pair + */ + run() { + let stops = 0; + const result = []; + // For each possible rotor setting + const nChecks = Math.pow(26, this.baseRotors.length); + for (let i=1; i<=nChecks; i++) { + this.wires.fill(false); + // Energise the test input, follow the current through each scrambler + // (and the diagonal board) + this.energiseCount = 0; + this.energise(this.testInput); + // Count the energised outputs + let count = 0; + for (let j=26*this.testRegister; j<26*(1+this.testRegister); j++) { + if (this.wires[j]) { + count++; + } + } + // If it's not all of them, we have a stop + if (count < 26) { + stops += 1; + let stecker; + // The Bombe tells us one stecker pair as well. The input wire and test register we + // started with are hypothesised to be a stecker pair. + if (count === 25) { + // Our steckering hypothesis is wrong. Correct value is the un-energised wire. + for (let j=0; j<26; j++) { + if (!this.wires[26*this.testRegister + j]) { + stecker = `${i2a(this.testRegister)} <-> ${i2a(j)}`; + break; + } + } + } else if (count === 1) { + // This means our hypothesis for the steckering is correct. + stecker = `${i2a(this.testRegister)} <-> ${i2a(this.testInput[1])}`; + } else { + // Unusual, probably indicative of a poor menu. I'm a little unclear on how + // this was really handled, but we'll return it for the moment. + stecker = `? (wire count: ${count})`; + } + result.push([this.indicator.getPos(), stecker]); + } + // Step all the scramblers + // This loop counts how many rotors have reached their starting position (meaning the + // next one needs to step as well) + let n = 1; + for (let j=1; j 2) { + const msg = `Bombe run with ${this.nLoops} loops in menu (2+ desirable): ${stops} stops, ${Math.floor(100 * i / nChecks)}% done`; + this.update(msg); + } + } + return result; + } +} diff --git a/src/core/lib/Enigma.mjs b/src/core/lib/Enigma.mjs index 845ce413..9fc0f7d0 100644 --- a/src/core/lib/Enigma.mjs +++ b/src/core/lib/Enigma.mjs @@ -103,15 +103,17 @@ export class Rotor { if (!/^[A-Z]$/.test(initialPosition)) { throw new OperationError("Rotor initial position must be exactly one uppercase letter"); } - this.map = {}; - this.revMap = {}; + this.map = new Array(26).fill(); + this.revMap = new Array(26).fill(); + const uniq = {}; for (let i=0; i S\)/, + recipeConfig: [ + { + "op": "Bombe", + "args": [ + "BDFHJLCPRTXVZNYEIWGAKMUSQO G\)/, + recipeConfig: [ + { + "op": "Bombe", + "args": [ + "BDFHJLCPRTXVZNYEIWGAKMUSQO S\)/, + recipeConfig: [ + { + "op": "Bombe", + "args": [ + "BDFHJLCPRTXVZNYEIWGAKMUSQO Date: Tue, 8 Jan 2019 18:25:42 +0000 Subject: [PATCH 08/47] Bombe: review, tests, validation --- src/core/lib/Bombe.mjs | 34 ++++----- src/core/lib/Enigma.mjs | 6 +- src/core/operations/Bombe.mjs | 31 ++++++-- src/core/operations/Enigma.mjs | 6 +- tests/operations/tests/Bombe.mjs | 127 +++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 31 deletions(-) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 70a2d2cb..53766560 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -77,10 +77,7 @@ class Edge { * @returns {number} */ getOther(node) { - if (this.node1 === node) { - return this.node2; - } - return this.node1; + return this.node1 === node ? this.node2 : this.node1; } } @@ -144,10 +141,7 @@ class Scrambler { * @returns {number} */ getOtherEnd(end) { - if (this.end1 === end) { - return this.end2; - } - return this.end1; + return this.end1 === end ? this.end2 : this.end1; } /** @@ -194,8 +188,11 @@ export class BombeMachine { * @param {function} update - Function to call to send status updates (optional) */ constructor(rotors, reflector, ciphertext, crib, update=undefined) { - if (ciphertext.length !== crib.length) { - throw new OperationError("Ciphertext and crib length differ"); + if (ciphertext.length < crib.length) { + throw new OperationError("Crib overruns supplied ciphertext"); + } + if (ciphertext.length > crib.length) { + throw new OperationError("Ciphertext is longer than crib"); } if (crib.length < 2) { // This is the absolute bare minimum to be sane, and even then it's likely too short to @@ -226,7 +223,7 @@ export class BombeMachine { // This is the bundle of wires corresponding to the 26 letters within each of the 26 // possible nodes in the menu - this.wires = new Array(26*26).fill(false); + this.wires = new Array(26*26); // These are the pseudo-Engima devices corresponding to each edge in the menu, and the // nodes in the menu they each connect to @@ -271,9 +268,9 @@ export class BombeMachine { * If we have a way of sending status messages, do so. * @param {string} msg - Message to send. */ - update(msg) { + update(...msg) { if (this.updateFn !== undefined) { - this.updateFn(msg); + this.updateFn(...msg); } } @@ -411,7 +408,10 @@ export class BombeMachine { // For each possible rotor setting const nChecks = Math.pow(26, this.baseRotors.length); for (let i=1; i<=nChecks; i++) { - this.wires.fill(false); + // Benchmarking suggests this is faster than using .fill() + for (let i=0; i 2) { - const msg = `Bombe run with ${this.nLoops} loops in menu (2+ desirable): ${stops} stops, ${Math.floor(100 * i / nChecks)}% done`; - this.update(msg); + // (note this won't be triggered on 3-rotor runs - they run fast enough it doesn't seem necessary) + if (n > 3) { + this.update(this.nLoops, stops, i/nChecks); } } return result; diff --git a/src/core/lib/Enigma.mjs b/src/core/lib/Enigma.mjs index 9fc0f7d0..cfc93933 100644 --- a/src/core/lib/Enigma.mjs +++ b/src/core/lib/Enigma.mjs @@ -103,8 +103,8 @@ export class Rotor { if (!/^[A-Z]$/.test(initialPosition)) { throw new OperationError("Rotor initial position must be exactly one uppercase letter"); } - this.map = new Array(26).fill(); - this.revMap = new Array(26).fill(); + this.map = new Array(26); + this.revMap = new Array(26); const uniq = {}; for (let i=0; i S\)/, + recipeConfig: [ + { + "op": "Bombe", + "args": [ + "BDFHJLCPRTXVZNYEIWGAKMUSQO Date: Tue, 8 Jan 2019 19:37:34 +0000 Subject: [PATCH 09/47] Bombe: add trial decryption preview --- src/core/lib/Bombe.mjs | 60 +++++++++++++++++++++++++++----- src/core/operations/Bombe.mjs | 8 ++--- tests/operations/tests/Bombe.mjs | 27 +++++++++++--- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 53766560..1e6c3d2d 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -191,9 +191,6 @@ export class BombeMachine { if (ciphertext.length < crib.length) { throw new OperationError("Crib overruns supplied ciphertext"); } - if (ciphertext.length > crib.length) { - throw new OperationError("Ciphertext is longer than crib"); - } if (crib.length < 2) { // This is the absolute bare minimum to be sane, and even then it's likely too short to // be useful @@ -204,7 +201,7 @@ export class BombeMachine { // A shorter crib is preferable to reduce this chance, of course throw new OperationError("Crib is too long"); } - for (let i=0; i ${i2a(j)}`; + stecker = [this.testRegister, j]; break; } } } else if (count === 1) { // This means our hypothesis for the steckering is correct. - stecker = `${i2a(this.testRegister)} <-> ${i2a(this.testInput[1])}`; + stecker = [this.testRegister, this.testInput[1]]; } else { // Unusual, probably indicative of a poor menu. I'm a little unclear on how // this was really handled, but we'll return it for the moment. - stecker = `? (wire count: ${count})`; + stecker = undefined; } - result.push([this.indicator.getPos(), stecker]); + const testDecrypt = this.tryDecrypt(stecker); + let steckerStr; + if (stecker !== undefined) { + steckerStr = `${i2a(stecker[0])}${i2a(stecker[1])}`; + } else { + steckerStr = `?? (wire count: ${count})`; + } + result.push([this.indicator.getPos(), steckerStr, testDecrypt]); } // Step all the scramblers // This loop counts how many rotors have reached their starting position (meaning the diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index e875e50d..9ddd4b7b 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -112,7 +112,7 @@ class Bombe extends Operation { // For symmetry with the Enigma op, for the input we'll just remove all invalid characters input = input.replace(/[^A-Za-z]/g, "").toUpperCase(); crib = crib.replace(/[^A-Za-z]/g, "").toUpperCase(); - const ciphertext = input.slice(offset, offset+crib.length); + const ciphertext = input.slice(offset); const reflector = new Reflector(reflectorstr); let update; if (ENVIRONMENT_IS_WORKER()) { @@ -122,9 +122,9 @@ class Bombe extends Operation { } const bombe = new BombeMachine(rotors, reflector, ciphertext, crib, update); const result = bombe.run(); - let msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotor positions are listed left to right and start at the beginning of the crib, and ignore stepping and the ring setting. One stecker pair is determined. Results:\n`; - for (const [setting, wires] of result) { - msg += `Stop: ${setting} (${wires})\n`; + let msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotor positions are listed left to right and start at the beginning of the crib, and ignore stepping and the ring setting. One stecker pair is determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; + for (const [setting, stecker, decrypt] of result) { + msg += `Stop: ${setting} (plugboard: ${stecker}): ${decrypt}\n`; } return msg; } diff --git a/tests/operations/tests/Bombe.mjs b/tests/operations/tests/Bombe.mjs index 8742cfc0..65f4b701 100644 --- a/tests/operations/tests/Bombe.mjs +++ b/tests/operations/tests/Bombe.mjs @@ -8,9 +8,10 @@ import TestRegister from "../TestRegister"; TestRegister.addTests([ { + // Plugboard for this test is BO LC KE GA name: "Bombe: 3 rotor (self-stecker)", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA \(S <-> S\)/, + expectedMatch: /LGA \(plugboard: SS\): VFISUSGTKSTMPSUNAK/, recipeConfig: [ { "op": "Bombe", @@ -28,7 +29,7 @@ TestRegister.addTests([ { name: "Bombe: 3 rotor (other stecker)", input: "JBYALIHDYNUAAVKBYM", - expectedMatch: /LGA \(A <-> G\)/, + expectedMatch: /LGA \(plugboard: AG\): QFIMUMAFKMQSKMYNGW/, recipeConfig: [ { "op": "Bombe", @@ -46,7 +47,7 @@ TestRegister.addTests([ { name: "Bombe: crib offset", input: "AAABBYFLTHHYIJQAYBBYS", // first three chars here are faked - expectedMatch: /LGA \(S <-> S\)/, + expectedMatch: /LGA \(plugboard: SS\): VFISUSGTKSTMPSUNAK/, recipeConfig: [ { "op": "Bombe", @@ -61,12 +62,30 @@ TestRegister.addTests([ } ] }, + { + name: "Bombe: multiple stops", + input: "BBYFLTHHYIJQAYBBYS", + expectedMatch: /LGA \(plugboard: TT\): VFISUSGTKSTMPSUNAK/, + recipeConfig: [ + { + "op": "Bombe", + "args": [ + "BDFHJLCPRTXVZNYEIWGAKMUSQO S\)/, + expectedMatch: /LHSC \(plugboard: SS\)/, recipeConfig: [ { "op": "Bombe", From 8c757d1e03918ef2f3fb9098e0642719260b6690 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Wed, 9 Jan 2019 20:44:14 +0000 Subject: [PATCH 10/47] Bombe: optimise This cuts about 85% off the execution time. --- src/core/lib/Bombe.mjs | 209 +++++++++++++++++++++++-------- tests/operations/tests/Bombe.mjs | 5 +- 2 files changed, 157 insertions(+), 57 deletions(-) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 1e6c3d2d..3103f56a 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -81,6 +81,104 @@ class Edge { } } +/** + * As all the Bombe's rotors move in step, at any given point the vast majority of the scramblers + * in the machine share the majority of their state, which is hosted in this class. + */ +class SharedScrambler { + /** + * SharedScrambler constructor. + * @param {Object[]} rotors - List of rotors in the shared state _only_. + * @param {Object} reflector - The reflector in use. + */ + constructor(rotors, reflector) { + this.reflector = reflector; + this.rotors = rotors; + this.rotorsRev = [].concat(rotors).reverse(); + this.lowerCache = new Array(26); + this.higherCache = new Array(26); + for (let i=0; i<26; i++) { + this.higherCache[i] = new Array(26); + } + this.cacheGen(); + } + + /** + * Step the rotors forward. + * @param {number} n - How many rotors to step. This includes the rotors which are not part of + * the shared state, so should be 2 or more. + */ + step(n) { + for (let i=0; i=this.rotors.length-n; i--) { - this.rotors[i].step(); - } + step() { + // The Bombe steps the slowest rotor on an actual Enigma fastest, for reasons. + // ...but for optimisation reasons I'm going to cheat and not do that, as this vastly + // simplifies caching the state of the majority of the scramblers. The results are the + // same, just in a slightly different order. + this.rotor.step(); } + /** * Run a letter through the scrambler. * @param {number} i - The letter to transform (as a number) @@ -125,13 +224,14 @@ class Scrambler { */ transform(i) { let letter = i; - 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); + const cached = this.baseScrambler.fullTransform(this.rotor.pos, i); + if (cached !== undefined) { + return cached; } + letter = this.rotor.transform(letter); + letter = this.baseScrambler.transform(letter); + letter = this.rotor.revTransform(letter); + this.baseScrambler.addCache(this.rotor.pos, i, letter); return letter; } @@ -155,15 +255,11 @@ class Scrambler { */ getPos() { let result = ""; - for (let i=0; i 1) { + this.sharedScrambler.step(n); + } for (const scrambler of this.allScramblers) { - scrambler.step(n); + scrambler.step(); } // Send status messages at what seems to be a reasonably sensible frequency // (note this won't be triggered on 3-rotor runs - they run fast enough it doesn't seem necessary) diff --git a/tests/operations/tests/Bombe.mjs b/tests/operations/tests/Bombe.mjs index 65f4b701..6a96884c 100644 --- a/tests/operations/tests/Bombe.mjs +++ b/tests/operations/tests/Bombe.mjs @@ -27,6 +27,7 @@ TestRegister.addTests([ ] }, { + // This test produces a menu that doesn't use the first letter, which is also a good test name: "Bombe: 3 rotor (other stecker)", input: "JBYALIHDYNUAAVKBYM", expectedMatch: /LGA \(plugboard: AG\): QFIMUMAFKMQSKMYNGW/, @@ -80,8 +81,7 @@ TestRegister.addTests([ } ] }, - /* - * Long test is long + // This test is a bit slow - it takes about 12s on my test hardware { name: "Bombe: 4 rotor", input: "LUOXGJSHGEDSRDOQQX", @@ -100,7 +100,6 @@ TestRegister.addTests([ } ] }, - */ { name: "Bombe: no crib", input: "JBYALIHDYNUAAVKBYM", From 3eb44708e503e42e3429ca9488b004efeef5b5e5 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 10 Jan 2019 18:04:02 +0000 Subject: [PATCH 11/47] Add MultiBombe Runs the Bombe multiple times with different rotor specs. Edits the core BombeMachine a little to add the ability to switch rotors without rewiring everything --- src/core/config/Categories.json | 3 +- src/core/lib/Bombe.mjs | 63 ++++- src/core/lib/Enigma.mjs | 1 + src/core/operations/MultipleBombe.mjs | 307 +++++++++++++++++++++++ tests/operations/index.mjs | 1 + tests/operations/tests/Bombe.mjs | 2 +- tests/operations/tests/MultipleBombe.mjs | 47 ++++ 7 files changed, 411 insertions(+), 13 deletions(-) create mode 100644 src/core/operations/MultipleBombe.mjs create mode 100644 tests/operations/tests/MultipleBombe.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 5a40846c..986606c9 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -104,7 +104,8 @@ "Citrix CTX1 Decode", "Pseudo-Random Number Generator", "Enigma", - "Bombe" + "Bombe", + "Multiple Bombe" ] }, { diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 3103f56a..8b781b68 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -92,14 +92,24 @@ class SharedScrambler { * @param {Object} reflector - The reflector in use. */ constructor(rotors, reflector) { - this.reflector = reflector; - this.rotors = rotors; - this.rotorsRev = [].concat(rotors).reverse(); this.lowerCache = new Array(26); this.higherCache = new Array(26); for (let i=0; i<26; i++) { this.higherCache[i] = new Array(26); } + this.changeRotors(rotors, reflector); + } + + /** + * Replace the rotors and reflector in this SharedScrambler. + * This takes care of flushing caches as well. + * @param {Object[]} rotors - List of rotors in the shared state _only_. + * @param {Object} reflector - The reflector in use. + */ + changeRotors(rotors, reflector) { + this.reflector = reflector; + this.rotors = rotors; + this.rotorsRev = [].concat(rotors).reverse(); this.cacheGen(); } @@ -195,13 +205,22 @@ class Scrambler { */ constructor(base, rotor, pos, end1, end2) { this.baseScrambler = base; - this.rotor = rotor; this.initialPos = pos; - this.rotor.pos += pos; + this.changeRotor(rotor); this.end1 = end1; this.end2 = end2; } + /** + * Replace the rotor in this scrambler. + * The position is reset automatically. + * @param {Object} rotor - New rotor + */ + changeRotor(rotor) { + this.rotor = rotor; + this.rotor.pos += this.initialPos; + } + /** * Step the rotors forward. * @@ -304,12 +323,7 @@ export class BombeMachine { } this.ciphertext = ciphertext; this.crib = crib; - // This is ordered from the Enigma fast rotor to the slow, so bottom to top for the Bombe - this.baseRotors = []; - for (const rstr of rotors) { - const rotor = new CopyRotor(rstr, "", "A", "A"); - this.baseRotors.push(rotor); - } + this.initRotors(rotors); this.updateFn = update; const [mostConnected, edges] = this.makeMenu(); @@ -355,6 +369,33 @@ export class BombeMachine { } } + /** + * Build Rotor objects from list of rotor wiring strings. + * @param {string[]} rotors - List of rotor wiring strings + */ + initRotors(rotors) { + // This is ordered from the Enigma fast rotor to the slow, so bottom to top for the Bombe + this.baseRotors = []; + for (const rstr of rotors) { + const rotor = new CopyRotor(rstr, "", "A", "A"); + this.baseRotors.push(rotor); + } + } + + /** + * Replace the rotors and reflector in all components of this Bombe. + * @param {string[]} rotors - List of rotor wiring strings + * @param {Object} reflector - Reflector object + */ + changeRotors(rotors, reflector) { + // At the end of the run, the rotors are all back in the same position they started + this.initRotors(rotors); + this.sharedScrambler.changeRotors(this.baseRotors.slice(1), reflector); + for (const scrambler of this.allScramblers) { + scrambler.changeRotor(this.baseRotors[0].copy()); + } + } + /** * If we have a way of sending status messages, do so. * @param {string} msg - Message to send. diff --git a/src/core/lib/Enigma.mjs b/src/core/lib/Enigma.mjs index cfc93933..0a083bce 100644 --- a/src/core/lib/Enigma.mjs +++ b/src/core/lib/Enigma.mjs @@ -171,6 +171,7 @@ class PairMapBase { 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; diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs new file mode 100644 index 00000000..6bcd1051 --- /dev/null +++ b/src/core/operations/MultipleBombe.mjs @@ -0,0 +1,307 @@ +/** + * Emulation of the Bombe machine. + * This version carries out multiple Bombe runs to handle unknown rotor configurations. + * + * @author s2224834 + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import {BombeMachine} from "../lib/Bombe"; +import {ROTORS, REFLECTORS, Reflector} from "../lib/Enigma"; + +/** + * Convenience method for flattening the preset ROTORS object into a newline-separated string. + * @param {Object[]} - Preset rotors object + * @param {number} s - Start index + * @param {number} n - End index + * @returns {string} + */ +function rotorsFormat(rotors, s, n) { + const res = []; + for (const i of rotors.slice(s, n)) { + res.push(i.value); + } + return res.join("\n"); +} + +/** + * Combinatorics choose function + * @param {number} n + * @param {number} k + * @returns number + */ +function choose(n, k) { + let res = 1; + for (let i=1; i<=k; i++) { + res *= (n + 1 - i) / i; + } + return res; +} + +/** + * Bombe operation + */ +class MultipleBombe extends Operation { + /** + * Bombe constructor + */ + constructor() { + super(); + + this.name = "Multiple Bombe"; + this.module = "Default"; + this.description = "Emulation of the Bombe machine used to attack Enigma. This version carries out multiple Bombe runs to handle unknown rotor configurations.

You should test your menu on the single Bombe operation before running it here. See the description of the Bombe operation for instructions on choosing a crib."; + this.infoURL = "https://wikipedia.org/wiki/Bombe"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Standard Enigmas", + "type": "populateOption", + "value": [ + { + name: "German Service Enigma (First - 3 rotor)", + value: rotorsFormat(ROTORS, 0, 5) + }, + { + name: "German Service Enigma (Second - 3 rotor)", + value: rotorsFormat(ROTORS, 0, 8) + }, + { + name: "German Service Enigma (Third - 4 rotor)", + value: rotorsFormat(ROTORS, 0, 8) + }, + { + name: "German Service Enigma (Fourth - 4 rotor)", + value: rotorsFormat(ROTORS, 0, 8) + }, + { + name: "User defined", + value: "" + }, + ], + "target": 1 + }, + { + name: "Main rotors", + type: "text", + value: "" + }, + { + "name": "Standard Enigmas", + "type": "populateOption", + "value": [ + { + name: "German Service Enigma (First - 3 rotor)", + value: "" + }, + { + name: "German Service Enigma (Second - 3 rotor)", + value: "" + }, + { + name: "German Service Enigma (Third - 4 rotor)", + value: rotorsFormat(ROTORS, 8, 9) + }, + { + name: "German Service Enigma (Fourth - 4 rotor)", + value: rotorsFormat(ROTORS, 8, 10) + }, + { + name: "User defined", + value: "" + }, + ], + "target": 3 + }, + { + name: "4th rotor", + type: "text", + value: "" + }, + { + "name": "Standard Enigmas", + "type": "populateOption", + "value": [ + { + name: "German Service Enigma (First - 3 rotor)", + value: rotorsFormat(REFLECTORS, 0, 1) + }, + { + name: "German Service Enigma (Second - 3 rotor)", + value: rotorsFormat(REFLECTORS, 0, 2) + }, + { + name: "German Service Enigma (Third - 4 rotor)", + value: rotorsFormat(REFLECTORS, 2, 3) + }, + { + name: "German Service Enigma (Fourth - 4 rotor)", + value: rotorsFormat(REFLECTORS, 2, 4) + }, + { + name: "User defined", + value: "" + }, + ], + "target": 5 + }, + { + name: "Reflectors", + type: "text", + value: "" + }, + { + name: "Crib", + type: "string", + value: "" + }, + { + name: "Crib offset", + type: "number", + value: 0 + } + ]; + } + + /** + * Format and send a status update message. + * @param {number} nLoops - Number of loops in the menu + * @param {number} nStops - How many stops so far + * @param {number} progress - Progress (as a float in the range 0..1) + */ + updateStatus(nLoops, nStops, progress) { + const msg = `Bombe run with ${nLoops} loops in menu (2+ desirable): ${nStops} stops, ${Math.floor(100 * progress)}% done`; + self.sendStatusMessage(msg); + } + + /** + * Early rotor description string validation. + * Drops stepping information. + * @param {string} rstr - The rotor description string + * @returns {string} - Rotor description with stepping stripped, if any + */ + validateRotor(rstr) { + // The Bombe doesn't take stepping into account so we'll just ignore it here + if (rstr.includes("<")) { + rstr = rstr.split("<", 2)[0]; + } + // Duplicate the validation of the rotor strings here, otherwise you might get an error + // thrown halfway into a big Bombe run + if (!/^[A-Z]{26}$/.test(rstr)) { + throw new OperationError("Rotor wiring must be 26 unique uppercase letters"); + } + if (new Set(rstr).size !== 26) { + throw new OperationError("Rotor wiring must be 26 unique uppercase letters"); + } + return rstr; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const mainRotorsStr = args[1]; + const fourthRotorsStr = args[3]; + const reflectorsStr = args[5]; + let crib = args[6]; + const offset = args[7]; + // TODO got this far + const rotors = []; + const fourthRotors = []; + const reflectors = []; + for (let rstr of mainRotorsStr.split("\n")) { + rstr = this.validateRotor(rstr); + rotors.push(rstr); + } + if (rotors.length < 3) { + throw new OperationError("A minimum of three rotors must be supplied"); + } + if (fourthRotorsStr !== "") { + for (let rstr of fourthRotorsStr.split("\n")) { + rstr = this.validateRotor(rstr); + fourthRotors.push(rstr); + } + } + if (fourthRotors.length === 0) { + fourthRotors.push(""); + } + for (const rstr of reflectorsStr.split("\n")) { + const reflector = new Reflector(rstr); + reflectors.push(reflector); + } + if (reflectors.length === 0) { + throw new OperationError("A minimum of one reflector must be supplied"); + } + if (crib.length === 0) { + throw new OperationError("Crib cannot be empty"); + } + if (offset < 0) { + throw new OperationError("Offset cannot be negative"); + } + // For symmetry with the Enigma op, for the input we'll just remove all invalid characters + input = input.replace(/[^A-Za-z]/g, "").toUpperCase(); + crib = crib.replace(/[^A-Za-z]/g, "").toUpperCase(); + const ciphertext = input.slice(offset); + let update; + if (ENVIRONMENT_IS_WORKER()) { + update = this.updateStatus; + } else { + update = undefined; + } + let bombe = undefined; + let msg; + // I could use a proper combinatorics algorithm here... but it would be more code to + // write one, and we don't seem to have one in our existing libraries, so massively nested + // for loop it is + const totalRuns = choose(rotors.length, 3) * 6 * fourthRotors.length * reflectors.length; + let nRuns = 0; + let nStops = 0; + for (const rotor1 of rotors) { + for (const rotor2 of rotors) { + if (rotor2 === rotor1) { + continue; + } + for (const rotor3 of rotors) { + if (rotor3 === rotor2 || rotor3 === rotor1) { + continue; + } + for (const rotor4 of fourthRotors) { + for (const reflector of reflectors) { + nRuns++; + const runRotors = [rotor1, rotor2, rotor3]; + if (rotor4 !== "") { + runRotors.push(rotor4); + } + if (bombe === undefined) { + bombe = new BombeMachine(runRotors, reflector, ciphertext, crib); + msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. One stecker pair is determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; + } else { + bombe.changeRotors(runRotors, reflector); + } + const result = bombe.run(); + nStops += result.length; + if (update !== undefined) { + update(bombe.nLoops, nStops, nRuns / totalRuns); + } + if (result.length > 0) { + msg += `Rotors: ${runRotors.join(", ")}\nReflector: ${reflector.pairs}\n`; + for (const [setting, stecker, decrypt] of result) { + msg += `Stop: ${setting} (plugboard: ${stecker}): ${decrypt}\n`; + } + } + } + } + } + } + } + return msg; + } +} + +export default MultipleBombe; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index dfd5fb1d..b5b25c0c 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -84,6 +84,7 @@ import "./tests/ParseTLV"; import "./tests/Media"; import "./tests/Enigma"; import "./tests/Bombe"; +import "./tests/MultipleBombe"; // Cannot test operations that use the File type yet //import "./tests/SplitColourChannels"; diff --git a/tests/operations/tests/Bombe.mjs b/tests/operations/tests/Bombe.mjs index 6a96884c..0a8af93f 100644 --- a/tests/operations/tests/Bombe.mjs +++ b/tests/operations/tests/Bombe.mjs @@ -85,7 +85,7 @@ TestRegister.addTests([ { name: "Bombe: 4 rotor", input: "LUOXGJSHGEDSRDOQQX", - expectedMatch: /LHSC \(plugboard: SS\)/, + expectedMatch: /LHSC \(plugboard: SS\): HHHSSSGQUUQPKSEKWK/, recipeConfig: [ { "op": "Bombe", diff --git a/tests/operations/tests/MultipleBombe.mjs b/tests/operations/tests/MultipleBombe.mjs new file mode 100644 index 00000000..5f7f43c4 --- /dev/null +++ b/tests/operations/tests/MultipleBombe.mjs @@ -0,0 +1,47 @@ +/** + * Bombe machine tests. + * @author s2224834 + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import TestRegister from "../TestRegister"; + +TestRegister.addTests([ + { + name: "Multi-Bombe: 3 rotor", + input: "BBYFLTHHYIJQAYBBYS", + expectedMatch: /LGA \(plugboard: SS\): VFISUSGTKSTMPSUNAK/, + recipeConfig: [ + { + "op": "Multiple Bombe", + "args": [ + // I, II and III + "User defined", "EKMFLGDQVZNTOWYHXUSPAIBRCJ Date: Thu, 10 Jan 2019 18:44:50 +0000 Subject: [PATCH 12/47] Bombe: Firefox optimisation Switch a couple of for of loops in the critical path for classic fors. This loses about 10% performance in Chrome, but it brings Firefox performance in line with Chrome's, rather than 2.5 times slower. --- src/core/lib/Bombe.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 8b781b68..7400c98a 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -514,7 +514,8 @@ export class BombeMachine { const idxPair = 26*j + i; this.wires[idxPair] = true; - for (const scrambler of this.scramblers[i]) { + for (let k=0; k Date: Fri, 11 Jan 2019 13:18:25 +0000 Subject: [PATCH 13/47] Bombe: Add checking machine --- src/core/lib/Bombe.mjs | 168 +++++++++++++++++------ src/core/lib/Enigma.mjs | 3 +- src/core/operations/Bombe.mjs | 12 +- src/core/operations/MultipleBombe.mjs | 13 +- tests/operations/tests/Bombe.mjs | 40 ++++-- tests/operations/tests/Enigma.mjs | 4 +- tests/operations/tests/MultipleBombe.mjs | 4 +- 7 files changed, 178 insertions(+), 66 deletions(-) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 7400c98a..4ae0ff7f 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -8,7 +8,7 @@ import OperationError from "../errors/OperationError"; import Utils from "../Utils"; -import {Rotor, a2i, i2a} from "./Enigma"; +import {Rotor, Plugboard, a2i, i2a} from "./Enigma"; /** * Convenience/optimisation subclass of Rotor @@ -302,7 +302,7 @@ export class BombeMachine { * @param {string} crib - Known plaintext for this ciphertext * @param {function} update - Function to call to send status updates (optional) */ - constructor(rotors, reflector, ciphertext, crib, update=undefined) { + constructor(rotors, reflector, ciphertext, crib, check, update=undefined) { if (ciphertext.length < crib.length) { throw new OperationError("Crib overruns supplied ciphertext"); } @@ -324,6 +324,7 @@ export class BombeMachine { this.ciphertext = ciphertext; this.crib = crib; this.initRotors(rotors); + this.check = check; this.updateFn = update; const [mostConnected, edges] = this.makeMenu(); @@ -507,7 +508,6 @@ export class BombeMachine { if (this.wires[idx]) { return; } - this.energiseCount ++; this.wires[idx] = true; // Welchman's diagonal board: if A steckers to B, that implies B steckers to A. Handle // both. @@ -564,16 +564,131 @@ export class BombeMachine { const fastRotor = this.indicator.rotor; const initialPos = fastRotor.pos; const res = []; + const plugboard = new Plugboard(stecker); // The indicator scrambler starts in the right place for the beginning of the ciphertext. for (let i=0; i 1) { + // This is an invalid stop. + return ""; + } else if (count === 0) { + // No information about steckering from this wire + continue; + } + results.add(this.formatPair(i, other)); + } + return [...results].join(" "); + } + + /** + * Check to see if the Bombe has stopped. If so, process the stop. + * @returns {(undefined|string[3])} - Undefined for no stop, or [rotor settings, plugboard settings, decryption preview] + */ + checkStop() { + // Count the energised outputs + let count = 0; + for (let j=26*this.testRegister; j<26*(1+this.testRegister); j++) { + if (this.wires[j]) { + count++; + } + } + if (count === 26) { + return undefined; + } + // If it's not all of them, we have a stop + let steckerPair; + // The Bombe tells us one stecker pair as well. The input wire and test register we + // started with are hypothesised to be a stecker pair. + if (count === 25) { + // Our steckering hypothesis is wrong. Correct value is the un-energised wire. + for (let j=0; j<26; j++) { + if (!this.wires[26*this.testRegister + j]) { + steckerPair = j; + break; + } + } + } else if (count === 1) { + // This means our hypothesis for the steckering is correct. + steckerPair = this.testInput[1]; + } else { + // If this happens a lot it implies the menu isn't good enough. We can't do + // anything useful with it as we don't have a stecker partner, so we'll just drop it + // and move on. This does risk eating the actual stop occasionally, but I've only seen + // this happen when the menu is bad enough we have thousands of stops, so I'm not sure + // it matters. + return undefined; + } + let stecker; + if (this.check) { + stecker = this.checkingMachine(steckerPair); + if (stecker === "") { + // Invalid stop - don't count it, don't return it + return undefined; + } + } else { + stecker = `${i2a(this.testRegister)}${i2a(steckerPair)}`; + } + const testDecrypt = this.tryDecrypt(stecker); + return [this.indicator.getPos(), stecker, testDecrypt]; + } + /** * Having set up the Bombe, do the actual attack run. This tries every possible rotor setting * and attempts to logically invalidate them. If it can't, it's added to the list of candidate @@ -592,45 +707,12 @@ export class BombeMachine { } // Energise the test input, follow the current through each scrambler // (and the diagonal board) - this.energiseCount = 0; this.energise(...this.testInput); - // Count the energised outputs - let count = 0; - for (let j=26*this.testRegister; j<26*(1+this.testRegister); j++) { - if (this.wires[j]) { - count++; - } - } - // If it's not all of them, we have a stop - if (count < 26) { - stops += 1; - let stecker; - // The Bombe tells us one stecker pair as well. The input wire and test register we - // started with are hypothesised to be a stecker pair. - if (count === 25) { - // Our steckering hypothesis is wrong. Correct value is the un-energised wire. - for (let j=0; j<26; j++) { - if (!this.wires[26*this.testRegister + j]) { - stecker = [this.testRegister, j]; - break; - } - } - } else if (count === 1) { - // This means our hypothesis for the steckering is correct. - stecker = [this.testRegister, this.testInput[1]]; - } else { - // Unusual, probably indicative of a poor menu. I'm a little unclear on how - // this was really handled, but we'll return it for the moment. - stecker = undefined; - } - const testDecrypt = this.tryDecrypt(stecker); - let steckerStr; - if (stecker !== undefined) { - steckerStr = `${i2a(stecker[0])}${i2a(stecker[1])}`; - } else { - steckerStr = `?? (wire count: ${count})`; - } - result.push([this.indicator.getPos(), steckerStr, testDecrypt]); + + const stop = this.checkStop(); + if (stop !== undefined) { + stops++; + result.push(stop); } // Step all the scramblers // This loop counts how many rotors have reached their starting position (meaning the diff --git a/src/core/lib/Enigma.mjs b/src/core/lib/Enigma.mjs index 0a083bce..6b6c4d63 100644 --- a/src/core/lib/Enigma.mjs +++ b/src/core/lib/Enigma.mjs @@ -182,7 +182,8 @@ class PairMapBase { } const a = a2i(pair[0]), b = a2i(pair[1]); if (a === b) { - throw new OperationError(`${name}: cannot connect ${pair[0]} to itself`); + // self-stecker + return; } if (this.map.hasOwnProperty(a)) { throw new OperationError(`${name} connects ${pair[0]} more than once`); diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index 9ddd4b7b..292017e8 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -23,7 +23,7 @@ class Bombe extends Operation { this.name = "Bombe"; this.module = "Default"; - this.description = "Emulation of the Bombe machine used to attack Enigma.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and one plugboard pair.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops, a large number of incorrect outputs will be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor."; + this.description = "Emulation of the Bombe machine used to attack Enigma.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops, a large number of incorrect outputs will be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine."; this.infoURL = "https://wikipedia.org/wiki/Bombe"; this.inputType = "string"; this.outputType = "string"; @@ -66,6 +66,11 @@ class Bombe extends Operation { name: "Crib offset", type: "number", value: 0 + }, + { + name: "Use checking machine", + type: "boolean", + value: true } ]; } @@ -90,6 +95,7 @@ class Bombe extends Operation { const reflectorstr = args[4]; let crib = args[5]; const offset = args[6]; + const check = args[7]; const rotors = []; for (let i=0; i<4; i++) { if (i === 3 && args[i] === "") { @@ -120,9 +126,9 @@ class Bombe extends Operation { } else { update = undefined; } - const bombe = new BombeMachine(rotors, reflector, ciphertext, crib, update); + const bombe = new BombeMachine(rotors, reflector, ciphertext, crib, check, update); const result = bombe.run(); - let msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotor positions are listed left to right and start at the beginning of the crib, and ignore stepping and the ring setting. One stecker pair is determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; + let msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotor positions are listed left to right and start at the beginning of the crib, and ignore stepping and the ring setting. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; for (const [setting, stecker, decrypt] of result) { msg += `Stop: ${setting} (plugboard: ${stecker}): ${decrypt}\n`; } diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs index 6bcd1051..4b3123a4 100644 --- a/src/core/operations/MultipleBombe.mjs +++ b/src/core/operations/MultipleBombe.mjs @@ -163,6 +163,11 @@ class MultipleBombe extends Operation { name: "Crib offset", type: "number", value: 0 + }, + { + name: "Use checking machine", + type: "boolean", + value: true } ]; } @@ -211,7 +216,7 @@ class MultipleBombe extends Operation { const reflectorsStr = args[5]; let crib = args[6]; const offset = args[7]; - // TODO got this far + const check = args[8]; const rotors = []; const fourthRotors = []; const reflectors = []; @@ -279,8 +284,8 @@ class MultipleBombe extends Operation { runRotors.push(rotor4); } if (bombe === undefined) { - bombe = new BombeMachine(runRotors, reflector, ciphertext, crib); - msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. One stecker pair is determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; + bombe = new BombeMachine(runRotors, reflector, ciphertext, crib, check); + msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; } else { bombe.changeRotors(runRotors, reflector); } @@ -290,7 +295,7 @@ class MultipleBombe extends Operation { update(bombe.nLoops, nStops, nRuns / totalRuns); } if (result.length > 0) { - msg += `Rotors: ${runRotors.join(", ")}\nReflector: ${reflector.pairs}\n`; + msg += `\nRotors: ${runRotors.join(", ")}\nReflector: ${reflector.pairs}\n`; for (const [setting, stecker, decrypt] of result) { msg += `Stop: ${setting} (plugboard: ${stecker}): ${decrypt}\n`; } diff --git a/tests/operations/tests/Bombe.mjs b/tests/operations/tests/Bombe.mjs index 0a8af93f..fca420d3 100644 --- a/tests/operations/tests/Bombe.mjs +++ b/tests/operations/tests/Bombe.mjs @@ -21,7 +21,7 @@ TestRegister.addTests([ "EKMFLGDQVZNTOWYHXUSPAIBRCJ Date: Fri, 11 Jan 2019 18:24:16 +0000 Subject: [PATCH 14/47] Bombe: wording/docs tweaks --- src/core/lib/Bombe.mjs | 2 +- src/core/operations/Bombe.mjs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 4ae0ff7f..81581d67 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -693,7 +693,7 @@ export class BombeMachine { * Having set up the Bombe, do the actual attack run. This tries every possible rotor setting * and attempts to logically invalidate them. If it can't, it's added to the list of candidate * solutions. - * @returns {string[][2]} - list of pairs of candidate rotor setting, and calculated stecker pair + * @returns {string[][3]} - list of 3-tuples of candidate rotor setting, plugboard settings, and decryption preview */ run() { let stops = 0; diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index 292017e8..3c29cd8c 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -35,19 +35,19 @@ class Bombe extends Operation { defaultIndex: 2 }, { - name: "2nd rotor", + name: "2nd (middle) rotor", type: "editableOption", value: ROTORS, defaultIndex: 1 }, { - name: "3rd rotor", + name: "3rd (left-hand) rotor", type: "editableOption", value: ROTORS, defaultIndex: 0 }, { - name: "4th rotor", + name: "4th (left-most, only some models) rotor", type: "editableOption", value: ROTORS_OPTIONAL, defaultIndex: 10 From 49f5c94a750c8a11c7a19f29efa3265549df3d8f Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Sat, 12 Jan 2019 01:10:47 +0000 Subject: [PATCH 15/47] Bombe: further optimisation --- src/core/lib/Bombe.mjs | 63 +++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 81581d67..1e3c5b3e 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -158,27 +158,6 @@ class SharedScrambler { } } - /** - * Get the fully cached result, if present. - * @param {number} pos - Position of the fast rotor - * @param {number} i - Letter - * @returns {number|undefined} - undefined if not cached - */ - fullTransform(pos, i) { - return this.higherCache[pos][i]; - } - - /** - * Add a value to the full result cache. - * @param {number} pos - Position of the fast rotor - * @param {number} i - Letter - * @param {number} val - Transformed letter - */ - addCache(pos, i, val) { - this.higherCache[pos][i] = val; - this.higherCache[pos][val] = i; - } - /** * Map a letter through this (partial) scrambler. * @param {number} i - The letter @@ -209,6 +188,9 @@ class Scrambler { this.changeRotor(rotor); this.end1 = end1; this.end2 = end2; + // For efficiency reasons, we pull the relevant shared cache from the baseScrambler into + // this object - this saves us a few pointer dereferences + this.cache = this.baseScrambler.higherCache[pos]; } /** @@ -233,6 +215,7 @@ class Scrambler { // simplifies caching the state of the majority of the scramblers. The results are the // same, just in a slightly different order. this.rotor.step(); + this.cache = this.baseScrambler.higherCache[this.rotor.pos]; } @@ -243,14 +226,15 @@ class Scrambler { */ transform(i) { let letter = i; - const cached = this.baseScrambler.fullTransform(this.rotor.pos, i); + const cached = this.cache[i]; if (cached !== undefined) { return cached; } letter = this.rotor.transform(letter); letter = this.baseScrambler.transform(letter); letter = this.rotor.revTransform(letter); - this.baseScrambler.addCache(this.rotor.pos, i, letter); + this.cache[i] = letter; + this.cache[letter] = i; return letter; } @@ -513,12 +497,26 @@ export class BombeMachine { // both. const idxPair = 26*j + i; this.wires[idxPair] = true; + if (i === this.testRegister || j === this.testRegister) { + this.energiseCount++; + if (this.energiseCount === 26) { + // no point continuing, bail out + return; + } + } for (let k=0; k Date: Sat, 12 Jan 2019 01:35:24 +0000 Subject: [PATCH 16/47] Bombe: tweaks Twiddle the default rotor sets a bit. Add a time remaining estimate for the multibombe. --- src/core/lib/Enigma.mjs | 8 ++++---- src/core/operations/Bombe.mjs | 4 ++-- src/core/operations/Enigma.mjs | 6 +++--- src/core/operations/MultipleBombe.mjs | 18 ++++++++++++------ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/core/lib/Enigma.mjs b/src/core/lib/Enigma.mjs index 6b6c4d63..1ed0ea2b 100644 --- a/src/core/lib/Enigma.mjs +++ b/src/core/lib/Enigma.mjs @@ -22,14 +22,14 @@ export const ROTORS = [ {name: "VI", value: "JPGVOUMFYQBENHZRDKASXLICTW 0) { msg += `\nRotors: ${runRotors.join(", ")}\nReflector: ${reflector.pairs}\n`; From eee92aa1aaf2156ba188e450a73efa4704048dde Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Sat, 12 Jan 2019 12:56:21 +0000 Subject: [PATCH 17/47] Bombe: fix some outdated docs --- src/core/lib/Bombe.mjs | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 1e3c5b3e..03413350 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -19,6 +19,7 @@ import {Rotor, Plugboard, a2i, i2a} from "./Enigma"; class CopyRotor extends Rotor { /** * Return a copy of this Rotor. + * @returns {Object} */ copy() { const clone = { @@ -204,10 +205,9 @@ class Scrambler { } /** - * Step the rotors forward. + * Step the rotor forward. * - * All nodes in the Bombe step in sync. - * @param {number} n - How many rotors to step + * The base SharedScrambler needs to be instructed to step separately. */ step() { // The Bombe steps the slowest rotor on an actual Enigma fastest, for reasons. @@ -284,6 +284,7 @@ export class BombeMachine { * @param {Object} reflector - Reflector object * @param {string} ciphertext - The ciphertext to attack * @param {string} crib - Known plaintext for this ciphertext + * @param {boolean} check - Whether to use the checking machine * @param {function} update - Function to call to send status updates (optional) */ constructor(rotors, reflector, ciphertext, crib, check, update=undefined) { @@ -383,7 +384,7 @@ export class BombeMachine { /** * If we have a way of sending status messages, do so. - * @param {string} msg - Message to send. + * @param {...*} msg - Message to send. */ update(...msg) { if (this.updateFn !== undefined) { @@ -485,7 +486,8 @@ export class BombeMachine { /** * Bombe electrical simulation. Energise a wire. For all connected wires (both via the diagonal * board and via the scramblers), energise them too, recursively. - * @param {number[2]} i - Bombe state wire + * @param {number} i - Bombe wire bundle + * @param {number} j - Bombe stecker hypothesis wire within bundle */ energise(i, j) { const idx = 26*i + j; @@ -535,33 +537,13 @@ export class BombeMachine { } } - /** - * Single-pair steckering. Used for trial decryption rather than building a whole plugboard - * object for one pair - * @param {number[2]} stecker - Known stecker pair. - * @param {number} x - Letter to transform. - * @result number - */ - singleStecker(stecker, x) { - if (stecker === undefined) { - return x; - } - if (x === stecker[0]) { - return stecker[1]; - } - if (x === stecker[1]) { - return stecker[0]; - } - return x; - } - /** * Trial decryption at the current setting. * Used after we get a stop. * This applies the detected stecker pair if we have one. It does not handle the other * steckering or stepping (which is why we limit it to 26 characters, since it's guaranteed to * be wrong after that anyway). - * @param {number[2]} stecker - Known stecker pair. + * @param {string} stecker - Known stecker spec string. * @returns {string} */ tryDecrypt(stecker) { From ffc4b0a0a8ef1576bf0b292e184aed5dace1994c Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Mon, 14 Jan 2019 17:15:54 +0000 Subject: [PATCH 18/47] Bombe: lol --- src/core/operations/Bombe.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index 9014fd3a..34a94e53 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -50,7 +50,7 @@ class Bombe extends Operation { name: "4th (left-most, only some models) rotor", type: "editableOption", value: ROTORS_FOURTH, - defaultIndex: 10 + defaultIndex: 0 }, { name: "Reflector", From 02b9dbdee962dfff2df1a6c4c61a62e9414415d9 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Tue, 15 Jan 2019 19:03:17 +0000 Subject: [PATCH 19/47] Replaced loading animation with animated Bombe SVG --- src/web/App.mjs | 2 + src/web/OutputWaiter.mjs | 41 ++++- src/web/html/index.html | 10 +- src/web/static/images/bombe.svg | 261 +++++++++++++++++++++++++++++ src/web/stylesheets/layout/_io.css | 32 ++-- src/web/stylesheets/preloader.css | 69 +++----- 6 files changed, 349 insertions(+), 66 deletions(-) create mode 100644 src/web/static/images/bombe.svg diff --git a/src/web/App.mjs b/src/web/App.mjs index 1dab16e6..453fba22 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -51,10 +51,12 @@ class App { */ setup() { document.dispatchEvent(this.manager.appstart); + this.initialiseSplitter(); this.loadLocalStorage(); this.populateOperationsList(); this.manager.setup(); + this.manager.output.saveBombe(); this.resetLayout(); this.setCompileMessage(); diff --git a/src/web/OutputWaiter.mjs b/src/web/OutputWaiter.mjs index 7203a16f..39d6e51b 100755 --- a/src/web/OutputWaiter.mjs +++ b/src/web/OutputWaiter.mjs @@ -334,24 +334,55 @@ class OutputWaiter { /** - * Shows or hides the loading icon. + * Save bombe object before it is removed so that it can be used later + */ + saveBombe() { + this.bombeEl = document.getElementById("bombe").cloneNode(); + this.bombeEl.setAttribute("width", "100%"); + this.bombeEl.setAttribute("height", "100%"); + } + + + /** + * Shows or hides the output loading screen. + * The animated Bombe SVG, whilst quite aesthetically pleasing, is reasonably CPU + * intensive, so we remove it from the DOM when not in use. We only show it if the + * recipe is taking longer than 200ms. We add it to the DOM just before that so that + * it is ready to fade in without stuttering. * - * @param {boolean} value + * @param {boolean} value - true == show loader */ toggleLoader(value) { + clearTimeout(this.appendBombeTimeout); + clearTimeout(this.outputLoaderTimeout); + const outputLoader = document.getElementById("output-loader"), - outputElement = document.getElementById("output-text"); + outputElement = document.getElementById("output-text"), + loader = outputLoader.querySelector(".loader"); if (value) { this.manager.controls.hideStaleIndicator(); - this.bakingStatusTimeout = setTimeout(function() { + + // Start a timer to add the Bombe to the DOM just before we make it + // visible so that there is no stuttering + this.appendBombeTimeout = setTimeout(function() { + loader.appendChild(this.bombeEl); + }.bind(this), 150); + + // Show the loading screen + this.outputLoaderTimeout = setTimeout(function() { outputElement.disabled = true; outputLoader.style.visibility = "visible"; outputLoader.style.opacity = 1; this.manager.controls.toggleBakeButtonFunction(true); }.bind(this), 200); } else { - clearTimeout(this.bakingStatusTimeout); + // Remove the Bombe from the DOM to save resources + this.outputLoaderTimeout = setTimeout(function () { + try { + loader.removeChild(this.bombeEl); + } catch (err) {} + }.bind(this), 500); outputElement.disabled = false; outputLoader.style.opacity = 0; outputLoader.style.visibility = "hidden"; diff --git a/src/web/html/index.html b/src/web/html/index.html index f03590ab..a1f915b6 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -81,7 +81,11 @@ if (!el.classList.contains("loading")) el.classList.add("loading"); // Causes CSS transition on first message el.innerHTML = msg; - } catch (err) {} // Ignore errors if DOM not yet ready + } catch (err) { + // This error was likely caused by the DOM not being ready yet, + // so we wait another second and then try again. + setTimeout(changeLoadingMsg, 1000); + } } changeLoadingMsg(); @@ -138,7 +142,9 @@
-
+
+ +
diff --git a/src/web/static/images/bombe.svg b/src/web/static/images/bombe.svg new file mode 100644 index 00000000..40857bdf --- /dev/null +++ b/src/web/static/images/bombe.svg @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + Z + Y + X + W + V + U + T + S + R + Q + P + O + N + M + L + K + J + I + H + G + F + E + D + C + B + A + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index 5b0433f6..0a1e4ec4 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -73,6 +73,28 @@ background-color: var(--primary-background-colour); visibility: hidden; opacity: 0; + display: flex; + justify-content: center; + align-items: center; + + transition: all 0.5s ease; +} + +#output-loader .loader { + width: 60%; + height: 60%; + left: unset; + top: 10%; +} + +#output-loader .loading-msg { + opacity: 1; + font-family: var(--primary-font-family); + line-height: var(--primary-line-height); + color: var(--primary-font-colour); + left: unset; + top: 30%; + position: relative; transition: all 0.5s ease; } @@ -138,16 +160,6 @@ margin-bottom: 5px; } -#output-loader .loading-msg { - opacity: 1; - font-family: var(--primary-font-family); - line-height: var(--primary-line-height); - color: var(--primary-font-colour); - top: 50%; - - transition: all 0.5s ease; -} - #magic { opacity: 1; visibility: visibile; diff --git a/src/web/stylesheets/preloader.css b/src/web/stylesheets/preloader.css index 702d04a6..bce0cd03 100755 --- a/src/web/stylesheets/preloader.css +++ b/src/web/stylesheets/preloader.css @@ -16,57 +16,28 @@ background-color: var(--secondary-border-colour); } +#loader-wrapper div { + animation: fadeIn 1s ease-in 0s; +} + .loader { display: block; - position: relative; - left: 50%; - top: 50%; - width: 150px; - height: 150px; - margin: -75px 0 0 -75px; - - border: 3px solid transparent; - border-top-color: #3498db; - border-radius: 50%; - - animation: spin 2s linear infinite; -} - -.loader:before, -.loader:after { - content: ""; position: absolute; - border: 3px solid transparent; - border-radius: 50%; -} - -.loader:before { - top: 5px; - left: 5px; - right: 5px; - bottom: 5px; - border-top-color: #e74c3c; - animation: spin 3s linear infinite; -} - -.loader:after { - top: 13px; - left: 13px; - right: 13px; - bottom: 13px; - border-top-color: #f9c922; - animation: spin 1.5s linear infinite; + left: calc(50% - 200px); + top: calc(50% - 160px); + width: 400px; + height: 260px; } .loading-msg { display: block; - position: relative; + position: absolute; width: 400px; left: calc(50% - 200px); - top: calc(50% + 50px); + top: calc(50% + 110px); text-align: center; - margin-top: 50px; opacity: 0; + font-size: 18px; } .loading-msg.loading { @@ -145,18 +116,18 @@ /* Animations */ -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - @keyframes bump { from { opacity: 0; transform: translate3d(0, 200px, 0); } } + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} From 87e956fe7df28bb82c0c57e01044154d0eea9996 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Wed, 16 Jan 2019 12:29:34 +0000 Subject: [PATCH 20/47] Added old loading icon back for initial page load. --- src/web/App.mjs | 3 ++ src/web/OutputWaiter.mjs | 13 ++++--- src/web/RecipeWaiter.mjs | 3 +- src/web/html/index.html | 8 ++--- src/web/static/images/bombe.svg | 4 +-- src/web/stylesheets/layout/_io.css | 6 ++-- src/web/stylesheets/preloader.css | 58 ++++++++++++++++++++++++------ 7 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src/web/App.mjs b/src/web/App.mjs index 453fba22..e203b85c 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -124,6 +124,9 @@ class App { // Reset attemptHighlight flag this.options.attemptHighlight = true; + // Remove all current indicators + this.manager.recipe.updateBreakpointIndicator(false); + this.manager.worker.bake( this.getInput(), // The user's input this.getRecipeConfig(), // The configuration of the recipe diff --git a/src/web/OutputWaiter.mjs b/src/web/OutputWaiter.mjs index 39d6e51b..5e7ae7e8 100755 --- a/src/web/OutputWaiter.mjs +++ b/src/web/OutputWaiter.mjs @@ -334,12 +334,11 @@ class OutputWaiter { /** - * Save bombe object before it is removed so that it can be used later + * Save bombe object then remove it from the DOM so that it does not cause performance issues. */ saveBombe() { - this.bombeEl = document.getElementById("bombe").cloneNode(); - this.bombeEl.setAttribute("width", "100%"); - this.bombeEl.setAttribute("height", "100%"); + this.bombeEl = document.getElementById("bombe"); + this.bombeEl.parentNode.removeChild(this.bombeEl); } @@ -358,7 +357,7 @@ class OutputWaiter { const outputLoader = document.getElementById("output-loader"), outputElement = document.getElementById("output-text"), - loader = outputLoader.querySelector(".loader"); + animation = document.getElementById("output-loader-animation"); if (value) { this.manager.controls.hideStaleIndicator(); @@ -366,7 +365,7 @@ class OutputWaiter { // Start a timer to add the Bombe to the DOM just before we make it // visible so that there is no stuttering this.appendBombeTimeout = setTimeout(function() { - loader.appendChild(this.bombeEl); + animation.appendChild(this.bombeEl); }.bind(this), 150); // Show the loading screen @@ -380,7 +379,7 @@ class OutputWaiter { // Remove the Bombe from the DOM to save resources this.outputLoaderTimeout = setTimeout(function () { try { - loader.removeChild(this.bombeEl); + animation.removeChild(this.bombeEl); } catch (err) {} }.bind(this), 500); outputElement.disabled = false; diff --git a/src/web/RecipeWaiter.mjs b/src/web/RecipeWaiter.mjs index b913fede..a8326b27 100755 --- a/src/web/RecipeWaiter.mjs +++ b/src/web/RecipeWaiter.mjs @@ -340,10 +340,11 @@ class RecipeWaiter { /** * Moves or removes the breakpoint indicator in the recipe based on the position. * - * @param {number} position + * @param {number|boolean} position - If boolean, turn off all indicators */ updateBreakpointIndicator(position) { const operations = document.querySelectorAll("#rec-list li.operation"); + if (typeof position === "boolean") position = operations.length; for (let i = 0; i < operations.length; i++) { if (i === position) { operations[i].classList.add("break"); diff --git a/src/web/html/index.html b/src/web/html/index.html index a1f915b6..478d2bb3 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -142,9 +142,7 @@
-
- -
+
@@ -321,7 +319,9 @@
-
+
+ +
diff --git a/src/web/static/images/bombe.svg b/src/web/static/images/bombe.svg index 40857bdf..1fdca842 100644 --- a/src/web/static/images/bombe.svg +++ b/src/web/static/images/bombe.svg @@ -56,13 +56,13 @@ - + diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index 0a1e4ec4..d4af353f 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -80,11 +80,13 @@ transition: all 0.5s ease; } -#output-loader .loader { +#output-loader-animation { + display: block; + position: absolute; width: 60%; height: 60%; - left: unset; top: 10%; + transition: all 0.5s ease; } #output-loader .loading-msg { diff --git a/src/web/stylesheets/preloader.css b/src/web/stylesheets/preloader.css index bce0cd03..690fe5c1 100755 --- a/src/web/stylesheets/preloader.css +++ b/src/web/stylesheets/preloader.css @@ -16,25 +16,54 @@ background-color: var(--secondary-border-colour); } -#loader-wrapper div { - animation: fadeIn 1s ease-in 0s; -} - .loader { display: block; + position: relative; + left: 50%; + top: 50%; + width: 150px; + height: 150px; + margin: -75px 0 0 -75px; + + border: 3px solid transparent; + border-top-color: #3498db; + border-radius: 50%; + + animation: spin 2s linear infinite; +} + +.loader:before, +.loader:after { + content: ""; position: absolute; - left: calc(50% - 200px); - top: calc(50% - 160px); - width: 400px; - height: 260px; + border: 3px solid transparent; + border-radius: 50%; +} + +.loader:before { + top: 5px; + left: 5px; + right: 5px; + bottom: 5px; + border-top-color: #e74c3c; + animation: spin 3s linear infinite; +} + +.loader:after { + top: 13px; + left: 13px; + right: 13px; + bottom: 13px; + border-top-color: #f9c922; + animation: spin 1.5s linear infinite; } .loading-msg { display: block; - position: absolute; + position: relative; width: 400px; left: calc(50% - 200px); - top: calc(50% + 110px); + top: calc(50% + 50px); text-align: center; opacity: 0; font-size: 18px; @@ -116,6 +145,15 @@ /* Animations */ +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + @keyframes bump { from { opacity: 0; From 220053c0444c6dfbcc785e61fe6188a5f937566a Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 7 Feb 2019 18:10:16 +0000 Subject: [PATCH 21/47] Typex: add ring setting --- src/core/lib/Typex.mjs | 9 ++++----- src/core/operations/Typex.mjs | 37 +++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/core/lib/Typex.mjs b/src/core/lib/Typex.mjs index df6e646b..b4cf297c 100644 --- a/src/core/lib/Typex.mjs +++ b/src/core/lib/Typex.mjs @@ -148,14 +148,13 @@ export class Rotor extends Enigma.Rotor { * @param {string} wiring - A 26 character string of the wiring order. * @param {string} steps - A 0..26 character string of stepping points. * @param {bool} reversed - Whether to reverse the rotor. + * @param {char} ringSetting - Ring setting of the rotor. * @param {char} initialPosition - The initial position of the rotor. */ - constructor(wiring, steps, reversed, initialPos) { - let initialPosMod = initialPos; + constructor(wiring, steps, reversed, ringSetting, initialPos) { let wiringMod = wiring; if (reversed) { - initialPosMod = Enigma.i2a(Utils.mod(26 - Enigma.a2i(initialPos), 26)); - const outMap = new Array(26).fill(); + const outMap = new Array(26); for (let i=0; i<26; i++) { // wiring[i] is the original output // Enigma.LETTERS[i] is the original input @@ -165,7 +164,7 @@ export class Rotor extends Enigma.Rotor { } wiringMod = outMap.join(""); } - super(wiringMod, steps, "A", initialPosMod); + super(wiringMod, steps, ringSetting, initialPos); } } diff --git a/src/core/operations/Typex.mjs b/src/core/operations/Typex.mjs index 79468645..504cb891 100644 --- a/src/core/operations/Typex.mjs +++ b/src/core/operations/Typex.mjs @@ -39,6 +39,11 @@ class Typex extends Operation { type: "boolean", value: false }, + { + name: "1st rotor ring setting", + type: "option", + value: LETTERS + }, { name: "1st rotor initial value", type: "option", @@ -55,6 +60,11 @@ class Typex extends Operation { type: "boolean", value: false }, + { + name: "2nd rotor ring setting", + type: "option", + value: LETTERS + }, { name: "2nd rotor initial value", type: "option", @@ -71,6 +81,11 @@ class Typex extends Operation { type: "boolean", value: false }, + { + name: "3rd rotor ring setting", + type: "option", + value: LETTERS + }, { name: "3rd rotor initial value", type: "option", @@ -87,6 +102,11 @@ class Typex extends Operation { type: "boolean", value: false }, + { + name: "4th rotor ring setting", + type: "option", + value: LETTERS + }, { name: "4th rotor initial value", type: "option", @@ -103,6 +123,11 @@ class Typex extends Operation { type: "boolean", value: false }, + { + name: "5th rotor ring setting", + type: "option", + value: LETTERS + }, { name: "5th rotor initial value", type: "option", @@ -156,14 +181,14 @@ class Typex extends Operation { * @returns {string} */ run(input, args) { - const reflectorstr = args[15]; - const plugboardstr = args[16]; - const typexKeyboard = args[17]; - const removeOther = args[18]; + const reflectorstr = args[20]; + const plugboardstr = args[21]; + const typexKeyboard = args[22]; + const removeOther = args[23]; const rotors = []; for (let i=0; i<5; i++) { - const [rotorwiring, rotorsteps] = this.parseRotorStr(args[i*3]); - rotors.push(new Rotor(rotorwiring, rotorsteps, args[i*3 + 1], args[i*3+2])); + const [rotorwiring, rotorsteps] = this.parseRotorStr(args[i*4]); + rotors.push(new Rotor(rotorwiring, rotorsteps, args[i*4 + 1], args[i*4+2], args[i*4+3])); } const reflector = new Reflector(reflectorstr); let plugboardstrMod = plugboardstr; From 53226c105069b97369e9a81ef23da2c88e0c9441 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Feb 2019 00:59:56 +0000 Subject: [PATCH 22/47] Added populateMultiOption ingredient type --- .gitignore | 1 + src/core/operations/MultipleBombe.mjs | 94 +++++++----------------- src/web/HTMLIngredient.mjs | 49 +++++++++++- src/web/RecipeWaiter.mjs | 10 +++ tests/operations/tests/MultipleBombe.mjs | 14 ++-- 5 files changed, 92 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index edbcf679..b5aad5d0 100755 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ docs/* !docs/*.ico .vscode .*.swp +.DS_Store src/core/config/modules/* src/core/config/OperationConfig.json src/core/operations/index.mjs diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs index dac7a334..a453ca34 100644 --- a/src/core/operations/MultipleBombe.mjs +++ b/src/core/operations/MultipleBombe.mjs @@ -60,95 +60,57 @@ class MultipleBombe extends Operation { this.args = [ { "name": "Standard Enigmas", - "type": "populateOption", + "type": "populateMultiOption", "value": [ { name: "German Service Enigma (First - 3 rotor)", - value: rotorsFormat(ROTORS, 0, 5) + value: [ + rotorsFormat(ROTORS, 0, 5), + "", + rotorsFormat(REFLECTORS, 0, 1) + ] }, { name: "German Service Enigma (Second - 3 rotor)", - value: rotorsFormat(ROTORS, 0, 8) + value: [ + rotorsFormat(ROTORS, 0, 8), + "", + rotorsFormat(REFLECTORS, 0, 2) + ] }, { name: "German Service Enigma (Third - 4 rotor)", - value: rotorsFormat(ROTORS, 0, 8) + value: [ + rotorsFormat(ROTORS, 0, 8), + rotorsFormat(ROTORS_FOURTH, 1, 2), + rotorsFormat(REFLECTORS, 2, 3) + ] }, { name: "German Service Enigma (Fourth - 4 rotor)", - value: rotorsFormat(ROTORS, 0, 8) + value: [ + rotorsFormat(ROTORS, 0, 8), + rotorsFormat(ROTORS_FOURTH, 1, 3), + rotorsFormat(REFLECTORS, 2, 4) + ] }, { name: "User defined", - value: "" + value: ["", "", ""] }, ], - "target": 1 + "target": [1, 2, 3] }, { name: "Main rotors", type: "text", value: "" }, - { - "name": "Standard Enigmas", - "type": "populateOption", - "value": [ - { - name: "German Service Enigma (First - 3 rotor)", - value: "" - }, - { - name: "German Service Enigma (Second - 3 rotor)", - value: "" - }, - { - name: "German Service Enigma (Third - 4 rotor)", - value: rotorsFormat(ROTORS_FOURTH, 1, 2) - }, - { - name: "German Service Enigma (Fourth - 4 rotor)", - value: rotorsFormat(ROTORS_FOURTH, 1, 3) - }, - { - name: "User defined", - value: "" - }, - ], - "target": 3 - }, { name: "4th rotor", type: "text", value: "" }, - { - "name": "Standard Enigmas", - "type": "populateOption", - "value": [ - { - name: "German Service Enigma (First - 3 rotor)", - value: rotorsFormat(REFLECTORS, 0, 1) - }, - { - name: "German Service Enigma (Second - 3 rotor)", - value: rotorsFormat(REFLECTORS, 0, 2) - }, - { - name: "German Service Enigma (Third - 4 rotor)", - value: rotorsFormat(REFLECTORS, 2, 3) - }, - { - name: "German Service Enigma (Fourth - 4 rotor)", - value: rotorsFormat(REFLECTORS, 2, 4) - }, - { - name: "User defined", - value: "" - }, - ], - "target": 5 - }, { name: "Reflectors", type: "text", @@ -217,11 +179,11 @@ class MultipleBombe extends Operation { */ run(input, args) { const mainRotorsStr = args[1]; - const fourthRotorsStr = args[3]; - const reflectorsStr = args[5]; - let crib = args[6]; - const offset = args[7]; - const check = args[8]; + const fourthRotorsStr = args[2]; + const reflectorsStr = args[3]; + let crib = args[4]; + const offset = args[5]; + const check = args[6]; const rotors = []; const fourthRotors = []; const reflectors = []; diff --git a/src/web/HTMLIngredient.mjs b/src/web/HTMLIngredient.mjs index bb01d7de..c7c024fb 100755 --- a/src/web/HTMLIngredient.mjs +++ b/src/web/HTMLIngredient.mjs @@ -39,7 +39,7 @@ class HTMLIngredient { */ toHtml() { let html = "", - i, m; + i, m, eventFn; switch (this.type) { case "string": @@ -142,10 +142,11 @@ class HTMLIngredient { `; break; case "populateOption": + case "populateMultiOption": html += `
${this.hint ? "" + this.hint + "" : ""}
`; - this.manager.addDynamicListener("#" + this.id, "change", this.populateOptionChange, this); + eventFn = this.type === "populateMultiOption" ? + this.populateMultiOptionChange : + this.populateOptionChange; + this.manager.addDynamicListener("#" + this.id, "change", eventFn, this); break; case "editableOption": html += `
@@ -248,6 +255,9 @@ class HTMLIngredient { * @param {event} e */ populateOptionChange(e) { + e.preventDefault(); + e.stopPropagation(); + const el = e.target; const op = el.parentNode.parentNode; const target = op.querySelectorAll(".arg")[this.target]; @@ -260,6 +270,37 @@ class HTMLIngredient { } + /** + * Handler for populate multi option changes. + * Populates the relevant arguments with the specified values. + * + * @param {event} e + */ + populateMultiOptionChange(e) { + e.preventDefault(); + e.stopPropagation(); + + const el = e.target; + const op = el.parentNode.parentNode; + const args = op.querySelectorAll(".arg"); + const targets = this.target.map(i => args[i]); + const vals = JSON.parse(el.childNodes[el.selectedIndex].getAttribute("populate-value")); + const evt = new Event("change"); + + for (let i = 0; i < targets.length; i++) { + targets[i].value = vals[i]; + } + + // Fire change event after all targets have been assigned + this.manager.recipe.ingChange(); + + // Send change event for each target once all have been assigned, to update the label placement. + for (const target of targets) { + target.dispatchEvent(evt); + } + } + + /** * Handler for editable option clicks. * Populates the input box with the selected value. diff --git a/src/web/RecipeWaiter.mjs b/src/web/RecipeWaiter.mjs index a8326b27..4c568c8b 100755 --- a/src/web/RecipeWaiter.mjs +++ b/src/web/RecipeWaiter.mjs @@ -205,6 +205,7 @@ class RecipeWaiter { * @fires Manager#statechange */ ingChange(e) { + if (e && e.target && e.target.classList.contains("no-state-change")) return; window.dispatchEvent(this.manager.statechange); } @@ -392,6 +393,15 @@ class RecipeWaiter { this.buildRecipeOperation(item); document.getElementById("rec-list").appendChild(item); + // Trigger populateOption events + const populateOptions = item.querySelectorAll(".populate-option"); + const evt = new Event("change", {bubbles: true}); + if (populateOptions.length) { + for (const el of populateOptions) { + el.dispatchEvent(evt); + } + } + item.dispatchEvent(this.manager.operationadd); return item; } diff --git a/tests/operations/tests/MultipleBombe.mjs b/tests/operations/tests/MultipleBombe.mjs index f809859c..5c06ece4 100644 --- a/tests/operations/tests/MultipleBombe.mjs +++ b/tests/operations/tests/MultipleBombe.mjs @@ -16,9 +16,10 @@ TestRegister.addTests([ "op": "Multiple Bombe", "args": [ // I, II and III - "User defined", "EKMFLGDQVZNTOWYHXUSPAIBRCJ Date: Fri, 8 Feb 2019 11:53:58 +0000 Subject: [PATCH 23/47] Fixed Bombe svg animation in standalone version --- package-lock.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/package-lock.json b/package-lock.json index 57175720..14b14f1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12074,6 +12074,28 @@ "resolved": "http://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" }, + "svg-url-loader": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/svg-url-loader/-/svg-url-loader-2.3.2.tgz", + "integrity": "sha1-3YaybBn+O5FPBOoQ7zlZTq3gRGQ=", + "dev": true, + "requires": { + "file-loader": "1.1.11", + "loader-utils": "1.1.0" + }, + "dependencies": { + "file-loader": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", + "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", + "dev": true, + "requires": { + "loader-utils": "^1.0.2", + "schema-utils": "^0.4.5" + } + } + } + }, "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", From 1079080f5c024b13fca415ed85cdfb92ecf3e506 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Feb 2019 15:21:14 +0000 Subject: [PATCH 24/47] Bombe results are now presented in a table --- src/core/operations/Bombe.mjs | 31 +++++++++++++++---- src/core/operations/MultipleBombe.mjs | 43 +++++++++++++++++++++------ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index 34a94e53..6b277a03 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -26,7 +26,8 @@ class Bombe extends Operation { this.description = "Emulation of the Bombe machine used to attack Enigma.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops, a large number of incorrect outputs will be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine."; this.infoURL = "https://wikipedia.org/wiki/Bombe"; this.inputType = "string"; - this.outputType = "string"; + this.outputType = "JSON"; + this.presentType = "html"; this.args = [ { name: "1st (right-hand) rotor", @@ -82,7 +83,7 @@ class Bombe extends Operation { * @param {number} progress - Progress (as a float in the range 0..1) */ updateStatus(nLoops, nStops, progress) { - const msg = `Bombe run with ${nLoops} loops in menu (2+ desirable): ${nStops} stops, ${Math.floor(100 * progress)}% done`; + const msg = `Bombe run with ${nLoops} loop${nLoops === 1 ? "" : "s"} in menu (2+ desirable): ${nStops} stops, ${Math.floor(100 * progress)}% done`; self.sendStatusMessage(msg); } @@ -128,11 +129,29 @@ class Bombe extends Operation { } const bombe = new BombeMachine(rotors, reflector, ciphertext, crib, check, update); const result = bombe.run(); - let msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotor positions are listed left to right and start at the beginning of the crib, and ignore stepping and the ring setting. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; - for (const [setting, stecker, decrypt] of result) { - msg += `Stop: ${setting} (plugboard: ${stecker}): ${decrypt}\n`; + return { + nLoops: bombe.nLoops, + result: result + }; + } + + + /** + * Displays the Bombe results in an HTML table + * + * @param {Object} output + * @param {number} output.nLoops + * @param {Array[]} output.result + * @returns {html} + */ + present(output) { + let html = `Bombe run on menu with ${output.nLoops} loop${output.nLoops === 1 ? "" : "s"} (2+ desirable). Note: Rotor positions are listed left to right and start at the beginning of the crib, and ignore stepping and the ring setting. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided.\n\n`; + html += ""; + for (const [setting, stecker, decrypt] of output.result) { + html += `\n`; } - return msg; + html += "
Rotor stopsPartial plugboardDecryption preview
${setting}${stecker}${decrypt}
"; + return html; } } diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs index a453ca34..7a0ae2fd 100644 --- a/src/core/operations/MultipleBombe.mjs +++ b/src/core/operations/MultipleBombe.mjs @@ -56,7 +56,8 @@ class MultipleBombe extends Operation { this.description = "Emulation of the Bombe machine used to attack Enigma. This version carries out multiple Bombe runs to handle unknown rotor configurations.

You should test your menu on the single Bombe operation before running it here. See the description of the Bombe operation for instructions on choosing a crib."; this.infoURL = "https://wikipedia.org/wiki/Bombe"; this.inputType = "string"; - this.outputType = "string"; + this.outputType = "JSON"; + this.presentType = "html"; this.args = [ { "name": "Standard Enigmas", @@ -146,7 +147,7 @@ class MultipleBombe extends Operation { const hours = Math.floor(remaining / 3600); const minutes = `0${Math.floor((remaining % 3600) / 60)}`.slice(-2); const seconds = `0${Math.floor(remaining % 60)}`.slice(-2); - const msg = `Bombe run with ${nLoops} loops in menu (2+ desirable): ${nStops} stops, ${Math.floor(100 * progress)}% done, ${hours}:${minutes}:${seconds} remaining`; + const msg = `Bombe run with ${nLoops} loop${nLoops === 1 ? "" : "s"} in menu (2+ desirable): ${nStops} stops, ${Math.floor(100 * progress)}% done, ${hours}:${minutes}:${seconds} remaining`; self.sendStatusMessage(msg); } @@ -227,7 +228,7 @@ class MultipleBombe extends Operation { update = undefined; } let bombe = undefined; - let msg; + const output = {bombeRuns: []}; // I could use a proper combinatorics algorithm here... but it would be more code to // write one, and we don't seem to have one in our existing libraries, so massively nested // for loop it is @@ -253,7 +254,7 @@ class MultipleBombe extends Operation { } if (bombe === undefined) { bombe = new BombeMachine(runRotors, reflector, ciphertext, crib, check); - msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; + output.nLoops = bombe.nLoops; } else { bombe.changeRotors(runRotors, reflector); } @@ -263,17 +264,41 @@ class MultipleBombe extends Operation { update(bombe.nLoops, nStops, nRuns / totalRuns, start); } if (result.length > 0) { - msg += `\nRotors: ${runRotors.join(", ")}\nReflector: ${reflector.pairs}\n`; - for (const [setting, stecker, decrypt] of result) { - msg += `Stop: ${setting} (plugboard: ${stecker}): ${decrypt}\n`; - } + output.bombeRuns.push({ + rotors: runRotors, + reflector: reflector.pairs, + result: result + }); } } } } } } - return msg; + return output; + } + + + /** + * Displays the MultiBombe results in an HTML table + * + * @param {Object} output + * @param {number} output.nLoops + * @param {Array[]} output.result + * @returns {html} + */ + present(output) { + let html = `Bombe run on menu with ${output.nLoops} loop${output.nLoops === 1 ? "" : "s"} (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided.\n`; + + for (const run of output.bombeRuns) { + html += `\nRotors: ${run.rotors.join(", ")}\nReflector: ${run.reflector}\n`; + html += ""; + for (const [setting, stecker, decrypt] of run.result) { + html += `\n`; + } + html += "
Rotor stopsPartial plugboardDecryption preview
${setting}${stecker}${decrypt}
\n"; + } + return html; } } From 5a2a8b4c8ecb4b91b3876d31f6260159f5f9f80f Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Fri, 8 Feb 2019 18:08:13 +0000 Subject: [PATCH 25/47] Typex: input wiring is reversed --- src/core/lib/Typex.mjs | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/core/lib/Typex.mjs b/src/core/lib/Typex.mjs index b4cf297c..484a1e6b 100644 --- a/src/core/lib/Typex.mjs +++ b/src/core/lib/Typex.mjs @@ -2,6 +2,7 @@ * Emulation of the Typex machine. * * @author s2224834 + * @author The National Museum of Computing - Bombe Rebuild Project * @copyright Crown Copyright 2019 * @license Apache-2.0 */ @@ -28,7 +29,7 @@ export const ROTORS = [ * An example Typex reflector. Again, randomised. */ export const REFLECTORS = [ - {name: "Standard", value: "AN BC FG IE KD LU MH OR TS VZ WQ XJ YP"}, + {name: "Example", value: "AN BC FG IE KD LU MH OR TS VZ WQ XJ YP"}, ]; // Special character handling on Typex keyboard @@ -172,6 +173,8 @@ export class Rotor extends Enigma.Rotor { * Typex input plugboard. Based on a Rotor, because it allows arbitrary maps, not just switches * like the Enigma plugboard. * Not to be confused with the reflector plugboard. + * This is also where the Typex's backwards input wiring is implemented - it's a bit of a hack, but + * it means everything else continues to work like in the Enigma. */ export class Plugboard extends Enigma.Rotor { /** @@ -180,10 +183,45 @@ export class Plugboard extends Enigma.Rotor { * @param {string} wiring - 26 character string of mappings from A-Z, as per rotors, or "". */ constructor(wiring) { + // Typex input wiring is backwards vs Enigma: that is, letters enter the rotors in a + // clockwise order, vs. Enigma's anticlockwise (or vice versa depending on which side + // you're looking at it from). I'm doing the transform here to avoid having to rewrite + // the Engima crypt() method in Typex as well. + // Note that the wiring for the reflector is the same way around as Enigma, so no + // transformation is necessary on that side. + // We're going to achieve this by mapping the plugboard settings through an additional + // transform that mirrors the alphabet before we pass it to the superclass. + if (!/^[A-Z]{26}$/.test(wiring)) { + throw new OperationError("Plugboard wiring must be 26 unique uppercase letters"); + } + const reversed = "AZYXWVUTSRQPONMLKJIHGFEDCB"; + wiring = wiring.replace(/./g, x => { + return reversed[Enigma.a2i(x)]; + }); try { super(wiring, "", "A", "A"); } catch (err) { throw new OperationError(err.message.replace("Rotor", "Plugboard")); } } + + /** + * 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); + } } From 5a8255a9f488817f44791bbc33cf23eb6988cc92 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Fri, 8 Feb 2019 19:25:28 +0000 Subject: [PATCH 26/47] Bombe: fix tests after output table patch --- tests/operations/tests/Bombe.mjs | 12 ++++++------ tests/operations/tests/MultipleBombe.mjs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/operations/tests/Bombe.mjs b/tests/operations/tests/Bombe.mjs index fca420d3..0f00f1be 100644 --- a/tests/operations/tests/Bombe.mjs +++ b/tests/operations/tests/Bombe.mjs @@ -11,7 +11,7 @@ TestRegister.addTests([ // Plugboard for this test is BO LC KE GA name: "Bombe: 3 rotor (self-stecker)", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA \(plugboard: SS\): VFISUSGTKSTMPSUNAK/, + expectedMatch: /LGA<\/td>SS<\/td>VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -30,7 +30,7 @@ TestRegister.addTests([ // This test produces a menu that doesn't use the first letter, which is also a good test name: "Bombe: 3 rotor (other stecker)", input: "JBYALIHDYNUAAVKBYM", - expectedMatch: /LGA \(plugboard: AG\): QFIMUMAFKMQSKMYNGW/, + expectedMatch: /LGA<\/td>AG<\/td>QFIMUMAFKMQSKMYNGW<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -48,7 +48,7 @@ TestRegister.addTests([ { name: "Bombe: crib offset", input: "AAABBYFLTHHYIJQAYBBYS", // first three chars here are faked - expectedMatch: /LGA \(plugboard: SS\): VFISUSGTKSTMPSUNAK/, + expectedMatch: /LGA<\/td>SS<\/td>VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -66,7 +66,7 @@ TestRegister.addTests([ { name: "Bombe: multiple stops", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA \(plugboard: TT\): VFISUSGTKSTMPSUNAK/, + expectedMatch: /LGA<\/td>TT<\/td>VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -84,7 +84,7 @@ TestRegister.addTests([ { name: "Bombe: checking machine", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /Stop: LGA \(plugboard: TT AG BO CL EK FF HH II JJ SS YY\): THISISATESTMESSAGE/, + expectedMatch: /LGA<\/td>TT AG BO CL EK FF HH II JJ SS YY<\/td>THISISATESTMESSAGE<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -103,7 +103,7 @@ TestRegister.addTests([ { name: "Bombe: 4 rotor", input: "LUOXGJSHGEDSRDOQQX", - expectedMatch: /LHSC \(plugboard: SS\): HHHSSSGQUUQPKSEKWK/, + expectedMatch: /LHSC<\/td>SS<\/td>HHHSSSGQUUQPKSEKWK<\/td>/, recipeConfig: [ { "op": "Bombe", diff --git a/tests/operations/tests/MultipleBombe.mjs b/tests/operations/tests/MultipleBombe.mjs index 5c06ece4..8e2cc685 100644 --- a/tests/operations/tests/MultipleBombe.mjs +++ b/tests/operations/tests/MultipleBombe.mjs @@ -10,7 +10,7 @@ TestRegister.addTests([ { name: "Multi-Bombe: 3 rotor", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA \(plugboard: SS\): VFISUSGTKSTMPSUNAK/, + expectedMatch: /LGA<\/td>SS<\/td>VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Multiple Bombe", @@ -30,7 +30,7 @@ TestRegister.addTests([ { name: "Multi-Bombe: 4 rotor", input: "LUOXGJSHGEDSRDOQQX", - expectedMatch: /LHSC \(plugboard: SS\): HHHSSSGQUUQPKSEKWK/, + expectedMatch: /LHSC<\/td>SS<\/td>HHHSSSGQUUQPKSEKWK<\/td>/, recipeConfig: [ { "op": "Multiple Bombe", From 61fee3122a5eac968ecaa5fa4ed6107581ba07f8 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Fri, 8 Feb 2019 21:16:42 +0000 Subject: [PATCH 27/47] Bombe: add Rebuild Project to authors --- src/core/lib/Bombe.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 03413350..a4cf24f4 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -2,6 +2,7 @@ * Emulation of the Bombe machine. * * @author s2224834 + * @author The National Museum of Computing - Bombe Rebuild Project * @copyright Crown Copyright 2019 * @license Apache-2.0 */ From 069d4956aac93021bb1984eb618379c74062da37 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Sat, 9 Feb 2019 22:57:57 +0000 Subject: [PATCH 28/47] Bombe: Handle boxing stop correctly --- src/core/lib/Bombe.mjs | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index a4cf24f4..ef796cd0 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -651,12 +651,34 @@ export class BombeMachine { // This means our hypothesis for the steckering is correct. steckerPair = this.testInput[1]; } else { - // If this happens a lot it implies the menu isn't good enough. We can't do - // anything useful with it as we don't have a stecker partner, so we'll just drop it - // and move on. This does risk eating the actual stop occasionally, but I've only seen - // this happen when the menu is bad enough we have thousands of stops, so I'm not sure - // it matters. - return undefined; + // This was known as a "boxing stop" - we have a stop but not a single hypothesis. + // If this happens a lot it implies the menu isn't good enough. + // If we have the checking machine enabled, we're going to just check each wire in + // turn. If we get 0 or 1 hit, great. + // If we get multiple hits, or the checking machine is off, the user will just have to + // deal with it. + if (!this.check) { + // We can't draw any conclusions about the steckering (one could maybe suggest + // options in some cases, but too hard to present clearly). + return [this.indicator.getPos(), "??", this.tryDecrypt("")]; + } + let stecker = undefined; + for (let i = 0; i < 26; i++) { + const newStecker = this.checkingMachine(i); + if (newStecker !== "") { + if (stecker !== undefined) { + // Multiple hypotheses can't be ruled out. + return [this.indicator.getPos(), "??", this.tryDecrypt("")]; + } + stecker = newStecker; + } + } + if (stecker === undefined) { + // Checking machine ruled all possibilities out. + return undefined; + } + // If we got here, there was just one possibility allowed by the checking machine. Success. + return [this.indicator.getPos(), stecker, this.tryDecrypt(stecker)]; } let stecker; if (this.check) { From dd9cbbac77ef5a8c8e85b14215b2501aad906b5b Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Sat, 9 Feb 2019 23:01:52 +0000 Subject: [PATCH 29/47] Bombe: add note about rotor step in crib --- src/core/lib/Bombe.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index ef796cd0..122edd40 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -281,6 +281,14 @@ export class BombeMachine { * ciphertext. It will check that the crib is sane (length is vaguely sensible and there's no * matching characters between crib and ciphertext) but cannot check further - if it's wrong * your results will be wrong! + * + * There is also no handling of rotor stepping - if the target Enigma stepped in the middle of + * your crib, you're out of luck. TODO: Allow specifying a step point - this is fairly easy to + * configure on a real Bombe, but we're not clear on whether it was ever actually done for + * real (there would almost certainly have been better ways of attacking in most situations + * than attempting to exhaust options for the stepping point, but in some circumstances, e.g. + * via Banburismus, the stepping point might have been known). + * * @param {string[]} rotors - list of rotor spec strings (without step points!) * @param {Object} reflector - Reflector object * @param {string} ciphertext - The ciphertext to attack From 4db6199fd947e9a06d6bf59c3ea54070054cd273 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Sun, 10 Feb 2019 21:00:36 +0000 Subject: [PATCH 30/47] Fixed timings for Bombe animation fast rotor --- src/web/static/images/bombe.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/web/static/images/bombe.svg b/src/web/static/images/bombe.svg index 1fdca842..1fd40554 100644 --- a/src/web/static/images/bombe.svg +++ b/src/web/static/images/bombe.svg @@ -23,7 +23,7 @@ const bbox = rotor.getBBox(); const x = bbox.width/2 + bbox.x; const y = bbox.height/2 + bbox.y; - const wait = row === 0 ? speed/26 : row === 1 ? speed : speed*26; + const wait = row === 0 ? speed/26/1.5 : row === 1 ? speed : speed*26; rotor.setAttribute("transform", "rotate(" + startPos + ", " + x + ", " + y + ")"); @@ -50,7 +50,7 @@ break; } } - }, speed/26 - 5); + }, speed/26/1.5 - 5); } // ]]> From c005c86c276eb8a9f16ebb82b21ce1cb94779c66 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 28 Feb 2019 15:27:35 +0000 Subject: [PATCH 31/47] Added argSelector ingredient type and reversed rotors in Enigma and Bombe operations. --- src/core/operations/Bombe.mjs | 57 ++++++--- src/core/operations/Enigma.mjs | 135 +++++++++++--------- src/web/App.mjs | 3 + src/web/HTMLIngredient.mjs | 48 ++++++++ src/web/RecipeWaiter.mjs | 28 +++-- tests/operations/tests/Bombe.mjs | 84 +++++++------ tests/operations/tests/Enigma.mjs | 198 +++++++++++++++++------------- 7 files changed, 344 insertions(+), 209 deletions(-) diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index 6b277a03..ea3210fa 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -30,28 +30,42 @@ class Bombe extends Operation { this.presentType = "html"; this.args = [ { - name: "1st (right-hand) rotor", - type: "editableOption", - value: ROTORS, - defaultIndex: 2 + name: "Model", + type: "argSelector", + value: [ + { + name: "3-rotor", + off: [1] + }, + { + name: "4-rotor", + on: [1] + } + ] }, { - name: "2nd (middle) rotor", + name: "Left-most rotor", + type: "editableOption", + value: ROTORS_FOURTH, + defaultIndex: 0 + }, + { + name: "Left-hand rotor", + type: "editableOption", + value: ROTORS, + defaultIndex: 0 + }, + { + name: "Middle rotor", type: "editableOption", value: ROTORS, defaultIndex: 1 }, { - name: "3rd (left-hand) rotor", + name: "Right-hand rotor", type: "editableOption", value: ROTORS, - defaultIndex: 0 - }, - { - name: "4th (left-most, only some models) rotor", - type: "editableOption", - value: ROTORS_FOURTH, - defaultIndex: 0 + defaultIndex: 2 }, { name: "Reflector", @@ -93,23 +107,26 @@ class Bombe extends Operation { * @returns {string} */ run(input, args) { - const reflectorstr = args[4]; - let crib = args[5]; - const offset = args[6]; - const check = args[7]; + const model = args[0]; + const reflectorstr = args[5]; + let crib = args[6]; + const offset = args[7]; + const check = args[8]; const rotors = []; for (let i=0; i<4; i++) { - if (i === 3 && args[i] === "") { + if (i === 0 && model === "3-rotor") { // No fourth rotor - break; + continue; } - let rstr = args[i]; + let rstr = args[i + 1]; // The Bombe doesn't take stepping into account so we'll just ignore it here if (rstr.includes("<")) { rstr = rstr.split("<", 2)[0]; } rotors.push(rstr); } + // Rotors are handled in reverse + rotors.reverse(); if (crib.length === 0) { throw new OperationError("Crib cannot be empty"); } diff --git a/src/core/operations/Enigma.mjs b/src/core/operations/Enigma.mjs index 4af79993..ace50604 100644 --- a/src/core/operations/Enigma.mjs +++ b/src/core/operations/Enigma.mjs @@ -28,67 +28,81 @@ class Enigma extends Operation { this.outputType = "string"; this.args = [ { - name: "1st (right-hand) rotor", + name: "Model", + type: "argSelector", + value: [ + { + name: "3-rotor", + off: [1, 2, 3] + }, + { + name: "4-rotor", + on: [1, 2, 3] + } + ] + }, + { + name: "Left-most rotor", + type: "editableOption", + value: ROTORS_FOURTH, + defaultIndex: 0 + }, + { + name: "Left-most rotor ring setting", + type: "option", + value: LETTERS + }, + { + name: "Left-most rotor initial value", + type: "option", + value: LETTERS + }, + { + name: "Left-hand rotor", + type: "editableOption", + value: ROTORS, + defaultIndex: 0 + }, + { + name: "Left-hand rotor ring setting", + type: "option", + value: LETTERS + }, + { + name: "Left-hand rotor initial value", + type: "option", + value: LETTERS + }, + { + name: "Middle rotor", + type: "editableOption", + value: ROTORS, + defaultIndex: 1 + }, + { + name: "Middle rotor ring setting", + type: "option", + value: LETTERS + }, + { + name: "Middle rotor initial value", + type: "option", + value: LETTERS + }, + { + name: "Right-hand rotor", type: "editableOption", value: ROTORS, // Default config is the rotors I-III *left to right* defaultIndex: 2 }, { - name: "1st rotor ring setting", + name: "Right-hand rotor ring setting", type: "option", value: LETTERS }, { - name: "1st rotor initial value", - type: "option", - value: LETTERS - }, - { - name: "2nd (middle) rotor", - type: "editableOption", - value: ROTORS, - defaultIndex: 1 - }, - { - name: "2nd rotor ring setting", - type: "option", - value: LETTERS - }, - { - name: "2nd rotor initial value", - type: "option", - value: LETTERS - }, - { - name: "3rd (left-hand) rotor", - type: "editableOption", - value: ROTORS, - defaultIndex: 0 - }, - { - name: "3rd rotor ring setting", - type: "option", - value: LETTERS - }, - { - name: "3rd rotor initial value", - type: "option", - value: LETTERS - }, - { - name: "4th (left-most, only some models) rotor", - type: "editableOption", - value: ROTORS_FOURTH, - defaultIndex: 0 - }, - { - name: "4th rotor ring setting", - type: "option", - value: LETTERS - }, - { - name: "4th rotor initial value", + name: "Right-hand rotor initial value", type: "option", value: LETTERS }, @@ -135,18 +149,21 @@ class Enigma extends Operation { * @returns {string} */ run(input, args) { - const reflectorstr = args[12]; - const plugboardstr = args[13]; - const removeOther = args[14]; + const model = args[0]; + const reflectorstr = args[13]; + const plugboardstr = args[14]; + const removeOther = args[15]; const rotors = []; for (let i=0; i<4; i++) { - if (i === 3 && args[i*3] === "") { - // No fourth rotor - break; + if (i === 0 && model === "3-rotor") { + // Skip the 4th rotor settings + continue; } - const [rotorwiring, rotorsteps] = this.parseRotorStr(args[i*3], 1); - rotors.push(new Rotor(rotorwiring, rotorsteps, args[i*3 + 1], args[i*3 + 2])); + const [rotorwiring, rotorsteps] = this.parseRotorStr(args[i*3 + 1], 1); + rotors.push(new Rotor(rotorwiring, rotorsteps, args[i*3 + 2], args[i*3 + 3])); } + // Rotors are handled in reverse + rotors.reverse(); const reflector = new Reflector(reflectorstr); const plugboard = new Plugboard(plugboardstr); if (removeOther) { diff --git a/src/web/App.mjs b/src/web/App.mjs index e203b85c..04846fb6 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -472,6 +472,7 @@ class App { const item = this.manager.recipe.addOperation(recipeConfig[i].op); // Populate arguments + log.debug(`Populating arguments for ${recipeConfig[i].op}`); const args = item.querySelectorAll(".arg"); for (let j = 0; j < args.length; j++) { if (recipeConfig[i].args[j] === undefined) continue; @@ -497,6 +498,8 @@ class App { item.querySelector(".breakpoint").click(); } + this.manager.recipe.triggerArgEvents(item); + this.progress = 0; } diff --git a/src/web/HTMLIngredient.mjs b/src/web/HTMLIngredient.mjs index c7c024fb..19c816ea 100755 --- a/src/web/HTMLIngredient.mjs +++ b/src/web/HTMLIngredient.mjs @@ -240,6 +240,27 @@ class HTMLIngredient { ${this.hint ? "" + this.hint + "" : ""}
`; break; + case "argSelector": + html += `
+ + + ${this.hint ? "" + this.hint + "" : ""} +
`; + + this.manager.addDynamicListener(".arg-selector", "change", this.argSelectorChange, this); + break; default: break; } @@ -321,6 +342,33 @@ class HTMLIngredient { this.manager.recipe.ingChange(); } + + /** + * Handler for argument selector changes. + * Shows or hides the relevant arguments for this operation. + * + * @param {event} e + */ + argSelectorChange(e) { + e.preventDefault(); + e.stopPropagation(); + + const option = e.target.options[e.target.selectedIndex]; + const op = e.target.closest(".operation"); + const args = op.querySelectorAll(".ingredients .form-group"); + const turnon = JSON.parse(option.getAttribute("turnon")); + const turnoff = JSON.parse(option.getAttribute("turnoff")); + + args.forEach((arg, i) => { + if (turnon.includes(i)) { + arg.classList.remove("d-none"); + } + if (turnoff.includes(i)) { + arg.classList.add("d-none"); + } + }); + } + } export default HTMLIngredient; diff --git a/src/web/RecipeWaiter.mjs b/src/web/RecipeWaiter.mjs index 4c568c8b..4eca4af7 100755 --- a/src/web/RecipeWaiter.mjs +++ b/src/web/RecipeWaiter.mjs @@ -393,15 +393,6 @@ class RecipeWaiter { this.buildRecipeOperation(item); document.getElementById("rec-list").appendChild(item); - // Trigger populateOption events - const populateOptions = item.querySelectorAll(".populate-option"); - const evt = new Event("change", {bubbles: true}); - if (populateOptions.length) { - for (const el of populateOptions) { - el.dispatchEvent(evt); - } - } - item.dispatchEvent(this.manager.operationadd); return item; } @@ -439,6 +430,23 @@ class RecipeWaiter { } + /** + * Triggers various change events for operation arguments that have just been initialised. + * + * @param {HTMLElement} op + */ + triggerArgEvents(op) { + // Trigger populateOption and argSelector events + const triggerableOptions = op.querySelectorAll(".populate-option, .arg-selector"); + const evt = new Event("change", {bubbles: true}); + if (triggerableOptions.length) { + for (const el of triggerableOptions) { + el.dispatchEvent(evt); + } + } + } + + /** * Handler for operationadd events. * @@ -448,6 +456,8 @@ class RecipeWaiter { */ opAdd(e) { log.debug(`'${e.target.querySelector(".op-title").textContent}' added to recipe`); + + this.triggerArgEvents(e.target); window.dispatchEvent(this.manager.statechange); } diff --git a/tests/operations/tests/Bombe.mjs b/tests/operations/tests/Bombe.mjs index 0f00f1be..9e5a79c6 100644 --- a/tests/operations/tests/Bombe.mjs +++ b/tests/operations/tests/Bombe.mjs @@ -16,10 +16,11 @@ TestRegister.addTests([ { "op": "Bombe", "args": [ - "BDFHJLCPRTXVZNYEIWGAKMUSQO Date: Thu, 28 Feb 2019 16:56:28 +0000 Subject: [PATCH 32/47] Tweaks for new rotor order --- src/core/lib/Enigma.mjs | 1 - src/core/operations/Bombe.mjs | 2 +- src/core/operations/Enigma.mjs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/lib/Enigma.mjs b/src/core/lib/Enigma.mjs index 1ed0ea2b..39193f69 100644 --- a/src/core/lib/Enigma.mjs +++ b/src/core/lib/Enigma.mjs @@ -25,7 +25,6 @@ export const ROTORS = [ ]; export const ROTORS_FOURTH = [ - {name: "None", value: ""}, {name: "Beta", value: "LEYJVCNIXWPBQMDRTAKZGFUHOS"}, {name: "Gamma", value: "FSOKANUERHMBTIYCWLQPZXVGJD"}, ]; diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index ea3210fa..5e128498 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -44,7 +44,7 @@ class Bombe extends Operation { ] }, { - name: "Left-most rotor", + name: "Left-most (4th) rotor", type: "editableOption", value: ROTORS_FOURTH, defaultIndex: 0 diff --git a/src/core/operations/Enigma.mjs b/src/core/operations/Enigma.mjs index ace50604..77333b18 100644 --- a/src/core/operations/Enigma.mjs +++ b/src/core/operations/Enigma.mjs @@ -42,7 +42,7 @@ class Enigma extends Operation { ] }, { - name: "Left-most rotor", + name: "Left-most (4th) rotor", type: "editableOption", value: ROTORS_FOURTH, defaultIndex: 0 From 1f9fd92b01db91518855039a41aee470daf3608f Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 28 Feb 2019 17:21:47 +0000 Subject: [PATCH 33/47] Typex: rotors in same order as Enigma --- src/core/operations/Typex.mjs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/core/operations/Typex.mjs b/src/core/operations/Typex.mjs index 504cb891..9c963357 100644 --- a/src/core/operations/Typex.mjs +++ b/src/core/operations/Typex.mjs @@ -29,10 +29,10 @@ class Typex extends Operation { this.outputType = "string"; this.args = [ { - name: "1st (right-hand, static) rotor", + name: "1st (left-hand) rotor", type: "editableOption", value: ROTORS, - defaultIndex: 4 + defaultIndex: 0 }, { name: "1st rotor reversed", @@ -50,10 +50,10 @@ class Typex extends Operation { value: LETTERS }, { - name: "2nd (static) rotor", + name: "2nd rotor", type: "editableOption", value: ROTORS, - defaultIndex: 3 + defaultIndex: 1 }, { name: "2nd rotor reversed", @@ -71,7 +71,7 @@ class Typex extends Operation { value: LETTERS }, { - name: "3rd rotor", + name: "3rd (middle) rotor", type: "editableOption", value: ROTORS, defaultIndex: 2 @@ -92,10 +92,10 @@ class Typex extends Operation { value: LETTERS }, { - name: "4th rotor", + name: "4th (static) rotor", type: "editableOption", value: ROTORS, - defaultIndex: 1 + defaultIndex: 3 }, { name: "4th rotor reversed", @@ -113,10 +113,10 @@ class Typex extends Operation { value: LETTERS }, { - name: "5th rotor", + name: "5th (right-hand, static) rotor", type: "editableOption", value: ROTORS, - defaultIndex: 0 + defaultIndex: 4 }, { name: "5th rotor reversed", @@ -190,6 +190,8 @@ class Typex extends Operation { const [rotorwiring, rotorsteps] = this.parseRotorStr(args[i*4]); rotors.push(new Rotor(rotorwiring, rotorsteps, args[i*4 + 1], args[i*4+2], args[i*4+3])); } + // Rotors are handled in reverse + rotors.reverse(); const reflector = new Reflector(reflectorstr); let plugboardstrMod = plugboardstr; if (plugboardstrMod === "") { From 765aded208b7f87ffef92d30294ac3edca763a2a Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 28 Feb 2019 17:22:09 +0000 Subject: [PATCH 34/47] Typex: add simple tests --- tests/operations/index.mjs | 1 + tests/operations/tests/Typex.mjs | 105 +++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 tests/operations/tests/Typex.mjs diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index ff967163..cff77217 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -86,6 +86,7 @@ import "./tests/ConvertCoordinateFormat"; import "./tests/Enigma"; import "./tests/Bombe"; import "./tests/MultipleBombe"; +import "./tests/Typex"; // Cannot test operations that use the File type yet //import "./tests/SplitColourChannels"; diff --git a/tests/operations/tests/Typex.mjs b/tests/operations/tests/Typex.mjs new file mode 100644 index 00000000..e3751e8a --- /dev/null +++ b/tests/operations/tests/Typex.mjs @@ -0,0 +1,105 @@ +/** + * Typex machine tests. + * @author s2224834 + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import TestRegister from "../TestRegister"; + +TestRegister.addTests([ + { + // Unlike Enigma we're not verifying against a real machine here, so this is just a test + // to catch inadvertent breakage. + name: "Typex: basic", + input: "hello world, this is a test message.", + expectedOutput: "VIXQQ VHLPN UCVLA QDZNZ EAYAT HWC", + recipeConfig: [ + { + "op": "Typex", + "args": [ + "MCYLPQUVRXGSAOWNBJEZDTFKHI Date: Thu, 28 Feb 2019 17:50:10 +0000 Subject: [PATCH 35/47] Add some files that escaped commit before --- package.json | 1 + webpack.config.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cb59db38..64ef09cc 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "sass-loader": "^7.1.0", "sitemap": "^2.1.0", "style-loader": "^0.23.1", + "svg-url-loader": "^2.3.2", "url-loader": "^1.1.2", "web-resource-inliner": "^4.2.1", "webpack": "^4.28.3", diff --git a/webpack.config.js b/webpack.config.js index 054152b2..e2a7c728 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -100,8 +100,15 @@ module.exports = { limit: 10000 } }, + { + test: /\.svg$/, + loader: "svg-url-loader", + options: { + encoding: "base64" + } + }, { // First party images are saved as files to be cached - test: /\.(png|jpg|gif|svg)$/, + test: /\.(png|jpg|gif)$/, exclude: /node_modules/, loader: "file-loader", options: { @@ -109,7 +116,7 @@ module.exports = { } }, { // Third party images are inlined - test: /\.(png|jpg|gif|svg)$/, + test: /\.(png|jpg|gif)$/, exclude: /web\/static/, loader: "url-loader", options: { From 9323737d1da3dc7b9a2d4f485e456829e7aa0e98 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 28 Feb 2019 18:37:48 +0000 Subject: [PATCH 36/47] Bombe: fix rotor listing order for multibombe --- src/core/operations/MultipleBombe.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs index 7a0ae2fd..6887bc46 100644 --- a/src/core/operations/MultipleBombe.mjs +++ b/src/core/operations/MultipleBombe.mjs @@ -291,7 +291,7 @@ class MultipleBombe extends Operation { let html = `Bombe run on menu with ${output.nLoops} loop${output.nLoops === 1 ? "" : "s"} (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided.\n`; for (const run of output.bombeRuns) { - html += `\nRotors: ${run.rotors.join(", ")}\nReflector: ${run.reflector}\n`; + html += `\nRotors: ${run.rotors.slice().reverse().join(", ")}\nReflector: ${run.reflector}\n`; html += ""; for (const [setting, stecker, decrypt] of run.result) { html += `\n`; From a446ec31c712d4a820e2cd484bed97b2a71c9e83 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 28 Feb 2019 18:48:36 +0000 Subject: [PATCH 37/47] Improve Enigma/Bombe descriptions a little. --- src/core/operations/Bombe.mjs | 2 +- src/core/operations/Enigma.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index 5e128498..f0d7048c 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -23,7 +23,7 @@ class Bombe extends Operation { this.name = "Bombe"; this.module = "Default"; - this.description = "Emulation of the Bombe machine used to attack Enigma.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops, a large number of incorrect outputs will be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine."; + this.description = "Emulation of the Bombe machine used at Bletchley Park to attack Enigma, based on work by Polish and British cryptanalysts.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops or is too short, a large number of incorrect outputs will usually be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine."; this.infoURL = "https://wikipedia.org/wiki/Bombe"; this.inputType = "string"; this.outputType = "JSON"; diff --git a/src/core/operations/Enigma.mjs b/src/core/operations/Enigma.mjs index 77333b18..71593070 100644 --- a/src/core/operations/Enigma.mjs +++ b/src/core/operations/Enigma.mjs @@ -22,7 +22,7 @@ class Enigma extends Operation { this.name = "Enigma"; this.module = "Default"; - this.description = "Encipher/decipher with the WW2 Enigma machine.

The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. AB CD EF connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.
This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses the thin reflectors and the beta or gamma rotor in the 4th slot)."; + this.description = "Encipher/decipher with the WW2 Enigma machine.

Enigma was used by the German military, among others, around the WW2 era as a portable cipher machine to protect sensitive military, diplomatic and commercial communications.

The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. AB CD EF connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.
This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses only the thin reflectors and the beta or gamma rotor in the 4th slot)."; this.infoURL = "https://wikipedia.org/wiki/Enigma_machine"; this.inputType = "string"; this.outputType = "string"; From 9a0b78415360c5d7c6ccf9ea025bedbea74f0d41 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Thu, 28 Feb 2019 18:56:59 +0000 Subject: [PATCH 38/47] Typex: improve operation description --- src/core/operations/Typex.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/Typex.mjs b/src/core/operations/Typex.mjs index 9c963357..760914f5 100644 --- a/src/core/operations/Typex.mjs +++ b/src/core/operations/Typex.mjs @@ -23,7 +23,7 @@ class Typex extends Operation { this.name = "Typex"; this.module = "Default"; - this.description = "Encipher/decipher with the WW2 Typex machine.

Typex rotors were changed regularly and none are public: a random example set are provided. Later Typexes had a reflector which could be configured with a plugboard: to configure this, enter a string of connected pairs of letters in the reflector box, e.g. AB CD EF connects A to B, C to D, and E to F (you'll need to connect every letter). These Typexes also have an input plugboard: unlike Enigma's plugboard, it's not restricted to pairs, so it's entered like a rotor (without stepping). To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points."; + this.description = "Encipher/decipher with the WW2 Typex machine.

Typex was originally built by the British Royal Air Force prior to WW2, and is based on the Enigma machine with some improvements made, including using five rotors with more stepping points and interchangeable wiring cores. It was used across the British and Commonewealth militaries. A number of later variants were produced; here we simulate a WW2 era Mark 22 Typex with plugboards for the reflector and input. Typex rotors were changed regularly and none are public: a random example set are provided.

To configure the reflector plugboard, enter a string of connected pairs of letters in the reflector box, e.g. AB CD EF connects A to B, C to D, and E to F (you'll need to connect every letter). There is also an input plugboard: unlike Enigma's plugboard, it's not restricted to pairs, so it's entered like a rotor (without stepping). To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points."; this.infoURL = "https://wikipedia.org/wiki/Typex"; this.inputType = "string"; this.outputType = "string"; From e2efc3e8e85f2c77def7c30ed757c527c1d75d1c Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Tue, 12 Mar 2019 18:21:27 +0000 Subject: [PATCH 39/47] package lock changes --- package-lock.json | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f56dc467..425b3f76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5125,14 +5125,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5152,8 +5150,7 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", @@ -5301,7 +5298,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5730,7 +5726,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, @@ -6486,7 +6482,7 @@ }, "html-webpack-plugin": { "version": "3.2.0", - "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { @@ -6611,7 +6607,7 @@ }, "http-proxy-middleware": { "version": "0.18.0", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", "dev": true, "requires": { @@ -9530,7 +9526,7 @@ }, "parse-asn1": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", "dev": true, "requires": { From b98cf9538db772ca7dc4dd123c83b1c6ae776250 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 11:43:23 +0000 Subject: [PATCH 40/47] Long HTML output is now replaced with an overlay in the same way as long string output. --- src/core/Chef.mjs | 21 ++++++++++++--------- src/core/operations/Bombe.mjs | 4 ++-- src/core/operations/MultipleBombe.mjs | 4 ++-- tests/operations/tests/Bombe.mjs | 12 ++++++------ tests/operations/tests/MultipleBombe.mjs | 2 +- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/core/Chef.mjs b/src/core/Chef.mjs index ed111b65..33b7acbe 100755 --- a/src/core/Chef.mjs +++ b/src/core/Chef.mjs @@ -89,23 +89,26 @@ class Chef { progress = err.progress; } - // Depending on the size of the output, we may send it back as a string or an ArrayBuffer. - // This can prevent unnecessary casting as an ArrayBuffer can be easily downloaded as a file. - // The threshold is specified in KiB. - const threshold = (options.ioDisplayThreshold || 1024) * 1024; - const returnType = this.dish.size > threshold ? Dish.ARRAY_BUFFER : Dish.STRING; - // Create a raw version of the dish, unpresented const rawDish = this.dish.clone(); // Present the raw result await recipe.present(this.dish); + // Depending on the size of the output, we may send it back as a string or an ArrayBuffer. + // This can prevent unnecessary casting as an ArrayBuffer can be easily downloaded as a file. + // The threshold is specified in KiB. + const threshold = (options.ioDisplayThreshold || 1024) * 1024; + const returnType = + this.dish.size > threshold ? + Dish.ARRAY_BUFFER : + this.dish.type === Dish.HTML ? + Dish.HTML : + Dish.STRING; + return { dish: rawDish, - result: this.dish.type === Dish.HTML ? - await this.dish.get(Dish.HTML, notUTF8) : - await this.dish.get(returnType, notUTF8), + result: await this.dish.get(returnType, notUTF8), type: Dish.enumLookup(this.dish.type), progress: progress, duration: new Date().getTime() - startTime, diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index f0d7048c..00d883ed 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -163,9 +163,9 @@ class Bombe extends Operation { */ present(output) { let html = `Bombe run on menu with ${output.nLoops} loop${output.nLoops === 1 ? "" : "s"} (2+ desirable). Note: Rotor positions are listed left to right and start at the beginning of the crib, and ignore stepping and the ring setting. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided.\n\n`; - html += "
Rotor stopsPartial plugboardDecryption preview
${setting}${stecker}${decrypt}
"; + html += "
Rotor stopsPartial plugboardDecryption preview
\n"; for (const [setting, stecker, decrypt] of output.result) { - html += `\n`; + html += `\n`; } html += "
Rotor stops Partial plugboard Decryption preview
${setting}${stecker}${decrypt}
${setting} ${stecker} ${decrypt}
"; return html; diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs index 6887bc46..03364a01 100644 --- a/src/core/operations/MultipleBombe.mjs +++ b/src/core/operations/MultipleBombe.mjs @@ -292,9 +292,9 @@ class MultipleBombe extends Operation { for (const run of output.bombeRuns) { html += `\nRotors: ${run.rotors.slice().reverse().join(", ")}\nReflector: ${run.reflector}\n`; - html += ""; + html += "
Rotor stopsPartial plugboardDecryption preview
\n"; for (const [setting, stecker, decrypt] of run.result) { - html += `\n`; + html += `\n`; } html += "
Rotor stops Partial plugboard Decryption preview
${setting}${stecker}${decrypt}
${setting} ${stecker} ${decrypt}
\n"; } diff --git a/tests/operations/tests/Bombe.mjs b/tests/operations/tests/Bombe.mjs index 9e5a79c6..b44e032c 100644 --- a/tests/operations/tests/Bombe.mjs +++ b/tests/operations/tests/Bombe.mjs @@ -11,7 +11,7 @@ TestRegister.addTests([ // Plugboard for this test is BO LC KE GA name: "Bombe: 3 rotor (self-stecker)", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA<\/td>SS<\/td>VFISUSGTKSTMPSUNAK<\/td>/, + expectedMatch: /LGA<\/td> {2}SS<\/td> {2}VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -31,7 +31,7 @@ TestRegister.addTests([ // This test produces a menu that doesn't use the first letter, which is also a good test name: "Bombe: 3 rotor (other stecker)", input: "JBYALIHDYNUAAVKBYM", - expectedMatch: /LGA<\/td>AG<\/td>QFIMUMAFKMQSKMYNGW<\/td>/, + expectedMatch: /LGA<\/td> {2}AG<\/td> {2}QFIMUMAFKMQSKMYNGW<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -50,7 +50,7 @@ TestRegister.addTests([ { name: "Bombe: crib offset", input: "AAABBYFLTHHYIJQAYBBYS", // first three chars here are faked - expectedMatch: /LGA<\/td>SS<\/td>VFISUSGTKSTMPSUNAK<\/td>/, + expectedMatch: /LGA<\/td> {2}SS<\/td> {2}VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -69,7 +69,7 @@ TestRegister.addTests([ { name: "Bombe: multiple stops", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA<\/td>TT<\/td>VFISUSGTKSTMPSUNAK<\/td>/, + expectedMatch: /LGA<\/td> {2}TT<\/td> {2}VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -88,7 +88,7 @@ TestRegister.addTests([ { name: "Bombe: checking machine", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA<\/td>TT AG BO CL EK FF HH II JJ SS YY<\/td>THISISATESTMESSAGE<\/td>/, + expectedMatch: /LGA<\/td> {2}TT AG BO CL EK FF HH II JJ SS YY<\/td> {2}THISISATESTMESSAGE<\/td>/, recipeConfig: [ { "op": "Bombe", @@ -108,7 +108,7 @@ TestRegister.addTests([ { name: "Bombe: 4 rotor", input: "LUOXGJSHGEDSRDOQQX", - expectedMatch: /LHSC<\/td>SS<\/td>HHHSSSGQUUQPKSEKWK<\/td>/, + expectedMatch: /LHSC<\/td> {2}SS<\/td> {2}HHHSSSGQUUQPKSEKWK<\/td>/, recipeConfig: [ { "op": "Bombe", diff --git a/tests/operations/tests/MultipleBombe.mjs b/tests/operations/tests/MultipleBombe.mjs index 8e2cc685..32d2db08 100644 --- a/tests/operations/tests/MultipleBombe.mjs +++ b/tests/operations/tests/MultipleBombe.mjs @@ -10,7 +10,7 @@ TestRegister.addTests([ { name: "Multi-Bombe: 3 rotor", input: "BBYFLTHHYIJQAYBBYS", - expectedMatch: /LGA<\/td>SS<\/td>VFISUSGTKSTMPSUNAK<\/td>/, + expectedMatch: /LGA<\/td> {2}SS<\/td> {2}VFISUSGTKSTMPSUNAK<\/td>/, recipeConfig: [ { "op": "Multiple Bombe", From cf32372a57e0cf4cf85c3b620979d229c1969895 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 12:08:35 +0000 Subject: [PATCH 41/47] Added Enigma wiki article link to Enigma, Typex, Bombe and Multi-Bombe operation descriptions. --- src/core/operations/Bombe.mjs | 2 +- src/core/operations/Enigma.mjs | 2 +- src/core/operations/MultipleBombe.mjs | 2 +- src/core/operations/Typex.mjs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index 00d883ed..c2ea82bf 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -23,7 +23,7 @@ class Bombe extends Operation { this.name = "Bombe"; this.module = "Default"; - this.description = "Emulation of the Bombe machine used at Bletchley Park to attack Enigma, based on work by Polish and British cryptanalysts.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops or is too short, a large number of incorrect outputs will usually be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine."; + this.description = "Emulation of the Bombe machine used at Bletchley Park to attack Enigma, based on work by Polish and British cryptanalysts.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops or is too short, a large number of incorrect outputs will usually be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine.

More detailed descriptions of the Enigma, Typex and Bombe operations can be found here."; this.infoURL = "https://wikipedia.org/wiki/Bombe"; this.inputType = "string"; this.outputType = "JSON"; diff --git a/src/core/operations/Enigma.mjs b/src/core/operations/Enigma.mjs index 71593070..542e8281 100644 --- a/src/core/operations/Enigma.mjs +++ b/src/core/operations/Enigma.mjs @@ -22,7 +22,7 @@ class Enigma extends Operation { this.name = "Enigma"; this.module = "Default"; - this.description = "Encipher/decipher with the WW2 Enigma machine.

Enigma was used by the German military, among others, around the WW2 era as a portable cipher machine to protect sensitive military, diplomatic and commercial communications.

The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. AB CD EF connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.
This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses only the thin reflectors and the beta or gamma rotor in the 4th slot)."; + this.description = "Encipher/decipher with the WW2 Enigma machine.

Enigma was used by the German military, among others, around the WW2 era as a portable cipher machine to protect sensitive military, diplomatic and commercial communications.

The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. AB CD EF connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.
This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses only the thin reflectors and the beta or gamma rotor in the 4th slot).

More detailed descriptions of the Enigma, Typex and Bombe operations can be found here."; this.infoURL = "https://wikipedia.org/wiki/Enigma_machine"; this.inputType = "string"; this.outputType = "string"; diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs index 03364a01..b6a48872 100644 --- a/src/core/operations/MultipleBombe.mjs +++ b/src/core/operations/MultipleBombe.mjs @@ -53,7 +53,7 @@ class MultipleBombe extends Operation { this.name = "Multiple Bombe"; this.module = "Default"; - this.description = "Emulation of the Bombe machine used to attack Enigma. This version carries out multiple Bombe runs to handle unknown rotor configurations.

You should test your menu on the single Bombe operation before running it here. See the description of the Bombe operation for instructions on choosing a crib."; + this.description = "Emulation of the Bombe machine used to attack Enigma. This version carries out multiple Bombe runs to handle unknown rotor configurations.

You should test your menu on the single Bombe operation before running it here. See the description of the Bombe operation for instructions on choosing a crib.

More detailed descriptions of the Enigma, Typex and Bombe operations can be found here."; this.infoURL = "https://wikipedia.org/wiki/Bombe"; this.inputType = "string"; this.outputType = "JSON"; diff --git a/src/core/operations/Typex.mjs b/src/core/operations/Typex.mjs index 760914f5..70b5f6c3 100644 --- a/src/core/operations/Typex.mjs +++ b/src/core/operations/Typex.mjs @@ -23,7 +23,7 @@ class Typex extends Operation { this.name = "Typex"; this.module = "Default"; - this.description = "Encipher/decipher with the WW2 Typex machine.

Typex was originally built by the British Royal Air Force prior to WW2, and is based on the Enigma machine with some improvements made, including using five rotors with more stepping points and interchangeable wiring cores. It was used across the British and Commonewealth militaries. A number of later variants were produced; here we simulate a WW2 era Mark 22 Typex with plugboards for the reflector and input. Typex rotors were changed regularly and none are public: a random example set are provided.

To configure the reflector plugboard, enter a string of connected pairs of letters in the reflector box, e.g. AB CD EF connects A to B, C to D, and E to F (you'll need to connect every letter). There is also an input plugboard: unlike Enigma's plugboard, it's not restricted to pairs, so it's entered like a rotor (without stepping). To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points."; + this.description = "Encipher/decipher with the WW2 Typex machine.

Typex was originally built by the British Royal Air Force prior to WW2, and is based on the Enigma machine with some improvements made, including using five rotors with more stepping points and interchangeable wiring cores. It was used across the British and Commonewealth militaries. A number of later variants were produced; here we simulate a WW2 era Mark 22 Typex with plugboards for the reflector and input. Typex rotors were changed regularly and none are public: a random example set are provided.

To configure the reflector plugboard, enter a string of connected pairs of letters in the reflector box, e.g. AB CD EF connects A to B, C to D, and E to F (you'll need to connect every letter). There is also an input plugboard: unlike Enigma's plugboard, it's not restricted to pairs, so it's entered like a rotor (without stepping). To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by < then a list of stepping points.

More detailed descriptions of the Enigma, Typex and Bombe operations can be found here."; this.infoURL = "https://wikipedia.org/wiki/Typex"; this.inputType = "string"; this.outputType = "string"; From 33db0e666a5b2a0115118a0e6c6e0ebc94769c07 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 12:11:41 +0000 Subject: [PATCH 42/47] Final tweaks to Bombe svg and preloader css --- src/web/static/images/bombe.svg | 4 ++-- src/web/stylesheets/preloader.css | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/web/static/images/bombe.svg b/src/web/static/images/bombe.svg index 1fd40554..a970903a 100644 --- a/src/web/static/images/bombe.svg +++ b/src/web/static/images/bombe.svg @@ -1,8 +1,8 @@ diff --git a/src/web/stylesheets/preloader.css b/src/web/stylesheets/preloader.css index 690fe5c1..288ffc28 100755 --- a/src/web/stylesheets/preloader.css +++ b/src/web/stylesheets/preloader.css @@ -160,12 +160,3 @@ transform: translate3d(0, 200px, 0); } } - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} From ef38897a010208f5311850351c71218714294a26 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 12:20:05 +0000 Subject: [PATCH 43/47] Updated CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3eca29..0d2eca7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All major and minor version changes will be documented in this file. Details of patch-level version changes can be found in [commit messages](https://github.com/gchq/CyberChef/commits/master). +### [8.26.0] - 2019-03-09 +- Various image manipulation operations added [@j433866] | [#506] + +### [8.25.0] - 2019-03-09 +- 'Extract Files' operation added and more file formats supported [@n1474335] | [#440] + ### [8.24.0] - 2019-02-08 - 'DNS over HTTPS' operation added [@h345983745] | [#489] @@ -106,6 +112,8 @@ All major and minor version changes will be documented in this file. Details of +[8.26.0]: https://github.com/gchq/CyberChef/releases/tag/v8.26.0 +[8.25.0]: https://github.com/gchq/CyberChef/releases/tag/v8.25.0 [8.24.0]: https://github.com/gchq/CyberChef/releases/tag/v8.24.0 [8.23.1]: https://github.com/gchq/CyberChef/releases/tag/v8.23.1 [8.23.0]: https://github.com/gchq/CyberChef/releases/tag/v8.23.0 @@ -180,6 +188,7 @@ All major and minor version changes will be documented in this file. Details of [#394]: https://github.com/gchq/CyberChef/pull/394 [#428]: https://github.com/gchq/CyberChef/pull/428 [#439]: https://github.com/gchq/CyberChef/pull/439 +[#440]: https://github.com/gchq/CyberChef/pull/440 [#441]: https://github.com/gchq/CyberChef/pull/441 [#443]: https://github.com/gchq/CyberChef/pull/443 [#446]: https://github.com/gchq/CyberChef/pull/446 @@ -192,3 +201,4 @@ All major and minor version changes will be documented in this file. Details of [#468]: https://github.com/gchq/CyberChef/pull/468 [#476]: https://github.com/gchq/CyberChef/pull/476 [#489]: https://github.com/gchq/CyberChef/pull/489 +[#506]: https://github.com/gchq/CyberChef/pull/506 From c8a2a8b003a31ddf9f0860e63db068972d3f820b Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 12:26:00 +0000 Subject: [PATCH 44/47] Updated CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d2eca7e..b21944fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All major and minor version changes will be documented in this file. Details of patch-level version changes can be found in [commit messages](https://github.com/gchq/CyberChef/commits/master). +### [8.27.0] - 2019-03-14 +- 'Enigma', 'Typex', 'Bombe' and 'Multiple Bombe' operations added [@s2224834] | [#516] +- See [this wiki article](https://github.com/gchq/CyberChef/wiki/Enigma,-the-Bombe,-and-Typex) for a full explanation of these operations. +- New Bombe-style loading animation added for long-running operations [@n1474335] +- New operation argument types added: `populateMultiOption` and `argSelector` [@n1474335] + ### [8.26.0] - 2019-03-09 - Various image manipulation operations added [@j433866] | [#506] From 3ff10bfeaebad28c72ceff53f8c78ebb4ebb0755 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 12:26:07 +0000 Subject: [PATCH 45/47] 8.27.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 425b3f76..9fd4a068 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "8.26.3", + "version": "8.27.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 453c9d96..e650b272 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "8.26.3", + "version": "8.27.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From bb5b92571e62630ad7a899089d5f7d2e4fd5f9d2 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Thu, 14 Mar 2019 16:08:25 +0000 Subject: [PATCH 46/47] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b21944fe..11a18d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ All major and minor version changes will be documented in this file. Details of +[8.27.0]: https://github.com/gchq/CyberChef/releases/tag/v8.27.0 [8.26.0]: https://github.com/gchq/CyberChef/releases/tag/v8.26.0 [8.25.0]: https://github.com/gchq/CyberChef/releases/tag/v8.25.0 [8.24.0]: https://github.com/gchq/CyberChef/releases/tag/v8.24.0 From a5703cb4f151a5d75f6bee5cdc6af77463f61529 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 15 Mar 2019 15:17:15 +0000 Subject: [PATCH 47/47] Updated CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a18d86..06a5eb63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,7 @@ All major and minor version changes will be documented in this file. Details of [@j433866]: https://github.com/j433866 [@GCHQ77703]: https://github.com/GCHQ77703 [@h345983745]: https://github.com/h345983745 +[@s2224834]: https://github.com/s2224834 [@artemisbot]: https://github.com/artemisbot [@picapi]: https://github.com/picapi [@Dachande663]: https://github.com/Dachande663 @@ -209,3 +210,4 @@ All major and minor version changes will be documented in this file. Details of [#476]: https://github.com/gchq/CyberChef/pull/476 [#489]: https://github.com/gchq/CyberChef/pull/489 [#506]: https://github.com/gchq/CyberChef/pull/506 +[#516]: https://github.com/gchq/CyberChef/pull/516