Bring up to date with master

This commit is contained in:
j433866 2019-07-02 14:23:17 +01:00
commit f473807459
86 changed files with 10685 additions and 4298 deletions

756
src/core/lib/Bombe.mjs Normal file
View file

@ -0,0 +1,756 @@
/**
* Emulation of the Bombe machine.
*
* @author s2224834
* @author The National Museum of Computing - Bombe Rebuild Project
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError";
import Utils from "../Utils";
import {Rotor, Plugboard, 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.
* @returns {Object}
*/
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) {
return this.node1 === node ? this.node2 : this.node1;
}
}
/**
* As all the Bombe's rotors move in step, at any given point the vast majority of the scramblers
* in the machine share the majority of their state, which is hosted in this class.
*/
class SharedScrambler {
/**
* SharedScrambler constructor.
* @param {Object[]} rotors - List of rotors in the shared state _only_.
* @param {Object} reflector - The reflector in use.
*/
constructor(rotors, reflector) {
this.lowerCache = new Array(26);
this.higherCache = new Array(26);
for (let i=0; i<26; i++) {
this.higherCache[i] = new Array(26);
}
this.changeRotors(rotors, reflector);
}
/**
* Replace the rotors and reflector in this SharedScrambler.
* This takes care of flushing caches as well.
* @param {Object[]} rotors - List of rotors in the shared state _only_.
* @param {Object} reflector - The reflector in use.
*/
changeRotors(rotors, reflector) {
this.reflector = reflector;
this.rotors = rotors;
this.rotorsRev = [].concat(rotors).reverse();
this.cacheGen();
}
/**
* Step the rotors forward.
* @param {number} n - How many rotors to step. This includes the rotors which are not part of
* the shared state, so should be 2 or more.
*/
step(n) {
for (let i=0; i<n-1; i++) {
this.rotors[i].step();
}
this.cacheGen();
}
/**
* Optimisation: We pregenerate all routes through the machine with the top rotor removed,
* as these rarely change. This saves a lot of lookups. This function generates this route
* table.
* We also just-in-time cache the full routes through the scramblers, because after stepping
* the fast rotor some scramblers will be in states occupied by other scrambles on previous
* iterations.
*/
cacheGen() {
for (let i=0; i<26; i++) {
this.lowerCache[i] = undefined;
for (let j=0; j<26; j++) {
this.higherCache[i][j] = undefined;
}
}
for (let i=0; i<26; i++) {
if (this.lowerCache[i] !== undefined) {
continue;
}
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);
}
// By symmetry
this.lowerCache[i] = letter;
this.lowerCache[letter] = i;
}
}
/**
* Map a letter through this (partial) scrambler.
* @param {number} i - The letter
* @returns {number}
*/
transform(i) {
return this.lowerCache[i];
}
}
/**
* 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} base - The SharedScrambler whose state this scrambler uses
* @param {Object} rotor - The non-shared fast rotor in this scrambler
* @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(base, rotor, pos, end1, end2) {
this.baseScrambler = base;
this.initialPos = pos;
this.changeRotor(rotor);
this.end1 = end1;
this.end2 = end2;
// For efficiency reasons, we pull the relevant shared cache from the baseScrambler into
// this object - this saves us a few pointer dereferences
this.cache = this.baseScrambler.higherCache[pos];
}
/**
* Replace the rotor in this scrambler.
* The position is reset automatically.
* @param {Object} rotor - New rotor
*/
changeRotor(rotor) {
this.rotor = rotor;
this.rotor.pos += this.initialPos;
}
/**
* Step the rotor forward.
*
* The base SharedScrambler needs to be instructed to step separately.
*/
step() {
// The Bombe steps the slowest rotor on an actual Enigma fastest, for reasons.
// ...but for optimisation reasons I'm going to cheat and not do that, as this vastly
// simplifies caching the state of the majority of the scramblers. The results are the
// same, just in a slightly different order.
this.rotor.step();
this.cache = this.baseScrambler.higherCache[this.rotor.pos];
}
/**
* Run a letter through the scrambler.
* @param {number} i - The letter to transform (as a number)
* @returns {number}
*/
transform(i) {
let letter = i;
const cached = this.cache[i];
if (cached !== undefined) {
return cached;
}
letter = this.rotor.transform(letter);
letter = this.baseScrambler.transform(letter);
letter = this.rotor.revTransform(letter);
this.cache[i] = letter;
this.cache[letter] = i;
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) {
return this.end1 === end ? this.end2 : 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 = "";
// Roll back the fast rotor by one step
let pos = Utils.mod(this.rotor.pos - 1, 26);
result += i2a(pos);
for (let i=0; i<this.baseScrambler.rotors.length; i++) {
pos = this.baseScrambler.rotors[i].pos;
result += i2a(pos);
}
return result.split("").reverse().join("");
}
}
/**
* Bombe simulator class.
*/
export class BombeMachine {
/**
* Construct a Bombe.
*
* Note that there is no handling of offsets here: the crib specified must exactly match the
* ciphertext. It will check that the crib is sane (length is vaguely sensible and there's no
* matching characters between crib and ciphertext) but cannot check further - if it's wrong
* your results will be wrong!
*
* There is also no handling of rotor stepping - if the target Enigma stepped in the middle of
* your crib, you're out of luck. TODO: Allow specifying a step point - this is fairly easy to
* configure on a real Bombe, but we're not clear on whether it was ever actually done for
* real (there would almost certainly have been better ways of attacking in most situations
* than attempting to exhaust options for the stepping point, but in some circumstances, e.g.
* via Banburismus, the stepping point might have been known).
*
* @param {string[]} rotors - list of rotor spec strings (without step points!)
* @param {Object} reflector - Reflector object
* @param {string} ciphertext - The ciphertext to attack
* @param {string} crib - Known plaintext for this ciphertext
* @param {boolean} check - Whether to use the checking machine
* @param {function} update - Function to call to send status updates (optional)
*/
constructor(rotors, reflector, ciphertext, crib, check, update=undefined) {
if (ciphertext.length < crib.length) {
throw new OperationError("Crib overruns supplied ciphertext");
}
if (crib.length < 2) {
// This is the absolute bare minimum to be sane, and even then it's likely too short to
// be useful
throw new OperationError("Crib is too short");
}
if (crib.length > 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<crib.length; i++) {
if (ciphertext[i] === crib[i]) {
throw new OperationError(`Invalid crib: character ${ciphertext[i]} at pos ${i} in both ciphertext and crib`);
}
}
this.ciphertext = ciphertext;
this.crib = crib;
this.initRotors(rotors);
this.check = check;
this.updateFn = update;
const [mostConnected, edges] = this.makeMenu();
// 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);
// These are the pseudo-Engima devices corresponding to each edge in the menu, and the
// nodes in the menu they each connect to
this.scramblers = new Array();
for (let i=0; i<26; i++) {
this.scramblers.push(new Array());
}
this.sharedScrambler = new SharedScrambler(this.baseRotors.slice(1), reflector);
this.allScramblers = new Array();
this.indicator = undefined;
for (const edge of edges) {
const cRotor = this.baseRotors[0].copy();
const end1 = a2i(edge.node1.letter);
const end2 = a2i(edge.node2.letter);
const scrambler = new Scrambler(this.sharedScrambler, cRotor, edge.pos, end1, end2);
if (edge.pos === 0) {
this.indicator = scrambler;
}
this.scramblers[end1].push(scrambler);
this.scramblers[end2].push(scrambler);
this.allScramblers.push(scrambler);
}
// The Bombe uses a set of rotors to keep track of what settings it's testing. We cheat and
// use one of the actual scramblers if there's one in the right position, but if not we'll
// just create one.
if (this.indicator === undefined) {
this.indicator = new Scrambler(this.sharedScrambler, this.baseRotors[0].copy(), 0, undefined, undefined);
this.allScramblers.push(this.indicator);
}
this.testRegister = a2i(mostConnected.letter);
// This is an arbitrary letter other than the most connected letter
for (const edge of mostConnected.edges) {
this.testInput = [this.testRegister, a2i(edge.getOther(mostConnected).letter)];
break;
}
}
/**
* Build Rotor objects from list of rotor wiring strings.
* @param {string[]} rotors - List of rotor wiring strings
*/
initRotors(rotors) {
// This is ordered from the Enigma fast rotor to the slow, so bottom to top for the Bombe
this.baseRotors = [];
for (const rstr of rotors) {
const rotor = new CopyRotor(rstr, "", "A", "A");
this.baseRotors.push(rotor);
}
}
/**
* Replace the rotors and reflector in all components of this Bombe.
* @param {string[]} rotors - List of rotor wiring strings
* @param {Object} reflector - Reflector object
*/
changeRotors(rotors, reflector) {
// At the end of the run, the rotors are all back in the same position they started
this.initRotors(rotors);
this.sharedScrambler.changeRotors(this.baseRotors.slice(1), reflector);
for (const scrambler of this.allScramblers) {
scrambler.changeRotor(this.baseRotors[0].copy());
}
}
/**
* If we have a way of sending status messages, do so.
* @param {...*} msg - Message to send.
*/
update(...msg) {
if (this.updateFn !== undefined) {
this.updateFn(...msg);
}
}
/**
* Recursive depth-first search on the menu graph.
* This is used to a) isolate unconnected sub-graphs, and b) count the number of loops in each
* of those graphs.
* @param {Object} node - Node object to start the search from
* @returns {[number, number, Object, number, Object[]} - loop count, node count, most connected
* node, order of most connected node, list of edges in this sub-graph
*/
dfs(node) {
let loops = 0;
let nNodes = 1;
let mostConnected = node;
let nConnections = mostConnected.edges.size;
let edges = new Set();
node.visited = true;
for (const edge of node.edges) {
if (edge.visited) {
// Already been here from the other end.
continue;
}
edge.visited = true;
edges.add(edge);
const other = edge.getOther(node);
if (other.visited) {
// We have a loop, record that and continue
loops += 1;
continue;
}
// This is a newly visited node
const [oLoops, oNNodes, oMostConnected, oNConnections, oEdges] = this.dfs(other);
loops += oLoops;
nNodes += oNNodes;
edges = new Set([...edges, ...oEdges]);
if (oNConnections > 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<this.crib.length; i++) {
const a = this.crib[i];
const b = this.ciphertext[i];
new Edge(i, nodes.get(a), nodes.get(b));
}
// list of [loop_count, node_count, most_connected_node, connections_on_most_connected, edges]
const graphs = [];
// Then, for each unconnected subgraph, we count the number of loops and nodes
for (const start of nodes.keys()) {
if (nodes.get(start).visited) {
continue;
}
const subgraph = this.dfs(nodes.get(start));
graphs.push(subgraph);
}
// Return the subgraph with the most loops (ties broken by node count)
graphs.sort((a, b) => {
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]];
}
/**
* 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} i - Bombe wire bundle
* @param {number} j - Bombe stecker hypothesis wire within bundle
*/
energise(i, j) {
const idx = 26*i + j;
if (this.wires[idx]) {
return;
}
this.wires[idx] = true;
// Welchman's diagonal board: if A steckers to B, that implies B steckers to A. Handle
// both.
const idxPair = 26*j + i;
this.wires[idxPair] = true;
if (i === this.testRegister || j === this.testRegister) {
this.energiseCount++;
if (this.energiseCount === 26) {
// no point continuing, bail out
return;
}
}
for (let k=0; k<this.scramblers[i].length; k++) {
const scrambler = this.scramblers[i][k];
const out = scrambler.transform(j);
const other = scrambler.getOtherEnd(i);
// Lift the pre-check before the call, to save some function call overhead
const otherIdx = 26*other + out;
if (!this.wires[otherIdx]) {
this.energise(other, out);
if (this.energiseCount === 26) {
return;
}
}
}
if (i === j) {
return;
}
for (let k=0; k<this.scramblers[j].length; k++) {
const scrambler = this.scramblers[j][k];
const out = scrambler.transform(i);
const other = scrambler.getOtherEnd(j);
const otherIdx = 26*other + out;
if (!this.wires[otherIdx]) {
this.energise(other, out);
if (this.energiseCount === 26) {
return;
}
}
}
}
/**
* Trial decryption at the current setting.
* Used after we get a stop.
* This applies the detected stecker pair if we have one. It does not handle the other
* steckering or stepping (which is why we limit it to 26 characters, since it's guaranteed to
* be wrong after that anyway).
* @param {string} stecker - Known stecker spec string.
* @returns {string}
*/
tryDecrypt(stecker) {
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(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;
}
this.energiseCount = 0;
// 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
const count = this.energiseCount;
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 {
// This was known as a "boxing stop" - we have a stop but not a single hypothesis.
// If this happens a lot it implies the menu isn't good enough.
// If we have the checking machine enabled, we're going to just check each wire in
// turn. If we get 0 or 1 hit, great.
// If we get multiple hits, or the checking machine is off, the user will just have to
// deal with it.
if (!this.check) {
// We can't draw any conclusions about the steckering (one could maybe suggest
// options in some cases, but too hard to present clearly).
return [this.indicator.getPos(), "??", this.tryDecrypt("")];
}
let stecker = undefined;
for (let i = 0; i < 26; i++) {
const newStecker = this.checkingMachine(i);
if (newStecker !== "") {
if (stecker !== undefined) {
// Multiple hypotheses can't be ruled out.
return [this.indicator.getPos(), "??", this.tryDecrypt("")];
}
stecker = newStecker;
}
}
if (stecker === undefined) {
// Checking machine ruled all possibilities out.
return undefined;
}
// If we got here, there was just one possibility allowed by the checking machine. Success.
return [this.indicator.getPos(), stecker, this.tryDecrypt(stecker)];
}
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
* solutions.
* @returns {string[][3]} - list of 3-tuples of candidate rotor setting, plugboard settings, and decryption preview
*/
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++) {
// Benchmarking suggests this is faster than using .fill()
for (let i=0; i<this.wires.length; i++) {
this.wires[i] = false;
}
this.energiseCount = 0;
// Energise the test input, follow the current through each scrambler
// (and the diagonal board)
this.energise(...this.testInput);
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
// next one needs to step as well)
let n = 1;
for (let j=1; j<this.baseRotors.length; j++) {
if ((i % Math.pow(26, j)) === 0) {
n++;
} else {
break;
}
}
if (n > 1) {
this.sharedScrambler.step(n);
}
for (const scrambler of this.allScramblers) {
scrambler.step();
}
// Send status messages at what seems to be a reasonably sensible frequency
// (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;
}
}

178
src/core/lib/Charts.mjs Normal file
View file

@ -0,0 +1,178 @@
/**
* @author tlwr [toby@toby.codes]
* @author Matt C [me@mitt.dev]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError";
/**
* @constant
* @default
*/
export const RECORD_DELIMITER_OPTIONS = ["Line feed", "CRLF"];
/**
* @constant
* @default
*/
export const FIELD_DELIMITER_OPTIONS = ["Space", "Comma", "Semi-colon", "Colon", "Tab"];
/**
* Default from colour
*
* @constant
* @default
*/
export const COLOURS = {
min: "white",
max: "black"
};
/**
* Gets values from input for a plot.
*
* @param {string} input
* @param {string} recordDelimiter
* @param {string} fieldDelimiter
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
* @param {number} length
* @returns {Object[]}
*/
export function getValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded, length) {
let headings;
const values = [];
input
.split(recordDelimiter)
.forEach((row, rowIndex) => {
const split = row.split(fieldDelimiter);
if (split.length !== length) throw new OperationError(`Each row must have length ${length}.`);
if (columnHeadingsAreIncluded && rowIndex === 0) {
headings = split;
} else {
values.push(split);
}
});
return { headings, values };
}
/**
* Gets values from input for a scatter plot.
*
* @param {string} input
* @param {string} recordDelimiter
* @param {string} fieldDelimiter
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
* @returns {Object[]}
*/
export function getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
let { headings, values } = getValues(
input,
recordDelimiter,
fieldDelimiter,
columnHeadingsAreIncluded,
2
);
if (headings) {
headings = {x: headings[0], y: headings[1]};
}
values = values.map(row => {
const x = parseFloat(row[0], 10),
y = parseFloat(row[1], 10);
if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10.");
if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10.");
return [x, y];
});
return { headings, values };
}
/**
* Gets values from input for a scatter plot with colour from the third column.
*
* @param {string} input
* @param {string} recordDelimiter
* @param {string} fieldDelimiter
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
* @returns {Object[]}
*/
export function getScatterValuesWithColour(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
let { headings, values } = getValues(
input,
recordDelimiter, fieldDelimiter,
columnHeadingsAreIncluded,
3
);
if (headings) {
headings = {x: headings[0], y: headings[1]};
}
values = values.map(row => {
const x = parseFloat(row[0], 10),
y = parseFloat(row[1], 10),
colour = row[2];
if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10.");
if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10.");
return [x, y, colour];
});
return { headings, values };
}
/**
* Gets values from input for a time series plot.
*
* @param {string} input
* @param {string} recordDelimiter
* @param {string} fieldDelimiter
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
* @returns {Object[]}
*/
export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
const { values } = getValues(
input,
recordDelimiter, fieldDelimiter,
false,
3
);
let xValues = new Set();
const series = {};
values.forEach(row => {
const serie = row[0],
xVal = row[1],
val = parseFloat(row[2], 10);
if (Number.isNaN(val)) throw new OperationError("Values must be numbers in base 10.");
xValues.add(xVal);
if (typeof series[serie] === "undefined") series[serie] = {};
series[serie][xVal] = val;
});
xValues = new Array(...xValues);
const seriesList = [];
for (const seriesName in series) {
const serie = series[seriesName];
seriesList.push({name: seriesName, data: serie});
}
return { xValues, series: seriesList };
}

369
src/core/lib/Enigma.mjs Normal file
View file

@ -0,0 +1,369 @@
/**
* 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"},
];
export const ROTORS_FOURTH = [
{name: "Beta", value: "LEYJVCNIXWPBQMDRTAKZGFUHOS"},
{name: "Gamma", value: "FSOKANUERHMBTIYCWLQPZXVGJD"},
];
/**
* 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 = new Array(26);
this.revMap = new Array(26);
const uniq = {};
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;
uniq[b] = true;
}
if (Object.keys(uniq).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.pairs = pairs;
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) {
// self-stecker
return;
}
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.
*
* Includes a couple of optimisations on that basis.
*/
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");
}
const optMap = new Array(26);
for (const x of Object.keys(this.map)) {
optMap[x] = this.map[x];
}
this.map = optMap;
}
/**
* Transform a character through this object.
*
* @param {number} c - The character.
* @returns {number}
*/
transform(c) {
return this.map[c];
}
}
/**
* 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

@ -100,7 +100,7 @@ export function fromHex(data, delim="Auto", byteLen=2) {
/**
* To Hexadecimal delimiters.
*/
export const TO_HEX_DELIM_OPTIONS = ["Space", "Comma", "Semi-colon", "Colon", "Line feed", "CRLF", "0x", "\\x", "None"];
export const TO_HEX_DELIM_OPTIONS = ["Space", "Percent", "Comma", "Semi-colon", "Colon", "Line feed", "CRLF", "0x", "\\x", "None"];
/**

285
src/core/lib/Protobuf.mjs Normal file
View file

@ -0,0 +1,285 @@
import Utils from "../Utils";
/**
* Protobuf lib. Contains functions to decode protobuf serialised
* data without a schema or .proto file.
*
* Provides utility functions to encode and decode variable length
* integers (varint).
*
* @author GCHQ Contributor [3]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
class Protobuf {
/**
* Protobuf constructor
*
* @param {byteArray} data
*/
constructor(data) {
// Check we have a byteArray
if (data instanceof Array) {
this.data = data;
} else {
throw new Error("Protobuf input must be a byteArray");
}
// Set up masks
this.TYPE = 0x07;
this.NUMBER = 0x78;
this.MSB = 0x80;
this.VALUE = 0x7f;
// Declare offset and length
this.offset = 0;
this.LENGTH = data.length;
}
// Public Functions
/**
* Encode a varint from a number
*
* @param {number} number
* @returns {byteArray}
*/
static varIntEncode(number) {
const MSB = 0x80,
VALUE = 0x7f,
MSBALL = ~VALUE,
INT = Math.pow(2, 31);
const out = [];
let offset = 0;
while (number >= INT) {
out[offset++] = (number & 0xff) | MSB;
number /= 128;
}
while (number & MSBALL) {
out[offset++] = (number & 0xff) | MSB;
number >>>= 7;
}
out[offset] = number | 0;
return out;
}
/**
* Decode a varint from the byteArray
*
* @param {byteArray} input
* @returns {number}
*/
static varIntDecode(input) {
const pb = new Protobuf(input);
return pb._varInt();
}
/**
* Parse Protobuf data
*
* @param {byteArray} input
* @returns {Object}
*/
static decode(input) {
const pb = new Protobuf(input);
return pb._parse();
}
// Private Class Functions
/**
* Main private parsing function
*
* @private
* @returns {Object}
*/
_parse() {
let object = {};
// Continue reading whilst we still have data
while (this.offset < this.LENGTH) {
const field = this._parseField();
object = this._addField(field, object);
}
// Throw an error if we have gone beyond the end of the data
if (this.offset > this.LENGTH) {
throw new Error("Exhausted Buffer");
}
return object;
}
/**
* Add a field read from the protobuf data into the Object. As
* protobuf fields can appear multiple times, if the field already
* exists we need to add the new field into an array of fields
* for that key.
*
* @private
* @param {Object} field
* @param {Object} object
* @returns {Object}
*/
_addField(field, object) {
// Get the field key/values
const key = field.key;
const value = field.value;
object[key] = object.hasOwnProperty(key) ?
object[key] instanceof Array ?
object[key].concat([value]) :
[object[key], value] :
value;
return object;
}
/**
* Parse a field and return the Object read from the record
*
* @private
* @returns {Object}
*/
_parseField() {
// Get the field headers
const header = this._fieldHeader();
const type = header.type;
const key = header.key;
switch (type) {
// varint
case 0:
return { "key": key, "value": this._varInt() };
// fixed 64
case 1:
return { "key": key, "value": this._uint64() };
// length delimited
case 2:
return { "key": key, "value": this._lenDelim() };
// fixed 32
case 5:
return { "key": key, "value": this._uint32() };
// unknown type
default:
throw new Error("Unknown type 0x" + type.toString(16));
}
}
/**
* Parse the field header and return the type and key
*
* @private
* @returns {Object}
*/
_fieldHeader() {
// Make sure we call type then number to preserve offset
return { "type": this._fieldType(), "key": this._fieldNumber() };
}
/**
* Parse the field type from the field header. Type is stored in the
* lower 3 bits of the tag byte. This does not move the offset on as
* we need to read the field number from the tag byte too.
*
* @private
* @returns {number}
*/
_fieldType() {
// Field type stored in lower 3 bits of tag byte
return this.data[this.offset] & this.TYPE;
}
/**
* Parse the field number (i.e. the key) from the field header. The
* field number is stored in the upper 5 bits of the tag byte - but
* is also varint encoded so the follow on bytes may need to be read
* when field numbers are > 15.
*
* @private
* @returns {number}
*/
_fieldNumber() {
let shift = -3;
let fieldNumber = 0;
do {
fieldNumber += shift < 28 ?
shift === -3 ?
(this.data[this.offset] & this.NUMBER) >> -shift :
(this.data[this.offset] & this.VALUE) << shift :
(this.data[this.offset] & this.VALUE) * Math.pow(2, shift);
shift += 7;
} while ((this.data[this.offset++] & this.MSD) === this.MSB);
return fieldNumber;
}
// Field Parsing Functions
/**
* Read off a varint from the data
*
* @private
* @returns {number}
*/
_varInt() {
let value = 0;
let shift = 0;
// Keep reading while upper bit set
do {
value += shift < 28 ?
(this.data[this.offset] & this.VALUE) << shift :
(this.data[this.offset] & this.VALUE) * Math.pow(2, shift);
shift += 7;
} while ((this.data[this.offset++] & this.MSB) === this.MSB);
return value;
}
/**
* Read off a 64 bit unsigned integer from the data
*
* @private
* @returns {number}
*/
_uint64() {
// Read off a Uint64
let num = this.data[this.offset++] * 0x1000000 + (this.data[this.offset++] << 16) + (this.data[this.offset++] << 8) + this.data[this.offset++];
num = num * 0x100000000 + this.data[this.offset++] * 0x1000000 + (this.data[this.offset++] << 16) + (this.data[this.offset++] << 8) + this.data[this.offset++];
return num;
}
/**
* Read off a length delimited field from the data
*
* @private
* @returns {Object|string}
*/
_lenDelim() {
// Read off the field length
const length = this._varInt();
const fieldBytes = this.data.slice(this.offset, this.offset + length);
let field;
try {
// Attempt to parse as a new Protobuf Object
const pbObject = new Protobuf(fieldBytes);
field = pbObject._parse();
} catch (err) {
// Otherwise treat as bytes
field = Utils.byteArrayToChars(fieldBytes);
}
// Move the offset and return the field
this.offset += length;
return field;
}
/**
* Read a 32 bit unsigned integer from the data
*
* @private
* @returns {number}
*/
_uint32() {
// Use a dataview to read off the integer
const dataview = new DataView(new Uint8Array(this.data.slice(this.offset, this.offset + 4)).buffer);
const value = dataview.getUint32(0);
this.offset += 4;
return value;
}
}
export default Protobuf;

227
src/core/lib/Typex.mjs Normal file
View file

@ -0,0 +1,227 @@
/**
* Emulation of the Typex machine.
*
* @author s2224834
* @author The National Museum of Computing - Bombe Rebuild Project
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError";
import * as Enigma from "../lib/Enigma";
import Utils from "../Utils";
/**
* A set of example Typex rotors. No Typex rotor wirings are publicly available, so these are
* randomised.
*/
export const ROTORS = [
{name: "Example 1", value: "MCYLPQUVRXGSAOWNBJEZDTFKHI<BFHNQUW"},
{name: "Example 2", value: "KHWENRCBISXJQGOFMAPVYZDLTU<BFHNQUW"},
{name: "Example 3", value: "BYPDZMGIKQCUSATREHOJNLFWXV<BFHNQUW"},
{name: "Example 4", value: "ZANJCGDLVHIXOBRPMSWQUKFYET<BFHNQUW"},
{name: "Example 5", value: "QXBGUTOVFCZPJIHSWERYNDAMLK<BFHNQUW"},
{name: "Example 6", value: "BDCNWUEIQVFTSXALOGZJYMHKPR<BFHNQUW"},
{name: "Example 7", value: "WJUKEIABMSGFTQZVCNPHORDXYL<BFHNQUW"},
{name: "Example 8", value: "TNVCZXDIPFWQKHSJMAOYLEURGB<BFHNQUW"},
];
/**
* An example Typex reflector. Again, randomised.
*/
export const REFLECTORS = [
{name: "Example", value: "AN BC FG IE KD LU MH OR TS VZ WQ XJ YP"},
];
// Special character handling on Typex keyboard
const KEYBOARD = {
"Q": "1", "W": "2", "E": "3", "R": "4", "T": "5", "Y": "6", "U": "7", "I": "8", "O": "9", "P": "0",
"A": "-", "S": "/", "D": "Z", "F": "%", "G": "X", "H": "£", "K": "(", "L": ")",
"C": "V", "B": "'", "N": ",", "M": "."
};
const KEYBOARD_REV = {};
for (const i of Object.keys(KEYBOARD)) {
KEYBOARD_REV[KEYBOARD[i]] = i;
}
/**
* Typex machine. A lot like the Enigma, but five rotors, of which the first two are static.
*/
export class TypexMachine extends Enigma.EnigmaBase {
/**
* TypexMachine constructor.
*
* @param {Object[]} rotors - List of Rotors.
* @param {Object} reflector - A Reflector.
* @param {Plugboard} plugboard - A Plugboard.
*/
constructor(rotors, reflector, plugboard, keyboard) {
super(rotors, reflector, plugboard);
if (rotors.length !== 5) {
throw new OperationError("Typex must have 5 rotors");
}
this.keyboard = keyboard;
}
/**
* This is the same as the Enigma step function, it's just that the right-
* most two rotors are static.
*/
step() {
const r0 = this.rotors[2];
const r1 = this.rotors[3];
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[4];
r2.step();
}
}
}
/**
* Encrypt/decrypt data. This is identical to the Enigma version cryptographically, but we have
* additional handling for the Typex's keyboard (which handles various special characters by
* mapping them to particular letter combinations).
*
* @param {string} input - The data to encrypt/decrypt.
* @return {string}
*/
crypt(input) {
let inputMod = input;
if (this.keyboard === "Encrypt") {
inputMod = "";
// true = in symbol mode
let mode = false;
for (const x of input) {
if (x === " ") {
inputMod += "X";
} else if (mode) {
if (KEYBOARD_REV.hasOwnProperty(x)) {
inputMod += KEYBOARD_REV[x];
} else {
mode = false;
inputMod += "V" + x;
}
} else {
if (KEYBOARD_REV.hasOwnProperty(x)) {
mode = true;
inputMod += "Z" + KEYBOARD_REV[x];
} else {
inputMod += x;
}
}
}
}
const output = super.crypt(inputMod);
let outputMod = output;
if (this.keyboard === "Decrypt") {
outputMod = "";
let mode = false;
for (const x of output) {
if (x === "X") {
outputMod += " ";
} else if (x === "V") {
mode = false;
} else if (x === "Z") {
mode = true;
} else if (mode) {
outputMod += KEYBOARD[x];
} else {
outputMod += x;
}
}
}
return outputMod;
}
}
/**
* Typex rotor. Like an Enigma rotor, but no ring setting, and can be reversed.
*/
export class Rotor extends Enigma.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 {bool} reversed - Whether to reverse the rotor.
* @param {char} ringSetting - Ring setting of the rotor.
* @param {char} initialPosition - The initial position of the rotor.
*/
constructor(wiring, steps, reversed, ringSetting, initialPos) {
let wiringMod = wiring;
if (reversed) {
const outMap = new Array(26);
for (let i=0; i<26; i++) {
// wiring[i] is the original output
// Enigma.LETTERS[i] is the original input
const input = Utils.mod(26 - Enigma.a2i(wiring[i]), 26);
const output = Enigma.i2a(Utils.mod(26 - Enigma.a2i(Enigma.LETTERS[i]), 26));
outMap[input] = output;
}
wiringMod = outMap.join("");
}
super(wiringMod, steps, ringSetting, initialPos);
}
}
/**
* Typex input plugboard. Based on a Rotor, because it allows arbitrary maps, not just switches
* like the Enigma plugboard.
* Not to be confused with the reflector plugboard.
* This is also where the Typex's backwards input wiring is implemented - it's a bit of a hack, but
* it means everything else continues to work like in the Enigma.
*/
export class Plugboard extends Enigma.Rotor {
/**
* Typex plugboard constructor.
*
* @param {string} wiring - 26 character string of mappings from A-Z, as per rotors, or "".
*/
constructor(wiring) {
// Typex input wiring is backwards vs Enigma: that is, letters enter the rotors in a
// clockwise order, vs. Enigma's anticlockwise (or vice versa depending on which side
// you're looking at it from). I'm doing the transform here to avoid having to rewrite
// the Engima crypt() method in Typex as well.
// Note that the wiring for the reflector is the same way around as Enigma, so no
// transformation is necessary on that side.
// We're going to achieve this by mapping the plugboard settings through an additional
// transform that mirrors the alphabet before we pass it to the superclass.
if (!/^[A-Z]{26}$/.test(wiring)) {
throw new OperationError("Plugboard wiring must be 26 unique uppercase letters");
}
const reversed = "AZYXWVUTSRQPONMLKJIHGFEDCB";
wiring = wiring.replace(/./g, x => {
return reversed[Enigma.a2i(x)];
});
try {
super(wiring, "", "A", "A");
} catch (err) {
throw new OperationError(err.message.replace("Rotor", "Plugboard"));
}
}
/**
* 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);
}
}