2019-01-10 18:04:02 +00:00
/**
* Emulation of the Bombe machine.
* This version carries out multiple Bombe runs to handle unknown rotor configurations.
*
* @author s2224834
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
2019-07-09 12:23:59 +01:00
import Operation from "../Operation.mjs" ;
import OperationError from "../errors/OperationError.mjs" ;
import { BombeMachine } from "../lib/Bombe.mjs" ;
import { ROTORS , ROTORS_FOURTH , REFLECTORS , Reflector } from "../lib/Enigma.mjs" ;
import { isWorkerEnvironment } from "../Utils.mjs" ;
2019-07-05 10:17:52 +01:00
2019-01-10 18:04:02 +00:00
/**
* Convenience method for flattening the preset ROTORS object into a newline-separated string.
* @param {Object[]} - Preset rotors object
* @param {number} s - Start index
* @param {number} n - End index
* @returns {string}
*/
function rotorsFormat ( rotors , s , n ) {
const res = [];
for ( const i of rotors . slice ( s , n )) {
res . push ( i . value );
}
return res . join ( "\n" );
}
/**
* Combinatorics choose function
* @param {number} n
* @param {number} k
* @returns number
*/
function choose ( n , k ) {
let res = 1 ;
for ( let i = 1 ; i <= k ; i ++ ) {
res *= ( n + 1 - i ) / i ;
}
return res ;
}
/**
* Bombe operation
*/
class MultipleBombe extends Operation {
/**
* Bombe constructor
*/
constructor () {
super ();
this . name = "Multiple Bombe" ;
2019-11-06 12:14:22 +00:00
this . module = "Bletchley" ;
2019-03-14 12:08:35 +00:00
this . description = "Emulation of the Bombe machine used to attack Enigma. This version carries out multiple Bombe runs to handle unknown rotor configurations.<br><br>You should test your menu on the single Bombe operation before running it here. See the description of the Bombe operation for instructions on choosing a crib.<br><br>More detailed descriptions of the Enigma, Typex and Bombe operations <a href='https://github.com/gchq/CyberChef/wiki/Enigma,-the-Bombe,-and-Typex'>can be found here</a>." ;
2019-01-10 18:04:02 +00:00
this . infoURL = "https://wikipedia.org/wiki/Bombe" ;
this . inputType = "string" ;
2019-02-08 15:21:14 +00:00
this . outputType = "JSON" ;
this . presentType = "html" ;
2019-01-10 18:04:02 +00:00
this . args = [
{
"name" : "Standard Enigmas" ,
2019-02-08 00:59:56 +00:00
"type" : "populateMultiOption" ,
2019-01-10 18:04:02 +00:00
"value" : [
{
name : "German Service Enigma (First - 3 rotor)" ,
2019-02-08 00:59:56 +00:00
value : [
rotorsFormat ( ROTORS , 0 , 5 ),
"" ,
rotorsFormat ( REFLECTORS , 0 , 1 )
]
2019-01-10 18:04:02 +00:00
},
{
name : "German Service Enigma (Second - 3 rotor)" ,
2019-02-08 00:59:56 +00:00
value : [
rotorsFormat ( ROTORS , 0 , 8 ),
"" ,
rotorsFormat ( REFLECTORS , 0 , 2 )
]
2019-01-10 18:04:02 +00:00
},
{
name : "German Service Enigma (Third - 4 rotor)" ,
2019-02-08 00:59:56 +00:00
value : [
rotorsFormat ( ROTORS , 0 , 8 ),
rotorsFormat ( ROTORS_FOURTH , 1 , 2 ),
rotorsFormat ( REFLECTORS , 2 , 3 )
]
2019-01-10 18:04:02 +00:00
},
{
name : "German Service Enigma (Fourth - 4 rotor)" ,
2019-02-08 00:59:56 +00:00
value : [
rotorsFormat ( ROTORS , 0 , 8 ),
rotorsFormat ( ROTORS_FOURTH , 1 , 3 ),
rotorsFormat ( REFLECTORS , 2 , 4 )
]
2019-01-10 18:04:02 +00:00
},
{
name : "User defined" ,
2019-02-08 00:59:56 +00:00
value : [ "" , "" , "" ]
2019-01-10 18:04:02 +00:00
},
],
2019-02-08 00:59:56 +00:00
"target" : [ 1 , 2 , 3 ]
2019-01-10 18:04:02 +00:00
},
{
name : "Main rotors" ,
type : "text" ,
value : ""
},
{
name : "4th rotor" ,
type : "text" ,
value : ""
},
{
name : "Reflectors" ,
type : "text" ,
value : ""
},
{
name : "Crib" ,
type : "string" ,
value : ""
},
{
name : "Crib offset" ,
type : "number" ,
value : 0
2019-01-11 13:18:25 +00:00
},
{
name : "Use checking machine" ,
type : "boolean" ,
value : true
2019-01-10 18:04:02 +00:00
}
];
}
/**
* Format and send a status update message.
* @param {number} nLoops - Number of loops in the menu
* @param {number} nStops - How many stops so far
* @param {number} progress - Progress (as a float in the range 0..1)
*/
2019-01-12 01:35:24 +00:00
updateStatus ( nLoops , nStops , progress , start ) {
2020-03-12 15:23:22 +00:00
const elapsed = Date . now () - start ;
2019-01-12 01:35:24 +00:00
const remaining = ( elapsed / progress ) * ( 1 - progress ) / 1000 ;
const hours = Math . floor ( remaining / 3600 );
const minutes = `0 ${ Math . floor (( remaining % 3600 ) / 60 ) } ` . slice ( - 2 );
const seconds = `0 ${ Math . floor ( remaining % 60 ) } ` . slice ( - 2 );
2019-02-08 15:21:14 +00:00
const msg = `Bombe run with ${ nLoops } loop ${ nLoops === 1 ? "" : "s" } in menu (2+ desirable): ${ nStops } stops, ${ Math . floor ( 100 * progress ) } % done, ${ hours } : ${ minutes } : ${ seconds } remaining` ;
2019-01-10 18:04:02 +00:00
self . sendStatusMessage ( msg );
}
/**
* Early rotor description string validation.
* Drops stepping information.
* @param {string} rstr - The rotor description string
* @returns {string} - Rotor description with stepping stripped, if any
*/
validateRotor ( rstr ) {
// The Bombe doesn't take stepping into account so we'll just ignore it here
if ( rstr . includes ( "<" )) {
rstr = rstr . split ( "<" , 2 )[ 0 ];
}
// Duplicate the validation of the rotor strings here, otherwise you might get an error
// thrown halfway into a big Bombe run
if ( ! /^[A-Z]{26}$/ . test ( rstr )) {
throw new OperationError ( "Rotor wiring must be 26 unique uppercase letters" );
}
if ( new Set ( rstr ). size !== 26 ) {
throw new OperationError ( "Rotor wiring must be 26 unique uppercase letters" );
}
return rstr ;
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run ( input , args ) {
const mainRotorsStr = args [ 1 ];
2019-02-08 00:59:56 +00:00
const fourthRotorsStr = args [ 2 ];
const reflectorsStr = args [ 3 ];
let crib = args [ 4 ];
const offset = args [ 5 ];
const check = args [ 6 ];
2019-01-10 18:04:02 +00:00
const rotors = [];
const fourthRotors = [];
const reflectors = [];
for ( let rstr of mainRotorsStr . split ( "\n" )) {
rstr = this . validateRotor ( rstr );
rotors . push ( rstr );
}
if ( rotors . length < 3 ) {
throw new OperationError ( "A minimum of three rotors must be supplied" );
}
if ( fourthRotorsStr !== "" ) {
for ( let rstr of fourthRotorsStr . split ( "\n" )) {
rstr = this . validateRotor ( rstr );
fourthRotors . push ( rstr );
}
}
if ( fourthRotors . length === 0 ) {
fourthRotors . push ( "" );
}
for ( const rstr of reflectorsStr . split ( "\n" )) {
const reflector = new Reflector ( rstr );
reflectors . push ( reflector );
}
if ( reflectors . length === 0 ) {
throw new OperationError ( "A minimum of one reflector must be supplied" );
}
if ( crib . length === 0 ) {
throw new OperationError ( "Crib cannot be empty" );
}
if ( offset < 0 ) {
throw new OperationError ( "Offset cannot be negative" );
}
// For symmetry with the Enigma op, for the input we'll just remove all invalid characters
input = input . replace ( /[^A-Za-z]/g , "" ). toUpperCase ();
crib = crib . replace ( /[^A-Za-z]/g , "" ). toUpperCase ();
const ciphertext = input . slice ( offset );
let update ;
2019-07-05 10:17:52 +01:00
if ( isWorkerEnvironment ()) {
2019-01-10 18:04:02 +00:00
update = this . updateStatus ;
} else {
update = undefined ;
}
let bombe = undefined ;
2019-02-08 15:21:14 +00:00
const output = { bombeRuns : []};
2019-01-10 18:04:02 +00:00
// I could use a proper combinatorics algorithm here... but it would be more code to
// write one, and we don't seem to have one in our existing libraries, so massively nested
// for loop it is
const totalRuns = choose ( rotors . length , 3 ) * 6 * fourthRotors . length * reflectors . length ;
let nRuns = 0 ;
let nStops = 0 ;
2020-03-12 15:23:22 +00:00
const start = Date . now ();
2019-01-10 18:04:02 +00:00
for ( const rotor1 of rotors ) {
for ( const rotor2 of rotors ) {
if ( rotor2 === rotor1 ) {
continue ;
}
for ( const rotor3 of rotors ) {
if ( rotor3 === rotor2 || rotor3 === rotor1 ) {
continue ;
}
for ( const rotor4 of fourthRotors ) {
for ( const reflector of reflectors ) {
nRuns ++ ;
const runRotors = [ rotor1 , rotor2 , rotor3 ];
if ( rotor4 !== "" ) {
runRotors . push ( rotor4 );
}
if ( bombe === undefined ) {
2019-01-11 13:18:25 +00:00
bombe = new BombeMachine ( runRotors , reflector , ciphertext , crib , check );
2019-02-08 15:21:14 +00:00
output . nLoops = bombe . nLoops ;
2019-01-10 18:04:02 +00:00
} else {
bombe . changeRotors ( runRotors , reflector );
}
const result = bombe . run ();
nStops += result . length ;
if ( update !== undefined ) {
2019-01-12 01:35:24 +00:00
update ( bombe . nLoops , nStops , nRuns / totalRuns , start );
2019-01-10 18:04:02 +00:00
}
if ( result . length > 0 ) {
2019-02-08 15:21:14 +00:00
output . bombeRuns . push ({
rotors : runRotors ,
reflector : reflector . pairs ,
result : result
});
2019-01-10 18:04:02 +00:00
}
}
}
}
}
}
2019-02-08 15:21:14 +00:00
return output ;
}
/**
* Displays the MultiBombe results in an HTML table
*
* @param {Object} output
* @param {number} output.nLoops
* @param {Array[]} output.result
* @returns {html}
*/
present ( output ) {
let html = `Bombe run on menu with ${ output . nLoops } loop ${ output . nLoops === 1 ? "" : "s" } (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided.\n` ;
for ( const run of output . bombeRuns ) {
2019-02-28 18:37:48 +00:00
html += `\nRotors: ${ run . rotors . slice (). reverse (). join ( ", " ) } \nReflector: ${ run . reflector } \n` ;
2019-03-14 11:43:23 +00:00
html += "<table class='table table-hover table-sm table-bordered table-nonfluid'><tr><th>Rotor stops</th> <th>Partial plugboard</th> <th>Decryption preview</th></tr>\n" ;
2019-02-08 15:21:14 +00:00
for ( const [ setting , stecker , decrypt ] of run . result ) {
2019-03-14 11:43:23 +00:00
html += `<tr><td> ${ setting } </td> <td> ${ stecker } </td> <td> ${ decrypt } </td></tr>\n` ;
2019-02-08 15:21:14 +00:00
}
html += "</table>\n" ;
}
return html ;
2019-01-10 18:04:02 +00:00
}
}
export default MultipleBombe ;