Bombe: review, tests, validation

This commit is contained in:
s2224834 2019-01-08 18:25:42 +00:00
parent b6eb4e84e4
commit ad6e30f3d4
5 changed files with 173 additions and 31 deletions

View File

@ -77,10 +77,7 @@ class Edge {
* @returns {number}
*/
getOther(node) {
if (this.node1 === node) {
return this.node2;
}
return this.node1;
return this.node1 === node ? this.node2 : this.node1;
}
}
@ -144,10 +141,7 @@ class Scrambler {
* @returns {number}
*/
getOtherEnd(end) {
if (this.end1 === end) {
return this.end2;
}
return this.end1;
return this.end1 === end ? this.end2 : this.end1;
}
/**
@ -194,8 +188,11 @@ export class BombeMachine {
* @param {function} update - Function to call to send status updates (optional)
*/
constructor(rotors, reflector, ciphertext, crib, update=undefined) {
if (ciphertext.length !== crib.length) {
throw new OperationError("Ciphertext and crib length differ");
if (ciphertext.length < crib.length) {
throw new OperationError("Crib overruns supplied ciphertext");
}
if (ciphertext.length > 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
@ -226,7 +223,7 @@ export class BombeMachine {
// This is the bundle of wires corresponding to the 26 letters within each of the 26
// possible nodes in the menu
this.wires = new Array(26*26).fill(false);
this.wires = new Array(26*26);
// These are the pseudo-Engima devices corresponding to each edge in the menu, and the
// nodes in the menu they each connect to
@ -271,9 +268,9 @@ export class BombeMachine {
* If we have a way of sending status messages, do so.
* @param {string} msg - Message to send.
*/
update(msg) {
update(...msg) {
if (this.updateFn !== undefined) {
this.updateFn(msg);
this.updateFn(...msg);
}
}
@ -411,7 +408,10 @@ export class BombeMachine {
// For each possible rotor setting
const nChecks = Math.pow(26, this.baseRotors.length);
for (let i=1; i<=nChecks; i++) {
this.wires.fill(false);
// Benchmarking suggests this is faster than using .fill()
for (let i=0; i<this.wires.length; i++) {
this.wires[i] = false;
}
// Energise the test input, follow the current through each scrambler
// (and the diagonal board)
this.energiseCount = 0;
@ -462,9 +462,9 @@ export class BombeMachine {
scrambler.step(n);
}
// Send status messages at what seems to be a reasonably sensible frequency
if (n > 2) {
const msg = `Bombe run with ${this.nLoops} loops in menu (2+ desirable): ${stops} stops, ${Math.floor(100 * i / nChecks)}% done`;
this.update(msg);
// (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;

View File

@ -103,8 +103,8 @@ export class Rotor {
if (!/^[A-Z]$/.test(initialPosition)) {
throw new OperationError("Rotor initial position must be exactly one uppercase letter");
}
this.map = new Array(26).fill();
this.revMap = new Array(26).fill();
this.map = new Array(26);
this.revMap = new Array(26);
const uniq = {};
for (let i=0; i<LETTERS.length; i++) {
const a = a2i(LETTERS[i]);
@ -235,7 +235,7 @@ export class Reflector extends PairMapBase {
if (s !== 26) {
throw new OperationError("Reflector must have exactly 13 pairs covering every letter");
}
const optMap = new Array(26).fill();
const optMap = new Array(26);
for (const x of Object.keys(this.map)) {
optMap[x] = this.map[x];
}

View File

@ -23,7 +23,7 @@ class Bombe extends Operation {
this.name = "Bombe";
this.module = "Default";
this.description = "";
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.infoURL = "https://wikipedia.org/wiki/Bombe";
this.inputType = "string";
this.outputType = "string";
@ -63,13 +63,24 @@ class Bombe extends Operation {
value: ""
},
{
name: "Offset",
name: "Crib offset",
type: "number",
value: 0
}
];
}
/**
* Format and send a status update message.
* @param {number} nLoops - Number of loops in the menu
* @param {number} nStops - How many stops so far
* @param {number} progress - Progress (as a float in the range 0..1)
*/
updateStatus(nLoops, nStops, progress) {
const msg = `Bombe run with ${nLoops} loops in menu (2+ desirable): ${nStops} stops, ${Math.floor(100 * progress)}% done`;
self.sendStatusMessage(msg);
}
/**
* @param {string} input
* @param {Object[]} args
@ -77,7 +88,7 @@ class Bombe extends Operation {
*/
run(input, args) {
const reflectorstr = args[4];
const crib = args[5];
let crib = args[5];
const offset = args[6];
const rotors = [];
for (let i=0; i<4; i++) {
@ -95,14 +106,18 @@ class Bombe extends Operation {
if (crib.length === 0) {
throw new OperationError("Crib cannot be empty");
}
input = input.replace(/[^A-Za-z]/g, "");
if (offset < 0) {
throw new OperationError("Offset cannot be negative");
}
// For symmetry with the Enigma op, for the input we'll just remove all invalid characters
input = input.replace(/[^A-Za-z]/g, "").toUpperCase();
crib = crib.replace(/[^A-Za-z]/g, "").toUpperCase();
const ciphertext = input.slice(offset, offset+crib.length);
const reflector = new Reflector(reflectorstr);
let update;
try {
update = self.sendStatusMessage;
} catch (e) {
// Happens when running headless
if (ENVIRONMENT_IS_WORKER()) {
update = this.updateStatus;
} else {
update = undefined;
}
const bombe = new BombeMachine(rotors, reflector, ciphertext, crib, update);

View File

@ -45,7 +45,7 @@ class Enigma extends Operation {
value: LETTERS
},
{
name: "2nd rotor",
name: "2nd (middle) rotor",
type: "editableOption",
value: ROTORS,
defaultIndex: 1
@ -61,7 +61,7 @@ class Enigma extends Operation {
value: LETTERS
},
{
name: "3rd rotor",
name: "3rd (left-hand) rotor",
type: "editableOption",
value: ROTORS,
defaultIndex: 0
@ -77,7 +77,7 @@ class Enigma extends Operation {
value: LETTERS
},
{
name: "4th rotor",
name: "4th (left-most, only some models) rotor",
type: "editableOption",
value: ROTORS_OPTIONAL,
defaultIndex: 10

View File

@ -43,6 +43,24 @@ TestRegister.addTests([
}
]
},
{
name: "Bombe: crib offset",
input: "AAABBYFLTHHYIJQAYBBYS", // first three chars here are faked
expectedMatch: /LGA \(S <-> S\)/,
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
"THISISATESTMESSAGE", 3,
]
}
]
},
/*
* Long test is long
{
@ -64,4 +82,113 @@ TestRegister.addTests([
]
},
*/
{
name: "Bombe: no crib",
input: "JBYALIHDYNUAAVKBYM",
expectedMatch: /Crib cannot be empty/,
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
"", 0,
]
}
]
},
{
name: "Bombe: short crib",
input: "JBYALIHDYNUAAVKBYM",
expectedMatch: /Crib is too short/,
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
"A", 0,
]
}
]
},
{
name: "Bombe: invalid crib",
input: "JBYALIHDYNUAAVKBYM",
expectedMatch: /Invalid crib: .* in both ciphertext and crib/,
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
"AAAAAAAA", 0,
]
}
]
},
{
name: "Bombe: long crib",
input: "JBYALIHDYNUAAVKBYM",
expectedMatch: /Crib overruns supplied ciphertext/,
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
"CCCCCCCCCCCCCCCCCCCCCC", 0,
]
}
]
},
{
name: "Bombe: really long crib",
input: "BBBBBBBBBBBBBBBBBBBBBBBBBB",
expectedMatch: /Crib is too long/,
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
"AAAAAAAAAAAAAAAAAAAAAAAAAA", 0,
]
}
]
},
{
name: "Bombe: negative offset",
input: "AAAAA",
expectedMatch: /Offset cannot be negative/,
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
"BBBBB", -1,
]
}
]
},
// Enigma tests cover validation of rotors and reflector
]);