Done challenge 7
[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 class PocketEnigma(object):
1141 """A pocket enigma machine
1142 The wheel is internally represented as a 26-element list self.wheel_map,
1143 where wheel_map[i] == j shows that the position i places on from the arrow
1144 maps to the position j places on.
1145 """
1146 def __init__(self, wheel=1, position='a'):
1147 """initialise the pocket enigma, including which wheel to use and the
1148 starting position of the wheel.
1149
1150 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
1151 pairs.
1152
1153 The position is the letter pointed to by the arrow on the wheel.
1154
1155 >>> pe.wheel_map
1156 [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]
1157 >>> pe.position
1158 0
1159 """
1160 self.wheel1 = [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
1161 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
1162 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
1163 self.wheel2 = [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
1164 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
1165 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
1166 if wheel == 1:
1167 self.make_wheel_map(self.wheel1)
1168 elif wheel == 2:
1169 self.make_wheel_map(self.wheel2)
1170 else:
1171 self.validate_wheel_spec(wheel)
1172 self.make_wheel_map(wheel)
1173 if position in string.ascii_lowercase:
1174 self.position = pos(position)
1175 else:
1176 self.position = position
1177
1178 def make_wheel_map(self, wheel_spec):
1179 """Expands a wheel specification from a list of letter-letter pairs
1180 into a full wheel_map.
1181
1182 >>> pe.make_wheel_map(pe.wheel2)
1183 [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]
1184 """
1185 self.validate_wheel_spec(wheel_spec)
1186 self.wheel_map = [0] * 26
1187 for p in wheel_spec:
1188 self.wheel_map[pos(p[0])] = pos(p[1])
1189 self.wheel_map[pos(p[1])] = pos(p[0])
1190 return self.wheel_map
1191
1192 def validate_wheel_spec(self, wheel_spec):
1193 """Validates that a wheel specificaiton will turn into a valid wheel
1194 map.
1195
1196 >>> pe.validate_wheel_spec([])
1197 Traceback (most recent call last):
1198 ...
1199 ValueError: Wheel specification has 0 pairs, requires 13
1200 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
1201 Traceback (most recent call last):
1202 ...
1203 ValueError: Not all mappings in wheel specificationhave two elements
1204 >>> pe.validate_wheel_spec([('a', 'b')]*13)
1205 Traceback (most recent call last):
1206 ...
1207 ValueError: Wheel specification does not contain 26 letters
1208 """
1209 if len(wheel_spec) != 13:
1210 raise ValueError("Wheel specification has {} pairs, requires 13".
1211 format(len(wheel_spec)))
1212 for p in wheel_spec:
1213 if len(p) != 2:
1214 raise ValueError("Not all mappings in wheel specification"
1215 "have two elements")
1216 if len(set([p[0] for p in wheel_spec] +
1217 [p[1] for p in wheel_spec])) != 26:
1218 raise ValueError("Wheel specification does not contain 26 letters")
1219
1220 def encipher_letter(self, letter):
1221 """Enciphers a single letter, by advancing the wheel before looking up
1222 the letter on the wheel.
1223
1224 >>> pe.set_position('f')
1225 5
1226 >>> pe.encipher_letter('k')
1227 'h'
1228 """
1229 self.advance()
1230 return self.lookup(letter)
1231 decipher_letter = encipher_letter
1232
1233 def lookup(self, letter):
1234 """Look up what a letter enciphers to, without turning the wheel.
1235
1236 >>> pe.set_position('f')
1237 5
1238 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
1239 'udhbfejcpgmokrliwntsayqzvx'
1240 >>> pe.lookup('A')
1241 ''
1242 """
1243 if letter in string.ascii_lowercase:
1244 return unpos(
1245 (self.wheel_map[(pos(letter) - self.position) % 26] +
1246 self.position))
1247 else:
1248 return ''
1249
1250 def advance(self):
1251 """Advances the wheel one position.
1252
1253 >>> pe.set_position('f')
1254 5
1255 >>> pe.advance()
1256 6
1257 """
1258 self.position = (self.position + 1) % 26
1259 return self.position
1260
1261 def encipher(self, message, starting_position=None):
1262 """Enciphers a whole message.
1263
1264 >>> pe.set_position('f')
1265 5
1266 >>> pe.encipher('helloworld')
1267 'kjsglcjoqc'
1268 >>> pe.set_position('f')
1269 5
1270 >>> pe.encipher('kjsglcjoqc')
1271 'helloworld'
1272 >>> pe.encipher('helloworld', starting_position = 'x')
1273 'egrekthnnf'
1274 """
1275 if starting_position:
1276 self.set_position(starting_position)
1277 transformed = ''
1278 for l in message:
1279 transformed += self.encipher_letter(l)
1280 return transformed
1281 decipher = encipher
1282
1283 def set_position(self, position):
1284 """Sets the position of the wheel, by specifying the letter the arrow
1285 points to.
1286
1287 >>> pe.set_position('a')
1288 0
1289 >>> pe.set_position('m')
1290 12
1291 >>> pe.set_position('z')
1292 25
1293 """
1294 self.position = pos(position)
1295 return self.position
1296
1297
1298 if __name__ == "__main__":
1299 import doctest
1300 doctest.testmod(extraglobs={'pe': PocketEnigma(1, 'a')})