/** * 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; } } /** * 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[]} rotors - List of rotors in this scrambler * @param {Object} reflector - This scrambler's reflector * @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(rotors, reflector, pos, end1, end2) { this.reflector = reflector; this.rotors = rotors; this.rotorsRev = [].concat(rotors).reverse(); this.initialPos = pos; this.rotors[0].pos += pos; this.end1 = end1; this.end2 = end2; } /** * Step the rotors forward. * * All nodes in the Bombe step in sync. * @param {number} n - How many rotors to step */ step(n) { // The Bombe steps the slowest rotor on an actual Enigma first. for (let i=this.rotors.length - 1; i>=this.rotors.length-n; i--) { this.rotors[i].step(); } } /** * Run a letter through the scrambler. * @param {number} i - The letter to transform (as a number) * @returns {number} */ transform(i) { 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); } 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 = ""; for (let i=0; i crib.length) { throw new OperationError("Ciphertext is longer than crib"); } 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 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]]; } /** * Implement Welchman's diagonal board: If A steckers to B, that implies B steckers to A, and * so forth. This function just gets the paired wire. * @param {number[2]} i - Bombe state wire * @returns {number[2]} */ getDiagonal(i) { return [i[1], i[0]]; } /** * 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) { const idx = 26*i[0] + i[1]; if (this.wires[idx]) { return; } this.energiseCount ++; this.wires[idx] = true; this.energise(this.getDiagonal(i)); for (const scrambler of this.scramblers[i[0]]) { const out = scrambler.transform(i[1]); const other = scrambler.getOtherEnd(i[0]); this.energise([other, out]); } } /** * 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[][2]} - list of pairs of candidate rotor setting, and calculated stecker pair */ 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 ${i2a(j)}`; break; } } } else if (count === 1) { // This means our hypothesis for the steckering is correct. stecker = `${i2a(this.testRegister)} <-> ${i2a(this.testInput[1])}`; } else { // Unusual, probably indicative of a poor menu. I'm a little unclear on how // this was really handled, but we'll return it for the moment. stecker = `? (wire count: ${count})`; } result.push([this.indicator.getPos(), stecker]); } // 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 3) { this.update(this.nLoops, stops, i/nChecks); } } return result; } }