5 from itertools
import zip_longest
, cycle
, chain
6 from language_models
import *
8 logger
= logging
.getLogger(__name__
)
9 logger
.addHandler(logging
.FileHandler('cipher.log'))
10 logger
.setLevel(logging
.WARNING
)
11 #logger.setLevel(logging.INFO)
12 #logger.setLevel(logging.DEBUG)
15 modular_division_table
= [[0]*26 for _
in range(26)]
19 modular_division_table
[b
][c
] = a
22 def every_nth(text
, n
, fillvalue
=''):
23 """Returns n strings, each of which consists of every nth character,
24 starting with the 0th, 1st, 2nd, ... (n-1)th character
26 >>> every_nth(string.ascii_lowercase, 5)
27 ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty']
28 >>> every_nth(string.ascii_lowercase, 1)
29 ['abcdefghijklmnopqrstuvwxyz']
30 >>> every_nth(string.ascii_lowercase, 26) # doctest: +NORMALIZE_WHITESPACE
31 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
32 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
33 >>> every_nth(string.ascii_lowercase, 5, fillvalue='!')
34 ['afkpuz', 'bglqv!', 'chmrw!', 'dinsx!', 'ejoty!']
36 split_text
= [text
[i
:i
+n
] for i
in range(0, len(text
), n
)]
37 return [''.join(l
) for l
in zip_longest(*split_text
, fillvalue
=fillvalue
)]
39 def combine_every_nth(split_text
):
40 """Reforms a text split into every_nth strings
42 >>> combine_every_nth(every_nth(string.ascii_lowercase, 5))
43 'abcdefghijklmnopqrstuvwxyz'
44 >>> combine_every_nth(every_nth(string.ascii_lowercase, 1))
45 'abcdefghijklmnopqrstuvwxyz'
46 >>> combine_every_nth(every_nth(string.ascii_lowercase, 26))
47 'abcdefghijklmnopqrstuvwxyz'
49 return ''.join([''.join(l
)
50 for l
in zip_longest(*split_text
, fillvalue
='')])
52 def chunks(text
, n
, fillvalue
=None):
53 """Split a text into chunks of n characters
55 >>> chunks('abcdefghi', 3)
57 >>> chunks('abcdefghi', 4)
59 >>> chunks('abcdefghi', 4, fillvalue='!')
60 ['abcd', 'efgh', 'i!!!']
63 padding
= fillvalue
[0] * (n
- len(text
) % n
)
66 return [(text
+padding
)[i
:i
+n
] for i
in range(0, len(text
), n
)]
68 def transpose(items
, transposition
):
69 """Moves items around according to the given transposition
71 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
73 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
75 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
76 [13, 12, 14, 11, 15, 10]
78 transposed
= [''] * len(transposition
)
79 for p
, t
in enumerate(transposition
):
80 transposed
[p
] = items
[t
]
83 def untranspose(items
, transposition
):
86 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
88 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
90 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
91 [10, 11, 12, 13, 14, 15]
93 transposed
= [''] * len(transposition
)
94 for p
, t
in enumerate(transposition
):
95 transposed
[t
] = items
[p
]
98 def deduplicate(text
):
99 return list(collections
.OrderedDict
.fromkeys(text
))
102 def caesar_encipher_letter(letter
, shift
):
103 """Encipher a letter, given a shift amount
105 >>> caesar_encipher_letter('a', 1)
107 >>> caesar_encipher_letter('a', 2)
109 >>> caesar_encipher_letter('b', 2)
111 >>> caesar_encipher_letter('x', 2)
113 >>> caesar_encipher_letter('y', 2)
115 >>> caesar_encipher_letter('z', 2)
117 >>> caesar_encipher_letter('z', -1)
119 >>> caesar_encipher_letter('a', -1)
122 if letter
in string
.ascii_letters
:
123 if letter
in string
.ascii_uppercase
:
124 alphabet_start
= ord('A')
126 alphabet_start
= ord('a')
127 return chr(((ord(letter
) - alphabet_start
+ shift
) % 26) +
132 def caesar_decipher_letter(letter
, shift
):
133 """Decipher a letter, given a shift amount
135 >>> caesar_decipher_letter('b', 1)
137 >>> caesar_decipher_letter('b', 2)
140 return caesar_encipher_letter(letter
, -shift
)
142 def caesar_encipher(message
, shift
):
143 """Encipher a message with the Caesar cipher of given shift
145 >>> caesar_encipher('abc', 1)
147 >>> caesar_encipher('abc', 2)
149 >>> caesar_encipher('abcxyz', 2)
151 >>> caesar_encipher('ab cx yz', 2)
154 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
155 return ''.join(enciphered
)
157 def caesar_decipher(message
, shift
):
158 """Encipher a message with the Caesar cipher of given shift
160 >>> caesar_decipher('bcd', 1)
162 >>> caesar_decipher('cde', 2)
164 >>> caesar_decipher('cd ez ab', 2)
167 return caesar_encipher(message
, -shift
)
169 def affine_encipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
170 """Encipher a letter, given a multiplier and adder
172 >>> ''.join([affine_encipher_letter(l, 3, 5, True) \
173 for l in string.ascii_uppercase])
174 'HKNQTWZCFILORUXADGJMPSVYBE'
175 >>> ''.join([affine_encipher_letter(l, 3, 5, False) \
176 for l in string.ascii_uppercase])
177 'FILORUXADGJMPSVYBEHKNQTWZC'
179 if letter
in string
.ascii_letters
:
180 if letter
in string
.ascii_uppercase
:
181 alphabet_start
= ord('A')
183 alphabet_start
= ord('a')
184 letter_number
= ord(letter
) - alphabet_start
185 if one_based
: letter_number
+= 1
186 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
187 if one_based
: cipher_number
-= 1
188 return chr(cipher_number
% 26 + alphabet_start
)
192 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
193 """Encipher a letter, given a multiplier and adder
195 >>> ''.join([affine_decipher_letter(l, 3, 5, True) \
196 for l in 'HKNQTWZCFILORUXADGJMPSVYBE'])
197 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
198 >>> ''.join([affine_decipher_letter(l, 3, 5, False) \
199 for l in 'FILORUXADGJMPSVYBEHKNQTWZC'])
200 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
202 if letter
in string
.ascii_letters
:
203 if letter
in string
.ascii_uppercase
:
204 alphabet_start
= ord('A')
206 alphabet_start
= ord('a')
207 cipher_number
= ord(letter
) - alphabet_start
208 if one_based
: cipher_number
+= 1
210 modular_division_table
[multiplier
]
211 [(cipher_number
- adder
) % 26] )
212 if one_based
: plaintext_number
-= 1
213 return chr(plaintext_number
% 26 + alphabet_start
)
217 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
218 """Encipher a message
220 >>> affine_encipher('hours passed during which jerico tried every ' \
221 'trick he could think of', 15, 22, True)
222 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
224 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
226 return ''.join(enciphered
)
228 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
229 """Decipher a message
231 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
232 'jfaoe ls omytd jlaxe mh', 15, 22, True)
233 'hours passed during which jerico tried every trick he could think of'
235 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
237 return ''.join(enciphered
)
240 def keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=0):
241 """Find the cipher alphabet given a keyword.
242 wrap_alphabet controls how the rest of the alphabet is added
245 1 : from the last letter in the sanitised keyword
246 2 : from the largest letter in the sanitised keyword
248 >>> keyword_cipher_alphabet_of('bayes')
249 'bayescdfghijklmnopqrtuvwxz'
250 >>> keyword_cipher_alphabet_of('bayes', 0)
251 'bayescdfghijklmnopqrtuvwxz'
252 >>> keyword_cipher_alphabet_of('bayes', 1)
253 'bayestuvwxzcdfghijklmnopqr'
254 >>> keyword_cipher_alphabet_of('bayes', 2)
255 'bayeszcdfghijklmnopqrtuvwx'
257 if wrap_alphabet
== 0:
258 cipher_alphabet
= ''.join(deduplicate(sanitise(keyword
) +
259 string
.ascii_lowercase
))
261 if wrap_alphabet
== 1:
262 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
264 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
265 last_keyword_position
= string
.ascii_lowercase
.find(
266 last_keyword_letter
) + 1
267 cipher_alphabet
= ''.join(
268 deduplicate(sanitise(keyword
) +
269 string
.ascii_lowercase
[last_keyword_position
:] +
270 string
.ascii_lowercase
))
271 return cipher_alphabet
274 def keyword_encipher(message
, keyword
, wrap_alphabet
=0):
275 """Enciphers a message with a keyword substitution cipher.
276 wrap_alphabet controls how the rest of the alphabet is added
279 1 : from the last letter in the sanitised keyword
280 2 : from the largest letter in the sanitised keyword
282 >>> keyword_encipher('test message', 'bayes')
284 >>> keyword_encipher('test message', 'bayes', 0)
286 >>> keyword_encipher('test message', 'bayes', 1)
288 >>> keyword_encipher('test message', 'bayes', 2)
291 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
292 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
293 return message
.lower().translate(cipher_translation
)
295 def keyword_decipher(message
, keyword
, wrap_alphabet
=0):
296 """Deciphers a message with a keyword substitution cipher.
297 wrap_alphabet controls how the rest of the alphabet is added
300 1 : from the last letter in the sanitised keyword
301 2 : from the largest letter in the sanitised keyword
303 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
305 >>> keyword_decipher('rsqr ksqqbds', 'bayes', 0)
307 >>> keyword_decipher('lskl dskkbus', 'bayes', 1)
309 >>> keyword_decipher('qspq jsppbcs', 'bayes', 2)
312 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
313 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
314 return message
.lower().translate(cipher_translation
)
317 def vigenere_encipher(message
, keyword
):
320 >>> vigenere_encipher('hello', 'abc')
323 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
324 pairs
= zip(message
, cycle(shifts
))
325 return ''.join([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
327 def vigenere_decipher(message
, keyword
):
330 >>> vigenere_decipher('hfnlp', 'abc')
333 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
334 pairs
= zip(message
, cycle(shifts
))
335 return ''.join([caesar_decipher_letter(l
, k
) for l
, k
in pairs
])
337 beaufort_encipher
=vigenere_decipher
338 beaufort_decipher
=vigenere_encipher
341 def transpositions_of(keyword
):
342 """Finds the transpostions given by a keyword. For instance, the keyword
343 'clever' rearranges to 'celrv', so the first column (0) stays first, the
344 second column (1) moves to third, the third column (2) moves to second,
347 If passed a tuple, assume it's already a transposition and just return it.
349 >>> transpositions_of('clever')
351 >>> transpositions_of('fred')
353 >>> transpositions_of((3, 2, 0, 1))
356 if isinstance(keyword
, tuple):
359 key
= deduplicate(keyword
)
360 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
361 return transpositions
363 def pad(message_len
, group_len
, fillvalue
):
364 padding_length
= group_len
- message_len
% group_len
365 if padding_length
== group_len
: padding_length
= 0
367 for i
in range(padding_length
):
368 if callable(fillvalue
):
369 padding
+= fillvalue()
374 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
375 fillcolumnwise
=False,
376 emptycolumnwise
=False):
377 """Enciphers using the column transposition cipher.
378 Message is padded to allow all rows to be the same length.
380 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
382 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
384 >>> column_transposition_encipher('hellothere', 'abcdef')
386 >>> column_transposition_encipher('hellothere', 'abcde')
388 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
390 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
392 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
394 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
396 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
398 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
400 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
402 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
404 >>> column_transposition_encipher('hellothere', 'cleverly')
406 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
408 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
411 transpositions
= transpositions_of(keyword
)
412 message
+= pad(len(message
), len(transpositions
), fillvalue
)
414 rows
= every_nth(message
, len(message
) // len(transpositions
))
416 rows
= chunks(message
, len(transpositions
))
417 transposed
= [transpose(r
, transpositions
) for r
in rows
]
419 return combine_every_nth(transposed
)
421 return ''.join(chain(*transposed
))
423 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
424 fillcolumnwise
=False,
425 emptycolumnwise
=False):
426 """Deciphers using the column transposition cipher.
427 Message is padded to allow all rows to be the same length.
429 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
431 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
433 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
435 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
437 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
439 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
441 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
443 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
446 transpositions
= transpositions_of(keyword
)
447 message
+= pad(len(message
), len(transpositions
), '*')
449 rows
= every_nth(message
, len(message
) // len(transpositions
))
451 rows
= chunks(message
, len(transpositions
))
452 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
454 return combine_every_nth(untransposed
)
456 return ''.join(chain(*untransposed
))
458 def scytale_encipher(message
, rows
, fillvalue
=' '):
459 """Enciphers using the scytale transposition cipher.
460 Message is padded with spaces to allow all rows to be the same length.
462 >>> scytale_encipher('thequickbrownfox', 3)
464 >>> scytale_encipher('thequickbrownfox', 4)
466 >>> scytale_encipher('thequickbrownfox', 5)
468 >>> scytale_encipher('thequickbrownfox', 6)
470 >>> scytale_encipher('thequickbrownfox', 7)
473 transpositions
= [i
for i
in range(math
.ceil(len(message
) / rows
))]
474 return column_transposition_encipher(message
, transpositions
,
475 fillcolumnwise
=False, emptycolumnwise
=True)
477 def scytale_decipher(message
, rows
):
478 """Deciphers using the scytale transposition cipher.
479 Assumes the message is padded so that all rows are the same length.
481 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
483 >>> scytale_decipher('tubnhirfecooqkwx', 4)
485 >>> scytale_decipher('tubnhirfecooqkwx', 5)
487 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
489 >>> scytale_decipher('tqcrnxhukof eibwo ', 7)
492 transpositions
= [i
for i
in range(math
.ceil(len(message
) / rows
))]
493 return column_transposition_decipher(message
, transpositions
,
494 fillcolumnwise
=False, emptycolumnwise
=True)
497 if __name__
== "__main__":