/** * 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 < n - 1; i++) { this.rotors[i].step(); } this.cacheGen(); } /** * Optimisation: We pregenerate all routes through the machine with the top rotor removed, * as these rarely change. This saves a lot of lookups. This function generates this route * table. * We also just-in-time cache the full routes through the scramblers, because after stepping * the fast rotor some scramblers will be in states occupied by other scrambles on previous * iterations. */ cacheGen() { for (let i = 0; i < 26; i++) { this.lowerCache[i] = undefined; for (let j = 0; j < 26; j++) { this.higherCache[i][j] = undefined; } } for (let i = 0; i < 26; i++) { if (this.lowerCache[i] !== undefined) { continue; } let letter = i; for (const rotor of this.rotors) { letter = rotor.transform(letter); } letter = this.reflector.transform(letter); for (const rotor of this.rotorsRev) { letter = rotor.revTransform(letter); } // By symmetry this.lowerCache[i] = letter; this.lowerCache[letter] = i; } } /** * Map a letter through this (partial) scrambler. * @param {number} i - The letter * @returns {number} */ transform(i) { return this.lowerCache[i]; } } /** * Scrambler. * * This is effectively just an Enigma machine, but it only operates on one character at a time and * the stepping mechanism is different. */ class Scrambler { /** Scrambler constructor. * @param {Object} base - The SharedScrambler whose state this scrambler uses * @param {Object} rotor - The non-shared fast rotor in this scrambler * @param {number} pos - Position offset from start of crib * @param {number} end1 - Letter in menu this scrambler is attached to * @param {number} end2 - Other letter in menu this scrambler is attached to */ constructor(base, rotor, pos, end1, end2) { this.baseScrambler = base; this.initialPos = pos; this.changeRotor(rotor); this.end1 = end1; this.end2 = end2; // For efficiency reasons, we pull the relevant shared cache from the baseScrambler into // this object - this saves us a few pointer dereferences this.cache = this.baseScrambler.higherCache[pos]; } /** * 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 rotor forward. * * The base SharedScrambler needs to be instructed to step separately. */ step() { // The Bombe steps the slowest rotor on an actual Enigma fastest, for reasons. // ...but for optimisation reasons I'm going to cheat and not do that, as this vastly // simplifies caching the state of the majority of the scramblers. The results are the // same, just in a slightly different order. this.rotor.step(); this.cache = this.baseScrambler.higherCache[this.rotor.pos]; } /** * Run a letter through the scrambler. * @param {number} i - The letter to transform (as a number) * @returns {number} */ transform(i) { let letter = i; const cached = this.cache[i]; if (cached !== undefined) { return cached; } letter = this.rotor.transform(letter); letter = this.baseScrambler.transform(letter); letter = this.rotor.revTransform(letter); this.cache[i] = letter; this.cache[letter] = i; return letter; } /** * Given one letter in the menu this scrambler maps to, return the other. * @param end {number} - The node we have * @returns {number} */ getOtherEnd(end) { return this.end1 === end ? this.end2 : this.end1; } /** * Read the position this scrambler is set to. * Note that because of Enigma's stepping, you need to set an actual Enigma to the previous * position in order to get it to make a certain set of electrical connections when a button * is pressed - this function *does* take this into account. * However, as with the rest of the Bombe, it does not take stepping into account - the middle * and slow rotors are treated as static. * @return {string} */ getPos() { let result = ""; // Roll back the fast rotor by one step let pos = Utils.mod(this.rotor.pos - 1, 26); result += i2a(pos); for (let i = 0; i < this.baseScrambler.rotors.length; i++) { pos = this.baseScrambler.rotors[i].pos; result += i2a(pos); } return result.split("").reverse().join(""); } } /** * Bombe simulator class. */ export class BombeMachine { /** * Construct a Bombe. * * Note that there is no handling of offsets here: the crib specified must exactly match the * ciphertext. It will check that the crib is sane (length is vaguely sensible and there's no * matching characters between crib and ciphertext) but cannot check further - if it's wrong * your results will be wrong! * * There is also no handling of rotor stepping - if the target Enigma stepped in the middle of * your crib, you're out of luck. TODO: Allow specifying a step point - this is fairly easy to * configure on a real Bombe, but we're not clear on whether it was ever actually done for * real (there would almost certainly have been better ways of attacking in most situations * than attempting to exhaust options for the stepping point, but in some circumstances, e.g. * via Banburismus, the stepping point might have been known). * * @param {string[]} rotors - list of rotor spec strings (without step points!) * @param {Object} reflector - Reflector object * @param {string} ciphertext - The ciphertext to attack * @param {string} crib - Known plaintext for this ciphertext * @param {boolean} check - Whether to use the checking machine * @param {function} update - Function to call to send status updates (optional) */ constructor( rotors, reflector, ciphertext, crib, check, update = undefined, ) { if (ciphertext.length < crib.length) { throw new OperationError("Crib overruns supplied ciphertext"); } if (crib.length < 2) { // This is the absolute bare minimum to be sane, and even then it's likely too short to // be useful throw new OperationError("Crib is too short"); } if (crib.length > 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 < crib.length; i++) { if (ciphertext[i] === crib[i]) { throw new OperationError( `Invalid crib: character ${ciphertext[i]} at pos ${i} in both ciphertext and crib`, ); } } this.ciphertext = ciphertext; this.crib = crib; this.initRotors(rotors); this.check = check; this.updateFn = update; const [mostConnected, edges] = this.makeMenu(); // This is the bundle of wires corresponding to the 26 letters within each of the 26 // possible nodes in the menu this.wires = new Array(26 * 26); // These are the pseudo-Engima devices corresponding to each edge in the menu, and the // nodes in the menu they each connect to this.scramblers = new Array(); for (let i = 0; i < 26; i++) { this.scramblers.push(new Array()); } this.sharedScrambler = new SharedScrambler( this.baseRotors.slice(1), reflector, ); this.allScramblers = new Array(); this.indicator = undefined; for (const edge of edges) { const cRotor = this.baseRotors[0].copy(); const end1 = a2i(edge.node1.letter); const end2 = a2i(edge.node2.letter); const scrambler = new Scrambler( this.sharedScrambler, cRotor, edge.pos, end1, end2, ); if (edge.pos === 0) { this.indicator = scrambler; } this.scramblers[end1].push(scrambler); this.scramblers[end2].push(scrambler); this.allScramblers.push(scrambler); } // The Bombe uses a set of rotors to keep track of what settings it's testing. We cheat and // use one of the actual scramblers if there's one in the right position, but if not we'll // just create one. if (this.indicator === undefined) { this.indicator = new Scrambler( this.sharedScrambler, this.baseRotors[0].copy(), 0, undefined, undefined, ); this.allScramblers.push(this.indicator); } this.testRegister = a2i(mostConnected.letter); // This is an arbitrary letter other than the most connected letter for (const edge of mostConnected.edges) { this.testInput = [ this.testRegister, a2i(edge.getOther(mostConnected).letter), ]; break; } } /** * 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 {...*} msg - Message to send. */ update(...msg) { if (this.updateFn !== undefined) { this.updateFn(...msg); } } /** * Recursive depth-first search on the menu graph. * This is used to a) isolate unconnected sub-graphs, and b) count the number of loops in each * of those graphs. * @param {Object} node - Node object to start the search from * @returns {[number, number, Object, number, Object[]} - loop count, node count, most connected * node, order of most connected node, list of edges in this sub-graph */ dfs(node) { let loops = 0; let nNodes = 1; let mostConnected = node; let nConnections = mostConnected.edges.size; let edges = new Set(); node.visited = true; for (const edge of node.edges) { if (edge.visited) { // Already been here from the other end. continue; } edge.visited = true; edges.add(edge); const other = edge.getOther(node); if (other.visited) { // We have a loop, record that and continue loops += 1; continue; } // This is a newly visited node const [oLoops, oNNodes, oMostConnected, oNConnections, oEdges] = this.dfs(other); loops += oLoops; nNodes += oNNodes; edges = new Set([...edges, ...oEdges]); if (oNConnections > 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 < this.crib.length; i++) { const a = this.crib[i]; const b = this.ciphertext[i]; new Edge(i, nodes.get(a), nodes.get(b)); } // list of [loop_count, node_count, most_connected_node, connections_on_most_connected, edges] const graphs = []; // Then, for each unconnected subgraph, we count the number of loops and nodes for (const start of nodes.keys()) { if (nodes.get(start).visited) { continue; } const subgraph = this.dfs(nodes.get(start)); graphs.push(subgraph); } // Return the subgraph with the most loops (ties broken by node count) graphs.sort((a, b) => { 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 < this.scramblers[i].length; k++) { const scrambler = this.scramblers[i][k]; const out = scrambler.transform(j); const other = scrambler.getOtherEnd(i); // Lift the pre-check before the call, to save some function call overhead const otherIdx = 26 * other + out; if (!this.wires[otherIdx]) { this.energise(other, out); if (this.energiseCount === 26) { return; } } } if (i === j) { return; } for (let k = 0; k < this.scramblers[j].length; k++) { const scrambler = this.scramblers[j][k]; const out = scrambler.transform(i); const other = scrambler.getOtherEnd(j); const otherIdx = 26 * other + out; if (!this.wires[otherIdx]) { this.energise(other, out); if (this.energiseCount === 26) { return; } } } } /** * Trial decryption at the current setting. * Used after we get a stop. * This applies the detected stecker pair if we have one. It does not handle the other * steckering or stepping (which is why we limit it to 26 characters, since it's guaranteed to * be wrong after that anyway). * @param {string} stecker - Known stecker spec string. * @returns {string} */ tryDecrypt(stecker) { const fastRotor = this.indicator.rotor; const initialPos = fastRotor.pos; const res = []; const plugboard = new Plugboard(stecker); // The indicator scrambler starts in the right place for the beginning of the ciphertext. for (let i = 0; i < Math.min(26, this.ciphertext.length); i++) { const t = this.indicator.transform( plugboard.transform(a2i(this.ciphertext[i])), ); res.push(i2a(plugboard.transform(t))); this.indicator.step(1); } fastRotor.pos = initialPos; return res.join(""); } /** * Format a steckered pair, in sorted order to allow uniquing. * @param {number} a - A letter * @param {number} b - Its stecker pair * @returns {string} */ formatPair(a, b) { if (a < b) { return `${i2a(a)}${i2a(b)}`; } return `${i2a(b)}${i2a(a)}`; } /** * The checking machine was used to manually verify Bombe stops. Using a device which was * effectively a non-stepping Enigma, the user would walk through each of the links in the * menu at the rotor positions determined by the Bombe. By starting with the stecker pair the * Bombe gives us, we find the stecker pair of each connected letter in the graph, and so on. * If a contradiction is reached, the stop is invalid. If not, we have most (but not * necessarily all) of the plugboard connections. * You will notice that this procedure is exactly the same as what the Bombe itself does, only * we start with an assumed good hypothesis and read out the stecker pair for every letter. * On the real hardware that wasn't practical, but fortunately we're not the real hardware, so * we don't need to implement the manual checking machine procedure. * @param {number} pair - The stecker pair of the test register. * @returns {string} - The empty string for invalid stops, or a plugboard configuration string * containing all known pairs. */ checkingMachine(pair) { if (pair !== this.testInput[1]) { // We have a new hypothesis for this stop - apply the new one. // De-energise the board for (let i = 0; i < this.wires.length; i++) { this.wires[i] = false; } this.energiseCount = 0; // Re-energise with the corrected hypothesis this.energise(this.testRegister, pair); } const results = new Set(); results.add(this.formatPair(this.testRegister, pair)); for (let i = 0; i < 26; i++) { let count = 0; let other; for (let j = 0; j < 26; j++) { if (this.wires[i * 26 + j]) { count++; other = j; } } if (count > 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 < this.wires.length; i++) { this.wires[i] = false; } this.energiseCount = 0; // Energise the test input, follow the current through each scrambler // (and the diagonal board) this.energise(...this.testInput); const stop = this.checkStop(); if (stop !== undefined) { stops++; result.push(stop); } // Step all the scramblers // This loop counts how many rotors have reached their starting position (meaning the // next one needs to step as well) let n = 1; for (let j = 1; j < this.baseRotors.length; j++) { if (i % Math.pow(26, j) === 0) { n++; } else { break; } } if (n > 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; } }