Worked on Enigma, mainly changing how the notch positions are handled
[cipher-training.git] / cipher.py
1 import string
2 import collections
3 import math
4 from enum import Enum
5 from itertools import zip_longest, cycle, chain, count
6 import numpy as np
7 from numpy import matrix
8 from numpy import linalg
9 from language_models import *
10 import pprint
11
12
13 ## Utility functions
14 cat = ''.join
15 wcat = ' '.join
16
17 def pos(letter):
18 if letter in string.ascii_lowercase:
19 return ord(letter) - ord('a')
20 elif letter in string.ascii_uppercase:
21 return ord(letter) - ord('A')
22 else:
23 return ''
24
25 def unpos(number): return chr(number % 26 + ord('a'))
26
27
28 modular_division_table = [[0]*26 for _ in range(26)]
29 for a in range(26):
30 for b in range(26):
31 c = (a * b) % 26
32 modular_division_table[b][c] = a
33
34
35 def every_nth(text, n, fillvalue=''):
36 """Returns n strings, each of which consists of every nth character,
37 starting with the 0th, 1st, 2nd, ... (n-1)th character
38
39 >>> every_nth(string.ascii_lowercase, 5)
40 ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty']
41 >>> every_nth(string.ascii_lowercase, 1)
42 ['abcdefghijklmnopqrstuvwxyz']
43 >>> every_nth(string.ascii_lowercase, 26) # doctest: +NORMALIZE_WHITESPACE
44 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
45 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
46 >>> every_nth(string.ascii_lowercase, 5, fillvalue='!')
47 ['afkpuz', 'bglqv!', 'chmrw!', 'dinsx!', 'ejoty!']
48 """
49 split_text = chunks(text, n, fillvalue)
50 return [cat(l) for l in zip_longest(*split_text, fillvalue=fillvalue)]
51
52 def combine_every_nth(split_text):
53 """Reforms a text split into every_nth strings
54
55 >>> combine_every_nth(every_nth(string.ascii_lowercase, 5))
56 'abcdefghijklmnopqrstuvwxyz'
57 >>> combine_every_nth(every_nth(string.ascii_lowercase, 1))
58 'abcdefghijklmnopqrstuvwxyz'
59 >>> combine_every_nth(every_nth(string.ascii_lowercase, 26))
60 'abcdefghijklmnopqrstuvwxyz'
61 """
62 return cat([cat(l)
63 for l in zip_longest(*split_text, fillvalue='')])
64
65 def chunks(text, n, fillvalue=None):
66 """Split a text into chunks of n characters
67
68 >>> chunks('abcdefghi', 3)
69 ['abc', 'def', 'ghi']
70 >>> chunks('abcdefghi', 4)
71 ['abcd', 'efgh', 'i']
72 >>> chunks('abcdefghi', 4, fillvalue='!')
73 ['abcd', 'efgh', 'i!!!']
74 """
75 if fillvalue:
76 padding = fillvalue[0] * (n - len(text) % n)
77 else:
78 padding = ''
79 return [(text+padding)[i:i+n] for i in range(0, len(text), n)]
80
81 def transpose(items, transposition):
82 """Moves items around according to the given transposition
83
84 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
85 ['a', 'b', 'c', 'd']
86 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
87 ['d', 'b', 'c', 'a']
88 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
89 [13, 12, 14, 11, 15, 10]
90 """
91 transposed = [''] * len(transposition)
92 for p, t in enumerate(transposition):
93 transposed[p] = items[t]
94 return transposed
95
96 def untranspose(items, transposition):
97 """Undoes a transpose
98
99 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
100 ['a', 'b', 'c', 'd']
101 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
102 ['a', 'b', 'c', 'd']
103 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
104 [10, 11, 12, 13, 14, 15]
105 """
106 transposed = [''] * len(transposition)
107 for p, t in enumerate(transposition):
108 transposed[t] = items[p]
109 return transposed
110
111 def deduplicate(text):
112 return list(collections.OrderedDict.fromkeys(text))
113
114
115 def caesar_encipher_letter(accented_letter, shift):
116 """Encipher a letter, given a shift amount
117
118 >>> caesar_encipher_letter('a', 1)
119 'b'
120 >>> caesar_encipher_letter('a', 2)
121 'c'
122 >>> caesar_encipher_letter('b', 2)
123 'd'
124 >>> caesar_encipher_letter('x', 2)
125 'z'
126 >>> caesar_encipher_letter('y', 2)
127 'a'
128 >>> caesar_encipher_letter('z', 2)
129 'b'
130 >>> caesar_encipher_letter('z', -1)
131 'y'
132 >>> caesar_encipher_letter('a', -1)
133 'z'
134 >>> caesar_encipher_letter('A', 1)
135 'B'
136 >>> caesar_encipher_letter('é', 1)
137 'f'
138 """
139 # letter = unaccent(accented_letter)
140 # if letter in string.ascii_letters:
141 # if letter in string.ascii_uppercase:
142 # alphabet_start = ord('A')
143 # else:
144 # alphabet_start = ord('a')
145 # return chr(((ord(letter) - alphabet_start + shift) % 26) +
146 # alphabet_start)
147 # else:
148 # return letter
149
150 letter = unaccent(accented_letter)
151 if letter in string.ascii_letters:
152 cipherletter = unpos(pos(letter) + shift)
153 if letter in string.ascii_uppercase:
154 return cipherletter.upper()
155 else:
156 return cipherletter
157 else:
158 return letter
159
160 def caesar_decipher_letter(letter, shift):
161 """Decipher a letter, given a shift amount
162
163 >>> caesar_decipher_letter('b', 1)
164 'a'
165 >>> caesar_decipher_letter('b', 2)
166 'z'
167 """
168 return caesar_encipher_letter(letter, -shift)
169
170 def caesar_encipher(message, shift):
171 """Encipher a message with the Caesar cipher of given shift
172
173 >>> caesar_encipher('abc', 1)
174 'bcd'
175 >>> caesar_encipher('abc', 2)
176 'cde'
177 >>> caesar_encipher('abcxyz', 2)
178 'cdezab'
179 >>> caesar_encipher('ab cx yz', 2)
180 'cd ez ab'
181 >>> caesar_encipher('Héllo World!', 2)
182 'Jgnnq Yqtnf!'
183 """
184 enciphered = [caesar_encipher_letter(l, shift) for l in message]
185 return cat(enciphered)
186
187 def caesar_decipher(message, shift):
188 """Decipher a message with the Caesar cipher of given shift
189
190 >>> caesar_decipher('bcd', 1)
191 'abc'
192 >>> caesar_decipher('cde', 2)
193 'abc'
194 >>> caesar_decipher('cd ez ab', 2)
195 'ab cx yz'
196 >>> caesar_decipher('Jgnnq Yqtnf!', 2)
197 'Hello World!'
198 """
199 return caesar_encipher(message, -shift)
200
201 def affine_encipher_letter(accented_letter, multiplier=1, adder=0, one_based=True):
202 """Encipher a letter, given a multiplier and adder
203
204 >>> cat(affine_encipher_letter(l, 3, 5, True) \
205 for l in string.ascii_letters)
206 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE'
207 >>> cat(affine_encipher_letter(l, 3, 5, False) \
208 for l in string.ascii_letters)
209 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC'
210 """
211 # letter = unaccent(accented_letter)
212 # if letter in string.ascii_letters:
213 # if letter in string.ascii_uppercase:
214 # alphabet_start = ord('A')
215 # else:
216 # alphabet_start = ord('a')
217 # letter_number = ord(letter) - alphabet_start
218 # if one_based: letter_number += 1
219 # cipher_number = (letter_number * multiplier + adder) % 26
220 # if one_based: cipher_number -= 1
221 # return chr(cipher_number % 26 + alphabet_start)
222 # else:
223 # return letter
224 letter = unaccent(accented_letter)
225 if letter in string.ascii_letters:
226 letter_number = pos(letter)
227 if one_based: letter_number += 1
228 cipher_number = (letter_number * multiplier + adder) % 26
229 if one_based: cipher_number -= 1
230 if letter in string.ascii_uppercase:
231 return unpos(cipher_number).upper()
232 else:
233 return unpos(cipher_number)
234 else:
235 return letter
236
237 def affine_decipher_letter(letter, multiplier=1, adder=0, one_based=True):
238 """Encipher a letter, given a multiplier and adder
239
240 >>> cat(affine_decipher_letter(l, 3, 5, True) \
241 for l in 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE')
242 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
243 >>> cat(affine_decipher_letter(l, 3, 5, False) \
244 for l in 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC')
245 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
246 """
247 # if letter in string.ascii_letters:
248 # if letter in string.ascii_uppercase:
249 # alphabet_start = ord('A')
250 # else:
251 # alphabet_start = ord('a')
252 # cipher_number = ord(letter) - alphabet_start
253 # if one_based: cipher_number += 1
254 # plaintext_number = (
255 # modular_division_table[multiplier]
256 # [(cipher_number - adder) % 26])
257 # if one_based: plaintext_number -= 1
258 # return chr(plaintext_number % 26 + alphabet_start)
259 # else:
260 # return letter
261 if letter in string.ascii_letters:
262 cipher_number = pos(letter)
263 if one_based: cipher_number += 1
264 plaintext_number = (
265 modular_division_table[multiplier]
266 [(cipher_number - adder) % 26])
267 if one_based: plaintext_number -= 1
268 if letter in string.ascii_uppercase:
269 return unpos(plaintext_number).upper()
270 else:
271 return unpos(plaintext_number)
272 else:
273 return letter
274
275 def affine_encipher(message, multiplier=1, adder=0, one_based=True):
276 """Encipher a message
277
278 >>> affine_encipher('hours passed during which jerico tried every ' \
279 'trick he could think of', 15, 22, True)
280 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
281 """
282 enciphered = [affine_encipher_letter(l, multiplier, adder, one_based)
283 for l in message]
284 return cat(enciphered)
285
286 def affine_decipher(message, multiplier=1, adder=0, one_based=True):
287 """Decipher a message
288
289 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
290 'jfaoe ls omytd jlaxe mh', 15, 22, True)
291 'hours passed during which jerico tried every trick he could think of'
292 """
293 enciphered = [affine_decipher_letter(l, multiplier, adder, one_based)
294 for l in message]
295 return cat(enciphered)
296
297
298 class KeywordWrapAlphabet(Enum):
299 from_a = 1
300 from_last = 2
301 from_largest = 3
302
303
304 def keyword_cipher_alphabet_of(keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
305 """Find the cipher alphabet given a keyword.
306 wrap_alphabet controls how the rest of the alphabet is added
307 after the keyword.
308
309 >>> keyword_cipher_alphabet_of('bayes')
310 'bayescdfghijklmnopqrtuvwxz'
311 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_a)
312 'bayescdfghijklmnopqrtuvwxz'
313 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_last)
314 'bayestuvwxzcdfghijklmnopqr'
315 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_largest)
316 'bayeszcdfghijklmnopqrtuvwx'
317 """
318 if wrap_alphabet == KeywordWrapAlphabet.from_a:
319 cipher_alphabet = cat(deduplicate(sanitise(keyword) +
320 string.ascii_lowercase))
321 else:
322 if wrap_alphabet == KeywordWrapAlphabet.from_last:
323 last_keyword_letter = deduplicate(sanitise(keyword))[-1]
324 else:
325 last_keyword_letter = sorted(sanitise(keyword))[-1]
326 last_keyword_position = string.ascii_lowercase.find(
327 last_keyword_letter) + 1
328 cipher_alphabet = cat(
329 deduplicate(sanitise(keyword) +
330 string.ascii_lowercase[last_keyword_position:] +
331 string.ascii_lowercase))
332 return cipher_alphabet
333
334
335 def keyword_encipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
336 """Enciphers a message with a keyword substitution cipher.
337 wrap_alphabet controls how the rest of the alphabet is added
338 after the keyword.
339 0 : from 'a'
340 1 : from the last letter in the sanitised keyword
341 2 : from the largest letter in the sanitised keyword
342
343 >>> keyword_encipher('test message', 'bayes')
344 'rsqr ksqqbds'
345 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a)
346 'rsqr ksqqbds'
347 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last)
348 'lskl dskkbus'
349 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest)
350 'qspq jsppbcs'
351 """
352 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
353 cipher_translation = ''.maketrans(string.ascii_lowercase, cipher_alphabet)
354 return unaccent(message).lower().translate(cipher_translation)
355
356 def keyword_decipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a):
357 """Deciphers a message with a keyword substitution cipher.
358 wrap_alphabet controls how the rest of the alphabet is added
359 after the keyword.
360 0 : from 'a'
361 1 : from the last letter in the sanitised keyword
362 2 : from the largest letter in the sanitised keyword
363
364 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
365 'test message'
366 >>> keyword_decipher('rsqr ksqqbds', 'bayes', KeywordWrapAlphabet.from_a)
367 'test message'
368 >>> keyword_decipher('lskl dskkbus', 'bayes', KeywordWrapAlphabet.from_last)
369 'test message'
370 >>> keyword_decipher('qspq jsppbcs', 'bayes', KeywordWrapAlphabet.from_largest)
371 'test message'
372 """
373 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
374 cipher_translation = ''.maketrans(cipher_alphabet, string.ascii_lowercase)
375 return message.lower().translate(cipher_translation)
376
377
378 def vigenere_encipher(message, keyword):
379 """Vigenere encipher
380
381 >>> vigenere_encipher('hello', 'abc')
382 'hfnlp'
383 """
384 shifts = [ord(l) - ord('a') for l in sanitise(keyword)]
385 pairs = zip(message, cycle(shifts))
386 return cat([caesar_encipher_letter(l, k) for l, k in pairs])
387
388 def vigenere_decipher(message, keyword):
389 """Vigenere decipher
390
391 >>> vigenere_decipher('hfnlp', 'abc')
392 'hello'
393 """
394 shifts = [ord(l) - ord('a') for l in sanitise(keyword)]
395 pairs = zip(message, cycle(shifts))
396 return cat([caesar_decipher_letter(l, k) for l, k in pairs])
397
398 beaufort_encipher=vigenere_decipher
399 beaufort_decipher=vigenere_encipher
400
401
402 def transpositions_of(keyword):
403 """Finds the transpostions given by a keyword. For instance, the keyword
404 'clever' rearranges to 'celrv', so the first column (0) stays first, the
405 second column (1) moves to third, the third column (2) moves to second,
406 and so on.
407
408 If passed a tuple, assume it's already a transposition and just return it.
409
410 >>> transpositions_of('clever')
411 (0, 2, 1, 4, 3)
412 >>> transpositions_of('fred')
413 (3, 2, 0, 1)
414 >>> transpositions_of((3, 2, 0, 1))
415 (3, 2, 0, 1)
416 """
417 if isinstance(keyword, tuple):
418 return keyword
419 else:
420 key = deduplicate(keyword)
421 transpositions = tuple(key.index(l) for l in sorted(key))
422 return transpositions
423
424 def pad(message_len, group_len, fillvalue):
425 padding_length = group_len - message_len % group_len
426 if padding_length == group_len: padding_length = 0
427 padding = ''
428 for i in range(padding_length):
429 if callable(fillvalue):
430 padding += fillvalue()
431 else:
432 padding += fillvalue
433 return padding
434
435 def column_transposition_encipher(message, keyword, fillvalue=' ',
436 fillcolumnwise=False,
437 emptycolumnwise=False):
438 """Enciphers using the column transposition cipher.
439 Message is padded to allow all rows to be the same length.
440
441 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
442 'hlohr eltee '
443 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
444 'hellothere '
445 >>> column_transposition_encipher('hellothere', 'abcdef')
446 'hellothere '
447 >>> column_transposition_encipher('hellothere', 'abcde')
448 'hellothere'
449 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
450 'hellothere'
451 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
452 'hlohreltee'
453 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
454 'htehlelroe'
455 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
456 'hellothere'
457 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
458 'heotllrehe'
459 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
460 'holrhetlee'
461 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
462 'htleehoelr'
463 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
464 'hleolteher'
465 >>> column_transposition_encipher('hellothere', 'cleverly')
466 'hleolthre e '
467 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
468 'hleolthre!e!'
469 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
470 'hleolthre*e*'
471 """
472 transpositions = transpositions_of(keyword)
473 message += pad(len(message), len(transpositions), fillvalue)
474 if fillcolumnwise:
475 rows = every_nth(message, len(message) // len(transpositions))
476 else:
477 rows = chunks(message, len(transpositions))
478 transposed = [transpose(r, transpositions) for r in rows]
479 if emptycolumnwise:
480 return combine_every_nth(transposed)
481 else:
482 return cat(chain(*transposed))
483
484 def column_transposition_decipher(message, keyword, fillvalue=' ',
485 fillcolumnwise=False,
486 emptycolumnwise=False):
487 """Deciphers using the column transposition cipher.
488 Message is padded to allow all rows to be the same length.
489
490 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
491 'hellothere'
492 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
493 'hellothere'
494 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
495 'hellothere'
496 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
497 'hellothere'
498 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
499 'hellothere'
500 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
501 'hellothere'
502 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
503 'hellothere'
504 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
505 'hellothere'
506 """
507 transpositions = transpositions_of(keyword)
508 message += pad(len(message), len(transpositions), fillvalue)
509 if emptycolumnwise:
510 rows = every_nth(message, len(message) // len(transpositions))
511 else:
512 rows = chunks(message, len(transpositions))
513 untransposed = [untranspose(r, transpositions) for r in rows]
514 if fillcolumnwise:
515 return combine_every_nth(untransposed)
516 else:
517 return cat(chain(*untransposed))
518
519 def scytale_encipher(message, rows, fillvalue=' '):
520 """Enciphers using the scytale transposition cipher.
521 Message is padded with spaces to allow all rows to be the same length.
522
523 >>> scytale_encipher('thequickbrownfox', 3)
524 'tcnhkfeboqrxuo iw '
525 >>> scytale_encipher('thequickbrownfox', 4)
526 'tubnhirfecooqkwx'
527 >>> scytale_encipher('thequickbrownfox', 5)
528 'tubn hirf ecoo qkwx '
529 >>> scytale_encipher('thequickbrownfox', 6)
530 'tqcrnxhukof eibwo '
531 >>> scytale_encipher('thequickbrownfox', 7)
532 'tqcrnx hukof eibwo '
533 """
534 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
535 # return column_transposition_encipher(message, transpositions,
536 # fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
537 transpositions = [i for i in range(rows)]
538 return column_transposition_encipher(message, transpositions,
539 fillvalue=fillvalue, fillcolumnwise=True, emptycolumnwise=False)
540
541 def scytale_decipher(message, rows):
542 """Deciphers using the scytale transposition cipher.
543 Assumes the message is padded so that all rows are the same length.
544
545 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
546 'thequickbrownfox '
547 >>> scytale_decipher('tubnhirfecooqkwx', 4)
548 'thequickbrownfox'
549 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
550 'thequickbrownfox '
551 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
552 'thequickbrownfox '
553 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
554 'thequickbrownfox '
555 """
556 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
557 # return column_transposition_decipher(message, transpositions,
558 # fillcolumnwise=False, emptycolumnwise=True)
559 transpositions = [i for i in range(rows)]
560 return column_transposition_decipher(message, transpositions,
561 fillcolumnwise=True, emptycolumnwise=False)
562
563
564 def railfence_encipher(message, height, fillvalue=''):
565 """Railfence cipher.
566 Works by splitting the text into sections, then reading across them to
567 generate the rows in the cipher. The rows are then combined to form the
568 ciphertext.
569
570 Example: the plaintext "hellotherefriends", with a height of four, written
571 out in the railfence as
572 h h i
573 etere*
574 lorfns
575 l e d
576 (with the * showing the one character to finish the last section).
577 Each 'section' is two columns, but unfolded. In the example, the first
578 section is 'hellot'.
579
580 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 2, fillvalue='!')
581 'hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!'
582 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3, fillvalue='!')
583 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!'
584 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5, fillvalue='!')
585 'hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!'
586 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 10, fillvalue='!')
587 'hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!'
588 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3)
589 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece'
590 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5)
591 'hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp'
592 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 7)
593 'haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic'
594 """
595 sections = chunks(message, (height - 1) * 2, fillvalue=fillvalue)
596 n_sections = len(sections)
597 # Add the top row
598 rows = [cat([s[0] for s in sections])]
599 # process the middle rows of the grid
600 for r in range(1, height-1):
601 rows += [cat([s[r:r+1] + s[height*2-r-2:height*2-r-1] for s in sections])]
602 # process the bottom row
603 rows += [cat([s[height - 1:height] for s in sections])]
604 # rows += [wcat([s[height - 1] for s in sections])]
605 return cat(rows)
606
607 def railfence_decipher(message, height, fillvalue=''):
608 """Railfence decipher.
609 Works by reconstructing the grid used to generate the ciphertext, then
610 unfolding the sections so the text can be concatenated together.
611
612 Example: given the ciphertext 'hhieterelorfnsled' and a height of 4, first
613 work out that the second row has a character missing, find the rows of the
614 grid, then split the section into its two columns.
615
616 'hhieterelorfnsled' is split into
617 h h i
618 etere
619 lorfns
620 l e d
621 (spaces added for clarity), which is stored in 'rows'. This is then split
622 into 'down_rows' and 'up_rows':
623
624 down_rows:
625 hhi
626 eee
627 lrn
628 led
629
630 up_rows:
631 tr
632 ofs
633
634 These are then zipped together (after the up_rows are reversed) to recover
635 the plaintext.
636
637 Most of the procedure is about finding the correct lengths for each row then
638 splitting the ciphertext into those rows.
639
640 >>> railfence_decipher('hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!', 2).strip('!')
641 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
642 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!', 3).strip('!')
643 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
644 >>> railfence_decipher('hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!', 5).strip('!')
645 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
646 >>> railfence_decipher('hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!', 10).strip('!')
647 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
648 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece', 3)
649 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
650 >>> railfence_decipher('hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp', 5)
651 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
652 >>> railfence_decipher('haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic', 7)
653 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
654 """
655 # find the number and size of the sections, including how many characters
656 # are missing for a full grid
657 n_sections = math.ceil(len(message) / ((height - 1) * 2))
658 padding_to_add = n_sections * (height - 1) * 2 - len(message)
659 # row_lengths are for the both up rows and down rows
660 row_lengths = [n_sections] * (height - 1) * 2
661 for i in range((height - 1) * 2 - 1, (height - 1) * 2 - (padding_to_add + 1), -1):
662 row_lengths[i] -= 1
663 # folded_rows are the combined row lengths in the middle of the railfence
664 folded_row_lengths = [row_lengths[0]]
665 for i in range(1, height-1):
666 folded_row_lengths += [row_lengths[i] + row_lengths[-i]]
667 folded_row_lengths += [row_lengths[height - 1]]
668 # find the rows that form the railfence grid
669 rows = []
670 row_start = 0
671 for i in folded_row_lengths:
672 rows += [message[row_start:row_start + i]]
673 row_start += i
674 # split the rows into the 'down_rows' (those that form the first column of
675 # a section) and the 'up_rows' (those that ofrm the second column of a
676 # section).
677 down_rows = [rows[0]]
678 up_rows = []
679 for i in range(1, height-1):
680 down_rows += [cat([c for n, c in enumerate(rows[i]) if n % 2 == 0])]
681 up_rows += [cat([c for n, c in enumerate(rows[i]) if n % 2 == 1])]
682 down_rows += [rows[-1]]
683 up_rows.reverse()
684 return cat(c for r in zip_longest(*(down_rows + up_rows), fillvalue='') for c in r)
685
686 def make_cadenus_keycolumn(doubled_letters = 'vw', start='a', reverse=False):
687 """Makes the key column for a Cadenus cipher (the column down between the
688 rows of letters)
689
690 >>> make_cadenus_keycolumn()['a']
691 0
692 >>> make_cadenus_keycolumn()['b']
693 1
694 >>> make_cadenus_keycolumn()['c']
695 2
696 >>> make_cadenus_keycolumn()['v']
697 21
698 >>> make_cadenus_keycolumn()['w']
699 21
700 >>> make_cadenus_keycolumn()['z']
701 24
702 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['a']
703 1
704 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['b']
705 0
706 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['c']
707 24
708 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['i']
709 18
710 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['j']
711 18
712 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['v']
713 6
714 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['z']
715 2
716 """
717 index_to_remove = string.ascii_lowercase.find(doubled_letters[0])
718 short_alphabet = string.ascii_lowercase[:index_to_remove] + string.ascii_lowercase[index_to_remove+1:]
719 if reverse:
720 short_alphabet = cat(reversed(short_alphabet))
721 start_pos = short_alphabet.find(start)
722 rotated_alphabet = short_alphabet[start_pos:] + short_alphabet[:start_pos]
723 keycolumn = {l: i for i, l in enumerate(rotated_alphabet)}
724 keycolumn[doubled_letters[0]] = keycolumn[doubled_letters[1]]
725 return keycolumn
726
727 def cadenus_encipher(message, keyword, keycolumn, fillvalue='a'):
728 """Encipher with the Cadenus cipher
729
730 >>> cadenus_encipher(sanitise('Whoever has made a voyage up the Hudson ' \
731 'must remember the Kaatskill mountains. ' \
732 'They are a dismembered branch of the great'), \
733 'wink', \
734 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
735 'antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
736 >>> cadenus_encipher(sanitise('a severe limitation on the usefulness of ' \
737 'the cadenus is that every message must be ' \
738 'a multiple of twenty-five letters long'), \
739 'easy', \
740 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
741 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
742 """
743 rows = chunks(message, len(message) // 25, fillvalue=fillvalue)
744 columns = zip(*rows)
745 rotated_columns = [col[start:] + col[:start] for start, col in zip([keycolumn[l] for l in keyword], columns)]
746 rotated_rows = zip(*rotated_columns)
747 transpositions = transpositions_of(keyword)
748 transposed = [transpose(r, transpositions) for r in rotated_rows]
749 return cat(chain(*transposed))
750
751 def cadenus_decipher(message, keyword, keycolumn, fillvalue='a'):
752 """
753 >>> cadenus_decipher('antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaa' \
754 'suvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned', \
755 'wink', \
756 make_cadenus_keycolumn(reverse=True))
757 'whoeverhasmadeavoyageupthehudsonmustrememberthekaatskillmountainstheyareadismemberedbranchofthegreat'
758 >>> cadenus_decipher('systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtof' \
759 'arenuseieeieltarlmentieetogevesitfaisltngeeuvowul', \
760 'easy', \
761 make_cadenus_keycolumn(reverse=True))
762 'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
763 """
764 rows = chunks(message, len(message) // 25, fillvalue=fillvalue)
765 transpositions = transpositions_of(keyword)
766 untransposed_rows = [untranspose(r, transpositions) for r in rows]
767 columns = zip(*untransposed_rows)
768 rotated_columns = [col[-start:] + col[:-start] for start, col in zip([keycolumn[l] for l in keyword], columns)]
769 rotated_rows = zip(*rotated_columns)
770 # return rotated_columns
771 return cat(chain(*rotated_rows))
772
773
774 def hill_encipher(matrix, message_letters, fillvalue='a'):
775 """Hill cipher
776
777 >>> hill_encipher(np.matrix([[7,8], [11,11]]), 'hellothere')
778 'drjiqzdrvx'
779 >>> hill_encipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
780 'hello there')
781 'tfjflpznvyac'
782 """
783 n = len(matrix)
784 sanitised_message = sanitise(message_letters)
785 if len(sanitised_message) % n != 0:
786 padding = fillvalue[0] * (n - len(sanitised_message) % n)
787 else:
788 padding = ''
789 message = [ord(c) - ord('a') for c in sanitised_message + padding]
790 message_chunks = [message[i:i+n] for i in range(0, len(message), n)]
791 # message_chunks = chunks(message, len(matrix), fillvalue=None)
792 enciphered_chunks = [((matrix * np.matrix(c).T).T).tolist()[0]
793 for c in message_chunks]
794 return cat([chr(int(round(l)) % 26 + ord('a'))
795 for l in sum(enciphered_chunks, [])])
796
797 def hill_decipher(matrix, message, fillvalue='a'):
798 """Hill cipher
799
800 >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
801 'hellothere'
802 >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
803 'tfjflpznvyac')
804 'hellothereaa'
805 """
806 adjoint = linalg.det(matrix)*linalg.inv(matrix)
807 inverse_determinant = modular_division_table[int(round(linalg.det(matrix))) % 26][1]
808 inverse_matrix = (inverse_determinant * adjoint) % 26
809 return hill_encipher(inverse_matrix, message, fillvalue)
810
811
812 # Where each piece of text ends up in the AMSCO transpositon cipher.
813 # 'index' shows where the slice appears in the plaintext, with the slice
814 # from 'start' to 'end'
815 AmscoSlice = collections.namedtuple('AmscoSlice', ['index', 'start', 'end'])
816
817 class AmscoFillStyle(Enum):
818 continuous = 1
819 same_each_row = 2
820 reverse_each_row = 3
821
822 def amsco_transposition_positions(message, keyword,
823 fillpattern=(1, 2),
824 fillstyle=AmscoFillStyle.continuous,
825 fillcolumnwise=False,
826 emptycolumnwise=True):
827 """Creates the grid for the AMSCO transposition cipher. Each element in the
828 grid shows the index of that slice and the start and end positions of the
829 plaintext that go to make it up.
830
831 >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
832 fillpattern=(1, 2)) # doctest: +NORMALIZE_WHITESPACE
833 [[AmscoSlice(index=3, start=4, end=6),
834 AmscoSlice(index=2, start=3, end=4),
835 AmscoSlice(index=0, start=0, end=1),
836 AmscoSlice(index=1, start=1, end=3),
837 AmscoSlice(index=4, start=6, end=7)],
838 [AmscoSlice(index=8, start=12, end=13),
839 AmscoSlice(index=7, start=10, end=12),
840 AmscoSlice(index=5, start=7, end=9),
841 AmscoSlice(index=6, start=9, end=10),
842 AmscoSlice(index=9, start=13, end=15)],
843 [AmscoSlice(index=13, start=19, end=21),
844 AmscoSlice(index=12, start=18, end=19),
845 AmscoSlice(index=10, start=15, end=16),
846 AmscoSlice(index=11, start=16, end=18),
847 AmscoSlice(index=14, start=21, end=22)],
848 [AmscoSlice(index=18, start=27, end=28),
849 AmscoSlice(index=17, start=25, end=27),
850 AmscoSlice(index=15, start=22, end=24),
851 AmscoSlice(index=16, start=24, end=25),
852 AmscoSlice(index=19, start=28, end=30)]]
853 """
854 transpositions = transpositions_of(keyword)
855 fill_iterator = cycle(fillpattern)
856 indices = count()
857 message_length = len(message)
858
859 current_position = 0
860 grid = []
861 current_fillpattern = fillpattern
862 while current_position < message_length:
863 row = []
864 if fillstyle == AmscoFillStyle.same_each_row:
865 fill_iterator = cycle(fillpattern)
866 if fillstyle == AmscoFillStyle.reverse_each_row:
867 fill_iterator = cycle(current_fillpattern)
868 for _ in range(len(transpositions)):
869 index = next(indices)
870 gap = next(fill_iterator)
871 row += [AmscoSlice(index, current_position, current_position + gap)]
872 current_position += gap
873 grid += [row]
874 if fillstyle == AmscoFillStyle.reverse_each_row:
875 current_fillpattern = list(reversed(current_fillpattern))
876 return [transpose(r, transpositions) for r in grid]
877
878 def amsco_transposition_encipher(message, keyword,
879 fillpattern=(1,2), fillstyle=AmscoFillStyle.reverse_each_row):
880 """AMSCO transposition encipher.
881
882 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
883 'hoteelhler'
884 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
885 'hetelhelor'
886 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
887 'hotelerelh'
888 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
889 'hetelorlhe'
890 >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
891 'etecstthhomoerereenisxip'
892 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
893 'hetcsoeisterereipexthomn'
894 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
895 'hecsoisttererteipexhomen'
896 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
897 'heecisoosttrrtepeixhemen'
898 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
899 'hxtomephescieretoeisnter'
900 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
901 'hxomeiphscerettoisenteer'
902 """
903 grid = amsco_transposition_positions(message, keyword,
904 fillpattern=fillpattern, fillstyle=fillstyle)
905 ct_as_grid = [[message[s.start:s.end] for s in r] for r in grid]
906 return combine_every_nth(ct_as_grid)
907
908
909 def amsco_transposition_decipher(message, keyword,
910 fillpattern=(1,2), fillstyle=AmscoFillStyle.reverse_each_row):
911 """AMSCO transposition decipher
912
913 >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
914 'hellothere'
915 >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
916 'hellothere'
917 >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
918 'hellothere'
919 >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
920 'hellothere'
921 >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
922 'hereissometexttoencipher'
923 >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
924 'hereissometexttoencipher'
925 >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
926 'hereissometexttoencipher'
927 >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
928 'hereissometexttoencipher'
929 >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
930 'hereissometexttoencipher'
931 >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
932 'hereissometexttoencipher'
933 """
934
935 grid = amsco_transposition_positions(message, keyword,
936 fillpattern=fillpattern, fillstyle=fillstyle)
937 transposed_sections = [s for c in [l for l in zip(*grid)] for s in c]
938 plaintext_list = [''] * len(transposed_sections)
939 current_pos = 0
940 for slice in transposed_sections:
941 plaintext_list[slice.index] = message[current_pos:current_pos-slice.start+slice.end][:len(message[slice.start:slice.end])]
942 current_pos += len(message[slice.start:slice.end])
943 return cat(plaintext_list)
944
945
946 def bifid_grid(keyword, wrap_alphabet, letter_mapping):
947 """Create the grids for a Bifid cipher
948 """
949 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
950 if letter_mapping is None:
951 letter_mapping = {'j': 'i'}
952 translation = ''.maketrans(letter_mapping)
953 cipher_alphabet = cat(collections.OrderedDict.fromkeys(cipher_alphabet.translate(translation)))
954 f_grid = {k: ((i // 5) + 1, (i % 5) + 1)
955 for i, k in enumerate(cipher_alphabet)}
956 r_grid = {((i // 5) + 1, (i % 5) + 1): k
957 for i, k in enumerate(cipher_alphabet)}
958 return translation, f_grid, r_grid
959
960 def bifid_encipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a,
961 letter_mapping=None, period=None, fillvalue=None):
962 """Bifid cipher
963
964 >>> bifid_encipher("indiajelly", 'iguana')
965 'ibidonhprm'
966 >>> bifid_encipher("indiacurry", 'iguana', period=4)
967 'ibnhgaqltm'
968 >>> bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x')
969 'ibnhgaqltzml'
970 """
971 translation, f_grid, r_grid = bifid_grid(keyword, wrap_alphabet, letter_mapping)
972
973 t_message = message.translate(translation)
974 pairs0 = [f_grid[l] for l in sanitise(t_message)]
975 if period:
976 chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
977 if len(chunked_pairs[-1]) < period and fillvalue:
978 chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
979 else:
980 chunked_pairs = [pairs0]
981
982 pairs1 = []
983 for c in chunked_pairs:
984 items = sum(list(list(i) for i in zip(*c)), [])
985 p = [(items[i], items[i+1]) for i in range(0, len(items), 2)]
986 pairs1 += p
987
988 return cat(r_grid[p] for p in pairs1)
989
990
991 def bifid_decipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a,
992 letter_mapping=None, period=None, fillvalue=None):
993 """Decipher with bifid cipher
994
995 >>> bifid_decipher('ibidonhprm', 'iguana')
996 'indiaielly'
997 >>> bifid_decipher("ibnhgaqltm", 'iguana', period=4)
998 'indiacurry'
999 >>> bifid_decipher("ibnhgaqltzml", 'iguana', period=4)
1000 'indiacurryxx'
1001 """
1002 translation, f_grid, r_grid = bifid_grid(keyword, wrap_alphabet, letter_mapping)
1003
1004 t_message = message.translate(translation)
1005 pairs0 = [f_grid[l] for l in sanitise(t_message)]
1006 if period:
1007 chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
1008 if len(chunked_pairs[-1]) < period and fillvalue:
1009 chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
1010 else:
1011 chunked_pairs = [pairs0]
1012
1013 pairs1 = []
1014 for c in chunked_pairs:
1015 items = [j for i in c for j in i]
1016 gap = len(c)
1017 p = [(items[i], items[i+gap]) for i in range(gap)]
1018 pairs1 += p
1019
1020 return cat(r_grid[p] for p in pairs1)
1021
1022 class PocketEnigma(object):
1023 """A pocket enigma machine
1024 The wheel is internally represented as a 26-element list self.wheel_map,
1025 where wheel_map[i] == j shows that the position i places on from the arrow
1026 maps to the position j places on.
1027 """
1028 def __init__(self, wheel=1, position='a'):
1029 """initialise the pocket enigma, including which wheel to use and the
1030 starting position of the wheel.
1031
1032 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
1033 pairs.
1034
1035 The position is the letter pointed to by the arrow on the wheel.
1036
1037 >>> pe.wheel_map
1038 [25, 4, 23, 10, 1, 7, 9, 5, 12, 6, 3, 17, 8, 14, 13, 21, 19, 11, 20, 16, 18, 15, 24, 2, 22, 0]
1039 >>> pe.position
1040 0
1041 """
1042 self.wheel1 = [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
1043 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
1044 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
1045 self.wheel2 = [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
1046 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
1047 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
1048 if wheel == 1:
1049 self.make_wheel_map(self.wheel1)
1050 elif wheel == 2:
1051 self.make_wheel_map(self.wheel2)
1052 else:
1053 self.validate_wheel_spec(wheel)
1054 self.make_wheel_map(wheel)
1055 if position in string.ascii_lowercase:
1056 self.position = ord(position) - ord('a')
1057 else:
1058 self.position = position
1059
1060 def make_wheel_map(self, wheel_spec):
1061 """Expands a wheel specification from a list of letter-letter pairs
1062 into a full wheel_map.
1063
1064 >>> pe.make_wheel_map(pe.wheel2)
1065 [2, 3, 0, 1, 22, 8, 15, 12, 5, 10, 9, 13, 7, 11, 16, 6, 14, 25, 20, 21, 18, 19, 4, 24, 23, 17]
1066 """
1067 self.validate_wheel_spec(wheel_spec)
1068 self.wheel_map = [0] * 26
1069 for p in wheel_spec:
1070 self.wheel_map[ord(p[0]) - ord('a')] = ord(p[1]) - ord('a')
1071 self.wheel_map[ord(p[1]) - ord('a')] = ord(p[0]) - ord('a')
1072 return self.wheel_map
1073
1074 def validate_wheel_spec(self, wheel_spec):
1075 """Validates that a wheel specificaiton will turn into a valid wheel
1076 map.
1077
1078 >>> pe.validate_wheel_spec([])
1079 Traceback (most recent call last):
1080 ...
1081 ValueError: Wheel specification has 0 pairs, requires 13
1082 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
1083 Traceback (most recent call last):
1084 ...
1085 ValueError: Not all mappings in wheel specificationhave two elements
1086 >>> pe.validate_wheel_spec([('a', 'b')]*13)
1087 Traceback (most recent call last):
1088 ...
1089 ValueError: Wheel specification does not contain 26 letters
1090 """
1091 if len(wheel_spec) != 13:
1092 raise ValueError("Wheel specification has {} pairs, requires 13".
1093 format(len(wheel_spec)))
1094 for p in wheel_spec:
1095 if len(p) != 2:
1096 raise ValueError("Not all mappings in wheel specification"
1097 "have two elements")
1098 if len(set([p[0] for p in wheel_spec] +
1099 [p[1] for p in wheel_spec])) != 26:
1100 raise ValueError("Wheel specification does not contain 26 letters")
1101
1102 def encipher_letter(self, letter):
1103 """Enciphers a single letter, by advancing the wheel before looking up
1104 the letter on the wheel.
1105
1106 >>> pe.set_position('f')
1107 5
1108 >>> pe.encipher_letter('k')
1109 'h'
1110 """
1111 self.advance()
1112 return self.lookup(letter)
1113 decipher_letter = encipher_letter
1114
1115 def lookup(self, letter):
1116 """Look up what a letter enciphers to, without turning the wheel.
1117
1118 >>> pe.set_position('f')
1119 5
1120 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
1121 'udhbfejcpgmokrliwntsayqzvx'
1122 >>> pe.lookup('A')
1123 ''
1124 """
1125 if letter in string.ascii_lowercase:
1126 return chr(
1127 (self.wheel_map[(ord(letter) - ord('a') - self.position) % 26] +
1128 self.position) % 26 +
1129 ord('a'))
1130 else:
1131 return ''
1132
1133 def advance(self):
1134 """Advances the wheel one position.
1135
1136 >>> pe.set_position('f')
1137 5
1138 >>> pe.advance()
1139 6
1140 """
1141 self.position = (self.position + 1) % 26
1142 return self.position
1143
1144 def encipher(self, message, starting_position=None):
1145 """Enciphers a whole message.
1146
1147 >>> pe.set_position('f')
1148 5
1149 >>> pe.encipher('helloworld')
1150 'kjsglcjoqc'
1151 >>> pe.set_position('f')
1152 5
1153 >>> pe.encipher('kjsglcjoqc')
1154 'helloworld'
1155 >>> pe.encipher('helloworld', starting_position = 'x')
1156 'egrekthnnf'
1157 """
1158 if starting_position:
1159 self.set_position(starting_position)
1160 transformed = ''
1161 for l in message:
1162 transformed += self.encipher_letter(l)
1163 return transformed
1164 decipher = encipher
1165
1166 def set_position(self, position):
1167 """Sets the position of the wheel, by specifying the letter the arrow
1168 points to.
1169
1170 >>> pe.set_position('a')
1171 0
1172 >>> pe.set_position('m')
1173 12
1174 >>> pe.set_position('z')
1175 25
1176 """
1177 self.position = ord(position) - ord('a')
1178 return self.position
1179
1180
1181 if __name__ == "__main__":
1182 import doctest
1183 doctest.testmod(extraglobs={'pe': PocketEnigma(1, 'a')})