/** * Emulation of the Bombe machine. * * @author s2224834 * @author The National Museum of Computing - Bombe Rebuild Project * @copyright Crown Copyright 2019 * @license Apache-2.0 */ import OperationError from "../errors/OperationError.mjs"; import Utils from "../Utils.mjs"; import {Rotor, Plugboard, a2i, i2a} from "./Enigma.mjs"; /** * 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. * @returns {Object} */ 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) { return this.node1 === node ? this.node2 : this.node1; } } /** * 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.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(); } /** * 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 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]]; } /** * 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} i - Bombe wire bundle * @param {number} j - Bombe stecker hypothesis wire within bundle */ energise(i, j) { const idx = 26*i + j; if (this.wires[idx]) { return; } this.wires[idx] = true; // Welchman's diagonal board: if A steckers to B, that implies B steckers to A. Handle // 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 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 const count = this.energiseCount; 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 { // 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) { 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 * solutions. * @returns {string[][3]} - list of 3-tuples of candidate rotor setting, plugboard settings, and decryption preview */ 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++) { // Benchmarking suggests this is faster than using .fill() for (let i=0; i 1) { this.sharedScrambler.step(n); } for (const scrambler of this.allScramblers) { 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) if (n > 3) { this.update(this.nLoops, stops, i/nChecks); } } return result; } }