Add Enigma operation

This commit is contained in:
s2224834 2019-01-03 16:36:56 +00:00
parent c82971f8db
commit 088864fd9c
6 changed files with 1071 additions and 1 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ docs/*
!docs/*.conf.json
!docs/*.ico
.vscode
.*.swp
src/core/config/modules/*
src/core/config/OperationConfig.json
src/core/operations/index.mjs

View File

@ -102,7 +102,8 @@
"JWT Decode",
"Citrix CTX1 Encode",
"Citrix CTX1 Decode",
"Pseudo-Random Number Generator"
"Pseudo-Random Number Generator",
"Enigma"
]
},
{

349
src/core/lib/Enigma.mjs Normal file
View File

@ -0,0 +1,349 @@
/**
* Emulation of the Enigma machine.
*
* @author s2224834
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError";
import Utils from "../Utils";
/**
* Provided default Enigma rotor set.
* These are specified as a list of mappings from the letters A through Z in order, optionally
* followed by < and a list of letters at which the rotor steps.
*/
export const ROTORS = [
{name: "I", value: "EKMFLGDQVZNTOWYHXUSPAIBRCJ<R"},
{name: "II", value: "AJDKSIRUXBLHWTMCQGZNPYFVOE<F"},
{name: "III", value: "BDFHJLCPRTXVZNYEIWGAKMUSQO<W"},
{name: "IV", value: "ESOVPZJAYQUIRHXLNFTGKDCMWB<K"},
{name: "V", value: "VZBRGITYUPSDNHLXAWMJQOFECK<A"},
{name: "VI", value: "JPGVOUMFYQBENHZRDKASXLICTW<AN"},
{name: "VII", value: "NZJHGRCXMYSWBOUFAIVLPEKQDT<AN"},
{name: "VIII", value: "FKQHTLXOCBJSPDZRAMEWNIUYGV<AN"},
{name: "Beta", value: "LEYJVCNIXWPBQMDRTAKZGFUHOS"},
{name: "Gamma", value: "FSOKANUERHMBTIYCWLQPZXVGJD"},
];
export const ROTORS_OPTIONAL = [].concat(ROTORS).concat([
{name: "None", value: ""},
]);
/**
* Provided default Enigma reflector set.
* These are specified as 13 space-separated transposed pairs covering every letter.
*/
export const REFLECTORS = [
{name: "B", value: "AY BR CU DH EQ FS GL IP JX KN MO TZ VW"},
{name: "C", value: "AF BV CP DJ EI GO HY KR LZ MX NW TQ SU"},
{name: "B Thin", value: "AE BN CK DQ FU GY HW IJ LO MP RX SZ TV"},
{name: "C Thin", value: "AR BD CO EJ FN GT HK IV LM PW QZ SX UY"},
];
export const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
/**
* Map a letter to a number in 0..25.
*
* @param {char} c
* @param {boolean} permissive - Case insensitive; don't throw errors on other chars.
* @returns {number}
*/
export function a2i(c, permissive=false) {
const i = Utils.ord(c);
if (i >= 65 && i <= 90) {
return i - 65;
}
if (permissive) {
// Allow case insensitivity
if (i >= 97 && i <= 122) {
return i - 97;
}
return -1;
}
throw new OperationError("a2i called on non-uppercase ASCII character");
}
/**
* Map a number in 0..25 to a letter.
*
* @param {number} i
* @returns {char}
*/
export function i2a(i) {
if (i >= 0 && i < 26) {
return Utils.chr(i+65);
}
throw new OperationError("i2a called on value outside 0..25");
}
/**
* A rotor in the Enigma machine.
*/
export class Rotor {
/**
* Rotor constructor.
*
* @param {string} wiring - A 26 character string of the wiring order.
* @param {string} steps - A 0..26 character string of stepping points.
* @param {char} ringSetting - The ring setting.
* @param {char} initialPosition - The initial position of the rotor.
*/
constructor(wiring, steps, ringSetting, initialPosition) {
if (!/^[A-Z]{26}$/.test(wiring)) {
throw new OperationError("Rotor wiring must be 26 unique uppercase letters");
}
if (!/^[A-Z]{0,26}$/.test(steps)) {
throw new OperationError("Rotor steps must be 0-26 unique uppercase letters");
}
if (!/^[A-Z]$/.test(ringSetting)) {
throw new OperationError("Rotor ring setting must be exactly one uppercase letter");
}
if (!/^[A-Z]$/.test(initialPosition)) {
throw new OperationError("Rotor initial position must be exactly one uppercase letter");
}
this.map = {};
this.revMap = {};
for (let i=0; i<LETTERS.length; i++) {
const a = a2i(LETTERS[i]);
const b = a2i(wiring[i]);
this.map[a] = b;
this.revMap[b] = a;
}
if (Object.keys(this.revMap).length !== LETTERS.length) {
throw new OperationError("Rotor wiring must have each letter exactly once");
}
const rs = a2i(ringSetting);
this.steps = new Set();
for (const x of steps) {
this.steps.add(Utils.mod(a2i(x) - rs, 26));
}
if (this.steps.size !== steps.length) {
// This isn't strictly fatal, but it's probably a mistake
throw new OperationError("Rotor steps must be unique");
}
this.pos = Utils.mod(a2i(initialPosition) - rs, 26);
}
/**
* Step the rotor forward by one.
*/
step() {
this.pos = Utils.mod(this.pos + 1, 26);
return this.pos;
}
/**
* Transform a character through this rotor forwards.
*
* @param {number} c - The character.
* @returns {number}
*/
transform(c) {
return Utils.mod(this.map[Utils.mod(c + this.pos, 26)] - this.pos, 26);
}
/**
* Transform a character through this rotor backwards.
*
* @param {number} c - The character.
* @returns {number}
*/
revTransform(c) {
return Utils.mod(this.revMap[Utils.mod(c + this.pos, 26)] - this.pos, 26);
}
}
/**
* Base class for plugboard and reflector (since these do effectively the same
* thing).
*/
class PairMapBase {
/**
* PairMapBase constructor.
*
* @param {string} pairs - A whitespace separated string of letter pairs to swap.
* @param {string} [name='PairMapBase'] - For errors, the name of this object.
*/
constructor(pairs, name="PairMapBase") {
// I've chosen to make whitespace significant here to make a) code and
// b) inputs easier to read
this.map = {};
if (pairs === "") {
return;
}
pairs.split(/\s+/).forEach(pair => {
if (!/^[A-Z]{2}$/.test(pair)) {
throw new OperationError(name + " must be a whitespace-separated list of uppercase letter pairs");
}
const a = a2i(pair[0]), b = a2i(pair[1]);
if (a === b) {
throw new OperationError(`${name}: cannot connect ${pair[0]} to itself`);
}
if (this.map.hasOwnProperty(a)) {
throw new OperationError(`${name} connects ${pair[0]} more than once`);
}
if (this.map.hasOwnProperty(b)) {
throw new OperationError(`${name} connects ${pair[1]} more than once`);
}
this.map[a] = b;
this.map[b] = a;
});
}
/**
* Transform a character through this object.
* Returns other characters unchanged.
*
* @param {number} c - The character.
* @returns {number}
*/
transform(c) {
if (!this.map.hasOwnProperty(c)) {
return c;
}
return this.map[c];
}
/**
* Alias for transform, to allow interchangeable use with rotors.
*
* @param {number} c - The character.
* @returns {number}
*/
revTransform(c) {
return this.transform(c);
}
}
/**
* Reflector. PairMapBase but requires that all characters are accounted for.
*/
export class Reflector extends PairMapBase {
/**
* Reflector constructor. See PairMapBase.
* Additional restriction: every character must be accounted for.
*/
constructor(pairs) {
super(pairs, "Reflector");
const s = Object.keys(this.map).length;
if (s !== 26) {
throw new OperationError("Reflector must have exactly 13 pairs covering every letter");
}
}
}
/**
* Plugboard. Unmodified PairMapBase.
*/
export class Plugboard extends PairMapBase {
/**
* Plugboard constructor. See PairMapbase.
*/
constructor(pairs) {
super(pairs, "Plugboard");
}
}
/**
* Base class for the Enigma machine itself. Holds rotors, a reflector, and a plugboard.
*/
export class EnigmaBase {
/**
* EnigmaBase constructor.
*
* @param {Object[]} rotors - List of Rotors.
* @param {Object} reflector - A Reflector.
* @param {Plugboard} plugboard - A Plugboard.
*/
constructor(rotors, reflector, plugboard) {
this.rotors = rotors;
this.rotorsRev = [].concat(rotors).reverse();
this.reflector = reflector;
this.plugboard = plugboard;
}
/**
* Step the rotors forward by one.
*
* This happens before the output character is generated.
*
* Note that rotor 4, if it's there, never steps.
*
* Why is all the logic in EnigmaBase and not a nice neat method on
* Rotor that knows when it should advance the next item?
* Because the double stepping anomaly is a thing. tl;dr if the left rotor
* should step the next time the middle rotor steps, the middle rotor will
* immediately step.
*/
step() {
const r0 = this.rotors[0];
const r1 = this.rotors[1];
r0.step();
// The second test here is the double-stepping anomaly
if (r0.steps.has(r0.pos) || r1.steps.has(Utils.mod(r1.pos + 1, 26))) {
r1.step();
if (r1.steps.has(r1.pos)) {
const r2 = this.rotors[2];
r2.step();
}
}
}
/**
* Encrypt (or decrypt) some data.
* Takes an arbitrary string and runs the Engima machine on that data from
* *its current state*, and outputs the result. Non-alphabetic characters
* are returned unchanged.
*
* @param {string} input - Data to encrypt.
* @returns {string}
*/
crypt(input) {
let result = "";
for (const c of input) {
let letter = a2i(c, true);
if (letter === -1) {
result += c;
continue;
}
// First, step the rotors forward.
this.step();
// Now, run through the plugboard.
letter = this.plugboard.transform(letter);
// Then through each wheel in sequence, through the reflector, and
// backwards through the wheels again.
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);
}
// Finally, back through the plugboard.
letter = this.plugboard.revTransform(letter);
result += i2a(letter);
}
return result;
}
}
/**
* The Enigma machine itself. Holds 3-4 rotors, a reflector, and a plugboard.
*/
export class EnigmaMachine extends EnigmaBase {
/**
* EnigmaMachine constructor.
*
* @param {Object[]} rotors - List of Rotors.
* @param {Object} reflector - A Reflector.
* @param {Plugboard} plugboard - A Plugboard.
*/
constructor(rotors, reflector, plugboard) {
super(rotors, reflector, plugboard);
if (rotors.length !== 3 && rotors.length !== 4) {
throw new OperationError("Enigma must have 3 or 4 rotors");
}
}
}

View File

@ -0,0 +1,200 @@
/**
* Emulation of the Enigma machine.
*
* @author s2224834
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import * as Enigma from "../lib/Enigma";
/**
* Enigma operation
*/
class EnigmaOp extends Operation {
/**
* Enigma constructor
*/
constructor() {
super();
this.name = "Enigma";
this.module = "Default";
this.description = "Encipher/decipher with the WW2 Enigma machine.<br><br>The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. <code>AB CD EF</code> connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by <code>&lt;</code> then a list of stepping points.<br>This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses the thin reflectors and the beta or gamma rotor in the 4th slot).";
this.infoURL = "https://wikipedia.org/wiki/Enigma_machine";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "1st (right-hand) rotor",
type: "editableOption",
value: Enigma.ROTORS,
// Default config is the rotors I-III *left to right*
defaultIndex: 2
},
{
name: "1st rotor ring setting",
type: "option",
value: Enigma.LETTERS
},
{
name: "1st rotor initial value",
type: "option",
value: Enigma.LETTERS
},
{
name: "2nd rotor",
type: "editableOption",
value: Enigma.ROTORS,
defaultIndex: 1
},
{
name: "2nd rotor ring setting",
type: "option",
value: Enigma.LETTERS
},
{
name: "2nd rotor initial value",
type: "option",
value: Enigma.LETTERS
},
{
name: "3rd rotor",
type: "editableOption",
value: Enigma.ROTORS,
defaultIndex: 0
},
{
name: "3rd rotor ring setting",
type: "option",
value: Enigma.LETTERS
},
{
name: "3rd rotor initial value",
type: "option",
value: Enigma.LETTERS
},
{
name: "4th rotor",
type: "editableOption",
value: Enigma.ROTORS_OPTIONAL,
defaultIndex: 10
},
{
name: "4th rotor initial value",
type: "option",
value: Enigma.LETTERS
},
{
name: "Reflector",
type: "editableOption",
value: Enigma.REFLECTORS
},
{
name: "Plugboard",
type: "string",
value: ""
},
{
name: "Strict output",
hint: "Remove non-alphabet letters and group output",
type: "boolean",
value: true
},
];
}
/**
* Helper - for ease of use rotors are specified as a single string; this
* method breaks the spec string into wiring and steps parts.
*
* @param {string} rotor - Rotor specification string.
* @param {number} i - For error messages, the number of this rotor.
* @returns {string[]}
*/
parseRotorStr(rotor, i) {
if (rotor === "") {
throw new OperationError(`Rotor ${i} must be provided.`);
}
if (!rotor.includes("<")) {
return [rotor, ""];
}
return rotor.split("<", 2);
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [
rotor1str, rotor1ring, rotor1pos,
rotor2str, rotor2ring, rotor2pos,
rotor3str, rotor3ring, rotor3pos,
rotor4str, rotor4pos,
reflectorstr, plugboardstr,
removeOther
] = args;
const rotors = [];
const [rotor1wiring, rotor1steps] = this.parseRotorStr(rotor1str, 1);
rotors.push(new Enigma.Rotor(rotor1wiring, rotor1steps, rotor1ring, rotor1pos));
const [rotor2wiring, rotor2steps] = this.parseRotorStr(rotor2str, 2);
rotors.push(new Enigma.Rotor(rotor2wiring, rotor2steps, rotor2ring, rotor2pos));
const [rotor3wiring, rotor3steps] = this.parseRotorStr(rotor3str, 3);
rotors.push(new Enigma.Rotor(rotor3wiring, rotor3steps, rotor3ring, rotor3pos));
if (rotor4str !== "") {
// Fourth rotor doesn't have a ring setting - A is equivalent to no setting
const [rotor4wiring, rotor4steps] = this.parseRotorStr(rotor4str, 4);
rotors.push(new Enigma.Rotor(rotor4wiring, rotor4steps, "A", rotor4pos));
}
const reflector = new Enigma.Reflector(reflectorstr);
const plugboard = new Enigma.Plugboard(plugboardstr);
if (removeOther) {
input = input.replace(/[^A-Za-z]/g, "");
}
const enigma = new Enigma.EnigmaMachine(rotors, reflector, plugboard);
let result = enigma.crypt(input);
if (removeOther) {
// Five character cipher groups is traditional
result = result.replace(/([A-Z]{5})(?!$)/g, "$1 ");
}
return result;
}
/**
* Highlight Enigma
* This is only possible if we're passing through non-alphabet characters.
*
* @param {Object[]} pos
* @param {number} pos[].start
* @param {number} pos[].end
* @param {Object[]} args
* @returns {Object[]} pos
*/
highlight(pos, args) {
if (args[13] === false) {
return pos;
}
}
/**
* Highlight Enigma in reverse
*
* @param {Object[]} pos
* @param {number} pos[].start
* @param {number} pos[].end
* @param {Object[]} args
* @returns {Object[]} pos
*/
highlightReverse(pos, args) {
if (args[13] === false) {
return pos;
}
}
}
export default EnigmaOp;

View File

@ -82,6 +82,7 @@ import "./tests/TranslateDateTimeFormat";
import "./tests/Magic";
import "./tests/ParseTLV";
import "./tests/Media";
import "./tests/Enigma";
// Cannot test operations that use the File type yet
//import "./tests/SplitColourChannels";

View File

@ -0,0 +1,518 @@
/**
* Enigma machine tests.
* @author s2224834
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import TestRegister from "../TestRegister";
TestRegister.addTests([
{
// Simplest test: A single keypress in the default position on a basic
// Enigma.
name: "Enigma: basic wiring",
input: "G",
expectedOutput: "P",
recipeConfig: [
{
"op": "Enigma",
"args": [
// Note: start on Z because it steps when the key is pressed
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "Z", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
""
]
}
]
},
{
// Rotor position test: single keypress, basic rotors, random start
// positions, no advancement of other rotors.
name: "Enigma: rotor position",
input: "A",
expectedOutput: "T",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "W",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "F",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "N",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
}
]
},
{
// Rotor ring setting test: single keypress, basic rotors, one rotor
// ring offset by one, basic start position, no advancement of other
// rotors.
name: "Enigma: rotor ring setting",
input: "A",
expectedOutput: "O",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "B", "Z",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
}
]
},
{
// Rotor ring setting test: single keypress, basic rotors, random ring
// settings, basic start position, no advancement of other rotors.
name: "Enigma: rotor ring setting 2",
input: "A",
expectedOutput: "F",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "W", "Z",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "F", "A",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "N", "A",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
}
]
},
{
// Stepping: basic configuration, enough input to cause middle rotor to
// step
name: "Enigma: stepping",
input: "AAAAA AAAAA AAAAA AAAAA AAAAA A",
expectedOutput: "UBDZG OWCXL TKSBT MCDLP BMUQO F",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "Z",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
}
]
},
{
// Ensure that we can decrypt an encrypted message.
name: "Enigma: reflectivity",
input: "AAAAA AAAAA AAAAA AAAAA AAAAA A",
expectedOutput: "AAAAA AAAAA AAAAA AAAAA AAAAA A",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "Z",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
},
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "Z",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
}
]
},
{
// Stepping: with rotors set so we're about to trigger the double step
// anomaly
name: "Enigma: double step anomaly",
input: "AAAAA",
expectedOutput: "EQIBM",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "U",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "D",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
}
]
},
{
// Stepping: with rotors set so we're about to trigger the double step
// anomaly
name: "Enigma: double step anomaly 2",
input: "AAAA",
expectedOutput: "BRNC",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "U",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "E",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
}
]
},
{
// Stepping: with rotors set so we're about to trigger the double step
// anomaly
name: "Enigma: double step anomaly 3",
input: "AAAAA AAA",
expectedOutput: "ZEEQI BMG",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "S",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "D",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
}
]
},
{
// Stepping: with a ring setting
name: "Enigma: ring setting stepping",
input: "AAAAA AAAAA AAAAA AAAAA AAAAA A",
expectedOutput: "PBMFE BOUBD ZGOWC XLTKS BTXSH I",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "H", "Z",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
}
]
},
{
// Stepping: with a ring setting and double step
name: "Enigma: ring setting double step",
input: "AAAAA AAAAA AAAAA AAAAA AAAAA A",
expectedOutput: "TEVFK UTIIW EDWVI JPMVP GDEZS P",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "H", "F",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "C", "D",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "Q", "A",
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW",
""
]
}
]
},
{
// Four-rotor Enigma, random settings, no plugboard
name: "Enigma: four rotor",
input: "AAAAA AAAAA AAAAA AAAAA AAAAA A",
expectedOutput: "GZXGX QUSUW JPWVI GVBTU DQZNZ J",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "D", "Q",
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "P", "F",
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "O", "E",
"LEYJVCNIXWPBQMDRTAKZGFUHOS", "X", // Beta
"AE BN CK DQ FU GY HW IJ LO MP RX SZ TV", // B thin
""
]
}
]
},
{
// Four-rotor Enigma, different wheel set, no plugboard
name: "Enigma: four rotor 2",
input: "AAAAA AAAAA AAAAA AAAAA AAAAA A",
expectedOutput: "HZJLP IKWBZ XNCWF FIHWL EROOZ C",
recipeConfig: [
{
"op": "Enigma",
"args": [
"ESOVPZJAYQUIRHXLNFTGKDCMWB<K", "W", "U", // IV
"VZBRGITYUPSDNHLXAWMJQOFECK<A", "M", "G", // V
"JPGVOUMFYQBENHZRDKASXLICTW<AN", "A", "J", // VI
"FSOKANUERHMBTIYCWLQPZXVGJD", "L", // Gamma
"AR BD CO EJ FN GT HK IV LM PW QZ SX UY", // C thin
""
]
}
]
},
{
// Four-rotor Enigma, different wheel set, random plugboard
name: "Enigma: plugboard",
input: "AAAAA AAAAA AAAAA AAAAA AAAAA A",
expectedOutput: "GHLIM OJIUW DKLWM JGNJK DYJVD K",
recipeConfig: [
{
"op": "Enigma",
"args": [
"FKQHTLXOCBJSPDZRAMEWNIUYGV<AN", "U", "Z", // VIII
"ESOVPZJAYQUIRHXLNFTGKDCMWB<K", "O", "O", // IV
"NZJHGRCXMYSWBOUFAIVLPEKQDT<AN", "I", "V", // VII
"FSOKANUERHMBTIYCWLQPZXVGJD", "I", // Gamma
"AE BN CK DQ FU GY HW IJ LO MP RX SZ TV", // B thin
"WN MJ LX YB FP QD US IH CE GR"
]
}
]
},
{
// Decryption test on above input
name: "Enigma: decryption",
input: "GHLIM OJIUW DKLWM JGNJK DYJVD K",
expectedOutput: "AAAAA AAAAA AAAAA AAAAA AAAAA A",
recipeConfig: [
{
"op": "Enigma",
"args": [
"FKQHTLXOCBJSPDZRAMEWNIUYGV<AN", "U", "Z", // VIII
"ESOVPZJAYQUIRHXLNFTGKDCMWB<K", "O", "O", // IV
"NZJHGRCXMYSWBOUFAIVLPEKQDT<AN", "I", "V", // VII
"FSOKANUERHMBTIYCWLQPZXVGJD", "I", // Gamma
"AE BN CK DQ FU GY HW IJ LO MP RX SZ TV", // B thin
"WN MJ LX YB FP QD US IH CE GR"
]
}
]
},
{
// Non-alphabet characters drop test
name: "Enigma: non-alphabet drop",
input: "Hello, world. This is a test.",
expectedOutput: "ILBDA AMTAZ MORNZ DDIOT U",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"", true
]
}
]
},
{
// Non-alphabet characters passthrough test
name: "Enigma: non-alphabet passthrough",
input: "Hello, world. This is a test.",
expectedOutput: "ILBDA, AMTAZ. MORN ZD D IOTU.",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
"", false
]
}
]
},
{
name: "Enigma: rotor validation 1",
input: "Hello, world. This is a test.",
expectedOutput: "Rotor wiring must be 26 unique uppercase letters",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQ", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
""
]
}
]
},
{
name: "Enigma: rotor validation 2",
input: "Hello, world. This is a test.",
expectedOutput: "Rotor wiring must be 26 unique uppercase letters",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQo", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
""
]
}
]
},
{
name: "Enigma: rotor validation 3",
input: "Hello, world. This is a test.",
expectedOutput: "Rotor wiring must have each letter exactly once",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQA", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
""
]
}
]
},
{
name: "Enigma: rotor validation 4",
input: "Hello, world. This is a test.",
expectedOutput: "Rotor steps must be unique",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<RR", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
""
]
}
]
},
{
name: "Enigma: rotor validation 5",
input: "Hello, world. This is a test.",
expectedOutput: "Rotor steps must be 0-26 unique uppercase letters",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<a", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO TZ VW", // B
""
]
}
]
},
// The ring setting and positions are dropdowns in the interface so not
// gonna bother testing them
{
name: "Enigma: reflector validation 1",
input: "Hello, world. This is a test.",
expectedOutput: "Reflector must have exactly 13 pairs covering every letter",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AY BR CU DH EQ FS GL IP JX KN MO", // B
""
]
}
]
},
{
name: "Enigma: reflector validation 2",
input: "Hello, world. This is a test.",
expectedOutput: "Reflector: cannot connect A to itself",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AA BR CU DH EQ FS GL IP JX KN MO TZ", // B
""
]
}
]
},
{
name: "Enigma: reflector validation 3",
input: "Hello, world. This is a test.",
expectedOutput: "Reflector connects A more than once",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AY AR CU DH EQ FS GL IP JX KN MO TZ", // B
""
]
}
]
},
{
name: "Enigma: reflector validation 4",
input: "Hello, world. This is a test.",
expectedOutput: "Reflector must be a whitespace-separated list of uppercase letter pairs",
recipeConfig: [
{
"op": "Enigma",
"args": [
"BDFHJLCPRTXVZNYEIWGAKMUSQO<W", "A", "A", // III
"AJDKSIRUXBLHWTMCQGZNPYFVOE<F", "A", "A", // II
"EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", "A", "A", // I
"", "A",
"AYBR CU DH EQ FS GL IP JX KN MO TZ", // B
""
]
}
]
},
]);