4c3f14b00ab3c8ce8c0f37aae144d9470bdb8d35
[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 beaufort_encipher=vigenere_decipher
399 beaufort_decipher=vigenere_encipher
400
401
402 def polybius_grid(keyword, column_order, row_order, letters_to_merge=None,
403 wrap_alphabet=KeywordWrapAlphabet.from_a):
404 """Grid for a Polybius cipher, using a keyword to rearrange the
405 alphabet.
406
407
408 >>> polybius_grid('a', 'abcde', 'abcde')['x'] == ('e', 'c')
409 True
410 >>> polybius_grid('elephant', 'abcde', 'abcde')['e'] == ('a', 'a')
411 True
412 >>> polybius_grid('elephant', 'abcde', 'abcde')['b'] == ('b', 'c')
413 True
414 """
415 alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet=wrap_alphabet)
416 if letters_to_merge is None:
417 letters_to_merge = {'j': 'i'}
418 grid = {l: k
419 for k, l in zip([(c, r) for c in column_order for r in row_order],
420 [l for l in alphabet if l not in letters_to_merge])}
421 for l in letters_to_merge:
422 grid[l] = grid[letters_to_merge[l]]
423 return grid
424
425 def polybius_reverse_grid(keyword, column_order, row_order, letters_to_merge=None,
426 wrap_alphabet=KeywordWrapAlphabet.from_a):
427 """Grid for decrypting using a Polybius cipher, using a keyword to
428 rearrange the alphabet.
429
430 >>> polybius_reverse_grid('a', 'abcde', 'abcde')['e', 'c'] == 'x'
431 True
432 >>> polybius_reverse_grid('elephant', 'abcde', 'abcde')['a', 'a'] == 'e'
433 True
434 >>> polybius_reverse_grid('elephant', 'abcde', 'abcde')['b', 'c'] == 'b'
435 True
436 """
437 alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet=wrap_alphabet)
438 if letters_to_merge is None:
439 letters_to_merge = {'j': 'i'}
440 grid = {k: l
441 for k, l in zip([(c, r) for c in column_order for r in row_order],
442 [l for l in alphabet if l not in letters_to_merge])}
443 return grid
444
445
446 def polybius_flatten(pair, column_first):
447 """Convert a series of pairs into a single list of characters"""
448 if column_first:
449 return str(pair[1]) + str(pair[0])
450 else:
451 return str(pair[0]) + str(pair[1])
452
453 def polybius_encipher(message, keyword, column_order, row_order,
454 column_first=False,
455 letters_to_merge=None, wrap_alphabet=KeywordWrapAlphabet.from_a):
456 """Encipher a message with Polybius cipher, using a keyword to rearrange
457 the alphabet
458
459
460 >>> polybius_encipher('this is a test message for the ' \
461 'polybius decipherment', 'elephant', \
462 [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], \
463 wrap_alphabet=KeywordWrapAlphabet.from_last)
464 '2214445544551522115522511155551543114252542214111352123234442355411135441314115451112122'
465 >>> polybius_encipher('this is a test message for the ' \
466 'polybius decipherment', 'elephant', 'abcde', 'abcde', \
467 column_first=False)
468 'bbadccddccddaebbaaddbbceaaddddaecbaacadadcbbadaaacdaabedbcccdeddbeaabdccacadaadcceaababb'
469 >>> polybius_encipher('this is a test message for the ' \
470 'polybius decipherment', 'elephant', 'abcde', 'abcde', \
471 column_first=True)
472 'bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaacaadbadecbccedddebaadbcccadaaacdecaaabbb'
473 """
474 grid = polybius_grid(keyword, column_order, row_order, letters_to_merge, wrap_alphabet)
475 return cat(polybius_flatten(grid[l], column_first)
476 for l in message
477 if l in grid)
478
479
480 def polybius_decipher(message, keyword, column_order, row_order,
481 column_first=False,
482 letters_to_merge=None, wrap_alphabet=KeywordWrapAlphabet.from_a):
483 """Decipher a message with a Polybius cipher, using a keyword to rearrange
484 the alphabet
485
486 >>> polybius_decipher('bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaaca'\
487 'adbadecbccedddebaadbcccadaaacdecaaabbb', 'elephant', 'abcde', 'abcde', \
488 column_first=False)
489 'toisisvtestxessvbephktoefhnugiysweqifoekxelt'
490
491 >>> polybius_decipher('bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaaca'\
492 'adbadecbccedddebaadbcccadaaacdecaaabbb', 'elephant', 'abcde', 'abcde', \
493 column_first=True)
494 'thisisatestmessageforthepolybiusdecipherment'
495 """
496 grid = polybius_reverse_grid(keyword, column_order, row_order, letters_to_merge, wrap_alphabet)
497 column_index_type = type(column_order[0])
498 row_index_type = type(row_order[0])
499 if column_first:
500 pairs = [(column_index_type(p[1]), row_index_type(p[0])) for p in chunks(message, 2)]
501 else:
502 pairs = [(row_index_type(p[0]), column_index_type(p[1])) for p in chunks(message, 2)]
503 return cat(grid[p] for p in pairs if p in grid)
504
505
506 def transpositions_of(keyword):
507 """Finds the transpostions given by a keyword. For instance, the keyword
508 'clever' rearranges to 'celrv', so the first column (0) stays first, the
509 second column (1) moves to third, the third column (2) moves to second,
510 and so on.
511
512 If passed a tuple, assume it's already a transposition and just return it.
513
514 >>> transpositions_of('clever')
515 (0, 2, 1, 4, 3)
516 >>> transpositions_of('fred')
517 (3, 2, 0, 1)
518 >>> transpositions_of((3, 2, 0, 1))
519 (3, 2, 0, 1)
520 """
521 if isinstance(keyword, tuple):
522 return keyword
523 else:
524 key = deduplicate(keyword)
525 transpositions = tuple(key.index(l) for l in sorted(key))
526 return transpositions
527
528 def pad(message_len, group_len, fillvalue):
529 padding_length = group_len - message_len % group_len
530 if padding_length == group_len: padding_length = 0
531 padding = ''
532 for i in range(padding_length):
533 if callable(fillvalue):
534 padding += fillvalue()
535 else:
536 padding += fillvalue
537 return padding
538
539 def column_transposition_encipher(message, keyword, fillvalue=' ',
540 fillcolumnwise=False,
541 emptycolumnwise=False):
542 """Enciphers using the column transposition cipher.
543 Message is padded to allow all rows to be the same length.
544
545 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
546 'hlohr eltee '
547 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
548 'hellothere '
549 >>> column_transposition_encipher('hellothere', 'abcdef')
550 'hellothere '
551 >>> column_transposition_encipher('hellothere', 'abcde')
552 'hellothere'
553 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
554 'hellothere'
555 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
556 'hlohreltee'
557 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
558 'htehlelroe'
559 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
560 'hellothere'
561 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
562 'heotllrehe'
563 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
564 'holrhetlee'
565 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
566 'htleehoelr'
567 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
568 'hleolteher'
569 >>> column_transposition_encipher('hellothere', 'cleverly')
570 'hleolthre e '
571 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
572 'hleolthre!e!'
573 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
574 'hleolthre*e*'
575 """
576 transpositions = transpositions_of(keyword)
577 message += pad(len(message), len(transpositions), fillvalue)
578 if fillcolumnwise:
579 rows = every_nth(message, len(message) // len(transpositions))
580 else:
581 rows = chunks(message, len(transpositions))
582 transposed = [transpose(r, transpositions) for r in rows]
583 if emptycolumnwise:
584 return combine_every_nth(transposed)
585 else:
586 return cat(chain(*transposed))
587
588 def column_transposition_decipher(message, keyword, fillvalue=' ',
589 fillcolumnwise=False,
590 emptycolumnwise=False):
591 """Deciphers using the column transposition cipher.
592 Message is padded to allow all rows to be the same length.
593
594 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
595 'hellothere'
596 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
597 'hellothere'
598 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
599 'hellothere'
600 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
601 'hellothere'
602 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
603 'hellothere'
604 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
605 'hellothere'
606 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
607 'hellothere'
608 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
609 'hellothere'
610 """
611 transpositions = transpositions_of(keyword)
612 message += pad(len(message), len(transpositions), fillvalue)
613 if emptycolumnwise:
614 rows = every_nth(message, len(message) // len(transpositions))
615 else:
616 rows = chunks(message, len(transpositions))
617 untransposed = [untranspose(r, transpositions) for r in rows]
618 if fillcolumnwise:
619 return combine_every_nth(untransposed)
620 else:
621 return cat(chain(*untransposed))
622
623 def scytale_encipher(message, rows, fillvalue=' '):
624 """Enciphers using the scytale transposition cipher.
625 Message is padded with spaces to allow all rows to be the same length.
626
627 >>> scytale_encipher('thequickbrownfox', 3)
628 'tcnhkfeboqrxuo iw '
629 >>> scytale_encipher('thequickbrownfox', 4)
630 'tubnhirfecooqkwx'
631 >>> scytale_encipher('thequickbrownfox', 5)
632 'tubn hirf ecoo qkwx '
633 >>> scytale_encipher('thequickbrownfox', 6)
634 'tqcrnxhukof eibwo '
635 >>> scytale_encipher('thequickbrownfox', 7)
636 'tqcrnx hukof eibwo '
637 """
638 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
639 # return column_transposition_encipher(message, transpositions,
640 # fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
641 transpositions = [i for i in range(rows)]
642 return column_transposition_encipher(message, transpositions,
643 fillvalue=fillvalue, fillcolumnwise=True, emptycolumnwise=False)
644
645 def scytale_decipher(message, rows):
646 """Deciphers using the scytale transposition cipher.
647 Assumes the message is padded so that all rows are the same length.
648
649 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
650 'thequickbrownfox '
651 >>> scytale_decipher('tubnhirfecooqkwx', 4)
652 'thequickbrownfox'
653 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
654 'thequickbrownfox '
655 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
656 'thequickbrownfox '
657 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
658 'thequickbrownfox '
659 """
660 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
661 # return column_transposition_decipher(message, transpositions,
662 # fillcolumnwise=False, emptycolumnwise=True)
663 transpositions = [i for i in range(rows)]
664 return column_transposition_decipher(message, transpositions,
665 fillcolumnwise=True, emptycolumnwise=False)
666
667
668 def railfence_encipher(message, height, fillvalue=''):
669 """Railfence cipher.
670 Works by splitting the text into sections, then reading across them to
671 generate the rows in the cipher. The rows are then combined to form the
672 ciphertext.
673
674 Example: the plaintext "hellotherefriends", with a height of four, written
675 out in the railfence as
676 h h i
677 etere*
678 lorfns
679 l e d
680 (with the * showing the one character to finish the last section).
681 Each 'section' is two columns, but unfolded. In the example, the first
682 section is 'hellot'.
683
684 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 2, fillvalue='!')
685 'hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!'
686 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3, fillvalue='!')
687 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!'
688 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5, fillvalue='!')
689 'hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!'
690 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 10, fillvalue='!')
691 'hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!'
692 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3)
693 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece'
694 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5)
695 'hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp'
696 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 7)
697 'haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic'
698 """
699 sections = chunks(message, (height - 1) * 2, fillvalue=fillvalue)
700 n_sections = len(sections)
701 # Add the top row
702 rows = [cat([s[0] for s in sections])]
703 # process the middle rows of the grid
704 for r in range(1, height-1):
705 rows += [cat([s[r:r+1] + s[height*2-r-2:height*2-r-1] for s in sections])]
706 # process the bottom row
707 rows += [cat([s[height - 1:height] for s in sections])]
708 # rows += [wcat([s[height - 1] for s in sections])]
709 return cat(rows)
710
711 def railfence_decipher(message, height, fillvalue=''):
712 """Railfence decipher.
713 Works by reconstructing the grid used to generate the ciphertext, then
714 unfolding the sections so the text can be concatenated together.
715
716 Example: given the ciphertext 'hhieterelorfnsled' and a height of 4, first
717 work out that the second row has a character missing, find the rows of the
718 grid, then split the section into its two columns.
719
720 'hhieterelorfnsled' is split into
721 h h i
722 etere
723 lorfns
724 l e d
725 (spaces added for clarity), which is stored in 'rows'. This is then split
726 into 'down_rows' and 'up_rows':
727
728 down_rows:
729 hhi
730 eee
731 lrn
732 led
733
734 up_rows:
735 tr
736 ofs
737
738 These are then zipped together (after the up_rows are reversed) to recover
739 the plaintext.
740
741 Most of the procedure is about finding the correct lengths for each row then
742 splitting the ciphertext into those rows.
743
744 >>> railfence_decipher('hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!', 2).strip('!')
745 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
746 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!', 3).strip('!')
747 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
748 >>> railfence_decipher('hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!', 5).strip('!')
749 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
750 >>> railfence_decipher('hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!', 10).strip('!')
751 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
752 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece', 3)
753 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
754 >>> railfence_decipher('hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp', 5)
755 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
756 >>> railfence_decipher('haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic', 7)
757 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
758 """
759 # find the number and size of the sections, including how many characters
760 # are missing for a full grid
761 n_sections = math.ceil(len(message) / ((height - 1) * 2))
762 padding_to_add = n_sections * (height - 1) * 2 - len(message)
763 # row_lengths are for the both up rows and down rows
764 row_lengths = [n_sections] * (height - 1) * 2
765 for i in range((height - 1) * 2 - 1, (height - 1) * 2 - (padding_to_add + 1), -1):
766 row_lengths[i] -= 1
767 # folded_rows are the combined row lengths in the middle of the railfence
768 folded_row_lengths = [row_lengths[0]]
769 for i in range(1, height-1):
770 folded_row_lengths += [row_lengths[i] + row_lengths[-i]]
771 folded_row_lengths += [row_lengths[height - 1]]
772 # find the rows that form the railfence grid
773 rows = []
774 row_start = 0
775 for i in folded_row_lengths:
776 rows += [message[row_start:row_start + i]]
777 row_start += i
778 # split the rows into the 'down_rows' (those that form the first column of
779 # a section) and the 'up_rows' (those that ofrm the second column of a
780 # section).
781 down_rows = [rows[0]]
782 up_rows = []
783 for i in range(1, height-1):
784 down_rows += [cat([c for n, c in enumerate(rows[i]) if n % 2 == 0])]
785 up_rows += [cat([c for n, c in enumerate(rows[i]) if n % 2 == 1])]
786 down_rows += [rows[-1]]
787 up_rows.reverse()
788 return cat(c for r in zip_longest(*(down_rows + up_rows), fillvalue='') for c in r)
789
790 def make_cadenus_keycolumn(doubled_letters = 'vw', start='a', reverse=False):
791 """Makes the key column for a Cadenus cipher (the column down between the
792 rows of letters)
793
794 >>> make_cadenus_keycolumn()['a']
795 0
796 >>> make_cadenus_keycolumn()['b']
797 1
798 >>> make_cadenus_keycolumn()['c']
799 2
800 >>> make_cadenus_keycolumn()['v']
801 21
802 >>> make_cadenus_keycolumn()['w']
803 21
804 >>> make_cadenus_keycolumn()['z']
805 24
806 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['a']
807 1
808 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['b']
809 0
810 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['c']
811 24
812 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['i']
813 18
814 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['j']
815 18
816 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['v']
817 6
818 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['z']
819 2
820 """
821 index_to_remove = string.ascii_lowercase.find(doubled_letters[0])
822 short_alphabet = string.ascii_lowercase[:index_to_remove] + string.ascii_lowercase[index_to_remove+1:]
823 if reverse:
824 short_alphabet = cat(reversed(short_alphabet))
825 start_pos = short_alphabet.find(start)
826 rotated_alphabet = short_alphabet[start_pos:] + short_alphabet[:start_pos]
827 keycolumn = {l: i for i, l in enumerate(rotated_alphabet)}
828 keycolumn[doubled_letters[0]] = keycolumn[doubled_letters[1]]
829 return keycolumn
830
831 def cadenus_encipher(message, keyword, keycolumn, fillvalue='a'):
832 """Encipher with the Cadenus cipher
833
834 >>> cadenus_encipher(sanitise('Whoever has made a voyage up the Hudson ' \
835 'must remember the Kaatskill mountains. ' \
836 'They are a dismembered branch of the great'), \
837 'wink', \
838 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
839 'antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
840 >>> cadenus_encipher(sanitise('a severe limitation on the usefulness of ' \
841 'the cadenus is that every message must be ' \
842 'a multiple of twenty-five letters long'), \
843 'easy', \
844 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
845 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
846 """
847 rows = chunks(message, len(message) // 25, fillvalue=fillvalue)
848 columns = zip(*rows)
849 rotated_columns = [col[start:] + col[:start] for start, col in zip([keycolumn[l] for l in keyword], columns)]
850 rotated_rows = zip(*rotated_columns)
851 transpositions = transpositions_of(keyword)
852 transposed = [transpose(r, transpositions) for r in rotated_rows]
853 return cat(chain(*transposed))
854
855 def cadenus_decipher(message, keyword, keycolumn, fillvalue='a'):
856 """
857 >>> cadenus_decipher('antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaa' \
858 'suvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned', \
859 'wink', \
860 make_cadenus_keycolumn(reverse=True))
861 'whoeverhasmadeavoyageupthehudsonmustrememberthekaatskillmountainstheyareadismemberedbranchofthegreat'
862 >>> cadenus_decipher('systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtof' \
863 'arenuseieeieltarlmentieetogevesitfaisltngeeuvowul', \
864 'easy', \
865 make_cadenus_keycolumn(reverse=True))
866 'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
867 """
868 rows = chunks(message, len(message) // 25, fillvalue=fillvalue)
869 transpositions = transpositions_of(keyword)
870 untransposed_rows = [untranspose(r, transpositions) for r in rows]
871 columns = zip(*untransposed_rows)
872 rotated_columns = [col[-start:] + col[:-start] for start, col in zip([keycolumn[l] for l in keyword], columns)]
873 rotated_rows = zip(*rotated_columns)
874 # return rotated_columns
875 return cat(chain(*rotated_rows))
876
877
878 def hill_encipher(matrix, message_letters, fillvalue='a'):
879 """Hill cipher
880
881 >>> hill_encipher(np.matrix([[7,8], [11,11]]), 'hellothere')
882 'drjiqzdrvx'
883 >>> hill_encipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
884 'hello there')
885 'tfjflpznvyac'
886 """
887 n = len(matrix)
888 sanitised_message = sanitise(message_letters)
889 if len(sanitised_message) % n != 0:
890 padding = fillvalue[0] * (n - len(sanitised_message) % n)
891 else:
892 padding = ''
893 message = [ord(c) - ord('a') for c in sanitised_message + padding]
894 message_chunks = [message[i:i+n] for i in range(0, len(message), n)]
895 # message_chunks = chunks(message, len(matrix), fillvalue=None)
896 enciphered_chunks = [((matrix * np.matrix(c).T).T).tolist()[0]
897 for c in message_chunks]
898 return cat([chr(int(round(l)) % 26 + ord('a'))
899 for l in sum(enciphered_chunks, [])])
900
901 def hill_decipher(matrix, message, fillvalue='a'):
902 """Hill cipher
903
904 >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
905 'hellothere'
906 >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
907 'tfjflpznvyac')
908 'hellothereaa'
909 """
910 adjoint = linalg.det(matrix)*linalg.inv(matrix)
911 inverse_determinant = modular_division_table[int(round(linalg.det(matrix))) % 26][1]
912 inverse_matrix = (inverse_determinant * adjoint) % 26
913 return hill_encipher(inverse_matrix, message, fillvalue)
914
915
916 # Where each piece of text ends up in the AMSCO transpositon cipher.
917 # 'index' shows where the slice appears in the plaintext, with the slice
918 # from 'start' to 'end'
919 AmscoSlice = collections.namedtuple('AmscoSlice', ['index', 'start', 'end'])
920
921 class AmscoFillStyle(Enum):
922 continuous = 1
923 same_each_row = 2
924 reverse_each_row = 3
925
926 def amsco_transposition_positions(message, keyword,
927 fillpattern=(1, 2),
928 fillstyle=AmscoFillStyle.continuous,
929 fillcolumnwise=False,
930 emptycolumnwise=True):
931 """Creates the grid for the AMSCO transposition cipher. Each element in the
932 grid shows the index of that slice and the start and end positions of the
933 plaintext that go to make it up.
934
935 >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
936 fillpattern=(1, 2)) # doctest: +NORMALIZE_WHITESPACE
937 [[AmscoSlice(index=3, start=4, end=6),
938 AmscoSlice(index=2, start=3, end=4),
939 AmscoSlice(index=0, start=0, end=1),
940 AmscoSlice(index=1, start=1, end=3),
941 AmscoSlice(index=4, start=6, end=7)],
942 [AmscoSlice(index=8, start=12, end=13),
943 AmscoSlice(index=7, start=10, end=12),
944 AmscoSlice(index=5, start=7, end=9),
945 AmscoSlice(index=6, start=9, end=10),
946 AmscoSlice(index=9, start=13, end=15)],
947 [AmscoSlice(index=13, start=19, end=21),
948 AmscoSlice(index=12, start=18, end=19),
949 AmscoSlice(index=10, start=15, end=16),
950 AmscoSlice(index=11, start=16, end=18),
951 AmscoSlice(index=14, start=21, end=22)],
952 [AmscoSlice(index=18, start=27, end=28),
953 AmscoSlice(index=17, start=25, end=27),
954 AmscoSlice(index=15, start=22, end=24),
955 AmscoSlice(index=16, start=24, end=25),
956 AmscoSlice(index=19, start=28, end=30)]]
957 """
958 transpositions = transpositions_of(keyword)
959 fill_iterator = cycle(fillpattern)
960 indices = count()
961 message_length = len(message)
962
963 current_position = 0
964 grid = []
965 current_fillpattern = fillpattern
966 while current_position < message_length:
967 row = []
968 if fillstyle == AmscoFillStyle.same_each_row:
969 fill_iterator = cycle(fillpattern)
970 if fillstyle == AmscoFillStyle.reverse_each_row:
971 fill_iterator = cycle(current_fillpattern)
972 for _ in range(len(transpositions)):
973 index = next(indices)
974 gap = next(fill_iterator)
975 row += [AmscoSlice(index, current_position, current_position + gap)]
976 current_position += gap
977 grid += [row]
978 if fillstyle == AmscoFillStyle.reverse_each_row:
979 current_fillpattern = list(reversed(current_fillpattern))
980 return [transpose(r, transpositions) for r in grid]
981
982 def amsco_transposition_encipher(message, keyword,
983 fillpattern=(1,2), fillstyle=AmscoFillStyle.reverse_each_row):
984 """AMSCO transposition encipher.
985
986 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
987 'hoteelhler'
988 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
989 'hetelhelor'
990 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
991 'hotelerelh'
992 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
993 'hetelorlhe'
994 >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
995 'etecstthhomoerereenisxip'
996 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
997 'hetcsoeisterereipexthomn'
998 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
999 'hecsoisttererteipexhomen'
1000 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
1001 'heecisoosttrrtepeixhemen'
1002 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
1003 'hxtomephescieretoeisnter'
1004 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
1005 'hxomeiphscerettoisenteer'
1006 """
1007 grid = amsco_transposition_positions(message, keyword,
1008 fillpattern=fillpattern, fillstyle=fillstyle)
1009 ct_as_grid = [[message[s.start:s.end] for s in r] for r in grid]
1010 return combine_every_nth(ct_as_grid)
1011
1012
1013 def amsco_transposition_decipher(message, keyword,
1014 fillpattern=(1,2), fillstyle=AmscoFillStyle.reverse_each_row):
1015 """AMSCO transposition decipher
1016
1017 >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
1018 'hellothere'
1019 >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
1020 'hellothere'
1021 >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
1022 'hellothere'
1023 >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
1024 'hellothere'
1025 >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
1026 'hereissometexttoencipher'
1027 >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
1028 'hereissometexttoencipher'
1029 >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
1030 'hereissometexttoencipher'
1031 >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
1032 'hereissometexttoencipher'
1033 >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
1034 'hereissometexttoencipher'
1035 >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
1036 'hereissometexttoencipher'
1037 """
1038
1039 grid = amsco_transposition_positions(message, keyword,
1040 fillpattern=fillpattern, fillstyle=fillstyle)
1041 transposed_sections = [s for c in [l for l in zip(*grid)] for s in c]
1042 plaintext_list = [''] * len(transposed_sections)
1043 current_pos = 0
1044 for slice in transposed_sections:
1045 plaintext_list[slice.index] = message[current_pos:current_pos-slice.start+slice.end][:len(message[slice.start:slice.end])]
1046 current_pos += len(message[slice.start:slice.end])
1047 return cat(plaintext_list)
1048
1049
1050 def bifid_grid(keyword, wrap_alphabet, letter_mapping):
1051 """Create the grids for a Bifid cipher
1052 """
1053 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
1054 if letter_mapping is None:
1055 letter_mapping = {'j': 'i'}
1056 translation = ''.maketrans(letter_mapping)
1057 cipher_alphabet = cat(collections.OrderedDict.fromkeys(cipher_alphabet.translate(translation)))
1058 f_grid = {k: ((i // 5) + 1, (i % 5) + 1)
1059 for i, k in enumerate(cipher_alphabet)}
1060 r_grid = {((i // 5) + 1, (i % 5) + 1): k
1061 for i, k in enumerate(cipher_alphabet)}
1062 return translation, f_grid, r_grid
1063
1064 def bifid_encipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a,
1065 letter_mapping=None, period=None, fillvalue=None):
1066 """Bifid cipher
1067
1068 >>> bifid_encipher("indiajelly", 'iguana')
1069 'ibidonhprm'
1070 >>> bifid_encipher("indiacurry", 'iguana', period=4)
1071 'ibnhgaqltm'
1072 >>> bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x')
1073 'ibnhgaqltzml'
1074 """
1075 translation, f_grid, r_grid = bifid_grid(keyword, wrap_alphabet, letter_mapping)
1076
1077 t_message = message.translate(translation)
1078 pairs0 = [f_grid[l] for l in sanitise(t_message)]
1079 if period:
1080 chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
1081 if len(chunked_pairs[-1]) < period and fillvalue:
1082 chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
1083 else:
1084 chunked_pairs = [pairs0]
1085
1086 pairs1 = []
1087 for c in chunked_pairs:
1088 items = sum(list(list(i) for i in zip(*c)), [])
1089 p = [(items[i], items[i+1]) for i in range(0, len(items), 2)]
1090 pairs1 += p
1091
1092 return cat(r_grid[p] for p in pairs1)
1093
1094
1095 def bifid_decipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a,
1096 letter_mapping=None, period=None, fillvalue=None):
1097 """Decipher with bifid cipher
1098
1099 >>> bifid_decipher('ibidonhprm', 'iguana')
1100 'indiaielly'
1101 >>> bifid_decipher("ibnhgaqltm", 'iguana', period=4)
1102 'indiacurry'
1103 >>> bifid_decipher("ibnhgaqltzml", 'iguana', period=4)
1104 'indiacurryxx'
1105 """
1106 translation, f_grid, r_grid = bifid_grid(keyword, wrap_alphabet, letter_mapping)
1107
1108 t_message = message.translate(translation)
1109 pairs0 = [f_grid[l] for l in sanitise(t_message)]
1110 if period:
1111 chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
1112 if len(chunked_pairs[-1]) < period and fillvalue:
1113 chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
1114 else:
1115 chunked_pairs = [pairs0]
1116
1117 pairs1 = []
1118 for c in chunked_pairs:
1119 items = [j for i in c for j in i]
1120 gap = len(c)
1121 p = [(items[i], items[i+gap]) for i in range(gap)]
1122 pairs1 += p
1123
1124 return cat(r_grid[p] for p in pairs1)
1125
1126 class PocketEnigma(object):
1127 """A pocket enigma machine
1128 The wheel is internally represented as a 26-element list self.wheel_map,
1129 where wheel_map[i] == j shows that the position i places on from the arrow
1130 maps to the position j places on.
1131 """
1132 def __init__(self, wheel=1, position='a'):
1133 """initialise the pocket enigma, including which wheel to use and the
1134 starting position of the wheel.
1135
1136 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
1137 pairs.
1138
1139 The position is the letter pointed to by the arrow on the wheel.
1140
1141 >>> pe.wheel_map
1142 [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]
1143 >>> pe.position
1144 0
1145 """
1146 self.wheel1 = [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
1147 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
1148 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
1149 self.wheel2 = [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
1150 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
1151 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
1152 if wheel == 1:
1153 self.make_wheel_map(self.wheel1)
1154 elif wheel == 2:
1155 self.make_wheel_map(self.wheel2)
1156 else:
1157 self.validate_wheel_spec(wheel)
1158 self.make_wheel_map(wheel)
1159 if position in string.ascii_lowercase:
1160 self.position = ord(position) - ord('a')
1161 else:
1162 self.position = position
1163
1164 def make_wheel_map(self, wheel_spec):
1165 """Expands a wheel specification from a list of letter-letter pairs
1166 into a full wheel_map.
1167
1168 >>> pe.make_wheel_map(pe.wheel2)
1169 [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]
1170 """
1171 self.validate_wheel_spec(wheel_spec)
1172 self.wheel_map = [0] * 26
1173 for p in wheel_spec:
1174 self.wheel_map[ord(p[0]) - ord('a')] = ord(p[1]) - ord('a')
1175 self.wheel_map[ord(p[1]) - ord('a')] = ord(p[0]) - ord('a')
1176 return self.wheel_map
1177
1178 def validate_wheel_spec(self, wheel_spec):
1179 """Validates that a wheel specificaiton will turn into a valid wheel
1180 map.
1181
1182 >>> pe.validate_wheel_spec([])
1183 Traceback (most recent call last):
1184 ...
1185 ValueError: Wheel specification has 0 pairs, requires 13
1186 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
1187 Traceback (most recent call last):
1188 ...
1189 ValueError: Not all mappings in wheel specificationhave two elements
1190 >>> pe.validate_wheel_spec([('a', 'b')]*13)
1191 Traceback (most recent call last):
1192 ...
1193 ValueError: Wheel specification does not contain 26 letters
1194 """
1195 if len(wheel_spec) != 13:
1196 raise ValueError("Wheel specification has {} pairs, requires 13".
1197 format(len(wheel_spec)))
1198 for p in wheel_spec:
1199 if len(p) != 2:
1200 raise ValueError("Not all mappings in wheel specification"
1201 "have two elements")
1202 if len(set([p[0] for p in wheel_spec] +
1203 [p[1] for p in wheel_spec])) != 26:
1204 raise ValueError("Wheel specification does not contain 26 letters")
1205
1206 def encipher_letter(self, letter):
1207 """Enciphers a single letter, by advancing the wheel before looking up
1208 the letter on the wheel.
1209
1210 >>> pe.set_position('f')
1211 5
1212 >>> pe.encipher_letter('k')
1213 'h'
1214 """
1215 self.advance()
1216 return self.lookup(letter)
1217 decipher_letter = encipher_letter
1218
1219 def lookup(self, letter):
1220 """Look up what a letter enciphers to, without turning the wheel.
1221
1222 >>> pe.set_position('f')
1223 5
1224 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
1225 'udhbfejcpgmokrliwntsayqzvx'
1226 >>> pe.lookup('A')
1227 ''
1228 """
1229 if letter in string.ascii_lowercase:
1230 return chr(
1231 (self.wheel_map[(ord(letter) - ord('a') - self.position) % 26] +
1232 self.position) % 26 +
1233 ord('a'))
1234 else:
1235 return ''
1236
1237 def advance(self):
1238 """Advances the wheel one position.
1239
1240 >>> pe.set_position('f')
1241 5
1242 >>> pe.advance()
1243 6
1244 """
1245 self.position = (self.position + 1) % 26
1246 return self.position
1247
1248 def encipher(self, message, starting_position=None):
1249 """Enciphers a whole message.
1250
1251 >>> pe.set_position('f')
1252 5
1253 >>> pe.encipher('helloworld')
1254 'kjsglcjoqc'
1255 >>> pe.set_position('f')
1256 5
1257 >>> pe.encipher('kjsglcjoqc')
1258 'helloworld'
1259 >>> pe.encipher('helloworld', starting_position = 'x')
1260 'egrekthnnf'
1261 """
1262 if starting_position:
1263 self.set_position(starting_position)
1264 transformed = ''
1265 for l in message:
1266 transformed += self.encipher_letter(l)
1267 return transformed
1268 decipher = encipher
1269
1270 def set_position(self, position):
1271 """Sets the position of the wheel, by specifying the letter the arrow
1272 points to.
1273
1274 >>> pe.set_position('a')
1275 0
1276 >>> pe.set_position('m')
1277 12
1278 >>> pe.set_position('z')
1279 25
1280 """
1281 self.position = ord(position) - ord('a')
1282 return self.position
1283
1284
1285 if __name__ == "__main__":
1286 import doctest
1287 doctest.testmod(extraglobs={'pe': PocketEnigma(1, 'a')})