From 21335e7d05abad8dda036ac999e761b6392944e2 Mon Sep 17 00:00:00 2001 From: s2224834 <46319860+s2224834@users.noreply.github.com> Date: Fri, 11 Jan 2019 13:18:25 +0000 Subject: [PATCH] Bombe: Add checking machine --- src/core/lib/Bombe.mjs | 168 +++++++++++++++++------ src/core/lib/Enigma.mjs | 3 +- src/core/operations/Bombe.mjs | 12 +- src/core/operations/MultipleBombe.mjs | 13 +- tests/operations/tests/Bombe.mjs | 40 ++++-- tests/operations/tests/Enigma.mjs | 4 +- tests/operations/tests/MultipleBombe.mjs | 4 +- 7 files changed, 178 insertions(+), 66 deletions(-) diff --git a/src/core/lib/Bombe.mjs b/src/core/lib/Bombe.mjs index 7400c98a..4ae0ff7f 100644 --- a/src/core/lib/Bombe.mjs +++ b/src/core/lib/Bombe.mjs @@ -8,7 +8,7 @@ import OperationError from "../errors/OperationError"; import Utils from "../Utils"; -import {Rotor, a2i, i2a} from "./Enigma"; +import {Rotor, Plugboard, a2i, i2a} from "./Enigma"; /** * Convenience/optimisation subclass of Rotor @@ -302,7 +302,7 @@ export class BombeMachine { * @param {string} crib - Known plaintext for this ciphertext * @param {function} update - Function to call to send status updates (optional) */ - constructor(rotors, reflector, ciphertext, crib, update=undefined) { + constructor(rotors, reflector, ciphertext, crib, check, update=undefined) { if (ciphertext.length < crib.length) { throw new OperationError("Crib overruns supplied ciphertext"); } @@ -324,6 +324,7 @@ export class BombeMachine { this.ciphertext = ciphertext; this.crib = crib; this.initRotors(rotors); + this.check = check; this.updateFn = update; const [mostConnected, edges] = this.makeMenu(); @@ -507,7 +508,6 @@ export class BombeMachine { 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. @@ -564,16 +564,131 @@ export class BombeMachine { 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 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 + let count = 0; + for (let j=26*this.testRegister; j<26*(1+this.testRegister); j++) { + if (this.wires[j]) { + count++; + } + } + 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 { + // If this happens a lot it implies the menu isn't good enough. We can't do + // anything useful with it as we don't have a stecker partner, so we'll just drop it + // and move on. This does risk eating the actual stop occasionally, but I've only seen + // this happen when the menu is bad enough we have thousands of stops, so I'm not sure + // it matters. + return undefined; + } + 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 @@ -592,45 +707,12 @@ export class BombeMachine { } // 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 = [this.testRegister, j]; - break; - } - } - } else if (count === 1) { - // This means our hypothesis for the steckering is correct. - stecker = [this.testRegister, 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 = undefined; - } - const testDecrypt = this.tryDecrypt(stecker); - let steckerStr; - if (stecker !== undefined) { - steckerStr = `${i2a(stecker[0])}${i2a(stecker[1])}`; - } else { - steckerStr = `?? (wire count: ${count})`; - } - result.push([this.indicator.getPos(), steckerStr, testDecrypt]); + + 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 diff --git a/src/core/lib/Enigma.mjs b/src/core/lib/Enigma.mjs index 0a083bce..6b6c4d63 100644 --- a/src/core/lib/Enigma.mjs +++ b/src/core/lib/Enigma.mjs @@ -182,7 +182,8 @@ class PairMapBase { } const a = a2i(pair[0]), b = a2i(pair[1]); if (a === b) { - throw new OperationError(`${name}: cannot connect ${pair[0]} to itself`); + // self-stecker + return; } if (this.map.hasOwnProperty(a)) { throw new OperationError(`${name} connects ${pair[0]} more than once`); diff --git a/src/core/operations/Bombe.mjs b/src/core/operations/Bombe.mjs index 9ddd4b7b..292017e8 100644 --- a/src/core/operations/Bombe.mjs +++ b/src/core/operations/Bombe.mjs @@ -23,7 +23,7 @@ class Bombe extends Operation { this.name = "Bombe"; this.module = "Default"; - this.description = "Emulation of the Bombe machine used to attack Enigma.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and one plugboard pair.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops, a large number of incorrect outputs will be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor."; + this.description = "Emulation of the Bombe machine used to attack Enigma.

To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.

Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops, a large number of incorrect outputs will be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.

Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.

By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine."; this.infoURL = "https://wikipedia.org/wiki/Bombe"; this.inputType = "string"; this.outputType = "string"; @@ -66,6 +66,11 @@ class Bombe extends Operation { name: "Crib offset", type: "number", value: 0 + }, + { + name: "Use checking machine", + type: "boolean", + value: true } ]; } @@ -90,6 +95,7 @@ class Bombe extends Operation { const reflectorstr = args[4]; let crib = args[5]; const offset = args[6]; + const check = args[7]; const rotors = []; for (let i=0; i<4; i++) { if (i === 3 && args[i] === "") { @@ -120,9 +126,9 @@ class Bombe extends Operation { } else { update = undefined; } - const bombe = new BombeMachine(rotors, reflector, ciphertext, crib, update); + const bombe = new BombeMachine(rotors, reflector, ciphertext, crib, check, update); const result = bombe.run(); - let msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotor positions are listed left to right and start at the beginning of the crib, and ignore stepping and the ring setting. One stecker pair is determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; + let msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotor positions are listed left to right and start at the beginning of the crib, and ignore stepping and the ring setting. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; for (const [setting, stecker, decrypt] of result) { msg += `Stop: ${setting} (plugboard: ${stecker}): ${decrypt}\n`; } diff --git a/src/core/operations/MultipleBombe.mjs b/src/core/operations/MultipleBombe.mjs index 6bcd1051..4b3123a4 100644 --- a/src/core/operations/MultipleBombe.mjs +++ b/src/core/operations/MultipleBombe.mjs @@ -163,6 +163,11 @@ class MultipleBombe extends Operation { name: "Crib offset", type: "number", value: 0 + }, + { + name: "Use checking machine", + type: "boolean", + value: true } ]; } @@ -211,7 +216,7 @@ class MultipleBombe extends Operation { const reflectorsStr = args[5]; let crib = args[6]; const offset = args[7]; - // TODO got this far + const check = args[8]; const rotors = []; const fourthRotors = []; const reflectors = []; @@ -279,8 +284,8 @@ class MultipleBombe extends Operation { runRotors.push(rotor4); } if (bombe === undefined) { - bombe = new BombeMachine(runRotors, reflector, ciphertext, crib); - msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. One stecker pair is determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; + bombe = new BombeMachine(runRotors, reflector, ciphertext, crib, check); + msg = `Bombe run on menu with ${bombe.nLoops} loops (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided. Results:\n`; } else { bombe.changeRotors(runRotors, reflector); } @@ -290,7 +295,7 @@ class MultipleBombe extends Operation { update(bombe.nLoops, nStops, nRuns / totalRuns); } if (result.length > 0) { - msg += `Rotors: ${runRotors.join(", ")}\nReflector: ${reflector.pairs}\n`; + msg += `\nRotors: ${runRotors.join(", ")}\nReflector: ${reflector.pairs}\n`; for (const [setting, stecker, decrypt] of result) { msg += `Stop: ${setting} (plugboard: ${stecker}): ${decrypt}\n`; } diff --git a/tests/operations/tests/Bombe.mjs b/tests/operations/tests/Bombe.mjs index 0a8af93f..fca420d3 100644 --- a/tests/operations/tests/Bombe.mjs +++ b/tests/operations/tests/Bombe.mjs @@ -21,7 +21,7 @@ TestRegister.addTests([ "EKMFLGDQVZNTOWYHXUSPAIBRCJ