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