MultipleBombe.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /**
  2. * Emulation of the Bombe machine.
  3. * This version carries out multiple Bombe runs to handle unknown rotor configurations.
  4. *
  5. * @author s2224834
  6. * @copyright Crown Copyright 2019
  7. * @license Apache-2.0
  8. */
  9. import Operation from "../Operation";
  10. import OperationError from "../errors/OperationError";
  11. import {BombeMachine} from "../lib/Bombe";
  12. import {ROTORS, ROTORS_FOURTH, REFLECTORS, Reflector} from "../lib/Enigma";
  13. /**
  14. * Convenience method for flattening the preset ROTORS object into a newline-separated string.
  15. * @param {Object[]} - Preset rotors object
  16. * @param {number} s - Start index
  17. * @param {number} n - End index
  18. * @returns {string}
  19. */
  20. function rotorsFormat(rotors, s, n) {
  21. const res = [];
  22. for (const i of rotors.slice(s, n)) {
  23. res.push(i.value);
  24. }
  25. return res.join("\n");
  26. }
  27. /**
  28. * Combinatorics choose function
  29. * @param {number} n
  30. * @param {number} k
  31. * @returns number
  32. */
  33. function choose(n, k) {
  34. let res = 1;
  35. for (let i=1; i<=k; i++) {
  36. res *= (n + 1 - i) / i;
  37. }
  38. return res;
  39. }
  40. /**
  41. * Bombe operation
  42. */
  43. class MultipleBombe extends Operation {
  44. /**
  45. * Bombe constructor
  46. */
  47. constructor() {
  48. super();
  49. this.name = "Multiple Bombe";
  50. this.module = "Default";
  51. 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>.";
  52. this.infoURL = "https://wikipedia.org/wiki/Bombe";
  53. this.inputType = "string";
  54. this.outputType = "JSON";
  55. this.presentType = "html";
  56. this.args = [
  57. {
  58. "name": "Standard Enigmas",
  59. "type": "populateMultiOption",
  60. "value": [
  61. {
  62. name: "German Service Enigma (First - 3 rotor)",
  63. value: [
  64. rotorsFormat(ROTORS, 0, 5),
  65. "",
  66. rotorsFormat(REFLECTORS, 0, 1)
  67. ]
  68. },
  69. {
  70. name: "German Service Enigma (Second - 3 rotor)",
  71. value: [
  72. rotorsFormat(ROTORS, 0, 8),
  73. "",
  74. rotorsFormat(REFLECTORS, 0, 2)
  75. ]
  76. },
  77. {
  78. name: "German Service Enigma (Third - 4 rotor)",
  79. value: [
  80. rotorsFormat(ROTORS, 0, 8),
  81. rotorsFormat(ROTORS_FOURTH, 1, 2),
  82. rotorsFormat(REFLECTORS, 2, 3)
  83. ]
  84. },
  85. {
  86. name: "German Service Enigma (Fourth - 4 rotor)",
  87. value: [
  88. rotorsFormat(ROTORS, 0, 8),
  89. rotorsFormat(ROTORS_FOURTH, 1, 3),
  90. rotorsFormat(REFLECTORS, 2, 4)
  91. ]
  92. },
  93. {
  94. name: "User defined",
  95. value: ["", "", ""]
  96. },
  97. ],
  98. "target": [1, 2, 3]
  99. },
  100. {
  101. name: "Main rotors",
  102. type: "text",
  103. value: ""
  104. },
  105. {
  106. name: "4th rotor",
  107. type: "text",
  108. value: ""
  109. },
  110. {
  111. name: "Reflectors",
  112. type: "text",
  113. value: ""
  114. },
  115. {
  116. name: "Crib",
  117. type: "string",
  118. value: ""
  119. },
  120. {
  121. name: "Crib offset",
  122. type: "number",
  123. value: 0
  124. },
  125. {
  126. name: "Use checking machine",
  127. type: "boolean",
  128. value: true
  129. }
  130. ];
  131. }
  132. /**
  133. * Format and send a status update message.
  134. * @param {number} nLoops - Number of loops in the menu
  135. * @param {number} nStops - How many stops so far
  136. * @param {number} progress - Progress (as a float in the range 0..1)
  137. */
  138. updateStatus(nLoops, nStops, progress, start) {
  139. const elapsed = new Date().getTime() - start;
  140. const remaining = (elapsed / progress) * (1 - progress) / 1000;
  141. const hours = Math.floor(remaining / 3600);
  142. const minutes = `0${Math.floor((remaining % 3600) / 60)}`.slice(-2);
  143. const seconds = `0${Math.floor(remaining % 60)}`.slice(-2);
  144. 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`;
  145. self.sendStatusMessage(msg);
  146. }
  147. /**
  148. * Early rotor description string validation.
  149. * Drops stepping information.
  150. * @param {string} rstr - The rotor description string
  151. * @returns {string} - Rotor description with stepping stripped, if any
  152. */
  153. validateRotor(rstr) {
  154. // The Bombe doesn't take stepping into account so we'll just ignore it here
  155. if (rstr.includes("<")) {
  156. rstr = rstr.split("<", 2)[0];
  157. }
  158. // Duplicate the validation of the rotor strings here, otherwise you might get an error
  159. // thrown halfway into a big Bombe run
  160. if (!/^[A-Z]{26}$/.test(rstr)) {
  161. throw new OperationError("Rotor wiring must be 26 unique uppercase letters");
  162. }
  163. if (new Set(rstr).size !== 26) {
  164. throw new OperationError("Rotor wiring must be 26 unique uppercase letters");
  165. }
  166. return rstr;
  167. }
  168. /**
  169. * @param {string} input
  170. * @param {Object[]} args
  171. * @returns {string}
  172. */
  173. run(input, args) {
  174. const mainRotorsStr = args[1];
  175. const fourthRotorsStr = args[2];
  176. const reflectorsStr = args[3];
  177. let crib = args[4];
  178. const offset = args[5];
  179. const check = args[6];
  180. const rotors = [];
  181. const fourthRotors = [];
  182. const reflectors = [];
  183. for (let rstr of mainRotorsStr.split("\n")) {
  184. rstr = this.validateRotor(rstr);
  185. rotors.push(rstr);
  186. }
  187. if (rotors.length < 3) {
  188. throw new OperationError("A minimum of three rotors must be supplied");
  189. }
  190. if (fourthRotorsStr !== "") {
  191. for (let rstr of fourthRotorsStr.split("\n")) {
  192. rstr = this.validateRotor(rstr);
  193. fourthRotors.push(rstr);
  194. }
  195. }
  196. if (fourthRotors.length === 0) {
  197. fourthRotors.push("");
  198. }
  199. for (const rstr of reflectorsStr.split("\n")) {
  200. const reflector = new Reflector(rstr);
  201. reflectors.push(reflector);
  202. }
  203. if (reflectors.length === 0) {
  204. throw new OperationError("A minimum of one reflector must be supplied");
  205. }
  206. if (crib.length === 0) {
  207. throw new OperationError("Crib cannot be empty");
  208. }
  209. if (offset < 0) {
  210. throw new OperationError("Offset cannot be negative");
  211. }
  212. // For symmetry with the Enigma op, for the input we'll just remove all invalid characters
  213. input = input.replace(/[^A-Za-z]/g, "").toUpperCase();
  214. crib = crib.replace(/[^A-Za-z]/g, "").toUpperCase();
  215. const ciphertext = input.slice(offset);
  216. let update;
  217. if (ENVIRONMENT_IS_WORKER()) {
  218. update = this.updateStatus;
  219. } else {
  220. update = undefined;
  221. }
  222. let bombe = undefined;
  223. const output = {bombeRuns: []};
  224. // I could use a proper combinatorics algorithm here... but it would be more code to
  225. // write one, and we don't seem to have one in our existing libraries, so massively nested
  226. // for loop it is
  227. const totalRuns = choose(rotors.length, 3) * 6 * fourthRotors.length * reflectors.length;
  228. let nRuns = 0;
  229. let nStops = 0;
  230. const start = new Date().getTime();
  231. for (const rotor1 of rotors) {
  232. for (const rotor2 of rotors) {
  233. if (rotor2 === rotor1) {
  234. continue;
  235. }
  236. for (const rotor3 of rotors) {
  237. if (rotor3 === rotor2 || rotor3 === rotor1) {
  238. continue;
  239. }
  240. for (const rotor4 of fourthRotors) {
  241. for (const reflector of reflectors) {
  242. nRuns++;
  243. const runRotors = [rotor1, rotor2, rotor3];
  244. if (rotor4 !== "") {
  245. runRotors.push(rotor4);
  246. }
  247. if (bombe === undefined) {
  248. bombe = new BombeMachine(runRotors, reflector, ciphertext, crib, check);
  249. output.nLoops = bombe.nLoops;
  250. } else {
  251. bombe.changeRotors(runRotors, reflector);
  252. }
  253. const result = bombe.run();
  254. nStops += result.length;
  255. if (update !== undefined) {
  256. update(bombe.nLoops, nStops, nRuns / totalRuns, start);
  257. }
  258. if (result.length > 0) {
  259. output.bombeRuns.push({
  260. rotors: runRotors,
  261. reflector: reflector.pairs,
  262. result: result
  263. });
  264. }
  265. }
  266. }
  267. }
  268. }
  269. }
  270. return output;
  271. }
  272. /**
  273. * Displays the MultiBombe results in an HTML table
  274. *
  275. * @param {Object} output
  276. * @param {number} output.nLoops
  277. * @param {Array[]} output.result
  278. * @returns {html}
  279. */
  280. present(output) {
  281. 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`;
  282. for (const run of output.bombeRuns) {
  283. html += `\nRotors: ${run.rotors.slice().reverse().join(", ")}\nReflector: ${run.reflector}\n`;
  284. 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";
  285. for (const [setting, stecker, decrypt] of run.result) {
  286. html += `<tr><td>${setting}</td> <td>${stecker}</td> <td>${decrypt}</td></tr>\n`;
  287. }
  288. html += "</table>\n";
  289. }
  290. return html;
  291. }
  292. }
  293. export default MultipleBombe;