Browse Source

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
s2224834 6 years ago
parent
commit
3eb44708e5

+ 2 - 1
src/core/config/Categories.json

@@ -104,7 +104,8 @@
             "Citrix CTX1 Decode",
             "Pseudo-Random Number Generator",
             "Enigma",
-            "Bombe"
+            "Bombe",
+            "Multiple Bombe"
         ]
     },
     {

+ 52 - 11
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.

+ 1 - 0
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;

+ 307 - 0
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.<br><br>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;

+ 1 - 0
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";

+ 1 - 1
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",

+ 47 - 0
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<R\nAJDKSIRUXBLHWTMCQGZNPYFVOE<F\nBDFHJLCPRTXVZNYEIWGAKMUSQO<W",
+                    "User defined", "",
+                    "User defined", "AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
+                    "THISISATESTMESSAGE", 0,
+                ]
+            }
+        ]
+    },
+    /*
+     * This is too slow to run regularly
+    {
+        name: "Multi-Bombe: 4 rotor",
+        input: "LUOXGJSHGEDSRDOQQX",
+        expectedMatch: /LHSC \(plugboard: SS\): HHHSSSGQUUQPKSEKWK/,
+        recipeConfig: [
+            {
+                "op": "Multiple Bombe",
+                "args": [
+                    // I, II and III
+                    "User defined", "EKMFLGDQVZNTOWYHXUSPAIBRCJ<R\nAJDKSIRUXBLHWTMCQGZNPYFVOE<F\nBDFHJLCPRTXVZNYEIWGAKMUSQO<W",
+                    "User defined", "LEYJVCNIXWPBQMDRTAKZGFUHOS", // Beta
+                    "User defined", "AE BN CK DQ FU GY HW IJ LO MP RX SZ TV", // B thin
+                    "THISISATESTMESSAGE", 0,
+                ]
+            }
+        ]
+    },
+    */
+]);