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