@ondoher/enigma 0.1.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,140 +4,50 @@ import '../enigma/standardInventory.js';
4
4
  import sentences from './hamlet.js'
5
5
  import inventory from '../enigma/Inventory.js';
6
6
  import { STANDARD_ALPHABET } from '../enigma/consts.js';
7
+ import Random from '../utils/Random.js';
7
8
 
8
9
 
9
10
  /**
10
- * @typedef {Object} EnigmaConfiguration specifies a configuration for the
11
- * Enigma model M3. This assumes an Enigma with standard reflector B
12
- * @property {Array.<String>} rotors an array of three rotor names
13
- * @property {Array.<Number>} ringSettings an array of offsets for the ring
14
- * settings
15
- * @property {Array.<Array>} plugs 10 pairs of letters that will be used as
16
- * connections on the plug board
11
+ * Use this class to generate random enigma configurations and messages. The
12
+ * methods in this class all the use the `Random` object, which can be seeded to
13
+ * produce a reproducible output
17
14
  */
18
-
19
15
  export default class Generator {
20
- constructor(embellish) {
21
- this.embellish = embellish;
22
- this.enigma = new Enigma({reflector: 'B'});
23
- this.indicators = {};
24
- }
25
-
26
- /**
27
- * call this method to pick a random number from an array and remove it
28
- * @param {Array} list the array of itens to chooise from
29
- *
30
- * @returns {*} the chosen item
31
- */
32
- pickOne(list) {
33
- var pos = Math.floor(Math.random() * list.length);
34
- var choice = list.splice(pos, 1);
35
- return choice[0];
36
- }
37
-
38
- /**
39
- * Call this method to pick two items from a given list. The items are
40
- * removed from the array
41
- *
42
- * @param {Array} list the array of items to choose
43
- * @returns {Array} the two chosen items
44
- */
45
- pickPair(list) {
46
- var result = [];
47
- result.push(this.pickOne(list))
48
- result.push(this.pickOne(list))
49
-
50
- return result;
51
- }
16
+ constructor() {}
52
17
 
53
18
  /**
54
- * Call this method to chose a random item from a list. The item is not
55
- * removed.
19
+ * Call this method to turn a string into valid characters for encryption,
20
+ * which is just the letters A-Z.
56
21
  *
57
- * @param {Array} list the list of items to choose from
58
- *
59
- * @returns {*} the chosen item
60
- */
61
- chooseOne(list) {
62
- var pos = Math.floor(Math.random() * list.length);
63
- return list[pos];
64
- }
65
-
66
- /**
67
- * Call this method to choose a given number of items from a list. The items
68
- * are removed.
69
- *
70
- * @param {Number} count te number of items to pick
71
- * @param {*} list the list of items to choose from
72
- *
73
- * @returns {Array} the chosen items
74
- */
75
- pick(count, list) {
76
-
77
- var result = [];
78
- for(var idx = 0; idx < count; idx++) {
79
- result.push(this.pickOne(list));
80
- }
81
-
82
- return result;
83
- }
84
-
85
- /**
86
- * Call this method to randomly pick a number of items from a list. The
87
- * items are not removed.
88
- *
89
- * @param {Number} count the number of items to choose
90
- * @param {Array} list the list of items to choose from
91
- *
92
- * @returns {Array} the list of items chosen
22
+ * @param {String} text the original text
23
+ * @returns {String} the text that has been normalized to what the Enigma
24
+ * can process
93
25
  */
94
- choose(count, list){
95
- var result = [];
96
- for(var idx = 0; idx < count; idx++) {
97
- result.push(this.chooseOne(list));
98
- }
26
+ cleanMessage(text) {
27
+ text = text.toUpperCase();
28
+ text = text.replace(/[^A-Z]/g, '');
99
29
 
100
- return result;
30
+ return text
101
31
  }
102
32
 
103
33
  /**
104
- * Call this method to randmply pick a given number of item pairs. The items
105
- * will be removed from the list.
34
+ * Call this method to break a string into groups of five letters with a
35
+ * space between them.
106
36
  *
107
- * @param {Number} count the number of pairs to pick
108
- * @param {Array} list the list of items to choose from
37
+ * @param {string} text - the original text
38
+ * @param {number} [size] - the size of the text groups, defaults to 5
109
39
  *
110
- * @returns {Array.<Array>} the item pairs chosen. Each pair is an array of
111
- * two items from the list
40
+ * @returns {string} the segmented string
112
41
  */
113
- pickPairs(count, list) {
114
- var result = [];
115
-
116
- for(var idx = 0; idx < count; idx++) {
117
- result.push(this.pickPair(list));
118
- }
119
-
120
- return result;
121
- }
122
-
123
- /**
124
- * Call this method to pick pairs of strings from an array of strings. This
125
- * returns an array of strings where each chosen pair has been concatenated.
126
- * The strings are removed from the list.
127
- *
128
- * @param {Number} the number of pairs to pick
129
- * @param {Array.<String>} list the strings to pick from
130
- * @returns {Array.<String>} the array of string pairs
131
- */
132
- pickStringPairs(count, list) {
133
- var result = [];
134
-
135
- for(var idx = 0; idx < count; idx++) {
136
- result.push(this.pickPair(list).join(''));
137
- }
42
+ groupText(text, size = 5) {
43
+ let textArray = [...text];
44
+ let result = textArray.reduce((text, letter, idx) => {
45
+ text = text + letter;
46
+ if (Math.floor(idx % size) === size - 1) text = text + ' ';
47
+ return text;
48
+ }, '');
138
49
 
139
50
  return result;
140
-
141
51
  }
142
52
 
143
53
  /**
@@ -148,64 +58,71 @@ export default class Generator {
148
58
  * @returns {String} the sentences separated by a ' ';
149
59
  */
150
60
  generateSentences(count) {
151
- var start = Math.floor(Math.random() * sentences.length) + count;
152
-
153
- return sentences.slice(start, start + count).join(' ');
61
+ return Random.chooseRange(count, sentences);
62
+ }
63
+
64
+ /**
65
+ * Call this method to get the possible setup and configuration options for
66
+ * the given model
67
+ *
68
+ * @param {Model} model
69
+ *
70
+ * @returns {ModelOptions}
71
+ */
72
+ getModelOptions(model) {
73
+ switch (model) {
74
+ case 'I':
75
+ return {
76
+ rotors: ['I', 'II', 'III', 'IV', 'V'],
77
+ reflectors: ['A', 'B', 'C'],
78
+ fixed: false
79
+ };
80
+ case "M3":
81
+ return {
82
+ rotors: ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII'],
83
+ reflectors: ['A', 'B', 'C'],
84
+ fixed: false
85
+ }
86
+ case "M4":
87
+ return {
88
+ reflectors: ['Thin-B', 'Thin-C'],
89
+ rotors: ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII'],
90
+ fixed: ['Beta', 'Gamma'],
91
+ }
92
+ }
154
93
  }
155
94
 
156
95
  /**
157
- * Call this method to create a three letter string as an identifier for a
158
- * a day in a key sheet
96
+ * Call this method to generate a random Enigma configuration
159
97
  *
160
- * @returns {String} the string
161
- */
162
- makeIndicator() {
163
- var letters = [...'abcdefghijklmnopqrstuvwxyzx'];
164
- var indicator;
165
-
166
- do {
167
- indicator = this.choose(3, letters).join('');
168
- } while (this.indicators[indicator]);
98
+ * @param {GeneratorSetup} [setup] options for settings
169
99
 
170
- this.indicators[indicator] = indicator;
171
-
172
- return indicator;
173
- }
174
-
175
- /**
176
- * Call this method to generate a random Enigma setup
177
- *
178
- * @param {Object} [settings] options for settings
179
- * @property {Array.<String>} [rotors] alternate list of rotors to choose
180
- * from
181
- * @property {Array.<String>} [fixed] an array of fixed rotors to pick one
182
- * of
183
- *
184
- * @returns {Object} the Enigma settings.
185
- * @property {Array.<String>} rotors an array of three rotor names, four if
186
- * fixed was given
187
- * @property {Array.<Number>} ringSettings an array of offsets for the ring
188
- * settings
189
- * @property {Array.<Array>} plugs 10 pairs of letters that will be used as
190
- * connections on the plug board
100
+ * @returns {SimplifiedConfiguration} the Enigma settings.
191
101
  */
192
- generateEnigmaSetup(settings = {}) {
193
- var rotorInventory = settings.rotors || inventory.getRotorNames();
194
- var connectors = [...Array(26).keys()];
195
- var letters = [...STANDARD_ALPHABET];
102
+ generateEnigmaConfiguration(setup = {}) {
103
+ /** @type {number []} */
104
+ let connectors = [...Array(26).keys()];
105
+ /** @type {string[]} */
106
+ let letters = [...STANDARD_ALPHABET];
107
+ let unfixed = inventory.getRotorNames(false);
108
+ let fixed = inventory.getRotorNames(true);
109
+ let useRotors = structuredClone(setup.rotors || unfixed);
110
+ let rotors = Random.pick(3, useRotors);
111
+ let ringSettings = Random.choose(3, connectors);
112
+ let plugsList = Random.pickPairs(10, letters);
113
+
114
+ plugsList = plugsList.map((pair) => pair.join(""));
196
115
 
197
- var rotors = this.pick(3, rotorInventory);
198
- var ringSettings = this.choose(3, connectors);
199
- var plugsA = this.pickStringPairs(10, letters);
200
116
  ringSettings[0]++;
201
117
  ringSettings[1]++;
202
118
  ringSettings[2]++;
203
119
 
204
- var plugs = plugsA.join(' ');
120
+ let plugs = plugsList.join(' ');
205
121
 
206
- if (settings.fixed) {
207
- rotors.unshift(this.pickOne(settings.fixed));
208
- ringSettings.push(this.chooseOne(connectors));
122
+ if (setup.fixed) {
123
+ let useRotors = structuredClone(Array.isArray(setup.fixed) ? setup.fixed : fixed);
124
+ rotors.push(Random.pickOne(useRotors));
125
+ ringSettings.push(Random.chooseOne(connectors));
209
126
  ringSettings[3]++;
210
127
  }
211
128
 
@@ -213,240 +130,31 @@ export default class Generator {
213
130
  }
214
131
 
215
132
  /**
216
- * Call this method to create a single days configuration for a key sheet.
217
- * This an Enigma configuration plus the other metadata.
218
- *
219
- * @param {idx} idx the day of the month
220
- *
221
- * @returns {Object} the single day of the key sheet
222
- * @property {Number} day the day of the month
223
- * @property {Array.<String>} rotors an array of three rotor names
224
- * @property {Array.<Number>} ringSettings an array of offsets for the ring
225
- * settings
226
- * @property {Array.<String>} plugs 10 pairs of letters that will be used as
227
- * connections on the plug board
228
- * @propery {Array.<String>} indicators and array of four three letter
229
- * strings. This will be unique across a key sheet
230
- */
231
- generateDay(idx) {
232
- var settings = this.generateEnigmaSetup();
233
-
234
- var indicators = [];
235
-
236
- indicators.push(this.makeIndicator());
237
- indicators.push(this.makeIndicator());
238
- indicators.push(this.makeIndicator());
239
- indicators.push(this.makeIndicator());
240
-
241
- return {...settings, indicators}
242
- }
243
-
244
- /**
245
- * Call this method to construct a key sheet for the given number of days
246
133
  *
247
- * @param {Number} days the number of days on the key sheet
248
- * @returns {Array.<Object>} the array of day objects
134
+ * @param {string[]} [reflectors] if given, specifies and alternate list of
135
+ * reflectors. Defaults to ['A', 'B', 'C'];
249
136
  */
250
- generateKeySheet(days) {
251
- var sheet = [];
252
- this.indicators = {};
253
-
254
- for(var idx = 0; idx < days; idx++) {
255
- sheet.push(this.generateDay(idx))
256
- }
257
-
258
- return sheet;
137
+ createRandomEnigma(model = "Enigma", reflectors = ['A', 'B', 'C']) {
138
+ let reflector = Random.pickOne(reflectors);
139
+ return new Enigma({model, reflector});
259
140
  }
260
141
 
261
142
  /**
262
- * Call this method to break a string into groups of five letters with a
263
- * space between them.
143
+ * Call this method to generate a random message text encoded with the given
144
+ * Enigma. The random text will be a few sentences from Hamlet.
264
145
  *
265
- * @param {String} text the original text
266
- * @returns {String} the segmented string
267
- */
268
- segment(text) {
269
- var textArray = [...text];
270
- var text = textArray.reduce(function(text, letter, idx) {
271
- text = text + letter;
272
- if (Math.floor(idx % 5) === 4) text = text + ' ';
273
- return text;
274
- }, '');
275
-
276
- return text
277
- }
278
-
279
- /**
280
- * Call this method to turn a string into valid characters for encyption,
281
- * which is just the letters A-Z. If the generator was created with the
282
- * embellish option then certain characters in the original text will be
283
- * relaced by uncommon letter triplets. This allows for better presentation
284
- * when the message as been decoded
146
+ * @param {Enigma} enigma
285
147
  *
286
- * @param {String} text the original text
287
- * @returns {String} the text that has been normalized to what the Enigma
288
- * can process
148
+ * @returns {GeneratedMessage} details of the generated text
289
149
  */
290
- cleanMessage(text) {
291
- text = text.toUpperCase();
292
-
293
- if (this.embellish) {
294
- text = text.replace(/,/g, 'ZIZ');
295
- text = text.replace(/\./g, 'YIY');
296
- text = text.replace(/\?/g, 'XIX');
297
- text = text.replace(/ +/g, 'WIW');
298
- text = text.replace(/'/g, 'VIV');
299
- text = text.replace(/'/g, 'UIU');
300
- text = text.replace(/0/g, 'NULL');
301
- text = text.replace(/1/g, 'ONE');
302
- text = text.replace(/2/g, 'TWO');
303
- text = text.replace(/3/g, 'THREE');
304
- text = text.replace(/4/g, 'FOUR');
305
- text = text.replace(/5/g, 'FIVE');
306
- text = text.replace(/6/g, 'SIX');
307
- text = text.replace(/7/g, 'SEVEN');
308
- text = text.replace(/8/g, 'EIGHT');
309
- text = text.replace(/9/g, 'NINE');
310
- }
311
- text = text.replace(/[^A-Z]/g, '');
312
-
313
- return this.segment(text);
314
- }
315
-
316
- /**
317
- * Call this method to process one submessage from a longer message. This is
318
- * part of code to generate a message as the Enigma would have been used.
319
- *
320
- * @param {string} indicator the three letter code that can be sent to
321
- * reference the configuration of the Enigma. These will be used to cross
322
- * reference the message to the machine configuration on the key sheet
323
- * @param {String} text the cleaned up string to be encoded
324
- *
325
- * @returns {Object} the generated message part
326
- * @property {String} key a randomly chosen key, this would be transmitted
327
- * with the message
328
- * @property {String} enc the encoded start position for the message. This
329
- * was encoded using the randomly chosen key. This was sent with the
330
- * message
331
- * @property {String} text the message text encoded using the unencoded
332
- * start position. The first five letters of this text string included the
333
- * unencrypted key identifier.
334
- * @property {String} start the the unencoded start position. This was not
335
- * sent with the message but s included here to verify an implementation of
336
- * this method.
337
- * @property {String} clear the message before being encoded. Given to
338
- * verify the value if data is being used to test the Enigma system
339
- * implementation
340
- */
341
- encodeOnePart(indicator, text) {
342
- var key = this.choose(3, STANDARD_ALPHABET).join('');
343
- var start = this.choose(3, STANDARD_ALPHABET).join('');
344
- var enc = this.enigma.encode(key, start);
345
- var clear = text;
346
-
347
- text = text.replace(/ /g, '');
348
- text = this.enigma.encode(start, text);
349
- text = this.segment(text);
350
- text = indicator + ' ' + text
351
-
352
- return {key, enc, start, text, clear};
353
- }
354
-
355
- /**
356
- * Call this method to generate a full message based on the data in a key
357
- * sheet. The message constructed reflects an actual message as the Enigma
358
- * was really used. An individual message cannot be more than 250
359
- * characters, so longer messages are broken into multiple parts. each one
360
- * encoded with a unique key
361
- *
362
- * @param {Object} sheet a key sheet generated using the generateKeySheet
363
- * method
364
- * @returns {Array} a list of encoded sub messages
365
- */
366
- generateMessage(sheet) {
367
- var dayIdx = Math.floor(Math.random() * 30);
368
- var day = sheet[dayIdx]
369
- var indicator = this.chooseOne(day.indicators);
370
- var count = Math.floor(Math.random() * 4) + 1;
371
-
372
- this.enigma.configure({plugs: day.plugs, rotors: day.rotors, ringSettings: day.ringSettings})
373
- var text = this.cleanMessage(this.generateSentences(count));
374
-
375
- var indicator = this.chooseOne(day.indicators);
376
- indicator = (this.choose(2, [...STANDARD_ALPHABET]).join('') + indicator).toUpperCase();
377
-
378
- var messageParts = [];
379
- while (text.length) {
380
- var segment = text.slice(0, 245);
381
- if (text.length)
382
- text = text.slice(246);
383
-
384
- messageParts.push(segment);
385
- }
386
-
387
- var messages = messageParts.map(function(part) {
388
- return this.encodeOnePart(indicator, part)
389
- }, this);
390
-
391
- return messages;
392
- }
393
-
394
- /**
395
- * Call this method to generate a given number of messages based on a
396
- * generated key sheet
397
- *
398
- * @param {Object} sheet the generated key sheet
399
- * @param {Number} count the number of messages to generate
400
- *
401
- * @returns {Array} the list of generated messages
402
- */
403
- generateMessages(sheet, count) {
404
- var result = [];
405
- for (var idx = 0; idx < count; idx++) {
406
- result.push(this.generateMessage(sheet));
407
- }
408
-
409
- return result;
410
- }
411
-
412
- /**
413
- * Call this method to generate some random text encoded with a random
414
- * Enigma configuration. The random text will be a few sentences from
415
- * Hamlet.
416
- *
417
- * @param {Object} settings alternate details to define the Enigma
418
- * configuration
419
- * @property {Array.<String>} [rotors] alternate list of rotors to choose
420
- * from
421
- * @property {Array.<String>} [fixed] an array of fixed rotors to pick from
422
- * @property {Array.<String>} [reflectors] an array of reflectors to choose
423
- * from
424
- *
425
- * @returns {Object} Details of the generated text
426
- * @property {EnigmaConfiguration} setup how the Enigma was configured to
427
- * encode the message
428
- * @property {String} start three letter string with the starting rotor
429
- * offsets used to encode the string.
430
- * @property {String} message the encoded text
431
- * @property {String} clear the unencoded text
432
- */
433
- generateEncodedText(settings) {
434
- var reflectors = settings.reflectors || ['A', 'B', 'C'];
435
- var reflector = this.pickOne(reflectors);
436
- var enigma = new Enigma({reflector: reflector});
437
- var setup = this.generateEnigmaSetup(settings);
438
- var count = Math.floor(Math.random() * 3) + 2;
439
-
440
- setup.reflector = reflector;
441
-
442
- var text = this.cleanMessage(this.generateSentences(count));
443
- var start = this.choose(settings.fixed ? 4 : 3, STANDARD_ALPHABET).join('');
150
+ generateMessage(enigma) {
151
+ let count = Random.random(3) + 2;
444
152
 
445
- enigma.configure(setup);
153
+ let decoded = this.cleanMessage(this.generateSentences(count));
154
+ let start = Random.choose(enigma.rotors.length, [...STANDARD_ALPHABET]).join('');
446
155
 
447
- text = text.replace(/ /g, '');
448
- var encoded = enigma.encode(start, text);
156
+ let encoded = enigma.encode(start, decoded);
449
157
 
450
- return {setup, message: { key: start, encoded, decoded: text}};
158
+ return {start, encoded, decoded, configuration: enigma.configuration};
451
159
  }
452
160
  }
@@ -0,0 +1,100 @@
1
+
2
+ /**
3
+ * Defines the options for generating a random Enigma configuration
4
+ */
5
+ type GeneratorSetup = {
6
+ /** if specified, limits the rotors to select from, defaults to the unfixed
7
+ * rotors in the inventory */
8
+ rotors?: string[];
9
+ /** if true a random fixed router will be chosen. If it is an array then one
10
+ * of the specified rotors will be chosen */
11
+ fixed?: boolean|string[];
12
+ /** */
13
+ }
14
+
15
+ type RouterInventorySpec = {
16
+ /* a string specifying the connector mapping. The index
17
+ * of the string is the logical coordinate of the connector, the character
18
+ * at that index is the output connector. To be exact, it would be the
19
+ * position of that character in the given alphabet. So, in the map '
20
+ * EKMFLGDQVZNTOWYHXUSPAIBRCJ', input connector 0 would map to output
21
+ * connector 4 and input connector 1 would map to output connector 10.
22
+ * Remember that the connectors are numbered starting at 0.*/
23
+ map: string;
24
+ /* a string of characters representing the
25
+ * turnover locations on the disk. These letter would be the value shown in
26
+ * the window to during turnover. In Rotor I this is 'Q' in rotors VI, VII,
27
+ * and VIII there are two turnover locations, 'M' and 'Z'. Use a empty
28
+ * string if this is a fixed rotor */
29
+ turnovers: string;
30
+ }
31
+
32
+ /**
33
+ * This is a simplified version of an Enigma configuration.
34
+ */
35
+ type SimplifiedConfiguration = {
36
+ rotors: string[];
37
+ /** an array of zero based offsets for the ring setting for each rotor */
38
+ ringSettings: number[];
39
+ /** ten pairs of plug setups */
40
+ plugs: string;
41
+ }
42
+
43
+ /**
44
+ * One day of a monthly key sheet
45
+ */
46
+ type KeySheetLine = {
47
+ /** the day of the month */
48
+ day: number;
49
+ /** the rotors for the day */
50
+ rotors: string[];
51
+ /** an array of zero based offsets for the ring setting for each rotor */
52
+ ringSettings: number[];
53
+ /** ten pairs of plug setups */
54
+ plugs: string;
55
+ /** an array of four three letter strings. This will be unique across a key sheet */
56
+ indicators: string[];
57
+ }
58
+
59
+ /**
60
+ * Defines one part of a message. Long messages may be broken into smaller parts.
61
+ */
62
+ type MessagePart = {
63
+ /** a random key for this message chosen by the operator */
64
+ key: string;
65
+
66
+ /** the starting position for the rotors, encoded using the chosen key */
67
+ enc: string;
68
+
69
+ /** the message text encoded using the unencoded start position. The first
70
+ * five letters of this text string included the unencrypted key identifier*/
71
+ text: string;
72
+
73
+ /** the unencoded start position */
74
+ start: string;
75
+
76
+ /** The clear text */
77
+ clear: string;
78
+ }
79
+
80
+ type KeyBookMessage = {
81
+ configuration: SimplifiedConfiguration;
82
+ parts: MessagePart[];
83
+ }
84
+
85
+ type GeneratedMessage = {
86
+ /** three letter start position for the message */
87
+ start: string;
88
+ /** the encoded string */
89
+ encoded: string;
90
+ /** the decoded string */
91
+ decoded: string;
92
+ }
93
+
94
+ type Model = ("I" | "M3" | "M4");
95
+
96
+ type ModelOptions = {
97
+ rotors: string[];
98
+ fixed: string[];
99
+ reflectors: string[];
100
+ }
@@ -1 +1,3 @@
1
1
  export {default as Generator} from './Generator.js'
2
+ export {default as CodeBook} from './CodeBook.js'
3
+ export {default as Random} from '../utils/Random.js'