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 Signal
= collections
.namedtuple('Signal', ['bank', 'wire'])
1008 Connection
= collections
.namedtuple('Connection', ['banks', 'scrambler'])
1009 MenuItem
= collections
.namedtuple('MenuIem', ['before', 'after', 'number'])
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
)
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
1026 return object.__getattribute
__(self
, name
)
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()
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
)
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
)
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
1059 self
.read_menu(menu
)
1061 self
.test_start
= start_signal
1062 self
.use_diagonal_board
= use_diagonal_board
1063 self
.verify_plugboard
= verify_plugboard
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
1071 return object.__getattribute
__(self
, name
)
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
)
1079 def add_connection(self
, bank_before
, bank_after
, scrambler
):
1080 self
.connections
+= [Connection([bank_before
, bank_after
], scrambler
)]
1082 def read_menu(self
, 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
)
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
))
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
}
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
)
1119 def propagate(self
, use_diagonal_board
):
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
)]
1135 def run(self
, run_start
=None, wheel1_pos
='a', wheel2_pos
='a', wheel3_pos
='a', use_diagonal_board
=True):
1137 run_start
= self
.test_start
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
]
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
1152 def possible_plugboards(self
):
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]))})
1164 def make_menu(plaintext
, ciphertext
):
1165 return [MenuItem(p
, c
, i
+1)
1166 for i
, (p
, c
) in enumerate(zip(plaintext
, ciphertext
))]
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)
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
),
1180 return [r
[0] for r
in res
if r
[1]]
1183 if __name__
== "__main__":
1185 # doctest.testmod(extraglobs={'lt': LetterTransformer(1, 'a')})