/** * Emulation of the Bombe machine. * * @author s2224834 * @copyright Crown Copyright 2019 * @license Apache-2.0 */ import OperationError from "../errors/OperationError"; import Utils from "../Utils"; import {Rotor, a2i, i2a} from "./Enigma"; /** * 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. */ 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[2]} i - Bombe state wire */ energise(i, j) { const idx = 26*i + j; if (this.wires[idx]) { return; } this.energiseCount ++; 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; for (let k=0; k 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; } }