a26d1d3041dc6509049624870e2e5afd123b9923
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
21 logger
= logging
.getLogger('engima')
22 logger
.setLevel(logging
.WARNING
)
23 # logger.setLevel(logging.INFO)
24 # logger.setLevel(logging.DEBUG)
26 # create the logging file handler
27 fh
= logging
.FileHandler("enigma.log")
28 formatter
= logging
.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
29 fh
.setFormatter(formatter
)
31 # add handler to logger object
35 # Some convenience functions
39 def clean(text
): return cat(l
.lower() for l
in text
if l
in string
.ascii_letters
)
42 if letter
in string
.ascii_lowercase
:
43 return ord(letter
) - ord('a')
44 elif letter
in string
.ascii_uppercase
:
45 return ord(letter
) - ord('A')
49 def unpos(number
): return chr(number
% 26 + ord('a'))
52 wheel_i_spec
= 'ekmflgdqvzntowyhxuspaibrcj'
53 wheel_ii_spec
= 'ajdksiruxblhwtmcqgznpyfvoe'
54 wheel_iii_spec
= 'bdfhjlcprtxvznyeiwgakmusqo'
55 wheel_iv_spec
= 'esovpzjayquirhxlnftgkdcmwb'
56 wheel_v_spec
= 'vzbrgityupsdnhlxawmjqofeck'
57 wheel_vi_spec
= 'jpgvoumfyqbenhzrdkasxlictw'
58 wheel_vii_spec
= 'nzjhgrcxmyswboufaivlpekqdt'
59 wheel_viii_spec
= 'fkqhtlxocbjspdzramewniuygv'
60 beta_wheel_spec
= 'leyjvcnixwpbqmdrtakzgfuhos'
61 gamma_wheel_spec
= 'fsokanuerhmbtiycwlqpzxvgjd'
65 wheel_iii_pegs
= ['v']
68 wheel_vi_pegs
= ['z', 'm']
69 wheel_vii_pegs
= ['z', 'm']
70 wheel_viii_pegs
= ['z', 'm']
72 reflector_b_spec
= 'ay br cu dh eq fs gl ip jx kn mo tz vw'
73 reflector_c_spec
= 'af bv cp dj ei go hy kr lz mx nw tq su'
77 class LetterTransformer(object):
78 """A generic substitution cipher, that has different transforms in the
79 forward and backward directions. It requires that the transforms for all
82 def __init__(self
, specification
, raw_transform
=False):
84 transform
= specification
86 transform
= self
.parse_specification(specification
)
87 self
.validate_transform(transform
)
88 self
.make_transform_map(transform
)
90 def parse_specification(self
, specification
):
91 return list(zip(string
.ascii_lowercase
, clean(specification
)))
92 # return specification
94 def validate_transform(self
, transform
):
95 """A set of pairs, of from-to"""
96 if len(transform
) != 26:
97 raise ValueError("Transform specification has {} pairs, requires 26".
98 format(len(transform
)))
101 raise ValueError("Not all mappings in transform "
103 if len(set([p
[0] for p
in transform
])) != 26:
104 raise ValueError("Transform specification must list 26 origin letters")
105 if len(set([p
[1] for p
in transform
])) != 26:
106 raise ValueError("Transform specification must list 26 destination letters")
108 def make_empty_transform(self
):
109 self
.forward_map
= [0] * 26
110 self
.backward_map
= [0] * 26
112 def make_transform_map(self
, transform
):
113 self
.make_empty_transform()
115 self
.forward_map
[pos(p
[0])] = pos(p
[1])
116 self
.backward_map
[pos(p
[1])] = pos(p
[0])
117 return self
.forward_map
, self
.backward_map
119 def forward(self
, letter
):
120 if letter
in string
.ascii_lowercase
:
121 return unpos(self
.forward_map
[pos(letter
)])
125 def backward(self
, letter
):
126 if letter
in string
.ascii_lowercase
:
127 return unpos(self
.backward_map
[pos(letter
)])
132 class Plugboard(LetterTransformer
):
133 """A plugboard, a type of letter transformer where forward and backward
134 transforms are the same. If a letter isn't explicitly transformed, it is
137 def parse_specification(self
, specification
):
138 return [tuple(clean(p
)) for p
in specification
.split()]
140 def validate_transform(self
, transform
):
141 """A set of pairs, of from-to"""
144 raise ValueError("Not all mappings in transform"
147 def make_empty_transform(self
):
148 self
.forward_map
= list(range(26))
149 self
.backward_map
= list(range(26))
151 def make_transform_map(self
, transform
):
152 expanded_transform
= transform
+ [tuple(reversed(p
)) for p
in transform
]
153 return super(Plugboard
, self
).make_transform_map(expanded_transform
)
158 class Reflector(Plugboard
):
159 """A reflector is a plugboard that requires 13 transforms.
161 def validate_transform(self
, transform
):
162 if len(transform
) != 13:
163 raise ValueError("Reflector specification has {} pairs, requires 13".
164 format(len(transform
)))
165 if len(set([p
[0] for p
in transform
] +
166 [p
[1] for p
in transform
])) != 26:
167 raise ValueError("Reflector specification does not contain 26 letters")
169 super(Reflector
, self
).validate_transform(transform
)
170 except ValueError as v
:
171 raise ValueError("Not all mappings in reflector have two elements")
176 class SimpleWheel(LetterTransformer
):
177 """A wheel is a transform that rotates.
179 Looking from the right, letters go in sequence a-b-c clockwise around the
182 The position of the wheel is the number of spaces anticlockwise the wheel
185 Letter inputs and outputs are given relative to the frame holding the wheel,
186 so if the wheel is advanced three places, an input of 'p' will enter the
187 wheel on the position under the wheel's 'q' label.
189 def __init__(self
, transform
, position
='a', raw_transform
=False):
190 super(SimpleWheel
, self
).__init
__(transform
, raw_transform
)
191 self
.set_position(position
)
193 def __getattribute__(self
,name
):
194 if name
=='position_l':
195 return unpos(self
.position
)
197 return object.__getattribute
__(self
, name
)
199 def set_position(self
, position
):
200 if isinstance(position
, str):
201 # self.position = ord(position) - ord('a')
202 self
.position
= pos(position
)
204 self
.position
= position
206 def forward(self
, letter
):
207 if letter
in string
.ascii_lowercase
:
208 return unpos((self
.forward_map
[(pos(letter
) + self
.position
) % 26] - self
.position
))
212 def backward(self
, letter
):
213 if letter
in string
.ascii_lowercase
:
214 return unpos((self
.backward_map
[(pos(letter
) + self
.position
) % 26] - self
.position
))
219 self
.position
= (self
.position
+ 1) % 26
223 class Wheel(SimpleWheel
):
224 """A wheel with a movable ring.
226 The ring holds the letters and the pegs that turn other wheels. The core
227 holds the wiring that does the transformation.
229 The ring position is how many steps the core is turned relative to the ring.
230 This is one-based, so a ring setting of 1 means the core and ring are
233 The position of the wheel is the position of the core (the transforms)
234 relative to the neutral position.
236 The position_l is the position of the ring, or what would be observed
237 by the user of the Enigma machine.
239 The peg_positions are the number of advances of this wheel before it will
240 advance the next wheel.
243 def __init__(self
, transform
, ring_peg_letters
, ring_setting
=1, position
='a', raw_transform
=False):
244 self
.ring_peg_letters
= ring_peg_letters
245 self
.ring_setting
= ring_setting
246 super(Wheel
, self
).__init
__(transform
, position
=position
, raw_transform
=raw_transform
)
247 self
.set_position(position
)
249 def __getattribute__(self
,name
):
250 if name
=='position_l':
251 return unpos(self
.position
+ self
.ring_setting
- 1)
253 return object.__getattribute
__(self
, name
)
255 def set_position(self
, position
):
256 if isinstance(position
, str):
257 self
.position
= (pos(position
) - self
.ring_setting
+ 1) % 26
259 self
.position
= (position
- self
.ring_setting
) % 26
260 # self.peg_positions = [(pos(p) - pos(position)) % 26 for p in self.ring_peg_letters]
261 self
.peg_positions
= [(pos(p
) - (self
.position
+ self
.ring_setting
- 1)) % 26 for p
in self
.ring_peg_letters
]
264 super(Wheel
, self
).advance()
265 self
.peg_positions
= [(p
- 1) % 26 for p
in self
.peg_positions
]
268 class Enigma(object):
269 """An Enigma machine.
273 def __init__(self
, reflector_spec
,
274 left_wheel_spec
, left_wheel_pegs
,
275 middle_wheel_spec
, middle_wheel_pegs
,
276 right_wheel_spec
, right_wheel_pegs
,
277 left_ring_setting
, middle_ring_setting
, right_ring_setting
,
279 self
.reflector
= Reflector(reflector_spec
)
280 self
.left_wheel
= Wheel(left_wheel_spec
, left_wheel_pegs
, ring_setting
=left_ring_setting
)
281 self
.middle_wheel
= Wheel(middle_wheel_spec
, middle_wheel_pegs
, ring_setting
=middle_ring_setting
)
282 self
.right_wheel
= Wheel(right_wheel_spec
, right_wheel_pegs
, ring_setting
=right_ring_setting
)
283 self
.plugboard
= Plugboard(plugboard_setting
)
285 def __getattribute__(self
,name
):
286 if name
=='wheel_positions':
287 return self
.left_wheel
.position
, self
.middle_wheel
.position
, self
.right_wheel
.position
288 elif name
=='wheel_positions_l':
289 return self
.left_wheel
.position_l
, self
.middle_wheel
.position_l
, self
.right_wheel
.position_l
290 elif name
=='peg_positions':
291 return self
.left_wheel
.peg_positions
, self
.middle_wheel
.peg_positions
, self
.right_wheel
.peg_positions
293 return object.__getattribute
__(self
, name
)
295 def set_wheels(self
, left_wheel_position
, middle_wheel_position
, right_wheel_position
):
296 self
.left_wheel
.set_position(left_wheel_position
)
297 self
.middle_wheel
.set_position(middle_wheel_position
)
298 self
.right_wheel
.set_position(right_wheel_position
)
300 def lookup(self
, letter
):
301 a
= self
.plugboard
.forward(letter
)
302 b
= self
.right_wheel
.forward(a
)
303 c
= self
.middle_wheel
.forward(b
)
304 d
= self
.left_wheel
.forward(c
)
305 e
= self
.reflector
.forward(d
)
306 f
= self
.left_wheel
.backward(e
)
307 g
= self
.middle_wheel
.backward(f
)
308 h
= self
.right_wheel
.backward(g
)
309 i
= self
.plugboard
.backward(h
)
313 advance_middle
= False
315 if 0 in self
.right_wheel
.peg_positions
:
316 advance_middle
= True
317 if 0 in self
.middle_wheel
.peg_positions
:
319 advance_middle
= True
320 self
.right_wheel
.advance()
321 if advance_middle
: self
.middle_wheel
.advance()
322 if advance_left
: self
.left_wheel
.advance()
324 def encipher_letter(self
, letter
):
326 return self
.lookup(letter
)
328 def encipher(self
, message
):
330 for letter
in clean(message
):
331 enciphered
+= self
.encipher_letter(letter
)
337 # for i in range(26):
339 # print('enigma.advance()')
340 # print("assert(enigma.wheel_positions == {})".format(enigma.wheel_positions))
341 # print("assert(cat(enigma.wheel_positions_l) == '{}')".format(cat(enigma.wheel_positions_l)))
342 # print("assert(enigma.peg_positions == {})".format(enigma.peg_positions))
343 # print("assert(cat(enigma.lookup(l) for l in string.ascii_lowercase) == '{}')".format(cat(enigma.lookup(l) for l in string.ascii_lowercase)))
347 if __name__
== "__main__":
349 # doctest.testmod(extraglobs={'lt': LetterTransformer(1, 'a')})