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