CyberChef/src/core/operations/MultipleBombe.mjs

319 lines
12 KiB
JavaScript
Raw Normal View History

/**
* 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.";
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_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",
value: ""
},
{
name: "Crib",
type: "string",
value: ""
},
{
name: "Crib offset",
type: "number",
value: 0
2019-01-11 14:18:25 +01:00
},
{
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} loops 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[3];
const reflectorsStr = args[5];
let crib = args[6];
const offset = args[7];
2019-01-11 14:18:25 +01:00
const check = args[8];
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;
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) {
2019-01-11 14:18:25 +01:00
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);
}
const result = bombe.run();
nStops += result.length;
if (update !== undefined) {
update(bombe.nLoops, nStops, nRuns / totalRuns, start);
}
if (result.length > 0) {
2019-01-11 14:18:25 +01:00
msg += `\nRotors: ${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;