72f480792170725c0b59462781d338961c534510
[cipher-training.git] / enigma.py
1
2 # coding: utf-8
3
4 ##################################
5 # # Enigma machine
6 ##################################
7 # Specification from [Codes and Ciphers](http://www.codesandciphers.org.uk/enigma/rotorspec.htm) page.
8 #
9 # Example Enigma machines from [Louise Dale](http://enigma.louisedade.co.uk/enigma.html) (full simulation) and [EnigmaCo](http://enigmaco.de/enigma/enigma.html) (good animation of the wheels, but no ring settings).
10 #
11 # There's also the nice Enigma simulator for Android by [Franklin Heath](https://franklinheath.co.uk/2012/02/04/our-first-app-published-enigma-simulator/), available on the [Google Play store](https://play.google.com/store/apps/details?id=uk.co.franklinheath.enigmasim&hl=en_GB).
12
13
14
15 import string
16 import collections
17 import multiprocessing
18 import itertools
19
20 # Some convenience functions
21
22 cat = ''.join
23
24 def clean(text): return cat(l.lower() for l in text if l in string.ascii_letters)
25
26 def pos(letter):
27 if letter in string.ascii_lowercase:
28 return ord(letter) - ord('a')
29 elif letter in string.ascii_uppercase:
30 return ord(letter) - ord('A')
31 else:
32 return ''
33
34 def unpos(number): return chr(number % 26 + ord('a'))
35
36
37 wheel_i_spec = 'ekmflgdqvzntowyhxuspaibrcj'
38 wheel_ii_spec = 'ajdksiruxblhwtmcqgznpyfvoe'
39 wheel_iii_spec = 'bdfhjlcprtxvznyeiwgakmusqo'
40 wheel_iv_spec = 'esovpzjayquirhxlnftgkdcmwb'
41 wheel_v_spec = 'vzbrgityupsdnhlxawmjqofeck'
42 wheel_vi_spec = 'jpgvoumfyqbenhzrdkasxlictw'
43 wheel_vii_spec = 'nzjhgrcxmyswboufaivlpekqdt'
44 wheel_viii_spec = 'fkqhtlxocbjspdzramewniuygv'
45 beta_wheel_spec = 'leyjvcnixwpbqmdrtakzgfuhos'
46 gamma_wheel_spec = 'fsokanuerhmbtiycwlqpzxvgjd'
47
48 wheel_i_pegs = ['q']
49 wheel_ii_pegs = ['e']
50 wheel_iii_pegs = ['v']
51 wheel_iv_pegs = ['j']
52 wheel_v_pegs = ['z']
53 wheel_vi_pegs = ['z', 'm']
54 wheel_vii_pegs = ['z', 'm']
55 wheel_viii_pegs = ['z', 'm']
56
57 reflector_b_spec = 'ay br cu dh eq fs gl ip jx kn mo tz vw'
58 reflector_c_spec = 'af bv cp dj ei go hy kr lz mx nw tq su'
59
60
61
62 class LetterTransformer(object):
63 """A generic substitution cipher, that has different transforms in the
64 forward and backward directions. It requires that the transforms for all
65 letters by provided.
66
67 >>> lt = LetterTransformer([('z', 'a')] + [(l, string.ascii_lowercase[i+1]) \
68 for i, l in enumerate(string.ascii_lowercase[:-1])], \
69 raw_transform = True)
70 >>> lt.forward_map
71 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0]
72 >>> lt.backward_map
73 [25, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
74
75 >>> lt = LetterTransformer(cat(collections.OrderedDict.fromkeys('zyxwc' + string.ascii_lowercase)))
76 >>> lt.forward_map
77 [25, 24, 23, 22, 2, 0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
78 >>> lt.backward_map
79 [5, 6, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 3, 2, 1, 0]
80 >>> cat(lt.forward(l) for l in string.ascii_lowercase)
81 'zyxwcabdefghijklmnopqrstuv'
82 >>> cat(lt.backward(l) for l in string.ascii_lowercase)
83 'fgehijklmnopqrstuvwxyzdcba'
84 """
85 def __init__(self, specification, raw_transform=False):
86 if raw_transform:
87 transform = specification
88 else:
89 transform = self.parse_specification(specification)
90 self.validate_transform(transform)
91 self.make_transform_map(transform)
92
93 def parse_specification(self, specification):
94 return list(zip(string.ascii_lowercase, clean(specification)))
95 # return specification
96
97 def validate_transform(self, transform):
98 """A set of pairs, of from-to"""
99 if len(transform) != 26:
100 raise ValueError("Transform specification has {} pairs, requires 26".
101 format(len(transform)))
102 for p in transform:
103 if len(p) != 2:
104 raise ValueError("Not all mappings in transform "
105 "have two elements")
106 if len(set([p[0] for p in transform])) != 26:
107 raise ValueError("Transform specification must list 26 origin letters")
108 if len(set([p[1] for p in transform])) != 26:
109 raise ValueError("Transform specification must list 26 destination letters")
110
111 def make_empty_transform(self):
112 self.forward_map = [0] * 26
113 self.backward_map = [0] * 26
114
115 def make_transform_map(self, transform):
116 self.make_empty_transform()
117 for p in transform:
118 self.forward_map[pos(p[0])] = pos(p[1])
119 self.backward_map[pos(p[1])] = pos(p[0])
120 return self.forward_map, self.backward_map
121
122 def forward(self, letter):
123 if letter in string.ascii_lowercase:
124 return unpos(self.forward_map[pos(letter)])
125 else:
126 return ''
127
128 def backward(self, letter):
129 if letter in string.ascii_lowercase:
130 return unpos(self.backward_map[pos(letter)])
131 else:
132 return ''
133
134
135 class Plugboard(LetterTransformer):
136 """A plugboard, a type of letter transformer where forward and backward
137 transforms are the same. If a letter isn't explicitly transformed, it is
138 kept as it is.
139
140 >>> pb = Plugboard('ua pf rq so ni ey bg hl tx zj'.upper())
141 >>> pb.forward_map
142 [20, 6, 2, 3, 24, 15, 1, 11, 13, 25, 10, 7, 12, 8, 18, 5, 17, 16, 14, 23, 0, 21, 22, 19, 4, 9]
143 >>> pb.forward_map == pb.backward_map
144 True
145 >>> cat(pb.forward(l) for l in string.ascii_lowercase)
146 'ugcdypblnzkhmisfrqoxavwtej'
147 >>> cat(pb.backward(l) for l in string.ascii_lowercase)
148 'ugcdypblnzkhmisfrqoxavwtej'
149 """
150 def parse_specification(self, specification):
151 return [tuple(clean(p)) for p in specification.split()]
152
153 def validate_transform(self, transform):
154 """A set of pairs, of from-to"""
155 for p in transform:
156 if len(p) != 2:
157 raise ValueError("Not all mappings in transform"
158 "have two elements")
159
160 def make_empty_transform(self):
161 self.forward_map = list(range(26))
162 self.backward_map = list(range(26))
163
164 def make_transform_map(self, transform):
165 expanded_transform = transform + [tuple(reversed(p)) for p in transform]
166 return super(Plugboard, self).make_transform_map(expanded_transform)
167
168
169
170
171 class Reflector(Plugboard):
172 """A reflector is a plugboard that requires 13 transforms.
173
174 >>> reflector_b = Reflector(reflector_b_spec)
175 >>> reflector_b.forward_map == reflector_b.backward_map
176 True
177 >>> reflector_b.forward_map
178 [24, 17, 20, 7, 16, 18, 11, 3, 15, 23, 13, 6, 14, 10, 12, 8, 4, 1, 5, 25, 2, 22, 21, 9, 0, 19]
179 >>> cat(reflector_b.forward(l) for l in string.ascii_lowercase)
180 'yruhqsldpxngokmiebfzcwvjat'
181 >>> cat(reflector_b.backward(l) for l in string.ascii_lowercase)
182 'yruhqsldpxngokmiebfzcwvjat'
183 """
184 def validate_transform(self, transform):
185 if len(transform) != 13:
186 raise ValueError("Reflector specification has {} pairs, requires 13".
187 format(len(transform)))
188 if len(set([p[0] for p in transform] +
189 [p[1] for p in transform])) != 26:
190 raise ValueError("Reflector specification does not contain 26 letters")
191 try:
192 super(Reflector, self).validate_transform(transform)
193 except ValueError as v:
194 raise ValueError("Not all mappings in reflector have two elements")
195
196
197
198
199 class SimpleWheel(LetterTransformer):
200 """A wheel is a transform that rotates.
201
202 Looking from the right, letters go in sequence a-b-c clockwise around the
203 wheel.
204
205 The position of the wheel is the number of spaces anticlockwise the wheel
206 has turned.
207
208 Letter inputs and outputs are given relative to the frame holding the wheel,
209 so if the wheel is advanced three places, an input of 'p' will enter the
210 wheel on the position under the wheel's 'q' label.
211
212 >>> rotor_1_transform = list(zip(string.ascii_lowercase, 'EKMFLGDQVZNTOWYHXUSPAIBRCJ'.lower()))
213 >>> wheel_1 = SimpleWheel(rotor_1_transform, raw_transform=True)
214 >>> cat(wheel_1.forward(l) for l in string.ascii_lowercase)
215 'ekmflgdqvzntowyhxuspaibrcj'
216 >>> cat(wheel_1.backward(l) for l in string.ascii_lowercase)
217 'uwygadfpvzbeckmthxslrinqoj'
218
219
220 >>> wheel_2 = SimpleWheel(wheel_ii_spec)
221 >>> cat(wheel_2.forward(l) for l in string.ascii_lowercase)
222 'ajdksiruxblhwtmcqgznpyfvoe'
223 >>> cat(wheel_2.backward(l) for l in string.ascii_lowercase)
224 'ajpczwrlfbdkotyuqgenhxmivs'
225
226 >>> wheel_3 = SimpleWheel(wheel_iii_spec)
227 >>> wheel_3.set_position('a')
228 >>> wheel_3.advance()
229 >>> cat(wheel_3.forward(l) for l in string.ascii_lowercase)
230 'cegikboqswuymxdhvfzjltrpna'
231 >>> cat(wheel_3.backward(l) for l in string.ascii_lowercase)
232 'zfaobrcpdteumygxhwivkqjnls'
233 >>> wheel_3.position
234 1
235 >>> wheel_3.position_l
236 'b'
237
238 >>> for _ in range(24): wheel_3.advance()
239 >>> wheel_3.position
240 25
241 >>> wheel_3.position_l
242 'z'
243 >>> cat(wheel_3.forward(l) for l in string.ascii_lowercase)
244 'pcegikmdqsuywaozfjxhblnvtr'
245 >>> cat(wheel_3.backward(l) for l in string.ascii_lowercase)
246 'nubhcqdterfvgwoaizjykxmslp'
247
248 >>> wheel_3.advance()
249 >>> wheel_3.position
250 0
251 >>> wheel_3.position_l
252 'a'
253 >>> cat(wheel_3.forward(l) for l in string.ascii_lowercase)
254 'bdfhjlcprtxvznyeiwgakmusqo'
255 >>> cat(wheel_3.backward(l) for l in string.ascii_lowercase)
256 'tagbpcsdqeufvnzhyixjwlrkom'
257 """
258 def __init__(self, transform, position='a', raw_transform=False):
259 super(SimpleWheel, self).__init__(transform, raw_transform)
260 self.set_position(position)
261
262 def __getattribute__(self,name):
263 if name=='position_l':
264 return unpos(self.position)
265 else:
266 return object.__getattribute__(self, name)
267
268 def set_position(self, position):
269 self.position = ord(position) - ord('a')
270
271 def forward(self, letter):
272 if letter in string.ascii_lowercase:
273 return unpos((self.forward_map[(pos(letter) + self.position) % 26] - self.position))
274 else:
275 return ''
276
277 def backward(self, letter):
278 if letter in string.ascii_lowercase:
279 return unpos((self.backward_map[(pos(letter) + self.position) % 26] - self.position))
280 else:
281 return ''
282
283 def advance(self):
284 self.position = (self.position + 1) % 26
285
286
287
288 class Wheel(SimpleWheel):
289 """A wheel with a movable ring.
290
291 The ring holds the letters and the pegs that turn other wheels. The core
292 holds the wiring that does the transformation.
293
294 The ring position is how many steps the core is turned relative to the ring.
295 This is one-based, so a ring setting of 1 means the core and ring are
296 aligned.
297
298 The position of the wheel is the position of the core (the transforms)
299 relative to the neutral position.
300
301 The position_l is the position of the ring, or what would be observed
302 by the user of the Enigma machine.
303
304 The peg_positions are the number of advances of this wheel before it will
305 advance the next wheel.
306
307 >>> wheel_3 = Wheel(wheel_iii_spec, wheel_iii_pegs, position='b', ring_setting=1)
308 >>> wheel_3.position
309 1
310 >>> wheel_3.peg_positions
311 [20]
312 >>> wheel_3.position_l
313 'b'
314 >>> wheel_3.advance()
315 >>> wheel_3.position
316 2
317 >>> wheel_3.peg_positions
318 [19]
319 >>> wheel_3.position_l
320 'c'
321
322 >>> wheel_6 = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', ring_setting=3)
323 >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
324 'xkqhwpvngzrcfoiaselbtymjdu'
325 >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
326 'ptlyrmidoxbswhnfckquzgeavj'
327 >>> wheel_6.position
328 25
329 >>> 11 in wheel_6.peg_positions
330 True
331 >>> 24 in wheel_6.peg_positions
332 True
333 >>> wheel_6.position_l
334 'b'
335
336 >>> wheel_6.advance()
337 >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
338 'jpgvoumfyqbenhzrdkasxlictw'
339 >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
340 'skxqlhcnwarvgmebjptyfdzuio'
341 >>> wheel_6.position
342 0
343 >>> 10 in wheel_6.peg_positions
344 True
345 >>> 23 in wheel_6.peg_positions
346 True
347 >>> wheel_6.position_l
348 'c'
349
350 >>> for _ in range(22): wheel_6.advance()
351 >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
352 'mgxantkzsyqjcufirldvhoewbp'
353 >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
354 'dymswobuplgraevzkqifntxcjh'
355 >>> wheel_6.position
356 22
357 >>> 1 in wheel_6.peg_positions
358 True
359 >>> 14 in wheel_6.peg_positions
360 True
361 >>> wheel_6.position_l
362 'y'
363
364 >>> wheel_6.advance()
365 >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
366 'fwzmsjyrxpibtehqkcugndvaol'
367 >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
368 'xlrvnatokfqzduyjphemswbigc'
369 >>> wheel_6.position
370 23
371 >>> 0 in wheel_6.peg_positions
372 True
373 >>> 13 in wheel_6.peg_positions
374 True
375 >>> wheel_6.position_l
376 'z'
377
378 >>> wheel_6.advance()
379 >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
380 'vylrixqwohasdgpjbtfmcuznke'
381 >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
382 'kqumzsnjepyctxiogdlrvahfbw'
383 >>> wheel_6.position
384 24
385 >>> 25 in wheel_6.peg_positions
386 True
387 >>> 12 in wheel_6.peg_positions
388 True
389 >>> wheel_6.position_l
390 'a'
391
392 >>> wheel_6.advance()
393 >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
394 'xkqhwpvngzrcfoiaselbtymjdu'
395 >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
396 'ptlyrmidoxbswhnfckquzgeavj'
397 >>> wheel_6.position
398 25
399 >>> 24 in wheel_6.peg_positions
400 True
401 >>> 11 in wheel_6.peg_positions
402 True
403 >>> wheel_6.position_l
404 'b'
405
406 >>> wheel_6.advance()
407 >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
408 'jpgvoumfyqbenhzrdkasxlictw'
409 >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
410 'skxqlhcnwarvgmebjptyfdzuio'
411 >>> wheel_6.position
412 0
413 >>> 23 in wheel_6.peg_positions
414 True
415 >>> 10 in wheel_6.peg_positions
416 True
417 >>> wheel_6.position_l
418 'c'
419
420 """
421 def __init__(self, transform, ring_peg_letters, ring_setting=1, position='a', raw_transform=False):
422 self.ring_peg_letters = ring_peg_letters
423 self.ring_setting = ring_setting
424 super(Wheel, self).__init__(transform, position=position, raw_transform=raw_transform)
425 self.set_position(position)
426
427 def __getattribute__(self,name):
428 if name=='position_l':
429 return unpos(self.position + self.ring_setting - 1)
430 else:
431 return object.__getattribute__(self, name)
432
433 def set_position(self, position):
434 self.position = (pos(position) - self.ring_setting + 1) % 26
435 self.peg_positions = [(pos(p) - pos(position)) % 26 for p in self.ring_peg_letters]
436
437 def advance(self):
438 super(Wheel, self).advance()
439 self.peg_positions = [(p - 1) % 26 for p in self.peg_positions]
440
441
442 class Enigma(object):
443 """An Enigma machine.
444
445 >>> enigma = Enigma(reflector_b_spec, \
446 wheel_i_spec, wheel_i_pegs, \
447 wheel_ii_spec, wheel_ii_pegs, \
448 wheel_iii_spec, wheel_iii_pegs, \
449 1, 1, 1, \
450 '')
451 >>> enigma.set_wheels('a', 'a', 't')
452 >>> enigma.wheel_positions
453 (0, 0, 19)
454 >>> cat(enigma.wheel_positions_l)
455 'aat'
456 >>> enigma.peg_positions
457 ([16], [4], [2])
458 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
459 'puvioztjdhxmlyeawsrgbcqknf'
460
461 >>> enigma.advance()
462 >>> enigma.wheel_positions
463 (0, 0, 20)
464 >>> cat(enigma.wheel_positions_l)
465 'aau'
466 >>> enigma.peg_positions
467 ([16], [4], [1])
468 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
469 'baigpldqcowfyzjehvtsxrkumn'
470
471 >>> enigma.advance()
472 >>> enigma.wheel_positions
473 (0, 0, 21)
474 >>> cat(enigma.wheel_positions_l)
475 'aav'
476 >>> enigma.peg_positions
477 ([16], [4], [0])
478 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
479 'mnvfydiwgzsoablrxpkutchqej'
480
481 >>> enigma.advance()
482 >>> enigma.wheel_positions
483 (0, 1, 22)
484 >>> cat(enigma.wheel_positions_l)
485 'abw'
486 >>> enigma.peg_positions
487 ([16], [3], [25])
488 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
489 'ulfopcykswhbzvderqixanjtgm'
490
491 >>> enigma.advance()
492 >>> enigma.wheel_positions
493 (0, 1, 23)
494 >>> cat(enigma.wheel_positions_l)
495 'abx'
496 >>> enigma.peg_positions
497 ([16], [3], [24])
498 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
499 'qmwftdyovursbzhxaklejicpgn'
500
501 >>> enigma.advance()
502 >>> enigma.wheel_positions
503 (0, 1, 24)
504 >>> cat(enigma.wheel_positions_l)
505 'aby'
506 >>> enigma.peg_positions
507 ([16], [3], [23])
508 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
509 'oljmzxrvucybdqasngpwihtfke'
510
511
512
513
514 >>> enigma.set_wheels('a', 'd', 't')
515 >>> enigma.wheel_positions
516 (0, 3, 19)
517 >>> cat(enigma.wheel_positions_l)
518 'adt'
519 >>> enigma.peg_positions
520 ([16], [1], [2])
521 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
522 'zcbpqxwsjiuonmldethrkygfva'
523
524 >>> enigma.advance()
525 >>> enigma.wheel_positions
526 (0, 3, 20)
527 >>> cat(enigma.wheel_positions_l)
528 'adu'
529 >>> enigma.peg_positions
530 ([16], [1], [1])
531 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
532 'ehprawjbngotxikcsdqlzyfmvu'
533
534 >>> enigma.advance()
535 >>> enigma.wheel_positions
536 (0, 3, 21)
537 >>> cat(enigma.wheel_positions_l)
538 'adv'
539 >>> enigma.peg_positions
540 ([16], [1], [0])
541 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
542 'eqzxarpihmnvjkwgbfuyslodtc'
543
544 >>> enigma.advance()
545 >>> enigma.wheel_positions
546 (0, 4, 22)
547 >>> cat(enigma.wheel_positions_l)
548 'aew'
549 >>> enigma.peg_positions
550 ([16], [0], [25])
551 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
552 'qedcbtpluzmhkongavwfirsyxj'
553
554 >>> enigma.advance()
555 >>> enigma.wheel_positions
556 (1, 5, 23)
557 >>> cat(enigma.wheel_positions_l)
558 'bfx'
559 >>> enigma.peg_positions
560 ([15], [25], [24])
561 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
562 'iwuedhsfazqxytvrkpgncoblmj'
563
564 >>> enigma.advance()
565 >>> enigma.wheel_positions
566 (1, 5, 24)
567 >>> cat(enigma.wheel_positions_l)
568 'bfy'
569 >>> enigma.peg_positions
570 ([15], [25], [23])
571 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
572 'baknstqzrmcxjdvygiefwoulph'
573
574
575 >>> enigma.set_wheels('a', 'a', 'a')
576 >>> ct = enigma.encipher('testmessage')
577 >>> ct
578 'olpfhnvflyn'
579
580 >>> enigma.set_wheels('a', 'd', 't')
581 >>> ct = enigma.encipher('testmessage')
582 >>> ct
583 'lawnjgpwjik'
584
585
586 >>> enigma.set_wheels('b', 'd', 'q')
587 >>> ct = enigma.encipher('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
588 >>> ct
589 'kvmmwrlqlqsqpeugjrcxzwpfyiyybwloewrouvkpoztceuwtfjzqwpbqldttsr'
590 >>> enigma.left_wheel.position_l
591 'c'
592 >>> enigma.middle_wheel.position_l
593 'h'
594 >>> enigma.right_wheel.position_l
595 'a'
596
597 # Setting sheet line 31 from http://www.codesandciphers.org.uk/enigma/enigma3.htm
598 # Enigma simulation settings are
599 # http://enigma.louisedade.co.uk/enigma.html?m3;b;b153;AFTX;AJEU;AU-BG-EY-FP-HL-IN-JZ-OS-QR-TX
600 >>> enigma31 = Enigma(reflector_b_spec, \
601 wheel_i_spec, wheel_i_pegs, \
602 wheel_v_spec, wheel_v_pegs, \
603 wheel_iii_spec, wheel_iii_pegs, \
604 6, 20, 24, \
605 'ua pf rq so ni ey bg hl tx zj')
606
607 >>> enigma31.set_wheels('j', 'e', 'u')
608
609 >>> enigma31.advance()
610 >>> enigma31.wheel_positions
611 (4, 11, 24)
612 >>> cat(enigma31.wheel_positions_l)
613 'jev'
614 >>> enigma31.peg_positions
615 ([7], [21], [0])
616 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
617 'mvqjlyowkdieasgzcunxrbhtfp'
618
619 >>> enigma31.advance()
620 >>> enigma31.wheel_positions
621 (4, 12, 25)
622 >>> cat(enigma31.wheel_positions_l)
623 'jfw'
624 >>> enigma31.peg_positions
625 ([7], [20], [25])
626 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
627 'sjolzuyvrbwdpxcmtiaqfhknge'
628
629 >>> enigma31.advance()
630 >>> enigma31.wheel_positions
631 (4, 12, 0)
632 >>> cat(enigma31.wheel_positions_l)
633 'jfx'
634 >>> enigma31.peg_positions
635 ([7], [20], [24])
636 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
637 'qrxedkoywufmlvgsabpzjnicht'
638
639 >>> enigma31.advance()
640 >>> enigma31.wheel_positions
641 (4, 12, 1)
642 >>> cat(enigma31.wheel_positions_l)
643 'jfy'
644 >>> enigma31.peg_positions
645 ([7], [20], [23])
646 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
647 'hpsukliagqefwvtbjxcodnmrzy'
648
649 >>> enigma31.advance()
650 >>> enigma31.wheel_positions
651 (4, 12, 2)
652 >>> cat(enigma31.wheel_positions_l)
653 'jfz'
654 >>> enigma31.peg_positions
655 ([7], [20], [22])
656 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
657 'zevnbpyqowrtxdifhkulscjmga'
658
659
660 >>> enigma31.set_wheels('i', 'd', 'z')
661
662 >>> enigma31.advance()
663 >>> enigma31.wheel_positions
664 (3, 10, 3)
665 >>> cat(enigma31.wheel_positions_l)
666 'ida'
667 >>> enigma31.peg_positions
668 ([8], [22], [21])
669 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
670 'ikhpqrvcambzjondefwyxgsutl'
671
672 >>> enigma31.advance()
673 >>> enigma31.wheel_positions
674 (3, 10, 4)
675 >>> cat(enigma31.wheel_positions_l)
676 'idb'
677 >>> enigma31.peg_positions
678 ([8], [22], [20])
679 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
680 'cdabskhgzwfmlqvunyexpojtri'
681
682 >>> enigma31.advance()
683 >>> enigma31.wheel_positions
684 (3, 10, 5)
685 >>> cat(enigma31.wheel_positions_l)
686 'idc'
687 >>> enigma31.peg_positions
688 ([8], [22], [19])
689 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
690 'pcbwiqhgemyvjsuaftnroldzkx'
691
692 >>> enigma31.advance()
693 >>> enigma31.wheel_positions
694 (3, 10, 6)
695 >>> cat(enigma31.wheel_positions_l)
696 'idd'
697 >>> enigma31.peg_positions
698 ([8], [22], [18])
699 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
700 'xcbfvdnouptmlghjzwykierasq'
701
702 >>> enigma31.advance()
703 >>> enigma31.wheel_positions
704 (3, 10, 7)
705 >>> cat(enigma31.wheel_positions_l)
706 'ide'
707 >>> enigma31.peg_positions
708 ([8], [22], [17])
709 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
710 'xfvglbdynuseriwqpmkzjcoaht'
711
712 >>> enigma31.advance()
713 >>> enigma31.wheel_positions
714 (3, 10, 8)
715 >>> cat(enigma31.wheel_positions_l)
716 'idf'
717 >>> enigma31.peg_positions
718 ([8], [22], [16])
719 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
720 'tfpqlbouynsewjgcdxkahzmriv'
721
722 >>> enigma31.advance()
723 >>> enigma31.wheel_positions
724 (3, 10, 9)
725 >>> cat(enigma31.wheel_positions_l)
726 'idg'
727 >>> enigma31.peg_positions
728 ([8], [22], [15])
729 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
730 'cjaunvlwtbygzexrspqidfhokm'
731
732 >>> enigma31.advance()
733 >>> enigma31.wheel_positions
734 (3, 10, 10)
735 >>> cat(enigma31.wheel_positions_l)
736 'idh'
737 >>> enigma31.peg_positions
738 ([8], [22], [14])
739 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
740 'yltxkrqvowebzpingfucshjdam'
741
742 >>> enigma31.advance()
743 >>> enigma31.wheel_positions
744 (3, 10, 11)
745 >>> cat(enigma31.wheel_positions_l)
746 'idi'
747 >>> enigma31.peg_positions
748 ([8], [22], [13])
749 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
750 'myktluzrnxceaiqsohpdfwvjbg'
751
752 >>> enigma31.advance()
753 >>> enigma31.wheel_positions
754 (3, 10, 12)
755 >>> cat(enigma31.wheel_positions_l)
756 'idj'
757 >>> enigma31.peg_positions
758 ([8], [22], [12])
759 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
760 'pynjrmiugdqxfcvakewzhoslbt'
761
762 >>> enigma31.advance()
763 >>> enigma31.wheel_positions
764 (3, 10, 13)
765 >>> cat(enigma31.wheel_positions_l)
766 'idk'
767 >>> enigma31.peg_positions
768 ([8], [22], [11])
769 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
770 'mwvedyplnoxhaijgrqtszcbkfu'
771
772 >>> enigma31.advance()
773 >>> enigma31.wheel_positions
774 (3, 10, 14)
775 >>> cat(enigma31.wheel_positions_l)
776 'idl'
777 >>> enigma31.peg_positions
778 ([8], [22], [10])
779 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
780 'qcbrfeutvoxpnmjladzhgiykws'
781
782 >>> enigma31.advance()
783 >>> enigma31.wheel_positions
784 (3, 10, 15)
785 >>> cat(enigma31.wheel_positions_l)
786 'idm'
787 >>> enigma31.peg_positions
788 ([8], [22], [9])
789 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
790 'dnoahryetsmukbcvwfjilpqzgx'
791
792 >>> enigma31.advance()
793 >>> enigma31.wheel_positions
794 (3, 10, 16)
795 >>> cat(enigma31.wheel_positions_l)
796 'idn'
797 >>> enigma31.peg_positions
798 ([8], [22], [8])
799 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
800 'nidcfehgbqsovalyjzkxwmutpr'
801
802 >>> enigma31.advance()
803 >>> enigma31.wheel_positions
804 (3, 10, 17)
805 >>> cat(enigma31.wheel_positions_l)
806 'ido'
807 >>> enigma31.peg_positions
808 ([8], [22], [7])
809 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
810 'joifxdulcarhzpbntkwqgysevm'
811
812 >>> enigma31.advance()
813 >>> enigma31.wheel_positions
814 (3, 10, 18)
815 >>> cat(enigma31.wheel_positions_l)
816 'idp'
817 >>> enigma31.peg_positions
818 ([8], [22], [6])
819 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
820 'ptnlsxvozmwdjchayuebrgkfqi'
821
822 >>> enigma31.advance()
823 >>> enigma31.wheel_positions
824 (3, 10, 19)
825 >>> cat(enigma31.wheel_positions_l)
826 'idq'
827 >>> enigma31.peg_positions
828 ([8], [22], [5])
829 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
830 'slwopzqnmxybihdeguavrtcjkf'
831
832 >>> enigma31.advance()
833 >>> enigma31.wheel_positions
834 (3, 10, 20)
835 >>> cat(enigma31.wheel_positions_l)
836 'idr'
837 >>> enigma31.peg_positions
838 ([8], [22], [4])
839 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
840 'hcbedwlamzogixkytsrqvufnpj'
841
842 >>> enigma31.advance()
843 >>> enigma31.wheel_positions
844 (3, 10, 21)
845 >>> cat(enigma31.wheel_positions_l)
846 'ids'
847 >>> enigma31.peg_positions
848 ([8], [22], [3])
849 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
850 'odxbjwzrmelkisavuhnyqpfctg'
851
852 >>> enigma31.advance()
853 >>> enigma31.wheel_positions
854 (3, 10, 22)
855 >>> cat(enigma31.wheel_positions_l)
856 'idt'
857 >>> enigma31.peg_positions
858 ([8], [22], [2])
859 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
860 'udgbfeclrwnhxksvtioqapjmzy'
861
862 >>> enigma31.advance()
863 >>> enigma31.wheel_positions
864 (3, 10, 23)
865 >>> cat(enigma31.wheel_positions_l)
866 'idu'
867 >>> enigma31.peg_positions
868 ([8], [22], [1])
869 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
870 'nrdczqxmowvshaiufblypkjgte'
871
872 >>> enigma31.advance()
873 >>> enigma31.wheel_positions
874 (3, 10, 24)
875 >>> cat(enigma31.wheel_positions_l)
876 'idv'
877 >>> enigma31.peg_positions
878 ([8], [22], [0])
879 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
880 'hkifjdoacebqtzgulyvmpsxwrn'
881
882 >>> enigma31.advance()
883 >>> enigma31.wheel_positions
884 (3, 11, 25)
885 >>> cat(enigma31.wheel_positions_l)
886 'iew'
887 >>> enigma31.peg_positions
888 ([8], [21], [25])
889 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
890 'yptzuhofqvnmlkgbixwcejsrad'
891
892 >>> enigma31.advance()
893 >>> enigma31.wheel_positions
894 (3, 11, 0)
895 >>> cat(enigma31.wheel_positions_l)
896 'iex'
897 >>> enigma31.peg_positions
898 ([8], [21], [24])
899 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
900 'vkdcwhqfjibzsptngumoraeyxl'
901
902 >>> enigma31.advance()
903 >>> enigma31.wheel_positions
904 (3, 11, 1)
905 >>> cat(enigma31.wheel_positions_l)
906 'iey'
907 >>> enigma31.peg_positions
908 ([8], [21], [23])
909 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
910 'wenpbqrouxlkychdfgzvitajms'
911
912
913 >>> enigma31.set_wheels('i', 'z', 'd')
914 >>> enigma31.encipher('verylongtestmessagewithanextrabitofmessageforgoodmeasure')
915 'apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki'
916 >>> enigma31.wheel_positions
917 (4, 9, 10)
918 >>> cat(enigma31.wheel_positions_l)
919 'jch'
920 >>> enigma31.peg_positions
921 ([7], [23], [14])
922 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
923 'mopnigfuesqwadbcktjrhylzvx'
924
925 >>> enigma31.set_wheels('i', 'z', 'd')
926 >>> enigma31.decipher('apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki')
927 'verylongtestmessagewithanextrabitofmessageforgoodmeasure'
928 """
929 def __init__(self, reflector_spec,
930 left_wheel_spec, left_wheel_pegs,
931 middle_wheel_spec, middle_wheel_pegs,
932 right_wheel_spec, right_wheel_pegs,
933 left_ring_setting, middle_ring_setting, right_ring_setting,
934 plugboard_setting):
935 self.reflector = Reflector(reflector_spec)
936 self.left_wheel = Wheel(left_wheel_spec, left_wheel_pegs, ring_setting=left_ring_setting)
937 self.middle_wheel = Wheel(middle_wheel_spec, middle_wheel_pegs, ring_setting=middle_ring_setting)
938 self.right_wheel = Wheel(right_wheel_spec, right_wheel_pegs, ring_setting=right_ring_setting)
939 self.plugboard = Plugboard(plugboard_setting)
940
941 def __getattribute__(self,name):
942 if name=='wheel_positions':
943 return self.left_wheel.position, self.middle_wheel.position, self.right_wheel.position
944 elif name=='wheel_positions_l':
945 return self.left_wheel.position_l, self.middle_wheel.position_l, self.right_wheel.position_l
946 elif name=='peg_positions':
947 return self.left_wheel.peg_positions, self.middle_wheel.peg_positions, self.right_wheel.peg_positions
948 else:
949 return object.__getattribute__(self, name)
950
951 def set_wheels(self, left_wheel_position, middle_wheel_position, right_wheel_position):
952 self.left_wheel.set_position(left_wheel_position)
953 self.middle_wheel.set_position(middle_wheel_position)
954 self.right_wheel.set_position(right_wheel_position)
955
956 def lookup(self, letter):
957 a = self.plugboard.forward(letter)
958 b = self.right_wheel.forward(a)
959 c = self.middle_wheel.forward(b)
960 d = self.left_wheel.forward(c)
961 e = self.reflector.forward(d)
962 f = self.left_wheel.backward(e)
963 g = self.middle_wheel.backward(f)
964 h = self.right_wheel.backward(g)
965 i = self.plugboard.backward(h)
966 return i
967
968 def advance(self):
969 advance_middle = False
970 advance_left = False
971 if 0 in self.right_wheel.peg_positions:
972 advance_middle = True
973 if 0 in self.middle_wheel.peg_positions:
974 advance_left = True
975 advance_middle = True
976 self.right_wheel.advance()
977 if advance_middle: self.middle_wheel.advance()
978 if advance_left: self.left_wheel.advance()
979
980 def encipher_letter(self, letter):
981 self.advance()
982 return self.lookup(letter)
983
984 def encipher(self, message):
985 enciphered = ''
986 for letter in clean(message):
987 enciphered += self.encipher_letter(letter)
988 return enciphered
989
990 decipher = encipher
991
992
993 # for i in range(26):
994 # enigma.advance()
995 # print('enigma.advance()')
996 # print("assert(enigma.wheel_positions == {})".format(enigma.wheel_positions))
997 # print("assert(cat(enigma.wheel_positions_l) == '{}')".format(cat(enigma.wheel_positions_l)))
998 # print("assert(enigma.peg_positions == {})".format(enigma.peg_positions))
999 # print("assert(cat(enigma.lookup(l) for l in string.ascii_lowercase) == '{}')".format(cat(enigma.lookup(l) for l in string.ascii_lowercase)))
1000 # print()
1001
1002
1003 ##################################
1004 # # Bombe
1005 ##################################
1006
1007 Signal = collections.namedtuple('Signal', ['bank', 'wire'])
1008 Connection = collections.namedtuple('Connection', ['banks', 'scrambler'])
1009 MenuItem = collections.namedtuple('MenuIem', ['before', 'after', 'number'])
1010
1011
1012 class Scrambler(object):
1013 def __init__(self, wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec,
1014 wheel1_pos='a', wheel2_pos='a', wheel3_pos='a'):
1015 self.wheel1 = SimpleWheel(wheel1_spec, position=wheel1_pos)
1016 self.wheel2 = SimpleWheel(wheel2_spec, position=wheel2_pos)
1017 self.wheel3 = SimpleWheel(wheel3_spec, position=wheel3_pos)
1018 self.reflector = Reflector(reflector_spec)
1019
1020 def __getattribute__(self, name):
1021 if name=='wheel_positions':
1022 return self.wheel1.position, self.wheel2.position, self.wheel3.position
1023 elif name=='wheel_positions_l':
1024 return self.wheel1.position_l, self.wheel2.position_l, self.wheel3.position_l
1025 else:
1026 return object.__getattribute__(self, name)
1027
1028 def advance(self, wheel1=False, wheel2=False, wheel3=True):
1029 if wheel1: self.wheel1.advance()
1030 if wheel2: self.wheel2.advance()
1031 if wheel3: self.wheel3.advance()
1032
1033 def lookup(self, letter):
1034 a = self.wheel3.forward(letter)
1035 b = self.wheel2.forward(a)
1036 c = self.wheel1.forward(b)
1037 d = self.reflector.forward(c)
1038 e = self.wheel1.backward(d)
1039 f = self.wheel2.backward(e)
1040 g = self.wheel3.backward(f)
1041 return g
1042
1043 def set_positions(self, wheel1_pos, wheel2_pos, wheel3_pos):
1044 self.wheel1.set_position(wheel1_pos)
1045 self.wheel2.set_position(wheel2_pos)
1046 self.wheel3.set_position(wheel3_pos)
1047
1048
1049 class Bombe(object):
1050 def __init__(self, wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec,
1051 menu=None, start_signal=None, use_diagonal_board=True,
1052 verify_plugboard=True):
1053 self.connections = []
1054 self.wheel1_spec = wheel1_spec
1055 self.wheel2_spec = wheel2_spec
1056 self.wheel3_spec = wheel3_spec
1057 self.reflector_spec = reflector_spec
1058 if menu:
1059 self.read_menu(menu)
1060 if start_signal:
1061 self.test_start = start_signal
1062 self.use_diagonal_board = use_diagonal_board
1063 self.verify_plugboard = verify_plugboard
1064
1065 def __getattribute__(self, name):
1066 if name=='wheel_positions':
1067 return self.connections[0].scrambler.wheel_positions
1068 elif name=='wheel_positions_l':
1069 return self.connections[0].scrambler.wheel_positions_l
1070 else:
1071 return object.__getattribute__(self, name)
1072
1073 def __call__(self, start_positions):
1074 return start_positions, self.test(initial_signal=self.test_start,
1075 start_positions=start_positions,
1076 use_diagonal_board=self.use_diagonal_board,
1077 verify_plugboard=self.verify_plugboard)
1078
1079 def add_connection(self, bank_before, bank_after, scrambler):
1080 self.connections += [Connection([bank_before, bank_after], scrambler)]
1081
1082 def read_menu(self, menu):
1083 for item in menu:
1084 scrambler = Scrambler(self.wheel1_spec, self.wheel2_spec, self.wheel3_spec,
1085 self.reflector_spec,
1086 wheel3_pos=unpos(item.number - 1))
1087 self.add_connection(item.before, item.after, scrambler)
1088 most_common_letter = (collections.Counter(m.before for m in menu) + \
1089 collections.Counter(m.after for m in menu)).most_common(1)[0][0]
1090 self.test_start = Signal(most_common_letter, most_common_letter)
1091
1092 def set_positions(self, wheel1_pos, wheel2_pos, wheel3_pos):
1093 for i, c in enumerate(self.connections):
1094 c.scrambler.set_positions(wheel1_pos, wheel2_pos, unpos(pos(wheel3_pos) + i))
1095
1096 def test(self, initial_signal=None, start_positions=None, use_diagonal_board=True,
1097 verify_plugboard=True):
1098 self.banks = {label:
1099 dict(zip(string.ascii_lowercase, [False]*len(string.ascii_lowercase)))
1100 for label in string.ascii_lowercase}
1101 if start_positions:
1102 self.set_positions(*start_positions)
1103 if not initial_signal:
1104 initial_signal = self.test_start
1105 self.pending = [initial_signal]
1106 self.propagate(use_diagonal_board)
1107 live_wire_count = len([self.banks[self.test_start.bank][w]
1108 for w in self.banks[self.test_start.bank]
1109 if self.banks[self.test_start.bank][w]])
1110 if live_wire_count < 26:
1111 if verify_plugboard:
1112 possibles = self.possible_plugboards()
1113 return all(s0.isdisjoint(s1) for s0 in possibles for s1 in possibles if s0 != s1)
1114 else:
1115 return True
1116 else:
1117 return False
1118
1119 def propagate(self, use_diagonal_board):
1120 while self.pending:
1121 current = self.pending[0]
1122 # print("processing", current)
1123 self.pending = self.pending[1:]
1124 if not self.banks[current.bank][current.wire]:
1125 self.banks[current.bank][current.wire] = True
1126 if use_diagonal_board:
1127 self.pending += [Signal(current.wire, current.bank)]
1128 for c in self.connections:
1129 if current.bank in c.banks:
1130 other_bank = [b for b in c.banks if b != current.bank][0]
1131 other_wire = c.scrambler.lookup(current.wire)
1132 # print(" adding", other_bank, other_wire, "because", c.banks)
1133 self.pending += [Signal(other_bank, other_wire)]
1134
1135 def run(self, run_start=None, wheel1_pos='a', wheel2_pos='a', wheel3_pos='a', use_diagonal_board=True):
1136 if not run_start:
1137 run_start = self.test_start
1138 self.solutions = []
1139 self.set_positions(wheel1_pos, wheel2_pos, wheel3_pos)
1140 for run_index in range(26*26*26):
1141 if self.test(initial_signal=run_start, use_diagonal_board=use_diagonal_board):
1142 self.solutions += [self.connections[0].scrambler.wheel_positions_l]
1143 advance3 = True
1144 advance2 = False
1145 advance1 = False
1146 if (run_index + 1) % 26 == 0: advance2 = True
1147 if (run_index + 1) % (26*26) == 0: advance1 = True
1148 for c in self.connections:
1149 c.scrambler.advance(advance1, advance2, advance3)
1150 return self.solutions
1151
1152 def possible_plugboards(self):
1153 possibles = set()
1154 for b in self.banks:
1155 active = [w for w in self.banks[b] if self.banks[b][w]]
1156 inactive = [w for w in self.banks[b] if not self.banks[b][w]]
1157 if len(active) == 1:
1158 possibles = possibles.union({frozenset((b, active[0]))})
1159 if len(inactive) == 1:
1160 possibles = possibles.union({frozenset((b, inactive[0]))})
1161 return possibles
1162
1163
1164 def make_menu(plaintext, ciphertext):
1165 return [MenuItem(p, c, i+1)
1166 for i, (p, c) in enumerate(zip(plaintext, ciphertext))]
1167
1168
1169 def run_multi_bombe(wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec, menu,
1170 start_signal=None, use_diagonal_board=True,
1171 verify_plugboard=True):
1172 allwheels = itertools.product(string.ascii_lowercase, repeat=3)
1173
1174 with multiprocessing.Pool() as pool:
1175 res = pool.map(Bombe(wheel1_spec, wheel2_spec, wheel3_spec,
1176 reflector_spec, menu=menu, start_signal=start_signal,
1177 use_diagonal_board=use_diagonal_board,
1178 verify_plugboard=verify_plugboard),
1179 allwheels)
1180 return [r[0] for r in res if r[1]]
1181
1182
1183 if __name__ == "__main__":
1184 import doctest
1185 # doctest.testmod(extraglobs={'lt': LetterTransformer(1, 'a')})
1186 doctest.testmod()
1187