@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,102 @@
1
+ import { STANDARD_ALPHABET } from "./consts.js";
2
+
3
+ /**
4
+ * This is the listener for Enigma and component events
5
+ * @callback Listener
6
+ * @param {String} event a string that identifies the event being fired
7
+ * @param {String} name the name of the component firing the event
8
+ * @param {String} message a human readable description of the evebnt details
9
+ * @param {Object} into event specific data for the event
10
+ */
11
+
12
+ /**
13
+ * This is the base class for an encoder. The default implementation of the
14
+ * encode method is to return the input as the output
15
+ */
16
+ export default class Encoder {
17
+ constructor(name, {cb, alphabet = STANDARD_ALPHABET}) {
18
+ this.name = name;
19
+ this.alphabet = alphabet;
20
+ this.contactCount = alphabet.length;
21
+ this.cb = cb;
22
+ }
23
+
24
+ /**
25
+ * given a connector number, normalize it to be between 0 and 25 inclusive.
26
+ *
27
+ * @param {Number} connector the connector being normalized
28
+ *
29
+ * @returns {Number} value between 0 and 25
30
+ */
31
+ normalize(connector) {
32
+ return (connector + this.contactCount * 2) % this.contactCount;
33
+ }
34
+
35
+ /**
36
+ * Given an alphabetic connection map, convert that into an array of
37
+ * numbers. The index into the array or string is the input connector, and
38
+ * the value at that position is the output connector
39
+ *
40
+ * @param {String} map connections map.
41
+ * @returns {Array.<Number>} the numerical map
42
+ */
43
+ makeMap(map) {
44
+ var letters = [...map];
45
+ return letters.map(function(letter) {
46
+ return this.alphabet.indexOf(letter);
47
+ }, this)
48
+ }
49
+
50
+ /**
51
+ * given an existing connection map from inoput to out put, create a new map
52
+ * that has the connections going in the other direction, output to input.
53
+ *
54
+ * @param {Array.<Number>} map connection map
55
+ * @returns {Array.<Number>} the reversed map
56
+ */
57
+ makeReverseMap(map) {
58
+ var reverseMap = new Array(map.length);
59
+
60
+ map.forEach(function(input, idx) {
61
+ reverseMap[input] = idx;
62
+ }, this);
63
+
64
+ return reverseMap;
65
+ }
66
+
67
+ /**
68
+ * Call this method to convert the input connector number to the output in
69
+ * the given direction The default encode method just passes the input value
70
+ * through
71
+ *
72
+ * @param {String} direction either right for moving towards the reflector
73
+ * or left if moving back
74
+ * @param {Number} input the specific connection receiving an input
75
+ *
76
+ * @returns {Number} The translated output connector number
77
+ */
78
+ encode(direction, input) {
79
+ return input;
80
+ }
81
+
82
+ /**
83
+ * Call this method to set a function to be called when important events
84
+ * happen to a component.
85
+
86
+ * @param {Listener} cb the function to be called.
87
+ */
88
+ listen(cb) {
89
+ this.cb = cb;
90
+ }
91
+
92
+ /**
93
+ * Call this method to call the event listener
94
+ *
95
+ * @param {String} name the name of the event
96
+ * @param {...any} rest the parameters to pass to the callback
97
+ */
98
+ fire(name, ...rest) {
99
+ if (this.cb) this.cb(name, ...rest);
100
+ }
101
+
102
+ }
@@ -0,0 +1,246 @@
1
+ import EntryDisc from "./EntryDisc.js";
2
+ import PlugBoard from "./PlugBoard.js";
3
+ import Rotor from "./Rotor.js";
4
+ import Reflector from "./Reflector.js";
5
+ import inventory from './Inventory.js'
6
+ import { STANDARD_ALPHABET } from "./consts.js";
7
+
8
+ /**
9
+ * Construct this class to get a new instance of the Enigma. Many of the
10
+ * parameters to the constructor and the config method reference the names of
11
+ * standard Enigma parts. These are retrived from the Inventory instance
12
+ */
13
+ export default class Enigma {
14
+ /**
15
+ * The constructor for the Enigma. This represents the unconfigurable
16
+ * settings of the device.
17
+ *
18
+ * @param {Object} settings the settings for the Enigma
19
+ * @property {String} [entryDisc] the name of entry disc in the inventory
20
+ * this defaults 'default'
21
+ * @param {String} reflector specifies one of possible reflectors, the
22
+ * predefined reflectors are A, B, C, Thin-B and Thin-C
23
+ * @param {String} [alphabet] the alphabet used by the system, usually just
24
+ * the uppercase latin letters
25
+ */
26
+ constructor(settings) {
27
+ var {entryDisc = 'default', reflector, alphabet = STANDARD_ALPHABET} = settings;
28
+ var entryDiscSettings = inventory.getEntryDisc(entryDisc)
29
+
30
+ var reflectorSettings = inventory.getReflector(reflector)
31
+ this.alphabet = alphabet;
32
+ this.plugboard = new PlugBoard('plugboard', {});
33
+ this.entryDisc = new EntryDisc('entry-disc', entryDiscSettings);
34
+ this.reflector = new Reflector('reflector', reflectorSettings)
35
+ this.length = alphabet.length;
36
+ }
37
+
38
+ /**
39
+ * Call this method to normalize a connector number to be within the
40
+ * the length of the currrent alphabet
41
+ *
42
+ * @param {Number} connector the number to be normalized
43
+ *
44
+ * @returns {Number} the normalized connector number
45
+ */
46
+ normalize(connector) {
47
+ connector += this.length;
48
+ connector = connector % this.length
49
+
50
+ return connector;
51
+ }
52
+
53
+ /**
54
+ * Configure the Enigma for encoding.
55
+ *
56
+ * @param {Object} settings the configuration of the Enigma. These settings
57
+ * represent the aspects of the Enigma that can can change for daily
58
+ * configuration.
59
+ * @property {Array.<String>|String} [plugs] array of strings with each
60
+ * element being a pair of letters from the alphabet that are being swapped
61
+ * on the plug board
62
+ * @property {Array.<String>} rotors the array of installed rotors. The
63
+ * order here is signicant and is given in the left to right direction.
64
+ * This means that last name in this list is the first rotor used in the
65
+ * forward direction and last used in the backward direction. Each element
66
+ * is the name of the rotor to use in the corresponding position. Stepping
67
+ * stops at the first fixed rotor
68
+ * @property {String|Array<Number>} [ringSettings] each letter in this
69
+ * string represents the offset of the key settings from the rotor start
70
+ * position. If it is an array, then each value is the one based key
71
+ * setting for the related rotor,
72
+ */
73
+ configure(settings) {
74
+ var { rotors, ringSettings = [], plugs = [] } = settings;
75
+
76
+ // make copies of these configurations so that we don't change the
77
+ // values from the caller, which in JavaScript are passed by reference.
78
+ rotors = JSON.parse(JSON.stringify(rotors));
79
+ ringSettings = JSON.parse(JSON.stringify(ringSettings));
80
+
81
+ // the rotors are given in the left to right direction, but are actually
82
+ // used in the right to left direction. So, here we reverse them
83
+ rotors = rotors.reverse();
84
+
85
+ this.plugboard.configure({plugs})
86
+
87
+ var ringOffsets = []
88
+
89
+ // because the rotors are secified in the reverse other they are used,
90
+ // we have to do the same for the ringSettings.
91
+ if (Array.isArray(ringSettings)) {
92
+ ringSettings = ringSettings.reverse();
93
+
94
+ // When specified with numbers they will be in the range 1-26, we
95
+ // need to shift these to be 0-25;
96
+ ringSettings.forEach(function(offset) {
97
+ ringOffsets.push(this.normalize(offset - 1))
98
+ }, this);
99
+ } else {
100
+ var letters = [...ringSettings];
101
+ letters = letters.reverse();
102
+ letters.forEach(function(letter) {
103
+ var offset = this.alphabet.indexOf(letter);
104
+ ringOffsets.push(offset);
105
+ }, this)
106
+ }
107
+
108
+ this.rotors = rotors.map(function(name, idx) {
109
+ return new Rotor(`rotor-${name}`, {...inventory.getRotor(name), alphabet: this.alphabet, ringSetting: ringOffsets[idx], cb: this.cb});
110
+ }, this);
111
+
112
+ this.encoders = [this.plugboard, this.entryDisc, ...this.rotors];
113
+ }
114
+
115
+ /**
116
+ * Call this method to step the rotors one time. This method will manage the
117
+ * stepping between all rotors
118
+ */
119
+ step() {
120
+ this.rotors.forEach(function(rotor, idx) {
121
+ if (rotor.isFixed()) return;
122
+
123
+ // This is the double stepping. Only do this for the middle rotor
124
+ if (rotor.willTurnover() && idx === 1) {
125
+ this.pending[idx] = true
126
+ };
127
+
128
+ if (this.pending[idx]) {
129
+ this.pending[idx] = false;
130
+ if (rotor.step()) this.pending[idx + 1] = true;
131
+ }
132
+ }, this);
133
+
134
+ // The first rotor is always stepping
135
+ this.pending[0] = true;
136
+ }
137
+
138
+ /**
139
+ * Call this method to set the starting rotation for the messages to encrypt
140
+ *
141
+ * @param {Array.Number>|String} the length of the string or the array
142
+ * should match the number of rotors and are given left to right. If start
143
+ * is a string then the letters of the string specify the start value seen
144
+ * in the window for the corresponding rotor. If it is an array then each
145
+ * number will be the one-based rotation.
146
+ */
147
+ setStart(start) {
148
+ if (Array.isArray(start)) {
149
+ var charArray = start.map(function(number) {
150
+ number--;
151
+ return this.alphabet[number];
152
+ }, this);
153
+
154
+ start = charArray.join('');
155
+ }
156
+ start = [...start].reverse();
157
+
158
+ // reset the rotation pending state
159
+ this.pending = {0: true};
160
+
161
+ this.rotors.forEach(function(rotor, idx) {
162
+ rotor.setStartPosition(start[idx]);
163
+ })
164
+ }
165
+
166
+ /**
167
+ * Call this method to simulate a keypress on the Enigma. This will output
168
+ * the encoded letter
169
+ *
170
+ * @param {String} letter the key pressed
171
+ * @returns {String} the encoded letter
172
+ */
173
+ keyPress(letter) {
174
+ letter = letter.toUpperCase();
175
+ if (letter.length !== 1 || this.alphabet.indexOf(letter) === -1) {
176
+ if (letter !== ' ')
177
+ console.warn(`Unexected character ${letter}`);
178
+ return;
179
+ }
180
+
181
+ this.fire('input', this.name, `input ${letter}`, {letter})
182
+ this.step();
183
+
184
+ // encode to the right
185
+ var connector = this.encoders.reduce(function(connector, encoder) {
186
+ connector = encoder.encode('right', connector);
187
+ return connector;
188
+ }.bind(this), this.alphabet.indexOf(letter));
189
+
190
+ // reflector
191
+ connector = this.reflector.encode('', connector);
192
+
193
+ // encode to the left
194
+ connector = this.encoders.reduceRight(function(connector, encoder) {
195
+ connector = encoder.encode('left', connector);
196
+ return connector;
197
+ }.bind(this), connector)
198
+
199
+ letter = this.alphabet[connector];
200
+ this.fire('output', this.name, `output ${letter}`, {letter})
201
+ return letter;
202
+ }
203
+
204
+ /**
205
+ * Call this shortcut method to encode a whole string
206
+ *
207
+ * @param {String} start the starting positon for the rotors
208
+ * @param {String} text the text to encode
209
+ *
210
+ * @returns {String} the encoded string.
211
+ */
212
+ encode(start, text) {
213
+ this.setStart(start)
214
+ var letters = [...text];
215
+ var output = letters.map(function(letter) {
216
+ return this.keyPress(letter);
217
+ }, this)
218
+
219
+ return output.join('');
220
+ }
221
+
222
+ /**
223
+ * Call this method to call the event listener
224
+ *
225
+ * @param {String} name the name of the event
226
+ * @param {...any} rest the parameters to pass to the callback
227
+ */
228
+ fire(type, ...rest) {
229
+ if (this.cb) this.cb(type, ...rest);
230
+ }
231
+
232
+ /**
233
+ * Call this method to set a function to be called when important events
234
+ * happen to a component.
235
+
236
+ * @param {Listener} cb the function to be called.
237
+ */
238
+ listen(cb) {
239
+ this.cb = cb;
240
+ this.encoders.forEach(function(encoder) {
241
+ encoder.listen(cb);
242
+ });
243
+
244
+ this.reflector.listen(cb);
245
+ }
246
+ }
@@ -0,0 +1,36 @@
1
+ import Encoder from './Encoder.js';
2
+ import { STANDARD_ALPHABET } from "./consts.js";
3
+
4
+
5
+ /**
6
+ * This is the class for an entry disc. Entry discs are fixed disks of connector
7
+ * pins.
8
+ */
9
+ export default class EntryDisc extends Encoder {
10
+ /**
11
+ * Constuctor for the entry disc.
12
+ *
13
+ * @param {String} name thge name of this entry disc
14
+ * @param {Object} settings contains the alphabet being used, and the map
15
+ * between input and output contacts
16
+ */
17
+ constructor(name, settings) {
18
+ super(name, settings);
19
+ var {map} = settings;
20
+ this.rightMap = this.makeMap(map);
21
+ this.leftMap = this.makeReverseMap(this.rightMap);
22
+ }
23
+
24
+ encode(direction, input) {
25
+ var result = direction === 'right' ? this.rightMap[input]: this.leftMap[input];
26
+ var evName = direction === 'right' ? 'encode-right' : 'encode-left';
27
+
28
+ this.fire(evName, this.name,
29
+ `${evName} ${this.name}, input: ${input}, output: ${result}`, {
30
+ input: input,
31
+ output: result,
32
+ }
33
+ );
34
+ return result;
35
+ }
36
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * This is the class used to manage the standard inmventory of components. An
3
+ * instance of this class is exported as the default of this module
4
+ */
5
+
6
+ class Inventory {
7
+ /**
8
+ * Constructor for the inventory class. Starts out empty
9
+ */
10
+ constructor() {
11
+ this.entryDiscs = {};
12
+ this.rotors = {};
13
+ this.fixedRotors = {};
14
+ this.reflectors = {};
15
+ }
16
+
17
+ /**
18
+ * Call this method to add a new Rotor type to the inventory.
19
+ *
20
+ * @param {String} name the name of the rotor being added. This name will be
21
+ * used when specifying the rotors to use for the Enigma configuration.
22
+ * @param {String} map a string specifying the connector mapping. The index
23
+ * of the string is the logical coordinate of the connector, the character
24
+ * at that index is the output connector. To be exact, it would be the
25
+ * position of that character in the given alphabet. So, in the map '
26
+ * EKMFLGDQVZNTOWYHXUSPAIBRCJ', input connector 0 would map to output
27
+ * connector 4 and input connector 1 would map to output connector 10.
28
+ * Remember that the connectors are numbered starting at 0.
29
+ * @param {String} turnovers this is a string of characters representing the
30
+ * turnover locations on the disk. These letter would be the value shown in
31
+ * the window to during turnover. In Rotor I this is 'Q' in rotors VI, VII,
32
+ * and VIII there are two turnover locations, 'M' and 'Z'. Pass an empty
33
+ * string if this is a fixed rotor
34
+ */
35
+
36
+ addRotor(name, map, turnovers) {
37
+ this.rotors[name] = {map, turnovers}
38
+ }
39
+
40
+ /**
41
+ * Call this method to add a new reflector definition.
42
+ *
43
+ * @param {String} name this is the name that will be used to reference this
44
+ * reflector when constructing an Enigma class.
45
+ * @param {String} map the mapping between connectors. this uses the same
46
+ * format used in the addRotor method
47
+ */
48
+ addReflector(name, map) {
49
+ this.reflectors[name] = {map};
50
+ }
51
+
52
+ /**
53
+ * Call this method to add a new entry disc. There was only one used in the
54
+ * standard military models, but there were other versions that defined it
55
+ * differently.
56
+ *
57
+ * @param {*} name this is the name that will be used to reference this
58
+ * entry disc when constructing an Enigma class.
59
+ * @param {*} map the mapping between connectors. this uses the same format
60
+ * used in the addRotor method
61
+ */
62
+ addEntryDisc(name, map) {
63
+ this.entryDiscs[name] = {map};
64
+ }
65
+
66
+ /**
67
+ * Call this method to get the setup for a defined rotor.
68
+ *
69
+ * @param {String} name the name of the rotor as it was added to the
70
+ * inventory.
71
+ *
72
+ * @returns {Object} the rotor defintion
73
+ * @property {String} map the connection map for the rotor
74
+ * @property {String} turnovers the locations where turnovers happen
75
+ */
76
+ getRotor(name) {
77
+ return this.rotors[name];
78
+ }
79
+
80
+ /**
81
+ * Call this method to get the setup for a defined reflector.
82
+ *
83
+ * @param {String} name the name of the reflector as it was added to the
84
+ * inventory.
85
+ * @returns {Object} the reflector definition
86
+ * @property {String} the connection map for the reflector
87
+ */
88
+ getReflector(name) {
89
+ return this.reflectors[name];
90
+ }
91
+
92
+ /**
93
+ * Call this method to get the setup for a defined entry disc.
94
+ *
95
+ * @param {String} name the name of the entry disc as it was added to the
96
+ * inventory.
97
+ * @returns {Object} the entry disc definition
98
+ * @property {String} the connection map for the entry disc
99
+ */
100
+ getEntryDisc(name) {
101
+ return this.entryDiscs[name];
102
+ }
103
+
104
+ /**
105
+ * Call this method to get the names of all the rotors in the inventory
106
+ *
107
+ * @returns {Array.<String>} the names of the rotors
108
+ */
109
+ getRotorNames() {
110
+ return Object.keys(this.rotors);
111
+ }
112
+ }
113
+
114
+
115
+ export default new Inventory();
@@ -0,0 +1,74 @@
1
+ import Encoder from "./Encoder.js";
2
+ import { STANDARD_ALPHABET } from "./consts.js";
3
+
4
+ /**
5
+ * This class represents the plugboard. There is only one type of plugboard
6
+ */
7
+ export default class PlugBoard extends Encoder {
8
+
9
+ /**
10
+ * Constructor for the plugboard.
11
+ *
12
+ * @param {String} name the name for the plugboard, defaults to 'plugboard'
13
+ * @param {Object} [settings] the settings for the plugboard. Only needed if
14
+ * using an alternate alphabet
15
+ */
16
+ constructor(name = 'plugboard', settings = {}) {
17
+ super(name, settings);
18
+
19
+ var {alphabet = STANDARD_ALPHABET, map} = settings;
20
+ this.alphabet = alphabet;
21
+ this.map = map || alphabet;
22
+ }
23
+
24
+ /**
25
+ * Call this method to configure the plug board. This will be used to
26
+ * provide the plug connections
27
+ *
28
+ * @param {Object} [settings] the configuration options for the plug
29
+ * board
30
+ * @property {Array.<String>|String} [plugs] either an array of strings or a
31
+ * single string. If it is a string, it must be a space separated list of
32
+ * letter pairs that connects one input letter to another. If it is an
33
+ * array then then each item is a pair of letters to specify how the plugs
34
+ * are connected
35
+ */
36
+ configure(settings = {}) {
37
+ var map = this.map;
38
+ var {plugs = []} = settings;
39
+
40
+ if (typeof plugs === 'string') plugs = plugs.split(' ');
41
+ plugs.forEach(function(plug) {
42
+ var firstIdx = this.alphabet.indexOf(plug[0]);
43
+ var secondIdx = this.alphabet.indexOf(plug[1]);
44
+ var first = map[firstIdx];
45
+ var second = map[secondIdx];
46
+ map = map.slice(0, firstIdx) + second + map.slice(firstIdx + 1);
47
+ map = map.slice(0, secondIdx) + first + map.slice(secondIdx + 1);
48
+ }, this)
49
+
50
+ this.rightMap = this.makeMap(map);
51
+ this.leftMap = this.makeReverseMap(this.rightMap);
52
+ }
53
+
54
+ /**
55
+ * Call this method to convert the input connector number to the output in
56
+ * the given direction.
57
+ *
58
+ * @param {String} direction either right for moving towards the reflector or
59
+ * left if moving back
60
+ * @param {Number} input the input connector
61
+ * @returns {Number} the output connector
62
+ */
63
+ encode(direction, input) {
64
+ var result = direction === 'right' ? this.rightMap[input]: this.leftMap[input];
65
+ var evName = direction === 'right' ? 'encode-right' : 'encode-left';
66
+ this.fire(evName, this.name,
67
+ `${evName} ${this.name}, input: ${input}, output: ${result}`, {
68
+ input: input,
69
+ output: result,
70
+ }
71
+ );
72
+ return result;
73
+ }
74
+ }
@@ -0,0 +1,51 @@
1
+ import Encoder from './Encoder.js';
2
+ import { STANDARD_ALPHABET } from './consts.js';
3
+
4
+ /**
5
+ * Make in instance of this class to construct a reflector
6
+ */
7
+ export default class Reflector extends Encoder {
8
+
9
+ /**
10
+ * constructor for the reflector class.
11
+ *
12
+ * @param {String} name the name of the reflector instance
13
+ * @param {Obect} The definition of the reflector
14
+ * @property {String} [alphabet] a string of letters that are an alternative
15
+ * to the standard A-Z. Defaults to A-Z
16
+ * @property {String} map a string that defines the mapping between the
17
+ * input and output connectors. The index into the string is the input
18
+ * connector and the value of this string at that index is the output
19
+ * connector. For example, 'YRUHQSLDPXNGOKMIEBFZCWVJAT' which is the map for
20
+ * standard reflector B.
21
+ */
22
+ constructor(name, settings) {
23
+ super(name, settings);
24
+ var {map} = settings;
25
+
26
+ this.map = this.makeMap(map);
27
+ }
28
+
29
+ /**
30
+ * Call this method when reversing the encoding direction of the Enigma. As
31
+ * the point where direction changes this does not have a distinction
32
+ * between a left and right signal path.
33
+ *
34
+ * @param {String} direction since this is the point where signal direction
35
+ * changes from right to left this parameter is not used.
36
+ * @param {Number} input this is the input connector
37
+ *
38
+ * @returns {Number} the mapped output connector
39
+ */
40
+ encode(direction, input) {
41
+ var result = this.map[input];
42
+ this.fire('encode', this.name,
43
+ `encode ${this.name} ${input} ${result}`,
44
+ {
45
+ input: input,
46
+ output: result,
47
+ });
48
+
49
+ return result;
50
+ }
51
+ }