Bombe: Add checking machine

This commit is contained in:
s2224834 2019-01-11 13:18:25 +00:00
parent 78768e00d4
commit 21335e7d05
7 changed files with 178 additions and 66 deletions

View file

@ -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<Math.min(26, this.ciphertext.length); i++) {
const t = this.indicator.transform(this.singleStecker(stecker, a2i(this.ciphertext[i])));
res.push(i2a(this.singleStecker(stecker, t)));
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;
}
// 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
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

View file

@ -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`);

View file

@ -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.<br><br>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.<br><br>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&lt;-&gt;C, B&lt;-&gt;A, and C&lt;-&gt;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.<br><br>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.<br><br>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.<br><br>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&lt;-&gt;C, B&lt;-&gt;A, and C&lt;-&gt;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.<br><br>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.<br><br>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`;
}

View file

@ -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`;
}

View file

@ -21,7 +21,7 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"THISISATESTMESSAGE", 0,
"THISISATESTMESSAGE", 0, false
]
}
]
@ -40,7 +40,7 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"THISISATESTMESSAGE", 0,
"THISISATESTMESSAGE", 0, false
]
}
]
@ -58,7 +58,7 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"THISISATESTMESSAGE", 3,
"THISISATESTMESSAGE", 3, false
]
}
]
@ -76,7 +76,25 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"THISISATESTM", 0,
"THISISATESTM", 0, false
]
}
]
},
{
name: "Bombe: checking machine",
input: "BBYFLTHHYIJQAYBBYS",
expectedMatch: /Stop: LGA \(plugboard: TT AG BO CL EK FF HH II JJ SS YY\): THISISATESTMESSAGE/,
recipeConfig: [
{
"op": "Bombe",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"THISISATESTM", 0, true
]
}
]
@ -95,7 +113,7 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"LEYJVCNIXWPBQMDRTAKZGFUHOS", // Beta
"AE BN CK DQ FU GY HW IJ LO MP RX SZ TV", // B thin
"THISISATESTMESSAGE", 0,
"THISISATESTMESSAGE", 0, false
]
}
]
@ -113,7 +131,7 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"", 0,
"", 0, false
]
}
]
@ -131,7 +149,7 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"A", 0,
"A", 0, false
]
}
]
@ -149,7 +167,7 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"AAAAAAAA", 0,
"AAAAAAAA", 0, false
]
}
]
@ -167,7 +185,7 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"CCCCCCCCCCCCCCCCCCCCCC", 0,
"CCCCCCCCCCCCCCCCCCCCCC", 0, false
]
}
]
@ -185,7 +203,7 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"AAAAAAAAAAAAAAAAAAAAAAAAAA", 0,
"AAAAAAAAAAAAAAAAAAAAAAAAAA", 0, false
]
}
]
@ -203,7 +221,7 @@ TestRegister.addTests([
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
"",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"BBBBB", -1,
"BBBBB", -1, false
]
}
]

View file

@ -483,7 +483,7 @@ TestRegister.addTests([
{
name: "Enigma: reflector validation 2",
input: "Hello, world. This is a test.",
expectedOutput: "Reflector: cannot connect A to itself",
expectedOutput: "Reflector must have exactly 13 pairs covering every letter",
recipeConfig: [
{
"op": "Enigma",
@ -492,7 +492,7 @@ TestRegister.addTests([
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A", "A",
"AA BR CU DH EQ FS GL IP JX KN MO TZ", // B
"AA BR CU DH EQ FS GL IP JX KN MO TZ VV WY", // B
""
]
}

View file

@ -19,7 +19,7 @@ TestRegister.addTests([
"User defined", "EKMFLGDQVZNTOWYHXUSPAIBRCJ<R\nAJDKSIRUXBLHWTMCQGZNPYFVOE<F\nBDFHJLCPRTXVZNYEIWGAKMUSQO<W",
"User defined", "",
"User defined", "AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"THISISATESTMESSAGE", 0,
"THISISATESTMESSAGE", 0, false
]
}
]
@ -38,7 +38,7 @@ TestRegister.addTests([
"User defined", "EKMFLGDQVZNTOWYHXUSPAIBRCJ<R\nAJDKSIRUXBLHWTMCQGZNPYFVOE<F\nBDFHJLCPRTXVZNYEIWGAKMUSQO<W",
"User defined", "LEYJVCNIXWPBQMDRTAKZGFUHOS", // Beta
"User defined", "AE BN CK DQ FU GY HW IJ LO MP RX SZ TV", // B thin
"THISISATESTMESSAGE", 0,
"THISISATESTMESSAGE", 0, false
]
}
]