diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 3e0d108e..5a40846c 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -103,7 +103,8 @@ "Citrix CTX1 Encode", "Citrix CTX1 Decode", "Pseudo-Random Number Generator", - "Enigma" + "Enigma", + "Bombe" ] }, { diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs new file mode 100644 index 00000000..70a2d2cb --- /dev/null +++ b/src/core/lib/Bombe.mjs @@ -0,0 +1,472 @@ +/** + * 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) { + if (this.node1 === node) { + return this.node2; + } + return 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) { + if (this.end1 === end) { + return this.end2; + } + return 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 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++) { + this.wires.fill(false); + // Energise the test input, follow the current through each scrambler + // (and the diagonal board) + this.energiseCount = 0; + this.energise(this.testInput); + // Count the energised outputs + let count = 0; + for (let j=26*this.testRegister; j<26*(1+this.testRegister); j++) { + if (this.wires[j]) { + count++; + } + } + // If it's not all of them, we have a stop + if (count < 26) { + stops += 1; + let stecker; + // 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]) { + stecker = `${i2a(this.testRegister)} <-> ${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 2) { + const msg = `Bombe run with ${this.nLoops} loops in menu (2+ desirable): ${stops} stops, ${Math.floor(100 * i / nChecks)}% done`; + this.update(msg); + } + } + return result; + } +} diff --git a/src/core/lib/Enigma.mjs b/src/core/lib/Enigma.mjs index 845ce413..9fc0f7d0 100644 --- a/src/core/lib/Enigma.mjs +++ b/src/core/lib/Enigma.mjs @@ -103,15 +103,17 @@ export class Rotor { if (!/^[A-Z]$/.test(initialPosition)) { throw new OperationError("Rotor initial position must be exactly one uppercase letter"); } - this.map = {}; - this.revMap = {}; + this.map = new Array(26).fill(); + this.revMap = new Array(26).fill(); + const uniq = {}; for (let i=0; i S\)/, + recipeConfig: [ + { + "op": "Bombe", + "args": [ + "BDFHJLCPRTXVZNYEIWGAKMUSQO G\)/, + recipeConfig: [ + { + "op": "Bombe", + "args": [ + "BDFHJLCPRTXVZNYEIWGAKMUSQO S\)/, + recipeConfig: [ + { + "op": "Bombe", + "args": [ + "BDFHJLCPRTXVZNYEIWGAKMUSQO