5 from itertools
import zip_longest
, cycle
, chain
, count
7 from numpy
import matrix
8 from numpy
import linalg
9 from language_models
import *
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')
25 def unpos(number
): return chr(number
% 26 + ord('a'))
28 modular_division_table
= [[0]*26 for _
in range(26)]
32 modular_division_table
[b
][c
] = a
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
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!']
49 split_text
= chunks(text
, n
, fillvalue
)
50 return [cat(l
) for l
in zip_longest(*split_text
, fillvalue
=fillvalue
)]
52 def combine_every_nth(split_text
):
53 """Reforms a text split into every_nth strings
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'
63 for l
in zip_longest(*split_text
, fillvalue
='')])
65 def chunks(text
, n
, fillvalue
=None):
66 """Split a text into chunks of n characters
68 >>> chunks('abcdefghi', 3)
70 >>> chunks('abcdefghi', 4)
72 >>> chunks('abcdefghi', 4, fillvalue='!')
73 ['abcd', 'efgh', 'i!!!']
76 padding
= fillvalue
[0] * (n
- len(text
) % n
)
79 return [(text
+padding
)[i
:i
+n
] for i
in range(0, len(text
), n
)]
81 def transpose(items
, transposition
):
82 """Moves items around according to the given transposition
84 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
86 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
88 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
89 [13, 12, 14, 11, 15, 10]
91 transposed
= [''] * len(transposition
)
92 for p
, t
in enumerate(transposition
):
93 transposed
[p
] = items
[t
]
96 def untranspose(items
, transposition
):
99 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
101 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
103 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
104 [10, 11, 12, 13, 14, 15]
106 transposed
= [''] * len(transposition
)
107 for p
, t
in enumerate(transposition
):
108 transposed
[t
] = items
[p
]
111 def deduplicate(text
):
112 return list(collections
.OrderedDict
.fromkeys(text
))
115 def caesar_encipher_letter(accented_letter
, shift
):
116 """Encipher a letter, given a shift amount
118 >>> caesar_encipher_letter('a', 1)
120 >>> caesar_encipher_letter('a', 2)
122 >>> caesar_encipher_letter('b', 2)
124 >>> caesar_encipher_letter('x', 2)
126 >>> caesar_encipher_letter('y', 2)
128 >>> caesar_encipher_letter('z', 2)
130 >>> caesar_encipher_letter('z', -1)
132 >>> caesar_encipher_letter('a', -1)
134 >>> caesar_encipher_letter('A', 1)
136 >>> caesar_encipher_letter('é', 1)
139 # letter = unaccent(accented_letter)
140 # if letter in string.ascii_letters:
141 # if letter in string.ascii_uppercase:
142 # alphabet_start = ord('A')
144 # alphabet_start = ord('a')
145 # return chr(((ord(letter) - alphabet_start + shift) % 26) +
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()
160 def caesar_decipher_letter(letter
, shift
):
161 """Decipher a letter, given a shift amount
163 >>> caesar_decipher_letter('b', 1)
165 >>> caesar_decipher_letter('b', 2)
168 return caesar_encipher_letter(letter
, -shift
)
170 def caesar_encipher(message
, shift
):
171 """Encipher a message with the Caesar cipher of given shift
173 >>> caesar_encipher('abc', 1)
175 >>> caesar_encipher('abc', 2)
177 >>> caesar_encipher('abcxyz', 2)
179 >>> caesar_encipher('ab cx yz', 2)
181 >>> caesar_encipher('Héllo World!', 2)
184 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
185 return cat(enciphered
)
187 def caesar_decipher(message
, shift
):
188 """Decipher a message with the Caesar cipher of given shift
190 >>> caesar_decipher('bcd', 1)
192 >>> caesar_decipher('cde', 2)
194 >>> caesar_decipher('cd ez ab', 2)
196 >>> caesar_decipher('Jgnnq Yqtnf!', 2)
199 return caesar_encipher(message
, -shift
)
201 def affine_encipher_letter(accented_letter
, multiplier
=1, adder
=0, one_based
=True):
202 """Encipher a letter, given a multiplier and adder
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'
211 # letter = unaccent(accented_letter)
212 # if letter in string.ascii_letters:
213 # if letter in string.ascii_uppercase:
214 # alphabet_start = ord('A')
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)
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()
233 return unpos(cipher_number
)
237 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
238 """Encipher a letter, given a multiplier and adder
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'
247 # if letter in string.ascii_letters:
248 # if letter in string.ascii_uppercase:
249 # alphabet_start = ord('A')
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)
261 if letter
in string
.ascii_letters
:
262 cipher_number
= pos(letter
)
263 if one_based
: cipher_number
+= 1
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()
271 return unpos(plaintext_number
)
275 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
276 """Encipher a message
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'
282 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
284 return cat(enciphered
)
286 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
287 """Decipher a message
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'
293 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
295 return cat(enciphered
)
298 class KeywordWrapAlphabet(Enum
):
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
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'
318 if wrap_alphabet
== KeywordWrapAlphabet
.from_a
:
319 cipher_alphabet
= cat(deduplicate(sanitise(keyword
) +
320 string
.ascii_lowercase
))
322 if wrap_alphabet
== KeywordWrapAlphabet
.from_last
:
323 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
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
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
340 1 : from the last letter in the sanitised keyword
341 2 : from the largest letter in the sanitised keyword
343 >>> keyword_encipher('test message', 'bayes')
345 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a)
347 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last)
349 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest)
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
)
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
361 1 : from the last letter in the sanitised keyword
362 2 : from the largest letter in the sanitised keyword
364 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
366 >>> keyword_decipher('rsqr ksqqbds', 'bayes', KeywordWrapAlphabet.from_a)
368 >>> keyword_decipher('lskl dskkbus', 'bayes', KeywordWrapAlphabet.from_last)
370 >>> keyword_decipher('qspq jsppbcs', 'bayes', KeywordWrapAlphabet.from_largest)
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
)
378 def vigenere_encipher(message
, keyword
):
381 >>> vigenere_encipher('hello', 'abc')
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
])
388 def vigenere_decipher(message
, keyword
):
391 >>> vigenere_decipher('hfnlp', 'abc')
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
])
399 def beaufort_encipher(message
, keyword
):
402 >>> beaufort_encipher('inhisjournaldatedtheidesofoctober', 'arcanaimperii')
403 'sevsvrusyrrxfayyxuteemazudmpjmmwr'
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
])
409 beaufort_decipher
= beaufort_encipher
411 beaufort_variant_encipher
=vigenere_decipher
412 beaufort_variant_decipher
=vigenere_encipher
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
421 >>> polybius_grid('a', 'abcde', 'abcde')['x'] == ('e', 'c')
423 >>> polybius_grid('elephant', 'abcde', 'abcde')['e'] == ('a', 'a')
425 >>> polybius_grid('elephant', 'abcde', 'abcde')['b'] == ('b', 'c')
428 alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=wrap_alphabet
)
429 if letters_to_merge
is None:
430 letters_to_merge
= {'j': 'i'}
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
]]
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.
443 >>> polybius_reverse_grid('a', 'abcde', 'abcde')['e', 'c'] == 'x'
445 >>> polybius_reverse_grid('elephant', 'abcde', 'abcde')['a', 'a'] == 'e'
447 >>> polybius_reverse_grid('elephant', 'abcde', 'abcde')['b', 'c'] == 'b'
450 alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=wrap_alphabet
)
451 if letters_to_merge
is None:
452 letters_to_merge
= {'j': 'i'}
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
])}
459 def polybius_flatten(pair
, column_first
):
460 """Convert a series of pairs into a single list of characters"""
462 return str(pair
[1]) + str(pair
[0])
464 return str(pair
[0]) + str(pair
[1])
466 def polybius_encipher(message
, keyword
, column_order
, row_order
,
468 letters_to_merge
=None, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
469 """Encipher a message with Polybius cipher, using a keyword to rearrange
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', \
481 'bbadccddccddaebbaaddbbceaaddddaecbaacadadcbbadaaacdaabedbcccdeddbeaabdccacadaadcceaababb'
482 >>> polybius_encipher('this is a test message for the ' \
483 'polybius decipherment', 'elephant', 'abcde', 'abcde', \
485 'bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaacaadbadecbccedddebaadbcccadaaacdecaaabbb'
487 grid
= polybius_grid(keyword
, column_order
, row_order
, letters_to_merge
, wrap_alphabet
)
488 return cat(polybius_flatten(grid
[l
], column_first
)
493 def polybius_decipher(message
, keyword
, column_order
, row_order
,
495 letters_to_merge
=None, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
496 """Decipher a message with a Polybius cipher, using a keyword to rearrange
499 >>> polybius_decipher('bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaaca'\
500 'adbadecbccedddebaadbcccadaaacdecaaabbb', 'elephant', 'abcde', 'abcde', \
502 'toisisvtestxessvbephktoefhnugiysweqifoekxelt'
504 >>> polybius_decipher('bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaaca'\
505 'adbadecbccedddebaadbcccadaaacdecaaabbb', 'elephant', 'abcde', 'abcde', \
507 'thisisatestmessageforthepolybiusdecipherment'
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])
513 pairs
= [(column_index_type(p
[1]), row_index_type(p
[0])) for p
in chunks(message
, 2)]
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
)
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,
525 If passed a tuple, assume it's already a transposition and just return it.
527 >>> transpositions_of('clever')
529 >>> transpositions_of('fred')
531 >>> transpositions_of((3, 2, 0, 1))
534 if isinstance(keyword
, tuple):
537 key
= deduplicate(keyword
)
538 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
539 return transpositions
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
545 for i
in range(padding_length
):
546 if callable(fillvalue
):
547 padding
+= fillvalue()
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.
558 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
560 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
562 >>> column_transposition_encipher('hellothere', 'abcdef')
564 >>> column_transposition_encipher('hellothere', 'abcde')
566 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
568 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
570 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
572 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
574 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
576 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
578 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
580 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
582 >>> column_transposition_encipher('hellothere', 'cleverly')
584 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
586 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
589 transpositions
= transpositions_of(keyword
)
590 message
+= pad(len(message
), len(transpositions
), fillvalue
)
592 rows
= every_nth(message
, len(message
) // len(transpositions
))
594 rows
= chunks(message
, len(transpositions
))
595 transposed
= [transpose(r
, transpositions
) for r
in rows
]
597 return combine_every_nth(transposed
)
599 return cat(chain(*transposed
))
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.
607 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
609 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
611 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
613 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
615 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
617 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
619 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
621 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
624 transpositions
= transpositions_of(keyword
)
625 message
+= pad(len(message
), len(transpositions
), fillvalue
)
627 rows
= every_nth(message
, len(message
) // len(transpositions
))
629 rows
= chunks(message
, len(transpositions
))
630 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
632 return combine_every_nth(untransposed
)
634 return cat(chain(*untransposed
))
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.
640 >>> scytale_encipher('thequickbrownfox', 3)
642 >>> scytale_encipher('thequickbrownfox', 4)
644 >>> scytale_encipher('thequickbrownfox', 5)
645 'tubn hirf ecoo qkwx '
646 >>> scytale_encipher('thequickbrownfox', 6)
648 >>> scytale_encipher('thequickbrownfox', 7)
649 'tqcrnx hukof eibwo '
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)
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.
662 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
664 >>> scytale_decipher('tubnhirfecooqkwx', 4)
666 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
668 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
670 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
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)
681 def railfence_encipher(message
, height
, fillvalue
=''):
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
687 Example: the plaintext "hellotherefriends", with a height of four, written
688 out in the railfence as
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
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'
712 sections
= chunks(message
, (height
- 1) * 2, fillvalue
=fillvalue
)
713 n_sections
= len(sections
)
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])]
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.
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.
733 'hhieterelorfnsled' is split into
738 (spaces added for clarity), which is stored in 'rows'. This is then split
739 into 'down_rows' and 'up_rows':
751 These are then zipped together (after the up_rows are reversed) to recover
754 Most of the procedure is about finding the correct lengths for each row then
755 splitting the ciphertext into those rows.
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'
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):
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
788 for i
in folded_row_lengths
:
789 rows
+= [message
[row_start
: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
794 down_rows
= [rows
[0]]
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]]
801 return cat(c
for r
in zip_longest(*(down_rows
+ up_rows
), fillvalue
='') for c
in r
)
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
807 >>> make_cadenus_keycolumn()['a']
809 >>> make_cadenus_keycolumn()['b']
811 >>> make_cadenus_keycolumn()['c']
813 >>> make_cadenus_keycolumn()['v']
815 >>> make_cadenus_keycolumn()['w']
817 >>> make_cadenus_keycolumn()['z']
819 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['a']
821 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['b']
823 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['c']
825 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['i']
827 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['j']
829 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['v']
831 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['z']
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:]
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]]
844 def cadenus_encipher(message
, keyword
, keycolumn
, fillvalue
='a'):
845 """Encipher with the Cadenus cipher
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'), \
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'), \
857 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
858 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
860 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
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
))
868 def cadenus_decipher(message
, keyword
, keycolumn
, fillvalue
='a'):
870 >>> cadenus_decipher('antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaa' \
871 'suvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned', \
873 make_cadenus_keycolumn(reverse=True))
874 'whoeverhasmadeavoyageupthehudsonmustrememberthekaatskillmountainstheyareadismemberedbranchofthegreat'
875 >>> cadenus_decipher('systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtof' \
876 'arenuseieeieltarlmentieetogevesitfaisltngeeuvowul', \
878 make_cadenus_keycolumn(reverse=True))
879 'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
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
))
891 def hill_encipher(matrix
, message_letters
, fillvalue
='a'):
894 >>> hill_encipher(np.matrix([[7,8], [11,11]]), 'hellothere')
896 >>> hill_encipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
901 sanitised_message
= sanitise(message_letters
)
902 if len(sanitised_message
) % n
!= 0:
903 padding
= fillvalue
[0] * (n
- len(sanitised_message
) % n
)
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
, [])])
914 def hill_decipher(matrix
, message
, fillvalue
='a'):
917 >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
919 >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
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
)
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'])
934 class AmscoFillStyle(Enum
):
939 def amsco_transposition_positions(message
, keyword
,
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.
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)]]
971 transpositions
= transpositions_of(keyword
)
972 fill_iterator
= cycle(fillpattern
)
974 message_length
= len(message
)
978 current_fillpattern
= fillpattern
979 while current_position
< message_length
:
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
991 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
992 current_fillpattern
= list(reversed(current_fillpattern
))
993 return [transpose(r
, transpositions
) for r
in grid
]
995 def amsco_transposition_encipher(message
, keyword
,
996 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
997 """AMSCO transposition encipher.
999 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
1001 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
1003 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
1005 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
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'
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
)
1026 def amsco_transposition_decipher(message
, keyword
,
1027 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
1028 """AMSCO transposition decipher
1030 >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
1032 >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
1034 >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
1036 >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
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'
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
)
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
)
1063 def bifid_grid(keyword
, wrap_alphabet
, letter_mapping
):
1064 """Create the grids for a Bifid cipher
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
1077 def bifid_encipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
1078 letter_mapping
=None, period
=None, fillvalue
=None):
1081 >>> bifid_encipher("indiajelly", 'iguana')
1083 >>> bifid_encipher("indiacurry", 'iguana', period=4)
1085 >>> bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x')
1088 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
1090 t_message
= message
.translate(translation
)
1091 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
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]))
1097 chunked_pairs
= [pairs0
]
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)]
1105 return cat(r_grid
[p
] for p
in pairs1
)
1108 def bifid_decipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
1109 letter_mapping
=None, period
=None, fillvalue
=None):
1110 """Decipher with bifid cipher
1112 >>> bifid_decipher('ibidonhprm', 'iguana')
1114 >>> bifid_decipher("ibnhgaqltm", 'iguana', period=4)
1116 >>> bifid_decipher("ibnhgaqltzml", 'iguana', period=4)
1119 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
1121 t_message
= message
.translate(translation
)
1122 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
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]))
1128 chunked_pairs
= [pairs0
]
1131 for c
in chunked_pairs
:
1132 items
= [j
for i
in c
for j
in i
]
1134 p
= [(items
[i
], items
[i
+gap
]) for i
in range(gap
)]
1137 return cat(r_grid
[p
] for p
in pairs1
)
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.
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.
1149 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
1152 The position is the letter pointed to by the arrow on the wheel.
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]
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')]
1166 self
.make_wheel_map(self
.wheel1
)
1168 self
.make_wheel_map(self
.wheel2
)
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')
1175 self
.position
= position
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.
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]
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
1191 def validate_wheel_spec(self
, wheel_spec
):
1192 """Validates that a wheel specificaiton will turn into a valid wheel
1195 >>> pe.validate_wheel_spec([])
1196 Traceback (most recent call last):
1198 ValueError: Wheel specification has 0 pairs, requires 13
1199 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
1200 Traceback (most recent call last):
1202 ValueError: Not all mappings in wheel specificationhave two elements
1203 >>> pe.validate_wheel_spec([('a', 'b')]*13)
1204 Traceback (most recent call last):
1206 ValueError: Wheel specification does not contain 26 letters
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
:
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")
1219 def encipher_letter(self
, letter
):
1220 """Enciphers a single letter, by advancing the wheel before looking up
1221 the letter on the wheel.
1223 >>> pe.set_position('f')
1225 >>> pe.encipher_letter('k')
1229 return self
.lookup(letter
)
1230 decipher_letter
= encipher_letter
1232 def lookup(self
, letter
):
1233 """Look up what a letter enciphers to, without turning the wheel.
1235 >>> pe.set_position('f')
1237 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
1238 'udhbfejcpgmokrliwntsayqzvx'
1242 if letter
in string
.ascii_lowercase
:
1244 (self
.wheel_map
[(ord(letter
) - ord('a') - self
.position
) % 26] +
1245 self
.position
) % 26 +
1251 """Advances the wheel one position.
1253 >>> pe.set_position('f')
1258 self
.position
= (self
.position
+ 1) % 26
1259 return self
.position
1261 def encipher(self
, message
, starting_position
=None):
1262 """Enciphers a whole message.
1264 >>> pe.set_position('f')
1266 >>> pe.encipher('helloworld')
1268 >>> pe.set_position('f')
1270 >>> pe.encipher('kjsglcjoqc')
1272 >>> pe.encipher('helloworld', starting_position = 'x')
1275 if starting_position
:
1276 self
.set_position(starting_position
)
1279 transformed
+= self
.encipher_letter(l
)
1283 def set_position(self
, position
):
1284 """Sets the position of the wheel, by specifying the letter the arrow
1287 >>> pe.set_position('a')
1289 >>> pe.set_position('m')
1291 >>> pe.set_position('z')
1294 self
.position
= ord(position
) - ord('a')
1295 return self
.position
1298 if __name__
== "__main__":
1300 doctest
.testmod(extraglobs
={'pe': PocketEnigma(1, 'a')})