@ondoher/enigma 0.1.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.
@@ -0,0 +1,452 @@
1
+
2
+ import Enigma from '../enigma/Enigma.js';
3
+ import '../enigma/standardInventory.js';
4
+ import sentences from './hamlet.js'
5
+ import inventory from '../enigma/Inventory.js';
6
+ import { STANDARD_ALPHABET } from '../enigma/consts.js';
7
+
8
+
9
+ /**
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
17
+ */
18
+
19
+ 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
+ }
52
+
53
+ /**
54
+ * Call this method to chose a random item from a list. The item is not
55
+ * removed.
56
+ *
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
93
+ */
94
+ choose(count, list){
95
+ var result = [];
96
+ for(var idx = 0; idx < count; idx++) {
97
+ result.push(this.chooseOne(list));
98
+ }
99
+
100
+ return result;
101
+ }
102
+
103
+ /**
104
+ * Call this method to randmply pick a given number of item pairs. The items
105
+ * will be removed from the list.
106
+ *
107
+ * @param {Number} count the number of pairs to pick
108
+ * @param {Array} list the list of items to choose from
109
+ *
110
+ * @returns {Array.<Array>} the item pairs chosen. Each pair is an array of
111
+ * two items from the list
112
+ */
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
+ }
138
+
139
+ return result;
140
+
141
+ }
142
+
143
+ /**
144
+ * Call this method to generate the given number of sentences. The sentences
145
+ * are pulled from the text of Hamlet.
146
+ *
147
+ * @param {Number} count the number of sentences
148
+ * @returns {String} the sentences separated by a ' ';
149
+ */
150
+ generateSentences(count) {
151
+ var start = Math.floor(Math.random() * sentences.length) + count;
152
+
153
+ return sentences.slice(start, start + count).join(' ');
154
+ }
155
+
156
+ /**
157
+ * Call this method to create a three letter string as an identifier for a
158
+ * a day in a key sheet
159
+ *
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]);
169
+
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
191
+ */
192
+ generateEnigmaSetup(settings = {}) {
193
+ var rotorInventory = settings.rotors || inventory.getRotorNames();
194
+ var connectors = [...Array(26).keys()];
195
+ var letters = [...STANDARD_ALPHABET];
196
+
197
+ var rotors = this.pick(3, rotorInventory);
198
+ var ringSettings = this.choose(3, connectors);
199
+ var plugsA = this.pickStringPairs(10, letters);
200
+ ringSettings[0]++;
201
+ ringSettings[1]++;
202
+ ringSettings[2]++;
203
+
204
+ var plugs = plugsA.join(' ');
205
+
206
+ if (settings.fixed) {
207
+ rotors.unshift(this.pickOne(settings.fixed));
208
+ ringSettings.push(this.chooseOne(connectors));
209
+ ringSettings[3]++;
210
+ }
211
+
212
+ return {rotors, plugs, ringSettings};
213
+ }
214
+
215
+ /**
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
+ *
247
+ * @param {Number} days the number of days on the key sheet
248
+ * @returns {Array.<Object>} the array of day objects
249
+ */
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;
259
+ }
260
+
261
+ /**
262
+ * Call this method to break a string into groups of five letters with a
263
+ * space between them.
264
+ *
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
285
+ *
286
+ * @param {String} text the original text
287
+ * @returns {String} the text that has been normalized to what the Enigma
288
+ * can process
289
+ */
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('');
444
+
445
+ enigma.configure(setup);
446
+
447
+ text = text.replace(/ /g, '');
448
+ var encoded = enigma.encode(start, text);
449
+
450
+ return {setup, message: { key: start, encoded, decoded: text}};
451
+ }
452
+ }