浏览代码

Add Bombe operation

Still needs some work, but functional
s2224834 6 年之前
父节点
当前提交
b6eb4e84e4

+ 2 - 1
src/core/config/Categories.json

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

+ 472 - 0
src/core/lib/Bombe.mjs

@@ -0,0 +1,472 @@
+/**
+ * Emulation of the Bombe machine.
+ *
+ * @author s2224834
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import OperationError from "../errors/OperationError";
+import Utils from "../Utils";
+import {Rotor, 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.
+     */
+    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) {
+        if (this.node1 === node) {
+            return this.node2;
+        }
+        return this.node1;
+    }
+}
+
+/**
+ * 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[]} rotors - List of rotors in this scrambler
+     * @param {Object} reflector - This scrambler's reflector
+     * @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(rotors, reflector, pos, end1, end2) {
+        this.reflector = reflector;
+        this.rotors = rotors;
+        this.rotorsRev = [].concat(rotors).reverse();
+        this.initialPos = pos;
+        this.rotors[0].pos += pos;
+        this.end1 = end1;
+        this.end2 = end2;
+    }
+
+    /**
+     * Step the rotors forward.
+     *
+     * All nodes in the Bombe step in sync.
+     * @param {number} n - How many rotors to step
+     */
+    step(n) {
+        // The Bombe steps the slowest rotor on an actual Enigma first.
+        for (let i=this.rotors.length - 1; i>=this.rotors.length-n; i--) {
+            this.rotors[i].step();
+        }
+    }
+
+    /**
+     * Run a letter through the scrambler.
+     * @param {number} i - The letter to transform (as a number)
+     * @returns {number}
+     */
+    transform(i) {
+        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);
+        }
+        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) {
+        if (this.end1 === end) {
+            return this.end2;
+        }
+        return 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 = "";
+        for (let i=0; i<this.rotors.length; i++) {
+            let pos = this.rotors[i].pos;
+            // Enigma steps *before* encrypting each character. This means we need to roll the fast
+            // rotor back by one before outputting it, to ensure the position is correct for the
+            // first character.
+            // As usual with the Bombe we do not take stepping of other rotors into account!
+            if (i === 0) {
+                pos = Utils.mod(pos - 1, 26);
+            }
+            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!
+     * @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 {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 (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<ciphertext.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 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);
+        }
+        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).fill(false);
+
+        // 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.allScramblers = new Array();
+        this.indicator = undefined;
+        for (const edge of edges) {
+            const cRotors = [];
+            for (const r of this.baseRotors) {
+                cRotors.push(r.copy());
+            }
+            const end1 = a2i(edge.node1.letter);
+            const end2 = a2i(edge.node2.letter);
+            const scrambler = new Scrambler(cRotors, reflector, 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.baseRotors, reflector, 0, 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;
+        }
+    }
+
+    /**
+     * If we have a way of sending status messages, do so.
+     * @param {string} 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]];
+    }
+
+    /**
+     * Implement Welchman's diagonal board: If A steckers to B, that implies B steckers to A, and
+     * so forth. This function just gets the paired wire.
+     * @param {number[2]} i - Bombe state wire
+     * @returns {number[2]}
+     */
+    getDiagonal(i) {
+        return [i[1], i[0]];
+    }
+
+    /**
+     * 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[2]} i - Bombe state wire
+     */
+    energise(i) {
+        const idx = 26*i[0] + i[1];
+        if (this.wires[idx]) {
+            return;
+        }
+        this.energiseCount ++;
+        this.wires[idx] = true;
+        this.energise(this.getDiagonal(i));
+
+        for (const scrambler of this.scramblers[i[0]]) {
+            const out = scrambler.transform(i[1]);
+            const other = scrambler.getOtherEnd(i[0]);
+            this.energise([other, out]);
+        }
+    }
+
+    /**
+     * 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[][2]} - list of pairs of candidate rotor setting, and calculated stecker pair
+     */
+    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++) {
+            this.wires.fill(false);
+            // 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 = `${i2a(this.testRegister)} <-> ${i2a(j)}`;
+                            break;
+                        }
+                    }
+                } else if (count === 1) {
+                    // This means our hypothesis for the steckering is correct.
+                    stecker = `${i2a(this.testRegister)} <-> ${i2a(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 = `? (wire count: ${count})`;
+                }
+                result.push([this.indicator.getPos(), stecker]);
+            }
+            // 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;
+                }
+            }
+            for (const scrambler of this.allScramblers) {
+                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);
+            }
+        }
+        return result;
+    }
+}

+ 22 - 3
src/core/lib/Enigma.mjs

@@ -103,15 +103,17 @@ export class Rotor {
         if (!/^[A-Z]$/.test(initialPosition)) {
         if (!/^[A-Z]$/.test(initialPosition)) {
             throw new OperationError("Rotor initial position must be exactly one uppercase letter");
             throw new OperationError("Rotor initial position must be exactly one uppercase letter");
         }
         }
-        this.map = {};
-        this.revMap = {};
+        this.map = new Array(26).fill();
+        this.revMap = new Array(26).fill();
+        const uniq = {};
         for (let i=0; i<LETTERS.length; i++) {
         for (let i=0; i<LETTERS.length; i++) {
             const a = a2i(LETTERS[i]);
             const a = a2i(LETTERS[i]);
             const b = a2i(wiring[i]);
             const b = a2i(wiring[i]);
             this.map[a] = b;
             this.map[a] = b;
             this.revMap[b] = a;
             this.revMap[b] = a;
+            uniq[b] = true;
         }
         }
-        if (Object.keys(this.revMap).length !== LETTERS.length) {
+        if (Object.keys(uniq).length !== LETTERS.length) {
             throw new OperationError("Rotor wiring must have each letter exactly once");
             throw new OperationError("Rotor wiring must have each letter exactly once");
         }
         }
         const rs = a2i(ringSetting);
         const rs = a2i(ringSetting);
@@ -219,6 +221,8 @@ class PairMapBase {
 
 
 /**
 /**
  * Reflector. PairMapBase but requires that all characters are accounted for.
  * Reflector. PairMapBase but requires that all characters are accounted for.
+ *
+ * Includes a couple of optimisations on that basis.
  */
  */
 export class Reflector extends PairMapBase {
 export class Reflector extends PairMapBase {
     /**
     /**
@@ -231,6 +235,21 @@ export class Reflector extends PairMapBase {
         if (s !== 26) {
         if (s !== 26) {
             throw new OperationError("Reflector must have exactly 13 pairs covering every letter");
             throw new OperationError("Reflector must have exactly 13 pairs covering every letter");
         }
         }
+        const optMap = new Array(26).fill();
+        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];
     }
     }
 }
 }
 
 

+ 118 - 0
src/core/operations/Bombe.mjs

@@ -0,0 +1,118 @@
+/**
+ * Emulation of the Bombe machine.
+ *
+ * @author s2224834
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+import {BombeMachine} from "../lib/Bombe";
+import {ROTORS, ROTORS_OPTIONAL, REFLECTORS, Reflector} from "../lib/Enigma";
+
+/**
+ * Bombe operation
+ */
+class Bombe extends Operation {
+    /**
+     * Bombe constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Bombe";
+        this.module = "Default";
+        this.description = "";
+        this.infoURL = "https://wikipedia.org/wiki/Bombe";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                name: "1st (right-hand) rotor",
+                type: "editableOption",
+                value: ROTORS,
+                defaultIndex: 2
+            },
+            {
+                name: "2nd rotor",
+                type: "editableOption",
+                value: ROTORS,
+                defaultIndex: 1
+            },
+            {
+                name: "3rd rotor",
+                type: "editableOption",
+                value: ROTORS,
+                defaultIndex: 0
+            },
+            {
+                name: "4th rotor",
+                type: "editableOption",
+                value: ROTORS_OPTIONAL,
+                defaultIndex: 10
+            },
+            {
+                name: "Reflector",
+                type: "editableOption",
+                value: REFLECTORS
+            },
+            {
+                name: "Crib",
+                type: "string",
+                value: ""
+            },
+            {
+                name: "Offset",
+                type: "number",
+                value: 0
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const reflectorstr = args[4];
+        const crib = args[5];
+        const offset = args[6];
+        const rotors = [];
+        for (let i=0; i<4; i++) {
+            if (i === 3 && args[i] === "") {
+                // No fourth rotor
+                break;
+            }
+            let rstr = args[i];
+            // The Bombe doesn't take stepping into account so we'll just ignore it here
+            if (rstr.includes("<")) {
+                rstr = rstr.split("<", 2)[0];
+            }
+            rotors.push(rstr);
+        }
+        if (crib.length === 0) {
+            throw new OperationError("Crib cannot be empty");
+        }
+        input = input.replace(/[^A-Za-z]/g, "");
+        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
+            update = undefined;
+        }
+        const bombe = new BombeMachine(rotors, reflector, ciphertext, crib, 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. Results:\n`;
+        for (const [setting, wires] of result) {
+            msg += `Stop: ${setting} (${wires})\n`;
+        }
+        return msg;
+    }
+}
+
+export default Bombe;

+ 1 - 0
tests/operations/index.mjs

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

+ 67 - 0
tests/operations/tests/Bombe.mjs

@@ -0,0 +1,67 @@
+/**
+ * Bombe machine tests.
+ * @author s2224834
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+import TestRegister from "../TestRegister";
+
+TestRegister.addTests([
+    {
+        name: "Bombe: 3 rotor (self-stecker)",
+        input: "BBYFLTHHYIJQAYBBYS",
+        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", 0,
+                ]
+            }
+        ]
+    },
+    {
+        name: "Bombe: 3 rotor (other stecker)",
+        input: "JBYALIHDYNUAAVKBYM",
+        expectedMatch: /LGA \(A <-> G\)/,
+        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", 0,
+                ]
+            }
+        ]
+    },
+    /*
+     * Long test is long
+    {
+        name: "Bombe: 4 rotor",
+        input: "LUOXGJSHGEDSRDOQQX",
+        expectedMatch: /LHSC \(S <-> S\)/,
+        recipeConfig: [
+            {
+                "op": "Bombe",
+                "args": [
+                    "BDFHJLCPRTXVZNYEIWGAKMUSQO<W", // III
+                    "AJDKSIRUXBLHWTMCQGZNPYFVOE<F", // II
+                    "EKMFLGDQVZNTOWYHXUSPAIBRCJ<R", // I
+                    "LEYJVCNIXWPBQMDRTAKZGFUHOS", // Beta
+                    "AE BN CK DQ FU GY HW IJ LO MP RX SZ TV", // B thin
+                    "THISISATESTMESSAGE", 0,
+                ]
+            }
+        ]
+    },
+    */
+]);