123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- /**
- * 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, ROTORS_FOURTH, 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.<br><br>More detailed descriptions of the Enigma, Typex and Bombe operations <a href='https://github.com/gchq/CyberChef/wiki/Enigma,-the-Bombe,-and-Typex'>can be found here</a>.";
- this.infoURL = "https://wikipedia.org/wiki/Bombe";
- this.inputType = "string";
- this.outputType = "JSON";
- this.presentType = "html";
- this.args = [
- {
- "name": "Standard Enigmas",
- "type": "populateMultiOption",
- "value": [
- {
- name: "German Service Enigma (First - 3 rotor)",
- value: [
- rotorsFormat(ROTORS, 0, 5),
- "",
- rotorsFormat(REFLECTORS, 0, 1)
- ]
- },
- {
- name: "German Service Enigma (Second - 3 rotor)",
- value: [
- rotorsFormat(ROTORS, 0, 8),
- "",
- rotorsFormat(REFLECTORS, 0, 2)
- ]
- },
- {
- name: "German Service Enigma (Third - 4 rotor)",
- 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),
- rotorsFormat(ROTORS_FOURTH, 1, 3),
- rotorsFormat(REFLECTORS, 2, 4)
- ]
- },
- {
- name: "User defined",
- value: ["", "", ""]
- },
- ],
- "target": [1, 2, 3]
- },
- {
- name: "Main rotors",
- type: "text",
- value: ""
- },
- {
- name: "4th rotor",
- type: "text",
- value: ""
- },
- {
- name: "Reflectors",
- type: "text",
- value: ""
- },
- {
- name: "Crib",
- type: "string",
- value: ""
- },
- {
- name: "Crib offset",
- type: "number",
- value: 0
- },
- {
- name: "Use checking machine",
- type: "boolean",
- value: true
- }
- ];
- }
- /**
- * 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, start) {
- const elapsed = new Date().getTime() - start;
- const remaining = (elapsed / progress) * (1 - progress) / 1000;
- 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} loop${nLoops === 1 ? "" : "s"} in menu (2+ desirable): ${nStops} stops, ${Math.floor(100 * progress)}% done, ${hours}:${minutes}:${seconds} remaining`;
- 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[2];
- const reflectorsStr = args[3];
- let crib = args[4];
- const offset = args[5];
- const check = args[6];
- 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;
- 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
- const totalRuns = choose(rotors.length, 3) * 6 * fourthRotors.length * reflectors.length;
- let nRuns = 0;
- let nStops = 0;
- const start = new Date().getTime();
- 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, check);
- output.nLoops = bombe.nLoops;
- } else {
- bombe.changeRotors(runRotors, reflector);
- }
- const result = bombe.run();
- nStops += result.length;
- if (update !== undefined) {
- update(bombe.nLoops, nStops, nRuns / totalRuns, start);
- }
- if (result.length > 0) {
- output.bombeRuns.push({
- rotors: runRotors,
- reflector: reflector.pairs,
- result: result
- });
- }
- }
- }
- }
- }
- }
- 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.slice().reverse().join(", ")}\nReflector: ${run.reflector}\n`;
- html += "<table class='table table-hover table-sm table-bordered table-nonfluid'><tr><th>Rotor stops</th> <th>Partial plugboard</th> <th>Decryption preview</th></tr>\n";
- for (const [setting, stecker, decrypt] of run.result) {
- html += `<tr><td>${setting}</td> <td>${stecker}</td> <td>${decrypt}</td></tr>\n`;
- }
- html += "</table>\n";
- }
- return html;
- }
- }
- export default MultipleBombe;
|