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(accented_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)
121 >>> caesar_encipher_letter('A', 1)
123 >>> caesar_encipher_letter('é', 1)
126 letter
= unaccent(accented_letter
)
127 if letter
in string
.ascii_letters
:
128 if letter
in string
.ascii_uppercase
:
129 alphabet_start
= ord('A')
131 alphabet_start
= ord('a')
132 return chr(((ord(letter
) - alphabet_start
+ shift
) % 26) +
137 def caesar_decipher_letter(letter
, shift
):
138 """Decipher a letter, given a shift amount
140 >>> caesar_decipher_letter('b', 1)
142 >>> caesar_decipher_letter('b', 2)
145 return caesar_encipher_letter(letter
, -shift
)
147 def caesar_encipher(message
, shift
):
148 """Encipher a message with the Caesar cipher of given shift
150 >>> caesar_encipher('abc', 1)
152 >>> caesar_encipher('abc', 2)
154 >>> caesar_encipher('abcxyz', 2)
156 >>> caesar_encipher('ab cx yz', 2)
158 >>> caesar_encipher('Héllo World!', 2)
161 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
162 return ''.join(enciphered
)
164 def caesar_decipher(message
, shift
):
165 """Decipher a message with the Caesar cipher of given shift
167 >>> caesar_decipher('bcd', 1)
169 >>> caesar_decipher('cde', 2)
171 >>> caesar_decipher('cd ez ab', 2)
174 return caesar_encipher(message
, -shift
)
176 def affine_encipher_letter(accented_letter
, multiplier
=1, adder
=0, one_based
=True):
177 """Encipher a letter, given a multiplier and adder
179 >>> ''.join([affine_encipher_letter(l, 3, 5, True) \
180 for l in string.ascii_uppercase])
181 'HKNQTWZCFILORUXADGJMPSVYBE'
182 >>> ''.join([affine_encipher_letter(l, 3, 5, False) \
183 for l in string.ascii_uppercase])
184 'FILORUXADGJMPSVYBEHKNQTWZC'
186 letter
= unaccent(accented_letter
)
187 if letter
in string
.ascii_letters
:
188 if letter
in string
.ascii_uppercase
:
189 alphabet_start
= ord('A')
191 alphabet_start
= ord('a')
192 letter_number
= ord(letter
) - alphabet_start
193 if one_based
: letter_number
+= 1
194 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
195 if one_based
: cipher_number
-= 1
196 return chr(cipher_number
% 26 + alphabet_start
)
200 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
201 """Encipher a letter, given a multiplier and adder
203 >>> ''.join([affine_decipher_letter(l, 3, 5, True) \
204 for l in 'HKNQTWZCFILORUXADGJMPSVYBE'])
205 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
206 >>> ''.join([affine_decipher_letter(l, 3, 5, False) \
207 for l in 'FILORUXADGJMPSVYBEHKNQTWZC'])
208 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
210 if letter
in string
.ascii_letters
:
211 if letter
in string
.ascii_uppercase
:
212 alphabet_start
= ord('A')
214 alphabet_start
= ord('a')
215 cipher_number
= ord(letter
) - alphabet_start
216 if one_based
: cipher_number
+= 1
218 modular_division_table
[multiplier
]
219 [(cipher_number
- adder
) % 26] )
220 if one_based
: plaintext_number
-= 1
221 return chr(plaintext_number
% 26 + alphabet_start
)
225 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
226 """Encipher a message
228 >>> affine_encipher('hours passed during which jerico tried every ' \
229 'trick he could think of', 15, 22, True)
230 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
232 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
234 return ''.join(enciphered
)
236 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
237 """Decipher a message
239 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
240 'jfaoe ls omytd jlaxe mh', 15, 22, True)
241 'hours passed during which jerico tried every trick he could think of'
243 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
245 return ''.join(enciphered
)
248 def keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=0):
249 """Find the cipher alphabet given a keyword.
250 wrap_alphabet controls how the rest of the alphabet is added
253 1 : from the last letter in the sanitised keyword
254 2 : from the largest letter in the sanitised keyword
256 >>> keyword_cipher_alphabet_of('bayes')
257 'bayescdfghijklmnopqrtuvwxz'
258 >>> keyword_cipher_alphabet_of('bayes', 0)
259 'bayescdfghijklmnopqrtuvwxz'
260 >>> keyword_cipher_alphabet_of('bayes', 1)
261 'bayestuvwxzcdfghijklmnopqr'
262 >>> keyword_cipher_alphabet_of('bayes', 2)
263 'bayeszcdfghijklmnopqrtuvwx'
265 if wrap_alphabet
== 0:
266 cipher_alphabet
= ''.join(deduplicate(sanitise(keyword
) +
267 string
.ascii_lowercase
))
269 if wrap_alphabet
== 1:
270 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
272 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
273 last_keyword_position
= string
.ascii_lowercase
.find(
274 last_keyword_letter
) + 1
275 cipher_alphabet
= ''.join(
276 deduplicate(sanitise(keyword
) +
277 string
.ascii_lowercase
[last_keyword_position
:] +
278 string
.ascii_lowercase
))
279 return cipher_alphabet
282 def keyword_encipher(message
, keyword
, wrap_alphabet
=0):
283 """Enciphers a message with a keyword substitution cipher.
284 wrap_alphabet controls how the rest of the alphabet is added
287 1 : from the last letter in the sanitised keyword
288 2 : from the largest letter in the sanitised keyword
290 >>> keyword_encipher('test message', 'bayes')
292 >>> keyword_encipher('test message', 'bayes', 0)
294 >>> keyword_encipher('test message', 'bayes', 1)
296 >>> keyword_encipher('test message', 'bayes', 2)
299 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
300 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
301 return unaccent(message
).lower().translate(cipher_translation
)
303 def keyword_decipher(message
, keyword
, wrap_alphabet
=0):
304 """Deciphers a message with a keyword substitution cipher.
305 wrap_alphabet controls how the rest of the alphabet is added
308 1 : from the last letter in the sanitised keyword
309 2 : from the largest letter in the sanitised keyword
311 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
313 >>> keyword_decipher('rsqr ksqqbds', 'bayes', 0)
315 >>> keyword_decipher('lskl dskkbus', 'bayes', 1)
317 >>> keyword_decipher('qspq jsppbcs', 'bayes', 2)
320 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
321 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
322 return message
.lower().translate(cipher_translation
)
325 def vigenere_encipher(message
, keyword
):
328 >>> vigenere_encipher('hello', 'abc')
331 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
332 pairs
= zip(message
, cycle(shifts
))
333 return ''.join([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
335 def vigenere_decipher(message
, keyword
):
338 >>> vigenere_decipher('hfnlp', 'abc')
341 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
342 pairs
= zip(message
, cycle(shifts
))
343 return ''.join([caesar_decipher_letter(l
, k
) for l
, k
in pairs
])
345 beaufort_encipher
=vigenere_decipher
346 beaufort_decipher
=vigenere_encipher
349 def transpositions_of(keyword
):
350 """Finds the transpostions given by a keyword. For instance, the keyword
351 'clever' rearranges to 'celrv', so the first column (0) stays first, the
352 second column (1) moves to third, the third column (2) moves to second,
355 If passed a tuple, assume it's already a transposition and just return it.
357 >>> transpositions_of('clever')
359 >>> transpositions_of('fred')
361 >>> transpositions_of((3, 2, 0, 1))
364 if isinstance(keyword
, tuple):
367 key
= deduplicate(keyword
)
368 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
369 return transpositions
371 def pad(message_len
, group_len
, fillvalue
):
372 padding_length
= group_len
- message_len
% group_len
373 if padding_length
== group_len
: padding_length
= 0
375 for i
in range(padding_length
):
376 if callable(fillvalue
):
377 padding
+= fillvalue()
382 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
383 fillcolumnwise
=False,
384 emptycolumnwise
=False):
385 """Enciphers using the column transposition cipher.
386 Message is padded to allow all rows to be the same length.
388 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
390 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
392 >>> column_transposition_encipher('hellothere', 'abcdef')
394 >>> column_transposition_encipher('hellothere', 'abcde')
396 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
398 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
400 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
402 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
404 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
406 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
408 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
410 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
412 >>> column_transposition_encipher('hellothere', 'cleverly')
414 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
416 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
419 transpositions
= transpositions_of(keyword
)
420 message
+= pad(len(message
), len(transpositions
), fillvalue
)
422 rows
= every_nth(message
, len(message
) // len(transpositions
))
424 rows
= chunks(message
, len(transpositions
))
425 transposed
= [transpose(r
, transpositions
) for r
in rows
]
427 return combine_every_nth(transposed
)
429 return ''.join(chain(*transposed
))
431 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
432 fillcolumnwise
=False,
433 emptycolumnwise
=False):
434 """Deciphers using the column transposition cipher.
435 Message is padded to allow all rows to be the same length.
437 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
439 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
441 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
443 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
445 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
447 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
449 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
451 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
454 transpositions
= transpositions_of(keyword
)
455 message
+= pad(len(message
), len(transpositions
), '*')
457 rows
= every_nth(message
, len(message
) // len(transpositions
))
459 rows
= chunks(message
, len(transpositions
))
460 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
462 return combine_every_nth(untransposed
)
464 return ''.join(chain(*untransposed
))
466 def scytale_encipher(message
, rows
, fillvalue
=' '):
467 """Enciphers using the scytale transposition cipher.
468 Message is padded with spaces to allow all rows to be the same length.
470 >>> scytale_encipher('thequickbrownfox', 3)
472 >>> scytale_encipher('thequickbrownfox', 4)
474 >>> scytale_encipher('thequickbrownfox', 5)
476 >>> scytale_encipher('thequickbrownfox', 6)
478 >>> scytale_encipher('thequickbrownfox', 7)
481 transpositions
= [i
for i
in range(math
.ceil(len(message
) / rows
))]
482 return column_transposition_encipher(message
, transpositions
,
483 fillcolumnwise
=False, emptycolumnwise
=True)
485 def scytale_decipher(message
, rows
):
486 """Deciphers using the scytale transposition cipher.
487 Assumes the message is padded so that all rows are the same length.
489 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
491 >>> scytale_decipher('tubnhirfecooqkwx', 4)
493 >>> scytale_decipher('tubnhirfecooqkwx', 5)
495 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
497 >>> scytale_decipher('tqcrnxhukof eibwo ', 7)
500 transpositions
= [i
for i
in range(math
.ceil(len(message
) / rows
))]
501 return column_transposition_decipher(message
, transpositions
,
502 fillcolumnwise
=False, emptycolumnwise
=True)
505 if __name__
== "__main__":