4 ##################################
6 ##################################
7 # Specification from [Codes and Ciphers](http://www.codesandciphers.org.uk/enigma/rotorspec.htm) page.
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).
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).
17 import multiprocessing
20 # Some convenience functions
24 def clean(text
): return cat(l
.lower() for l
in text
if l
in string
.ascii_letters
)
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')
34 def unpos(number
): return chr(number
% 26 + ord('a'))
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'
50 wheel_iii_pegs
= ['v']
53 wheel_vi_pegs
= ['z', 'm']
54 wheel_vii_pegs
= ['z', 'm']
55 wheel_viii_pegs
= ['z', 'm']
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'
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
67 >>> lt = LetterTransformer([('z', 'a')] + [(l, string.ascii_lowercase[i+1]) \
68 for i, l in enumerate(string.ascii_lowercase[:-1])], \
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]
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]
75 >>> lt = LetterTransformer(cat(collections.OrderedDict.fromkeys('zyxwc' + string.ascii_lowercase)))
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]
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'
85 def __init__(self
, specification
, raw_transform
=False):
87 transform
= specification
89 transform
= self
.parse_specification(specification
)
90 self
.validate_transform(transform
)
91 self
.make_transform_map(transform
)
93 def parse_specification(self
, specification
):
94 return list(zip(string
.ascii_lowercase
, clean(specification
)))
95 # return specification
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
)))
104 raise ValueError("Not all mappings in transform "
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")
111 def make_empty_transform(self
):
112 self
.forward_map
= [0] * 26
113 self
.backward_map
= [0] * 26
115 def make_transform_map(self
, transform
):
116 self
.make_empty_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
122 def forward(self
, letter
):
123 if letter
in string
.ascii_lowercase
:
124 return unpos(self
.forward_map
[pos(letter
)])
128 def backward(self
, letter
):
129 if letter
in string
.ascii_lowercase
:
130 return unpos(self
.backward_map
[pos(letter
)])
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
140 >>> pb = Plugboard('ua pf rq so ni ey bg hl tx zj'.upper())
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
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'
150 def parse_specification(self
, specification
):
151 return [tuple(clean(p
)) for p
in specification
.split()]
153 def validate_transform(self
, transform
):
154 """A set of pairs, of from-to"""
157 raise ValueError("Not all mappings in transform"
160 def make_empty_transform(self
):
161 self
.forward_map
= list(range(26))
162 self
.backward_map
= list(range(26))
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
)
171 class Reflector(Plugboard
):
172 """A reflector is a plugboard that requires 13 transforms.
174 >>> reflector_b = Reflector(reflector_b_spec)
175 >>> reflector_b.forward_map == reflector_b.backward_map
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'
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")
192 super(Reflector
, self
).validate_transform(transform
)
193 except ValueError as v
:
194 raise ValueError("Not all mappings in reflector have two elements")
199 class SimpleWheel(LetterTransformer
):
200 """A wheel is a transform that rotates.
202 Looking from the right, letters go in sequence a-b-c clockwise around the
205 The position of the wheel is the number of spaces anticlockwise the wheel
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.
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'
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'
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'
235 >>> wheel_3.position_l
238 >>> for _ in range(24): wheel_3.advance()
241 >>> wheel_3.position_l
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'
248 >>> wheel_3.advance()
251 >>> wheel_3.position_l
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'
258 def __init__(self
, transform
, position
='a', raw_transform
=False):
259 super(SimpleWheel
, self
).__init
__(transform
, raw_transform
)
260 self
.set_position(position
)
262 def __getattribute__(self
,name
):
263 if name
=='position_l':
264 return unpos(self
.position
)
266 return object.__getattribute
__(self
, name
)
268 def set_position(self
, position
):
269 self
.position
= ord(position
) - ord('a')
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
))
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
))
284 self
.position
= (self
.position
+ 1) % 26
288 class Wheel(SimpleWheel
):
289 """A wheel with a movable ring.
291 The ring holds the letters and the pegs that turn other wheels. The core
292 holds the wiring that does the transformation.
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
298 The position of the wheel is the position of the core (the transforms)
299 relative to the neutral position.
301 The position_l is the position of the ring, or what would be observed
302 by the user of the Enigma machine.
304 The peg_positions are the number of advances of this wheel before it will
305 advance the next wheel.
307 >>> wheel_3 = Wheel(wheel_iii_spec, wheel_iii_pegs, position='b', ring_setting=1)
310 >>> wheel_3.peg_positions
312 >>> wheel_3.position_l
314 >>> wheel_3.advance()
317 >>> wheel_3.peg_positions
319 >>> wheel_3.position_l
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'
329 >>> 11 in wheel_6.peg_positions
331 >>> 24 in wheel_6.peg_positions
333 >>> wheel_6.position_l
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'
343 >>> 10 in wheel_6.peg_positions
345 >>> 23 in wheel_6.peg_positions
347 >>> wheel_6.position_l
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'
357 >>> 1 in wheel_6.peg_positions
359 >>> 14 in wheel_6.peg_positions
361 >>> wheel_6.position_l
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'
371 >>> 0 in wheel_6.peg_positions
373 >>> 13 in wheel_6.peg_positions
375 >>> wheel_6.position_l
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'
385 >>> 25 in wheel_6.peg_positions
387 >>> 12 in wheel_6.peg_positions
389 >>> wheel_6.position_l
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'
399 >>> 24 in wheel_6.peg_positions
401 >>> 11 in wheel_6.peg_positions
403 >>> wheel_6.position_l
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'
413 >>> 23 in wheel_6.peg_positions
415 >>> 10 in wheel_6.peg_positions
417 >>> wheel_6.position_l
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
)
427 def __getattribute__(self
,name
):
428 if name
=='position_l':
429 return unpos(self
.position
+ self
.ring_setting
- 1)
431 return object.__getattribute
__(self
, name
)
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
]
438 super(Wheel
, self
).advance()
439 self
.peg_positions
= [(p
- 1) % 26 for p
in self
.peg_positions
]
442 class Enigma(object):
443 """An Enigma machine.
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, \
451 >>> enigma.set_wheels('a', 'a', 't')
452 >>> enigma.wheel_positions
454 >>> cat(enigma.wheel_positions_l)
456 >>> enigma.peg_positions
458 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
459 'puvioztjdhxmlyeawsrgbcqknf'
462 >>> enigma.wheel_positions
464 >>> cat(enigma.wheel_positions_l)
466 >>> enigma.peg_positions
468 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
469 'baigpldqcowfyzjehvtsxrkumn'
472 >>> enigma.wheel_positions
474 >>> cat(enigma.wheel_positions_l)
476 >>> enigma.peg_positions
478 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
479 'mnvfydiwgzsoablrxpkutchqej'
482 >>> enigma.wheel_positions
484 >>> cat(enigma.wheel_positions_l)
486 >>> enigma.peg_positions
488 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
489 'ulfopcykswhbzvderqixanjtgm'
492 >>> enigma.wheel_positions
494 >>> cat(enigma.wheel_positions_l)
496 >>> enigma.peg_positions
498 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
499 'qmwftdyovursbzhxaklejicpgn'
502 >>> enigma.wheel_positions
504 >>> cat(enigma.wheel_positions_l)
506 >>> enigma.peg_positions
508 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
509 'oljmzxrvucybdqasngpwihtfke'
514 >>> enigma.set_wheels('a', 'd', 't')
515 >>> enigma.wheel_positions
517 >>> cat(enigma.wheel_positions_l)
519 >>> enigma.peg_positions
521 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
522 'zcbpqxwsjiuonmldethrkygfva'
525 >>> enigma.wheel_positions
527 >>> cat(enigma.wheel_positions_l)
529 >>> enigma.peg_positions
531 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
532 'ehprawjbngotxikcsdqlzyfmvu'
535 >>> enigma.wheel_positions
537 >>> cat(enigma.wheel_positions_l)
539 >>> enigma.peg_positions
541 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
542 'eqzxarpihmnvjkwgbfuyslodtc'
545 >>> enigma.wheel_positions
547 >>> cat(enigma.wheel_positions_l)
549 >>> enigma.peg_positions
551 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
552 'qedcbtpluzmhkongavwfirsyxj'
555 >>> enigma.wheel_positions
557 >>> cat(enigma.wheel_positions_l)
559 >>> enigma.peg_positions
561 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
562 'iwuedhsfazqxytvrkpgncoblmj'
565 >>> enigma.wheel_positions
567 >>> cat(enigma.wheel_positions_l)
569 >>> enigma.peg_positions
571 >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
572 'baknstqzrmcxjdvygiefwoulph'
575 >>> enigma.set_wheels('a', 'a', 'a')
576 >>> ct = enigma.encipher('testmessage')
580 >>> enigma.set_wheels('a', 'd', 't')
581 >>> ct = enigma.encipher('testmessage')
586 >>> enigma.set_wheels('b', 'd', 'q')
587 >>> ct = enigma.encipher('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
589 'kvmmwrlqlqsqpeugjrcxzwpfyiyybwloewrouvkpoztceuwtfjzqwpbqldttsr'
590 >>> enigma.left_wheel.position_l
592 >>> enigma.middle_wheel.position_l
594 >>> enigma.right_wheel.position_l
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, \
605 'ua pf rq so ni ey bg hl tx zj')
607 >>> enigma31.set_wheels('j', 'e', 'u')
609 >>> enigma31.advance()
610 >>> enigma31.wheel_positions
612 >>> cat(enigma31.wheel_positions_l)
614 >>> enigma31.peg_positions
616 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
617 'mvqjlyowkdieasgzcunxrbhtfp'
619 >>> enigma31.advance()
620 >>> enigma31.wheel_positions
622 >>> cat(enigma31.wheel_positions_l)
624 >>> enigma31.peg_positions
626 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
627 'sjolzuyvrbwdpxcmtiaqfhknge'
629 >>> enigma31.advance()
630 >>> enigma31.wheel_positions
632 >>> cat(enigma31.wheel_positions_l)
634 >>> enigma31.peg_positions
636 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
637 'qrxedkoywufmlvgsabpzjnicht'
639 >>> enigma31.advance()
640 >>> enigma31.wheel_positions
642 >>> cat(enigma31.wheel_positions_l)
644 >>> enigma31.peg_positions
646 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
647 'hpsukliagqefwvtbjxcodnmrzy'
649 >>> enigma31.advance()
650 >>> enigma31.wheel_positions
652 >>> cat(enigma31.wheel_positions_l)
654 >>> enigma31.peg_positions
656 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
657 'zevnbpyqowrtxdifhkulscjmga'
660 >>> enigma31.set_wheels('i', 'd', 'z')
662 >>> enigma31.advance()
663 >>> enigma31.wheel_positions
665 >>> cat(enigma31.wheel_positions_l)
667 >>> enigma31.peg_positions
669 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
670 'ikhpqrvcambzjondefwyxgsutl'
672 >>> enigma31.advance()
673 >>> enigma31.wheel_positions
675 >>> cat(enigma31.wheel_positions_l)
677 >>> enigma31.peg_positions
679 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
680 'cdabskhgzwfmlqvunyexpojtri'
682 >>> enigma31.advance()
683 >>> enigma31.wheel_positions
685 >>> cat(enigma31.wheel_positions_l)
687 >>> enigma31.peg_positions
689 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
690 'pcbwiqhgemyvjsuaftnroldzkx'
692 >>> enigma31.advance()
693 >>> enigma31.wheel_positions
695 >>> cat(enigma31.wheel_positions_l)
697 >>> enigma31.peg_positions
699 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
700 'xcbfvdnouptmlghjzwykierasq'
702 >>> enigma31.advance()
703 >>> enigma31.wheel_positions
705 >>> cat(enigma31.wheel_positions_l)
707 >>> enigma31.peg_positions
709 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
710 'xfvglbdynuseriwqpmkzjcoaht'
712 >>> enigma31.advance()
713 >>> enigma31.wheel_positions
715 >>> cat(enigma31.wheel_positions_l)
717 >>> enigma31.peg_positions
719 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
720 'tfpqlbouynsewjgcdxkahzmriv'
722 >>> enigma31.advance()
723 >>> enigma31.wheel_positions
725 >>> cat(enigma31.wheel_positions_l)
727 >>> enigma31.peg_positions
729 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
730 'cjaunvlwtbygzexrspqidfhokm'
732 >>> enigma31.advance()
733 >>> enigma31.wheel_positions
735 >>> cat(enigma31.wheel_positions_l)
737 >>> enigma31.peg_positions
739 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
740 'yltxkrqvowebzpingfucshjdam'
742 >>> enigma31.advance()
743 >>> enigma31.wheel_positions
745 >>> cat(enigma31.wheel_positions_l)
747 >>> enigma31.peg_positions
749 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
750 'myktluzrnxceaiqsohpdfwvjbg'
752 >>> enigma31.advance()
753 >>> enigma31.wheel_positions
755 >>> cat(enigma31.wheel_positions_l)
757 >>> enigma31.peg_positions
759 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
760 'pynjrmiugdqxfcvakewzhoslbt'
762 >>> enigma31.advance()
763 >>> enigma31.wheel_positions
765 >>> cat(enigma31.wheel_positions_l)
767 >>> enigma31.peg_positions
769 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
770 'mwvedyplnoxhaijgrqtszcbkfu'
772 >>> enigma31.advance()
773 >>> enigma31.wheel_positions
775 >>> cat(enigma31.wheel_positions_l)
777 >>> enigma31.peg_positions
779 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
780 'qcbrfeutvoxpnmjladzhgiykws'
782 >>> enigma31.advance()
783 >>> enigma31.wheel_positions
785 >>> cat(enigma31.wheel_positions_l)
787 >>> enigma31.peg_positions
789 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
790 'dnoahryetsmukbcvwfjilpqzgx'
792 >>> enigma31.advance()
793 >>> enigma31.wheel_positions
795 >>> cat(enigma31.wheel_positions_l)
797 >>> enigma31.peg_positions
799 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
800 'nidcfehgbqsovalyjzkxwmutpr'
802 >>> enigma31.advance()
803 >>> enigma31.wheel_positions
805 >>> cat(enigma31.wheel_positions_l)
807 >>> enigma31.peg_positions
809 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
810 'joifxdulcarhzpbntkwqgysevm'
812 >>> enigma31.advance()
813 >>> enigma31.wheel_positions
815 >>> cat(enigma31.wheel_positions_l)
817 >>> enigma31.peg_positions
819 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
820 'ptnlsxvozmwdjchayuebrgkfqi'
822 >>> enigma31.advance()
823 >>> enigma31.wheel_positions
825 >>> cat(enigma31.wheel_positions_l)
827 >>> enigma31.peg_positions
829 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
830 'slwopzqnmxybihdeguavrtcjkf'
832 >>> enigma31.advance()
833 >>> enigma31.wheel_positions
835 >>> cat(enigma31.wheel_positions_l)
837 >>> enigma31.peg_positions
839 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
840 'hcbedwlamzogixkytsrqvufnpj'
842 >>> enigma31.advance()
843 >>> enigma31.wheel_positions
845 >>> cat(enigma31.wheel_positions_l)
847 >>> enigma31.peg_positions
849 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
850 'odxbjwzrmelkisavuhnyqpfctg'
852 >>> enigma31.advance()
853 >>> enigma31.wheel_positions
855 >>> cat(enigma31.wheel_positions_l)
857 >>> enigma31.peg_positions
859 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
860 'udgbfeclrwnhxksvtioqapjmzy'
862 >>> enigma31.advance()
863 >>> enigma31.wheel_positions
865 >>> cat(enigma31.wheel_positions_l)
867 >>> enigma31.peg_positions
869 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
870 'nrdczqxmowvshaiufblypkjgte'
872 >>> enigma31.advance()
873 >>> enigma31.wheel_positions
875 >>> cat(enigma31.wheel_positions_l)
877 >>> enigma31.peg_positions
879 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
880 'hkifjdoacebqtzgulyvmpsxwrn'
882 >>> enigma31.advance()
883 >>> enigma31.wheel_positions
885 >>> cat(enigma31.wheel_positions_l)
887 >>> enigma31.peg_positions
889 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
890 'yptzuhofqvnmlkgbixwcejsrad'
892 >>> enigma31.advance()
893 >>> enigma31.wheel_positions
895 >>> cat(enigma31.wheel_positions_l)
897 >>> enigma31.peg_positions
899 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
900 'vkdcwhqfjibzsptngumoraeyxl'
902 >>> enigma31.advance()
903 >>> enigma31.wheel_positions
905 >>> cat(enigma31.wheel_positions_l)
907 >>> enigma31.peg_positions
909 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
910 'wenpbqrouxlkychdfgzvitajms'
913 >>> enigma31.set_wheels('i', 'z', 'd')
914 >>> enigma31.encipher('verylongtestmessagewithanextrabitofmessageforgoodmeasure')
915 'apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki'
916 >>> enigma31.wheel_positions
918 >>> cat(enigma31.wheel_positions_l)
920 >>> enigma31.peg_positions
922 >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
923 'mopnigfuesqwadbcktjrhylzvx'
925 >>> enigma31.set_wheels('i', 'z', 'd')
926 >>> enigma31.decipher('apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki')
927 'verylongtestmessagewithanextrabitofmessageforgoodmeasure'
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
,
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
)
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
949 return object.__getattribute
__(self
, name
)
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
)
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
)
969 advance_middle
= False
971 if 0 in self
.right_wheel
.peg_positions
:
972 advance_middle
= True
973 if 0 in self
.middle_wheel
.peg_positions
:
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()
980 def encipher_letter(self
, letter
):
982 return self
.lookup(letter
)
984 def encipher(self
, message
):
986 for letter
in clean(message
):
987 enciphered
+= self
.encipher_letter(letter
)
993 # for i in range(26):
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)))
1003 ##################################
1005 ##################################
1007 # Good explanation of [how the bombe worked](http://www.ellsbury.com/enigmabombe.htm) by Graham Ellsbury
1010 Signal
= collections
.namedtuple('Signal', ['bank', 'wire'])
1011 Connection
= collections
.namedtuple('Connection', ['banks', 'scrambler'])
1012 MenuItem
= collections
.namedtuple('MenuIem', ['before', 'after', 'number'])
1015 class Scrambler(object):
1016 def __init__(self
, wheel1_spec
, wheel2_spec
, wheel3_spec
, reflector_spec
,
1017 wheel1_pos
='a', wheel2_pos
='a', wheel3_pos
='a'):
1018 self
.wheel1
= SimpleWheel(wheel1_spec
, position
=wheel1_pos
)
1019 self
.wheel2
= SimpleWheel(wheel2_spec
, position
=wheel2_pos
)
1020 self
.wheel3
= SimpleWheel(wheel3_spec
, position
=wheel3_pos
)
1021 self
.reflector
= Reflector(reflector_spec
)
1023 def __getattribute__(self
, name
):
1024 if name
=='wheel_positions':
1025 return self
.wheel1
.position
, self
.wheel2
.position
, self
.wheel3
.position
1026 elif name
=='wheel_positions_l':
1027 return self
.wheel1
.position_l
, self
.wheel2
.position_l
, self
.wheel3
.position_l
1029 return object.__getattribute
__(self
, name
)
1031 def advance(self
, wheel1
=False, wheel2
=False, wheel3
=True):
1032 if wheel1
: self
.wheel1
.advance()
1033 if wheel2
: self
.wheel2
.advance()
1034 if wheel3
: self
.wheel3
.advance()
1036 def lookup(self
, letter
):
1037 a
= self
.wheel3
.forward(letter
)
1038 b
= self
.wheel2
.forward(a
)
1039 c
= self
.wheel1
.forward(b
)
1040 d
= self
.reflector
.forward(c
)
1041 e
= self
.wheel1
.backward(d
)
1042 f
= self
.wheel2
.backward(e
)
1043 g
= self
.wheel3
.backward(f
)
1046 def set_positions(self
, wheel1_pos
, wheel2_pos
, wheel3_pos
):
1047 self
.wheel1
.set_position(wheel1_pos
)
1048 self
.wheel2
.set_position(wheel2_pos
)
1049 self
.wheel3
.set_position(wheel3_pos
)
1052 class Bombe(object):
1053 def __init__(self
, wheel1_spec
, wheel2_spec
, wheel3_spec
, reflector_spec
,
1054 menu
=None, start_signal
=None, use_diagonal_board
=True,
1055 verify_plugboard
=True):
1056 self
.connections
= []
1057 self
.wheel1_spec
= wheel1_spec
1058 self
.wheel2_spec
= wheel2_spec
1059 self
.wheel3_spec
= wheel3_spec
1060 self
.reflector_spec
= reflector_spec
1062 self
.read_menu(menu
)
1064 self
.test_start
= start_signal
1065 self
.use_diagonal_board
= use_diagonal_board
1066 self
.verify_plugboard
= verify_plugboard
1068 def __getattribute__(self
, name
):
1069 if name
=='wheel_positions':
1070 return self
.connections
[0].scrambler
.wheel_positions
1071 elif name
=='wheel_positions_l':
1072 return self
.connections
[0].scrambler
.wheel_positions_l
1074 return object.__getattribute
__(self
, name
)
1076 def __call__(self
, start_positions
):
1077 return start_positions
, self
.test(initial_signal
=self
.test_start
,
1078 start_positions
=start_positions
,
1079 use_diagonal_board
=self
.use_diagonal_board
,
1080 verify_plugboard
=self
.verify_plugboard
)
1082 def add_connection(self
, bank_before
, bank_after
, scrambler
):
1083 self
.connections
+= [Connection([bank_before
, bank_after
], scrambler
)]
1085 def read_menu(self
, menu
):
1087 scrambler
= Scrambler(self
.wheel1_spec
, self
.wheel2_spec
, self
.wheel3_spec
,
1088 self
.reflector_spec
,
1089 wheel3_pos
=unpos(item
.number
- 1))
1090 self
.add_connection(item
.before
, item
.after
, scrambler
)
1091 most_common_letter
= (collections
.Counter(m
.before
for m
in menu
) + \
1092 collections
.Counter(m
.after
for m
in menu
)).most_common(1)[0][0]
1093 self
.test_start
= Signal(most_common_letter
, most_common_letter
)
1095 def set_positions(self
, wheel1_pos
, wheel2_pos
, wheel3_pos
):
1096 for i
, c
in enumerate(self
.connections
):
1097 c
.scrambler
.set_positions(wheel1_pos
, wheel2_pos
, unpos(pos(wheel3_pos
) + i
))
1099 def test(self
, initial_signal
=None, start_positions
=None, use_diagonal_board
=True,
1100 verify_plugboard
=True):
1101 self
.banks
= {label
:
1102 dict(zip(string
.ascii_lowercase
, [False]*len(string
.ascii_lowercase
)))
1103 for label
in string
.ascii_lowercase
}
1105 self
.set_positions(*start_positions
)
1106 if not initial_signal
:
1107 initial_signal
= self
.test_start
1108 self
.pending
= [initial_signal
]
1109 self
.propagate(use_diagonal_board
)
1110 live_wire_count
= len([self
.banks
[self
.test_start
.bank
][w
]
1111 for w
in self
.banks
[self
.test_start
.bank
]
1112 if self
.banks
[self
.test_start
.bank
][w
]])
1113 if live_wire_count
< 26:
1114 if verify_plugboard
:
1115 possibles
= self
.possible_plugboards()
1116 return all(s0
.isdisjoint(s1
) for s0
in possibles
for s1
in possibles
if s0
!= s1
)
1122 def propagate(self
, use_diagonal_board
):
1124 current
= self
.pending
[0]
1125 # print("processing", current)
1126 self
.pending
= self
.pending
[1:]
1127 if not self
.banks
[current
.bank
][current
.wire
]:
1128 self
.banks
[current
.bank
][current
.wire
] = True
1129 if use_diagonal_board
:
1130 self
.pending
+= [Signal(current
.wire
, current
.bank
)]
1131 for c
in self
.connections
:
1132 if current
.bank
in c
.banks
:
1133 other_bank
= [b
for b
in c
.banks
if b
!= current
.bank
][0]
1134 other_wire
= c
.scrambler
.lookup(current
.wire
)
1135 # print(" adding", other_bank, other_wire, "because", c.banks)
1136 self
.pending
+= [Signal(other_bank
, other_wire
)]
1138 def run(self
, run_start
=None, wheel1_pos
='a', wheel2_pos
='a', wheel3_pos
='a', use_diagonal_board
=True):
1140 run_start
= self
.test_start
1142 self
.set_positions(wheel1_pos
, wheel2_pos
, wheel3_pos
)
1143 for run_index
in range(26*26*26):
1144 if self
.test(initial_signal
=run_start
, use_diagonal_board
=use_diagonal_board
):
1145 self
.solutions
+= [self
.connections
[0].scrambler
.wheel_positions_l
]
1149 if (run_index
+ 1) % 26 == 0: advance2
= True
1150 if (run_index
+ 1) % (26*26) == 0: advance1
= True
1151 for c
in self
.connections
:
1152 c
.scrambler
.advance(advance1
, advance2
, advance3
)
1153 return self
.solutions
1155 def possible_plugboards(self
):
1157 for b
in self
.banks
:
1158 active
= [w
for w
in self
.banks
[b
] if self
.banks
[b
][w
]]
1159 inactive
= [w
for w
in self
.banks
[b
] if not self
.banks
[b
][w
]]
1160 if len(active
) == 1:
1161 possibles
= possibles
.union({frozenset((b
, active
[0]))})
1162 if len(inactive
) == 1:
1163 possibles
= possibles
.union({frozenset((b
, inactive
[0]))})
1167 def make_menu(plaintext
, ciphertext
):
1168 return [MenuItem(p
, c
, i
+1)
1169 for i
, (p
, c
) in enumerate(zip(plaintext
, ciphertext
))]
1172 def run_multi_bombe(wheel1_spec
, wheel2_spec
, wheel3_spec
, reflector_spec
, menu
,
1173 start_signal
=None, use_diagonal_board
=True,
1174 verify_plugboard
=True):
1175 allwheels
= itertools
.product(string
.ascii_lowercase
, repeat
=3)
1177 with multiprocessing
.Pool() as pool
:
1178 res
= pool
.map(Bombe(wheel1_spec
, wheel2_spec
, wheel3_spec
,
1179 reflector_spec
, menu
=menu
, start_signal
=start_signal
,
1180 use_diagonal_board
=use_diagonal_board
,
1181 verify_plugboard
=verify_plugboard
),
1183 return [r
[0] for r
in res
if r
[1]]
1186 if __name__
== "__main__":
1188 # doctest.testmod(extraglobs={'lt': LetterTransformer(1, 'a')})