5 from itertools
import zip_longest
, cycle
, chain
, count
7 from numpy
import matrix
8 from numpy
import linalg
9 from language_models
import *
19 if letter
in string
.ascii_lowercase
:
20 return ord(letter
) - ord('a')
21 elif letter
in string
.ascii_uppercase
:
22 return ord(letter
) - ord('A')
26 def unpos(number
): return chr(number
% 26 + ord('a'))
29 modular_division_table
= [[0]*26 for _
in range(26)]
33 modular_division_table
[b
][c
] = a
36 def every_nth(text
, n
, fillvalue
=''):
37 """Returns n strings, each of which consists of every nth character,
38 starting with the 0th, 1st, 2nd, ... (n-1)th character
40 >>> every_nth(string.ascii_lowercase, 5)
41 ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty']
42 >>> every_nth(string.ascii_lowercase, 1)
43 ['abcdefghijklmnopqrstuvwxyz']
44 >>> every_nth(string.ascii_lowercase, 26) # doctest: +NORMALIZE_WHITESPACE
45 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
46 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
47 >>> every_nth(string.ascii_lowercase, 5, fillvalue='!')
48 ['afkpuz', 'bglqv!', 'chmrw!', 'dinsx!', 'ejoty!']
50 split_text
= chunks(text
, n
, fillvalue
)
51 return [cat(l
) for l
in zip_longest(*split_text
, fillvalue
=fillvalue
)]
53 def combine_every_nth(split_text
):
54 """Reforms a text split into every_nth strings
56 >>> combine_every_nth(every_nth(string.ascii_lowercase, 5))
57 'abcdefghijklmnopqrstuvwxyz'
58 >>> combine_every_nth(every_nth(string.ascii_lowercase, 1))
59 'abcdefghijklmnopqrstuvwxyz'
60 >>> combine_every_nth(every_nth(string.ascii_lowercase, 26))
61 'abcdefghijklmnopqrstuvwxyz'
64 for l
in zip_longest(*split_text
, fillvalue
='')])
66 def chunks(text
, n
, fillvalue
=None):
67 """Split a text into chunks of n characters
69 >>> chunks('abcdefghi', 3)
71 >>> chunks('abcdefghi', 4)
73 >>> chunks('abcdefghi', 4, fillvalue='!')
74 ['abcd', 'efgh', 'i!!!']
77 padding
= fillvalue
[0] * (n
- len(text
) % n
)
80 return [(text
+padding
)[i
:i
+n
] for i
in range(0, len(text
), n
)]
82 def transpose(items
, transposition
):
83 """Moves items around according to the given transposition
85 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
87 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
89 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
90 [13, 12, 14, 11, 15, 10]
92 transposed
= [''] * len(transposition
)
93 for p
, t
in enumerate(transposition
):
94 transposed
[p
] = items
[t
]
97 def untranspose(items
, transposition
):
100 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
102 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
104 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
105 [10, 11, 12, 13, 14, 15]
107 transposed
= [''] * len(transposition
)
108 for p
, t
in enumerate(transposition
):
109 transposed
[t
] = items
[p
]
112 def deduplicate(text
):
113 return list(collections
.OrderedDict
.fromkeys(text
))
116 def caesar_encipher_letter(accented_letter
, shift
):
117 """Encipher a letter, given a shift amount
119 >>> caesar_encipher_letter('a', 1)
121 >>> caesar_encipher_letter('a', 2)
123 >>> caesar_encipher_letter('b', 2)
125 >>> caesar_encipher_letter('x', 2)
127 >>> caesar_encipher_letter('y', 2)
129 >>> caesar_encipher_letter('z', 2)
131 >>> caesar_encipher_letter('z', -1)
133 >>> caesar_encipher_letter('a', -1)
135 >>> caesar_encipher_letter('A', 1)
137 >>> caesar_encipher_letter('é', 1)
140 # letter = unaccent(accented_letter)
141 # if letter in string.ascii_letters:
142 # if letter in string.ascii_uppercase:
143 # alphabet_start = ord('A')
145 # alphabet_start = ord('a')
146 # return chr(((ord(letter) - alphabet_start + shift) % 26) +
151 letter
= unaccent(accented_letter
)
152 if letter
in string
.ascii_letters
:
153 cipherletter
= unpos(pos(letter
) + shift
)
154 if letter
in string
.ascii_uppercase
:
155 return cipherletter
.upper()
161 def caesar_decipher_letter(letter
, shift
):
162 """Decipher a letter, given a shift amount
164 >>> caesar_decipher_letter('b', 1)
166 >>> caesar_decipher_letter('b', 2)
169 return caesar_encipher_letter(letter
, -shift
)
171 def caesar_encipher(message
, shift
):
172 """Encipher a message with the Caesar cipher of given shift
174 >>> caesar_encipher('abc', 1)
176 >>> caesar_encipher('abc', 2)
178 >>> caesar_encipher('abcxyz', 2)
180 >>> caesar_encipher('ab cx yz', 2)
182 >>> caesar_encipher('Héllo World!', 2)
185 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
186 return cat(enciphered
)
188 def caesar_decipher(message
, shift
):
189 """Decipher a message with the Caesar cipher of given shift
191 >>> caesar_decipher('bcd', 1)
193 >>> caesar_decipher('cde', 2)
195 >>> caesar_decipher('cd ez ab', 2)
197 >>> caesar_decipher('Jgnnq Yqtnf!', 2)
200 return caesar_encipher(message
, -shift
)
202 def affine_encipher_letter(accented_letter
, multiplier
=1, adder
=0, one_based
=True):
203 """Encipher a letter, given a multiplier and adder
205 >>> cat(affine_encipher_letter(l, 3, 5, True) \
206 for l in string.ascii_letters)
207 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE'
208 >>> cat(affine_encipher_letter(l, 3, 5, False) \
209 for l in string.ascii_letters)
210 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC'
212 # letter = unaccent(accented_letter)
213 # if letter in string.ascii_letters:
214 # if letter in string.ascii_uppercase:
215 # alphabet_start = ord('A')
217 # alphabet_start = ord('a')
218 # letter_number = ord(letter) - alphabet_start
219 # if one_based: letter_number += 1
220 # cipher_number = (letter_number * multiplier + adder) % 26
221 # if one_based: cipher_number -= 1
222 # return chr(cipher_number % 26 + alphabet_start)
225 letter
= unaccent(accented_letter
)
226 if letter
in string
.ascii_letters
:
227 letter_number
= pos(letter
)
228 if one_based
: letter_number
+= 1
229 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
230 if one_based
: cipher_number
-= 1
231 if letter
in string
.ascii_uppercase
:
232 return unpos(cipher_number
).upper()
234 return unpos(cipher_number
)
238 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
239 """Encipher a letter, given a multiplier and adder
241 >>> cat(affine_decipher_letter(l, 3, 5, True) \
242 for l in 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE')
243 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
244 >>> cat(affine_decipher_letter(l, 3, 5, False) \
245 for l in 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC')
246 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
248 # if letter in string.ascii_letters:
249 # if letter in string.ascii_uppercase:
250 # alphabet_start = ord('A')
252 # alphabet_start = ord('a')
253 # cipher_number = ord(letter) - alphabet_start
254 # if one_based: cipher_number += 1
255 # plaintext_number = (
256 # modular_division_table[multiplier]
257 # [(cipher_number - adder) % 26])
258 # if one_based: plaintext_number -= 1
259 # return chr(plaintext_number % 26 + alphabet_start)
262 if letter
in string
.ascii_letters
:
263 cipher_number
= pos(letter
)
264 if one_based
: cipher_number
+= 1
266 modular_division_table
[multiplier
]
267 [(cipher_number
- adder
) % 26])
268 if one_based
: plaintext_number
-= 1
269 if letter
in string
.ascii_uppercase
:
270 return unpos(plaintext_number
).upper()
272 return unpos(plaintext_number
)
276 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
277 """Encipher a message
279 >>> affine_encipher('hours passed during which jerico tried every ' \
280 'trick he could think of', 15, 22, True)
281 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
283 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
285 return cat(enciphered
)
287 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
288 """Decipher a message
290 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
291 'jfaoe ls omytd jlaxe mh', 15, 22, True)
292 'hours passed during which jerico tried every trick he could think of'
294 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
296 return cat(enciphered
)
299 class KeywordWrapAlphabet(Enum
):
305 def keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
306 """Find the cipher alphabet given a keyword.
307 wrap_alphabet controls how the rest of the alphabet is added
310 >>> keyword_cipher_alphabet_of('bayes')
311 'bayescdfghijklmnopqrtuvwxz'
312 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_a)
313 'bayescdfghijklmnopqrtuvwxz'
314 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_last)
315 'bayestuvwxzcdfghijklmnopqr'
316 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_largest)
317 'bayeszcdfghijklmnopqrtuvwx'
319 if wrap_alphabet
== KeywordWrapAlphabet
.from_a
:
320 cipher_alphabet
= cat(deduplicate(sanitise(keyword
) +
321 string
.ascii_lowercase
))
323 if wrap_alphabet
== KeywordWrapAlphabet
.from_last
:
324 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
326 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
327 last_keyword_position
= string
.ascii_lowercase
.find(
328 last_keyword_letter
) + 1
329 cipher_alphabet
= cat(
330 deduplicate(sanitise(keyword
) +
331 string
.ascii_lowercase
[last_keyword_position
:] +
332 string
.ascii_lowercase
))
333 return cipher_alphabet
336 def keyword_encipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
337 """Enciphers a message with a keyword substitution cipher.
338 wrap_alphabet controls how the rest of the alphabet is added
341 1 : from the last letter in the sanitised keyword
342 2 : from the largest letter in the sanitised keyword
344 >>> keyword_encipher('test message', 'bayes')
346 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a)
348 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last)
350 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest)
353 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
354 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
355 return unaccent(message
).lower().translate(cipher_translation
)
357 def keyword_decipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
358 """Deciphers a message with a keyword substitution cipher.
359 wrap_alphabet controls how the rest of the alphabet is added
362 1 : from the last letter in the sanitised keyword
363 2 : from the largest letter in the sanitised keyword
365 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
367 >>> keyword_decipher('rsqr ksqqbds', 'bayes', KeywordWrapAlphabet.from_a)
369 >>> keyword_decipher('lskl dskkbus', 'bayes', KeywordWrapAlphabet.from_last)
371 >>> keyword_decipher('qspq jsppbcs', 'bayes', KeywordWrapAlphabet.from_largest)
374 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
375 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
376 return message
.lower().translate(cipher_translation
)
379 def vigenere_encipher(message
, keyword
):
382 >>> vigenere_encipher('hello', 'abc')
385 shifts
= [pos(l
) for l
in sanitise(keyword
)]
386 pairs
= zip(message
, cycle(shifts
))
387 return cat([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
389 def vigenere_decipher(message
, keyword
):
392 >>> vigenere_decipher('hfnlp', 'abc')
395 shifts
= [pos(l
) for l
in sanitise(keyword
)]
396 pairs
= zip(message
, cycle(shifts
))
397 return cat([caesar_decipher_letter(l
, k
) for l
, k
in pairs
])
400 def beaufort_encipher(message
, keyword
):
403 >>> beaufort_encipher('inhisjournaldatedtheidesofoctober', 'arcanaimperii')
404 'sevsvrusyrrxfayyxuteemazudmpjmmwr'
406 shifts
= [pos(l
) for l
in sanitise(keyword
)]
407 pairs
= zip(message
, cycle(shifts
))
408 return cat([unpos(k
- pos(l
)) for l
, k
in pairs
])
410 beaufort_decipher
= beaufort_encipher
412 beaufort_variant_encipher
=vigenere_decipher
413 beaufort_variant_decipher
=vigenere_encipher
416 def polybius_grid(keyword
, column_order
, row_order
, letters_to_merge
=None,
417 wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
418 """Grid for a Polybius cipher, using a keyword to rearrange the
422 >>> polybius_grid('a', 'abcde', 'abcde')['x'] == ('e', 'c')
424 >>> polybius_grid('elephant', 'abcde', 'abcde')['e'] == ('a', 'a')
426 >>> polybius_grid('elephant', 'abcde', 'abcde')['b'] == ('b', 'c')
429 alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=wrap_alphabet
)
430 if letters_to_merge
is None:
431 letters_to_merge
= {'j': 'i'}
433 for k
, l
in zip([(c
, r
) for c
in column_order
for r
in row_order
],
434 [l
for l
in alphabet
if l
not in letters_to_merge
])}
435 for l
in letters_to_merge
:
436 grid
[l
] = grid
[letters_to_merge
[l
]]
439 def polybius_reverse_grid(keyword
, column_order
, row_order
, letters_to_merge
=None,
440 wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
441 """Grid for decrypting using a Polybius cipher, using a keyword to
442 rearrange the alphabet.
444 >>> polybius_reverse_grid('a', 'abcde', 'abcde')['e', 'c'] == 'x'
446 >>> polybius_reverse_grid('elephant', 'abcde', 'abcde')['a', 'a'] == 'e'
448 >>> polybius_reverse_grid('elephant', 'abcde', 'abcde')['b', 'c'] == 'b'
451 alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=wrap_alphabet
)
452 if letters_to_merge
is None:
453 letters_to_merge
= {'j': 'i'}
455 for k
, l
in zip([(c
, r
) for c
in column_order
for r
in row_order
],
456 [l
for l
in alphabet
if l
not in letters_to_merge
])}
460 def polybius_flatten(pair
, column_first
):
461 """Convert a series of pairs into a single list of characters"""
463 return str(pair
[1]) + str(pair
[0])
465 return str(pair
[0]) + str(pair
[1])
467 def polybius_encipher(message
, keyword
, column_order
, row_order
,
469 letters_to_merge
=None, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
470 """Encipher a message with Polybius cipher, using a keyword to rearrange
474 >>> polybius_encipher('this is a test message for the ' \
475 'polybius decipherment', 'elephant', \
476 [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], \
477 wrap_alphabet=KeywordWrapAlphabet.from_last)
478 '2214445544551522115522511155551543114252542214111352123234442355411135441314115451112122'
479 >>> polybius_encipher('this is a test message for the ' \
480 'polybius decipherment', 'elephant', 'abcde', 'abcde', \
482 'bbadccddccddaebbaaddbbceaaddddaecbaacadadcbbadaaacdaabedbcccdeddbeaabdccacadaadcceaababb'
483 >>> polybius_encipher('this is a test message for the ' \
484 'polybius decipherment', 'elephant', 'abcde', 'abcde', \
486 'bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaacaadbadecbccedddebaadbcccadaaacdecaaabbb'
488 grid
= polybius_grid(keyword
, column_order
, row_order
, letters_to_merge
, wrap_alphabet
)
489 return cat(polybius_flatten(grid
[l
], column_first
)
494 def polybius_decipher(message
, keyword
, column_order
, row_order
,
496 letters_to_merge
=None, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
497 """Decipher a message with a Polybius cipher, using a keyword to rearrange
500 >>> polybius_decipher('bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaaca'\
501 'adbadecbccedddebaadbcccadaaacdecaaabbb', 'elephant', 'abcde', 'abcde', \
503 'toisisvtestxessvbephktoefhnugiysweqifoekxelt'
505 >>> polybius_decipher('bbdaccddccddeabbaaddbbecaaddddeabcaaacadcdbbdaaaca'\
506 'adbadecbccedddebaadbcccadaaacdecaaabbb', 'elephant', 'abcde', 'abcde', \
508 'thisisatestmessageforthepolybiusdecipherment'
510 grid
= polybius_reverse_grid(keyword
, column_order
, row_order
, letters_to_merge
, wrap_alphabet
)
511 column_index_type
= type(column_order
[0])
512 row_index_type
= type(row_order
[0])
514 pairs
= [(column_index_type(p
[1]), row_index_type(p
[0])) for p
in chunks(message
, 2)]
516 pairs
= [(row_index_type(p
[0]), column_index_type(p
[1])) for p
in chunks(message
, 2)]
517 return cat(grid
[p
] for p
in pairs
if p
in grid
)
520 def transpositions_of(keyword
):
521 """Finds the transpostions given by a keyword. For instance, the keyword
522 'clever' rearranges to 'celrv', so the first column (0) stays first, the
523 second column (1) moves to third, the third column (2) moves to second,
526 If passed a tuple, assume it's already a transposition and just return it.
528 >>> transpositions_of('clever')
530 >>> transpositions_of('fred')
532 >>> transpositions_of((3, 2, 0, 1))
535 if isinstance(keyword
, tuple):
538 key
= deduplicate(keyword
)
539 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
540 return transpositions
542 def pad(message_len
, group_len
, fillvalue
):
543 padding_length
= group_len
- message_len
% group_len
544 if padding_length
== group_len
: padding_length
= 0
546 for i
in range(padding_length
):
547 if callable(fillvalue
):
548 padding
+= fillvalue()
553 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
554 fillcolumnwise
=False,
555 emptycolumnwise
=False):
556 """Enciphers using the column transposition cipher.
557 Message is padded to allow all rows to be the same length.
559 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
561 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
563 >>> column_transposition_encipher('hellothere', 'abcdef')
565 >>> column_transposition_encipher('hellothere', 'abcde')
567 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
569 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
571 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
573 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
575 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
577 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
579 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
581 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
583 >>> column_transposition_encipher('hellothere', 'cleverly')
585 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
587 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
590 transpositions
= transpositions_of(keyword
)
591 message
+= pad(len(message
), len(transpositions
), fillvalue
)
593 rows
= every_nth(message
, len(message
) // len(transpositions
))
595 rows
= chunks(message
, len(transpositions
))
596 transposed
= [transpose(r
, transpositions
) for r
in rows
]
598 return combine_every_nth(transposed
)
600 return cat(chain(*transposed
))
602 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
603 fillcolumnwise
=False,
604 emptycolumnwise
=False):
605 """Deciphers using the column transposition cipher.
606 Message is padded to allow all rows to be the same length.
608 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
610 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
612 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
614 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
616 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
618 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
620 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
622 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
625 transpositions
= transpositions_of(keyword
)
626 message
+= pad(len(message
), len(transpositions
), fillvalue
)
628 rows
= every_nth(message
, len(message
) // len(transpositions
))
630 rows
= chunks(message
, len(transpositions
))
631 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
633 return combine_every_nth(untransposed
)
635 return cat(chain(*untransposed
))
637 def scytale_encipher(message
, rows
, fillvalue
=' '):
638 """Enciphers using the scytale transposition cipher.
639 Message is padded with spaces to allow all rows to be the same length.
641 >>> scytale_encipher('thequickbrownfox', 3)
643 >>> scytale_encipher('thequickbrownfox', 4)
645 >>> scytale_encipher('thequickbrownfox', 5)
646 'tubn hirf ecoo qkwx '
647 >>> scytale_encipher('thequickbrownfox', 6)
649 >>> scytale_encipher('thequickbrownfox', 7)
650 'tqcrnx hukof eibwo '
652 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
653 # return column_transposition_encipher(message, transpositions,
654 # fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
655 transpositions
= [i
for i
in range(rows
)]
656 return column_transposition_encipher(message
, transpositions
,
657 fillvalue
=fillvalue
, fillcolumnwise
=True, emptycolumnwise
=False)
659 def scytale_decipher(message
, rows
):
660 """Deciphers using the scytale transposition cipher.
661 Assumes the message is padded so that all rows are the same length.
663 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
665 >>> scytale_decipher('tubnhirfecooqkwx', 4)
667 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
669 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
671 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
674 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
675 # return column_transposition_decipher(message, transpositions,
676 # fillcolumnwise=False, emptycolumnwise=True)
677 transpositions
= [i
for i
in range(rows
)]
678 return column_transposition_decipher(message
, transpositions
,
679 fillcolumnwise
=True, emptycolumnwise
=False)
682 def railfence_encipher(message
, height
, fillvalue
=''):
684 Works by splitting the text into sections, then reading across them to
685 generate the rows in the cipher. The rows are then combined to form the
688 Example: the plaintext "hellotherefriends", with a height of four, written
689 out in the railfence as
694 (with the * showing the one character to finish the last section).
695 Each 'section' is two columns, but unfolded. In the example, the first
698 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 2, fillvalue='!')
699 'hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!'
700 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3, fillvalue='!')
701 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!'
702 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5, fillvalue='!')
703 'hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!'
704 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 10, fillvalue='!')
705 'hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!'
706 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3)
707 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece'
708 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5)
709 'hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp'
710 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 7)
711 'haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic'
713 sections
= chunks(message
, (height
- 1) * 2, fillvalue
=fillvalue
)
714 n_sections
= len(sections
)
716 rows
= [cat([s
[0] for s
in sections
])]
717 # process the middle rows of the grid
718 for r
in range(1, height
-1):
719 rows
+= [cat([s
[r
:r
+1] + s
[height
*2-r
-2:height
*2-r
-1] for s
in sections
])]
720 # process the bottom row
721 rows
+= [cat([s
[height
- 1:height
] for s
in sections
])]
722 # rows += [wcat([s[height - 1] for s in sections])]
725 def railfence_decipher(message
, height
, fillvalue
=''):
726 """Railfence decipher.
727 Works by reconstructing the grid used to generate the ciphertext, then
728 unfolding the sections so the text can be concatenated together.
730 Example: given the ciphertext 'hhieterelorfnsled' and a height of 4, first
731 work out that the second row has a character missing, find the rows of the
732 grid, then split the section into its two columns.
734 'hhieterelorfnsled' is split into
739 (spaces added for clarity), which is stored in 'rows'. This is then split
740 into 'down_rows' and 'up_rows':
752 These are then zipped together (after the up_rows are reversed) to recover
755 Most of the procedure is about finding the correct lengths for each row then
756 splitting the ciphertext into those rows.
758 >>> railfence_decipher('hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!', 2).strip('!')
759 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
760 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!', 3).strip('!')
761 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
762 >>> railfence_decipher('hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!', 5).strip('!')
763 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
764 >>> railfence_decipher('hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!', 10).strip('!')
765 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
766 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece', 3)
767 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
768 >>> railfence_decipher('hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp', 5)
769 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
770 >>> railfence_decipher('haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic', 7)
771 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
773 # find the number and size of the sections, including how many characters
774 # are missing for a full grid
775 n_sections
= math
.ceil(len(message
) / ((height
- 1) * 2))
776 padding_to_add
= n_sections
* (height
- 1) * 2 - len(message
)
777 # row_lengths are for the both up rows and down rows
778 row_lengths
= [n_sections
] * (height
- 1) * 2
779 for i
in range((height
- 1) * 2 - 1, (height
- 1) * 2 - (padding_to_add
+ 1), -1):
781 # folded_rows are the combined row lengths in the middle of the railfence
782 folded_row_lengths
= [row_lengths
[0]]
783 for i
in range(1, height
-1):
784 folded_row_lengths
+= [row_lengths
[i
] + row_lengths
[-i
]]
785 folded_row_lengths
+= [row_lengths
[height
- 1]]
786 # find the rows that form the railfence grid
789 for i
in folded_row_lengths
:
790 rows
+= [message
[row_start
:row_start
+ i
]]
792 # split the rows into the 'down_rows' (those that form the first column of
793 # a section) and the 'up_rows' (those that ofrm the second column of a
795 down_rows
= [rows
[0]]
797 for i
in range(1, height
-1):
798 down_rows
+= [cat([c
for n
, c
in enumerate(rows
[i
]) if n
% 2 == 0])]
799 up_rows
+= [cat([c
for n
, c
in enumerate(rows
[i
]) if n
% 2 == 1])]
800 down_rows
+= [rows
[-1]]
802 return cat(c
for r
in zip_longest(*(down_rows
+ up_rows
), fillvalue
='') for c
in r
)
804 def make_cadenus_keycolumn(doubled_letters
= 'vw', start
='a', reverse
=False):
805 """Makes the key column for a Cadenus cipher (the column down between the
808 >>> make_cadenus_keycolumn()['a']
810 >>> make_cadenus_keycolumn()['b']
812 >>> make_cadenus_keycolumn()['c']
814 >>> make_cadenus_keycolumn()['v']
816 >>> make_cadenus_keycolumn()['w']
818 >>> make_cadenus_keycolumn()['z']
820 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['a']
822 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['b']
824 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['c']
826 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['i']
828 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['j']
830 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['v']
832 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['z']
835 index_to_remove
= string
.ascii_lowercase
.find(doubled_letters
[0])
836 short_alphabet
= string
.ascii_lowercase
[:index_to_remove
] + string
.ascii_lowercase
[index_to_remove
+1:]
838 short_alphabet
= cat(reversed(short_alphabet
))
839 start_pos
= short_alphabet
.find(start
)
840 rotated_alphabet
= short_alphabet
[start_pos
:] + short_alphabet
[:start_pos
]
841 keycolumn
= {l
: i
for i
, l
in enumerate(rotated_alphabet
)}
842 keycolumn
[doubled_letters
[0]] = keycolumn
[doubled_letters
[1]]
845 def cadenus_encipher(message
, keyword
, keycolumn
, fillvalue
='a'):
846 """Encipher with the Cadenus cipher
848 >>> cadenus_encipher(sanitise('Whoever has made a voyage up the Hudson ' \
849 'must remember the Kaatskill mountains. ' \
850 'They are a dismembered branch of the great'), \
852 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
853 'antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
854 >>> cadenus_encipher(sanitise('a severe limitation on the usefulness of ' \
855 'the cadenus is that every message must be ' \
856 'a multiple of twenty-five letters long'), \
858 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
859 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
861 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
863 rotated_columns
= [col
[start
:] + col
[:start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
864 rotated_rows
= zip(*rotated_columns
)
865 transpositions
= transpositions_of(keyword
)
866 transposed
= [transpose(r
, transpositions
) for r
in rotated_rows
]
867 return cat(chain(*transposed
))
869 def cadenus_decipher(message
, keyword
, keycolumn
, fillvalue
='a'):
871 >>> cadenus_decipher('antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaa' \
872 'suvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned', \
874 make_cadenus_keycolumn(reverse=True))
875 'whoeverhasmadeavoyageupthehudsonmustrememberthekaatskillmountainstheyareadismemberedbranchofthegreat'
876 >>> cadenus_decipher('systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtof' \
877 'arenuseieeieltarlmentieetogevesitfaisltngeeuvowul', \
879 make_cadenus_keycolumn(reverse=True))
880 'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
882 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
883 transpositions
= transpositions_of(keyword
)
884 untransposed_rows
= [untranspose(r
, transpositions
) for r
in rows
]
885 columns
= zip(*untransposed_rows
)
886 rotated_columns
= [col
[-start
:] + col
[:-start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
887 rotated_rows
= zip(*rotated_columns
)
888 # return rotated_columns
889 return cat(chain(*rotated_rows
))
892 def hill_encipher(matrix
, message_letters
, fillvalue
='a'):
895 >>> hill_encipher(np.matrix([[7,8], [11,11]]), 'hellothere')
897 >>> hill_encipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
902 sanitised_message
= sanitise(message_letters
)
903 if len(sanitised_message
) % n
!= 0:
904 padding
= fillvalue
[0] * (n
- len(sanitised_message
) % n
)
907 message
= [pos(c
) for c
in sanitised_message
+ padding
]
908 message_chunks
= [message
[i
:i
+n
] for i
in range(0, len(message
), n
)]
909 # message_chunks = chunks(message, len(matrix), fillvalue=None)
910 enciphered_chunks
= [((matrix
* np
.matrix(c
).T
).T
).tolist()[0]
911 for c
in message_chunks
]
912 return cat([unpos(round(l
))
913 for l
in sum(enciphered_chunks
, [])])
915 def hill_decipher(matrix
, message
, fillvalue
='a'):
918 >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
920 >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
924 adjoint
= linalg
.det(matrix
)*linalg
.inv(matrix
)
925 inverse_determinant
= modular_division_table
[int(round(linalg
.det(matrix
))) % 26][1]
926 inverse_matrix
= (inverse_determinant
* adjoint
) % 26
927 return hill_encipher(inverse_matrix
, message
, fillvalue
)
930 # Where each piece of text ends up in the AMSCO transpositon cipher.
931 # 'index' shows where the slice appears in the plaintext, with the slice
932 # from 'start' to 'end'
933 AmscoSlice
= collections
.namedtuple('AmscoSlice', ['index', 'start', 'end'])
935 class AmscoFillStyle(Enum
):
940 def amsco_transposition_positions(message
, keyword
,
942 fillstyle
=AmscoFillStyle
.continuous
,
943 fillcolumnwise
=False,
944 emptycolumnwise
=True):
945 """Creates the grid for the AMSCO transposition cipher. Each element in the
946 grid shows the index of that slice and the start and end positions of the
947 plaintext that go to make it up.
949 >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
950 fillpattern=(1, 2)) # doctest: +NORMALIZE_WHITESPACE
951 [[AmscoSlice(index=3, start=4, end=6),
952 AmscoSlice(index=2, start=3, end=4),
953 AmscoSlice(index=0, start=0, end=1),
954 AmscoSlice(index=1, start=1, end=3),
955 AmscoSlice(index=4, start=6, end=7)],
956 [AmscoSlice(index=8, start=12, end=13),
957 AmscoSlice(index=7, start=10, end=12),
958 AmscoSlice(index=5, start=7, end=9),
959 AmscoSlice(index=6, start=9, end=10),
960 AmscoSlice(index=9, start=13, end=15)],
961 [AmscoSlice(index=13, start=19, end=21),
962 AmscoSlice(index=12, start=18, end=19),
963 AmscoSlice(index=10, start=15, end=16),
964 AmscoSlice(index=11, start=16, end=18),
965 AmscoSlice(index=14, start=21, end=22)],
966 [AmscoSlice(index=18, start=27, end=28),
967 AmscoSlice(index=17, start=25, end=27),
968 AmscoSlice(index=15, start=22, end=24),
969 AmscoSlice(index=16, start=24, end=25),
970 AmscoSlice(index=19, start=28, end=30)]]
972 transpositions
= transpositions_of(keyword
)
973 fill_iterator
= cycle(fillpattern
)
975 message_length
= len(message
)
979 current_fillpattern
= fillpattern
980 while current_position
< message_length
:
982 if fillstyle
== AmscoFillStyle
.same_each_row
:
983 fill_iterator
= cycle(fillpattern
)
984 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
985 fill_iterator
= cycle(current_fillpattern
)
986 for _
in range(len(transpositions
)):
987 index
= next(indices
)
988 gap
= next(fill_iterator
)
989 row
+= [AmscoSlice(index
, current_position
, current_position
+ gap
)]
990 current_position
+= gap
992 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
993 current_fillpattern
= list(reversed(current_fillpattern
))
994 return [transpose(r
, transpositions
) for r
in grid
]
996 def amsco_transposition_encipher(message
, keyword
,
997 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
998 """AMSCO transposition encipher.
1000 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
1002 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
1004 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
1006 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
1008 >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
1009 'etecstthhomoerereenisxip'
1010 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
1011 'hetcsoeisterereipexthomn'
1012 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
1013 'hecsoisttererteipexhomen'
1014 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
1015 'heecisoosttrrtepeixhemen'
1016 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
1017 'hxtomephescieretoeisnter'
1018 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
1019 'hxomeiphscerettoisenteer'
1021 grid
= amsco_transposition_positions(message
, keyword
,
1022 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
1023 ct_as_grid
= [[message
[s
.start
:s
.end
] for s
in r
] for r
in grid
]
1024 return combine_every_nth(ct_as_grid
)
1027 def amsco_transposition_decipher(message
, keyword
,
1028 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
1029 """AMSCO transposition decipher
1031 >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
1033 >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
1035 >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
1037 >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
1039 >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
1040 'hereissometexttoencipher'
1041 >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
1042 'hereissometexttoencipher'
1043 >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
1044 'hereissometexttoencipher'
1045 >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
1046 'hereissometexttoencipher'
1047 >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
1048 'hereissometexttoencipher'
1049 >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
1050 'hereissometexttoencipher'
1053 grid
= amsco_transposition_positions(message
, keyword
,
1054 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
1055 transposed_sections
= [s
for c
in [l
for l
in zip(*grid
)] for s
in c
]
1056 plaintext_list
= [''] * len(transposed_sections
)
1058 for slice in transposed_sections
:
1059 plaintext_list
[slice.index
] = message
[current_pos
:current_pos
-slice.start
+slice.end
][:len(message
[slice.start
:slice.end
])]
1060 current_pos
+= len(message
[slice.start
:slice.end
])
1061 return cat(plaintext_list
)
1064 def bifid_grid(keyword
, wrap_alphabet
, letter_mapping
):
1065 """Create the grids for a Bifid cipher
1067 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
1068 if letter_mapping
is None:
1069 letter_mapping
= {'j': 'i'}
1070 translation
= ''.maketrans(letter_mapping
)
1071 cipher_alphabet
= cat(collections
.OrderedDict
.fromkeys(cipher_alphabet
.translate(translation
)))
1072 f_grid
= {k
: ((i
// 5) + 1, (i
% 5) + 1)
1073 for i
, k
in enumerate(cipher_alphabet
)}
1074 r_grid
= {((i
// 5) + 1, (i
% 5) + 1): k
1075 for i
, k
in enumerate(cipher_alphabet
)}
1076 return translation
, f_grid
, r_grid
1078 def bifid_encipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
1079 letter_mapping
=None, period
=None, fillvalue
=None):
1082 >>> bifid_encipher("indiajelly", 'iguana')
1084 >>> bifid_encipher("indiacurry", 'iguana', period=4)
1086 >>> bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x')
1089 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
1091 t_message
= message
.translate(translation
)
1092 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
1094 chunked_pairs
= [pairs0
[i
:i
+period
] for i
in range(0, len(pairs0
), period
)]
1095 if len(chunked_pairs
[-1]) < period
and fillvalue
:
1096 chunked_pairs
[-1] += [f_grid
[fillvalue
]] * (period
- len(chunked_pairs
[-1]))
1098 chunked_pairs
= [pairs0
]
1101 for c
in chunked_pairs
:
1102 items
= sum(list(list(i
) for i
in zip(*c
)), [])
1103 p
= [(items
[i
], items
[i
+1]) for i
in range(0, len(items
), 2)]
1106 return cat(r_grid
[p
] for p
in pairs1
)
1109 def bifid_decipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
1110 letter_mapping
=None, period
=None, fillvalue
=None):
1111 """Decipher with bifid cipher
1113 >>> bifid_decipher('ibidonhprm', 'iguana')
1115 >>> bifid_decipher("ibnhgaqltm", 'iguana', period=4)
1117 >>> bifid_decipher("ibnhgaqltzml", 'iguana', period=4)
1120 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
1122 t_message
= message
.translate(translation
)
1123 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
1125 chunked_pairs
= [pairs0
[i
:i
+period
] for i
in range(0, len(pairs0
), period
)]
1126 if len(chunked_pairs
[-1]) < period
and fillvalue
:
1127 chunked_pairs
[-1] += [f_grid
[fillvalue
]] * (period
- len(chunked_pairs
[-1]))
1129 chunked_pairs
= [pairs0
]
1132 for c
in chunked_pairs
:
1133 items
= [j
for i
in c
for j
in i
]
1135 p
= [(items
[i
], items
[i
+gap
]) for i
in range(gap
)]
1138 return cat(r_grid
[p
] for p
in pairs1
)
1141 def autokey_encipher(message
, keyword
):
1142 """Encipher with the autokey cipher
1144 >>> autokey_encipher('meetatthefountain', 'kilt')
1147 shifts
= [pos(l
) for l
in keyword
+ message
]
1148 pairs
= zip(message
, shifts
)
1149 return cat([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
1151 def autokey_decipher(ciphertext
, keyword
):
1152 """Decipher with the autokey cipher
1154 >>> autokey_decipher('wmpmmxxaeyhbryoca', 'kilt')
1158 keys
= list(keyword
)
1159 for c
in ciphertext
:
1160 plaintext_letter
= caesar_decipher_letter(c
, pos(keys
[0]))
1161 plaintext
+= [plaintext_letter
]
1162 keys
= keys
[1:] + [plaintext_letter
]
1163 return cat(plaintext
)
1166 class PocketEnigma(object):
1167 """A pocket enigma machine
1168 The wheel is internally represented as a 26-element list self.wheel_map,
1169 where wheel_map[i] == j shows that the position i places on from the arrow
1170 maps to the position j places on.
1172 def __init__(self
, wheel
=1, position
='a'):
1173 """initialise the pocket enigma, including which wheel to use and the
1174 starting position of the wheel.
1176 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
1179 The position is the letter pointed to by the arrow on the wheel.
1182 [25, 4, 23, 10, 1, 7, 9, 5, 12, 6, 3, 17, 8, 14, 13, 21, 19, 11, 20, 16, 18, 15, 24, 2, 22, 0]
1186 self
.wheel1
= [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
1187 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
1188 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
1189 self
.wheel2
= [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
1190 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
1191 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
1193 self
.make_wheel_map(self
.wheel1
)
1195 self
.make_wheel_map(self
.wheel2
)
1197 self
.validate_wheel_spec(wheel
)
1198 self
.make_wheel_map(wheel
)
1199 if position
in string
.ascii_lowercase
:
1200 self
.position
= pos(position
)
1202 self
.position
= position
1204 def make_wheel_map(self
, wheel_spec
):
1205 """Expands a wheel specification from a list of letter-letter pairs
1206 into a full wheel_map.
1208 >>> pe.make_wheel_map(pe.wheel2)
1209 [2, 3, 0, 1, 22, 8, 15, 12, 5, 10, 9, 13, 7, 11, 16, 6, 14, 25, 20, 21, 18, 19, 4, 24, 23, 17]
1211 self
.validate_wheel_spec(wheel_spec
)
1212 self
.wheel_map
= [0] * 26
1213 for p
in wheel_spec
:
1214 self
.wheel_map
[pos(p
[0])] = pos(p
[1])
1215 self
.wheel_map
[pos(p
[1])] = pos(p
[0])
1216 return self
.wheel_map
1218 def validate_wheel_spec(self
, wheel_spec
):
1219 """Validates that a wheel specificaiton will turn into a valid wheel
1222 >>> pe.validate_wheel_spec([])
1223 Traceback (most recent call last):
1225 ValueError: Wheel specification has 0 pairs, requires 13
1226 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
1227 Traceback (most recent call last):
1229 ValueError: Not all mappings in wheel specificationhave two elements
1230 >>> pe.validate_wheel_spec([('a', 'b')]*13)
1231 Traceback (most recent call last):
1233 ValueError: Wheel specification does not contain 26 letters
1235 if len(wheel_spec
) != 13:
1236 raise ValueError("Wheel specification has {} pairs, requires 13".
1237 format(len(wheel_spec
)))
1238 for p
in wheel_spec
:
1240 raise ValueError("Not all mappings in wheel specification"
1241 "have two elements")
1242 if len(set([p
[0] for p
in wheel_spec
] +
1243 [p
[1] for p
in wheel_spec
])) != 26:
1244 raise ValueError("Wheel specification does not contain 26 letters")
1246 def encipher_letter(self
, letter
):
1247 """Enciphers a single letter, by advancing the wheel before looking up
1248 the letter on the wheel.
1250 >>> pe.set_position('f')
1252 >>> pe.encipher_letter('k')
1256 return self
.lookup(letter
)
1257 decipher_letter
= encipher_letter
1259 def lookup(self
, letter
):
1260 """Look up what a letter enciphers to, without turning the wheel.
1262 >>> pe.set_position('f')
1264 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
1265 'udhbfejcpgmokrliwntsayqzvx'
1269 if letter
in string
.ascii_lowercase
:
1271 (self
.wheel_map
[(pos(letter
) - self
.position
) % 26] +
1277 """Advances the wheel one position.
1279 >>> pe.set_position('f')
1284 self
.position
= (self
.position
+ 1) % 26
1285 return self
.position
1287 def encipher(self
, message
, starting_position
=None):
1288 """Enciphers a whole message.
1290 >>> pe.set_position('f')
1292 >>> pe.encipher('helloworld')
1294 >>> pe.set_position('f')
1296 >>> pe.encipher('kjsglcjoqc')
1298 >>> pe.encipher('helloworld', starting_position = 'x')
1301 if starting_position
:
1302 self
.set_position(starting_position
)
1305 transformed
+= self
.encipher_letter(l
)
1309 def set_position(self
, position
):
1310 """Sets the position of the wheel, by specifying the letter the arrow
1313 >>> pe.set_position('a')
1315 >>> pe.set_position('m')
1317 >>> pe.set_position('z')
1320 self
.position
= pos(position
)
1321 return self
.position
1324 if __name__
== "__main__":
1326 doctest
.testmod(extraglobs
={'pe': PocketEnigma(1, 'a')})