4 from itertools
import zip_longest
, cycle
7 logger
= logging
.getLogger(__name__
)
8 logger
.addHandler(logging
.FileHandler('cipher.log'))
9 logger
.setLevel(logging
.WARNING
)
10 #logger.setLevel(logging.INFO)
11 #logger.setLevel(logging.DEBUG)
14 modular_division_table
= [[0]*26 for x
in range(26)]
18 modular_division_table
[b
][c
] = a
21 """Remove all non-alphabetic characters from a text
22 >>> letters('The Quick')
24 >>> letters('The Quick BROWN fox jumped! over... the (9lazy) DOG')
25 'TheQuickBROWNfoxjumpedoverthelazyDOG'
27 return ''.join([c
for c
in text
if c
in string
.ascii_letters
])
30 """Remove all non-alphabetic characters and convert the text to lowercase
32 >>> sanitise('The Quick')
34 >>> sanitise('The Quick BROWN fox jumped! over... the (9lazy) DOG')
35 'thequickbrownfoxjumpedoverthelazydog'
37 # sanitised = [c.lower() for c in text if c in string.ascii_letters]
38 # return ''.join(sanitised)
39 return letters(text
).lower()
42 """Returns all n-grams of a text
44 >>> ngrams(sanitise('the quick brown fox'), 2) # doctest: +NORMALIZE_WHITESPACE
45 ['th', 'he', 'eq', 'qu', 'ui', 'ic', 'ck', 'kb', 'br', 'ro', 'ow', 'wn',
47 >>> ngrams(sanitise('the quick brown fox'), 4) # doctest: +NORMALIZE_WHITESPACE
48 ['theq', 'hequ', 'equi', 'quic', 'uick', 'ickb', 'ckbr', 'kbro', 'brow',
49 'rown', 'ownf', 'wnfo', 'nfox']
51 return [text
[i
:i
+n
] for i
in range(len(text
)-n
+1)]
53 def every_nth(text
, n
, fillvalue
=''):
54 """Returns n strings, each of which consists of every nth character,
55 starting with the 0th, 1st, 2nd, ... (n-1)th character
57 >>> every_nth(string.ascii_lowercase, 5)
58 ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty']
59 >>> every_nth(string.ascii_lowercase, 1)
60 ['abcdefghijklmnopqrstuvwxyz']
61 >>> every_nth(string.ascii_lowercase, 26) # doctest: +NORMALIZE_WHITESPACE
62 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
63 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
64 >>> every_nth(string.ascii_lowercase, 5, fillvalue='!')
65 ['afkpuz', 'bglqv!', 'chmrw!', 'dinsx!', 'ejoty!']
67 split_text
= [text
[i
:i
+n
] for i
in range(0, len(text
), n
)]
68 return [''.join(l
) for l
in zip_longest(*split_text
, fillvalue
=fillvalue
)]
70 def combine_every_nth(split_text
):
71 """Reforms a text split into every_nth strings
73 >>> combine_every_nth(every_nth(string.ascii_lowercase, 5))
74 'abcdefghijklmnopqrstuvwxyz'
75 >>> combine_every_nth(every_nth(string.ascii_lowercase, 1))
76 'abcdefghijklmnopqrstuvwxyz'
77 >>> combine_every_nth(every_nth(string.ascii_lowercase, 26))
78 'abcdefghijklmnopqrstuvwxyz'
80 return ''.join([''.join(l
)
81 for l
in zip_longest(*split_text
, fillvalue
='')])
83 def transpose(items
, transposition
):
84 """Moves items around according to the given transposition
86 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
88 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
90 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
91 [13, 12, 14, 11, 15, 10]
93 transposed
= [''] * len(transposition
)
94 for p
, t
in enumerate(transposition
):
95 transposed
[p
] = items
[t
]
98 def untranspose(items
, transposition
):
101 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
103 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
105 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
106 [10, 11, 12, 13, 14, 15]
108 transposed
= [''] * len(transposition
)
109 for p
, t
in enumerate(transposition
):
110 transposed
[t
] = items
[p
]
115 def deduplicate(text
):
116 return list(collections
.OrderedDict
.fromkeys(text
))
120 def caesar_encipher_letter(letter
, shift
):
121 """Encipher a letter, given a shift amount
123 >>> caesar_encipher_letter('a', 1)
125 >>> caesar_encipher_letter('a', 2)
127 >>> caesar_encipher_letter('b', 2)
129 >>> caesar_encipher_letter('x', 2)
131 >>> caesar_encipher_letter('y', 2)
133 >>> caesar_encipher_letter('z', 2)
135 >>> caesar_encipher_letter('z', -1)
137 >>> caesar_encipher_letter('a', -1)
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 def caesar_decipher_letter(letter
, shift
):
151 """Decipher a letter, given a shift amount
153 >>> caesar_decipher_letter('b', 1)
155 >>> caesar_decipher_letter('b', 2)
158 return caesar_encipher_letter(letter
, -shift
)
160 def caesar_encipher(message
, shift
):
161 """Encipher a message with the Caesar cipher of given shift
163 >>> caesar_encipher('abc', 1)
165 >>> caesar_encipher('abc', 2)
167 >>> caesar_encipher('abcxyz', 2)
169 >>> caesar_encipher('ab cx yz', 2)
172 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
173 return ''.join(enciphered
)
175 def caesar_decipher(message
, shift
):
176 """Encipher a message with the Caesar cipher of given shift
178 >>> caesar_decipher('bcd', 1)
180 >>> caesar_decipher('cde', 2)
182 >>> caesar_decipher('cd ez ab', 2)
185 return caesar_encipher(message
, -shift
)
187 def affine_encipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
188 """Encipher a letter, given a multiplier and adder
190 >>> ''.join([affine_encipher_letter(l, 3, 5, True) \
191 for l in string.ascii_uppercase])
192 'HKNQTWZCFILORUXADGJMPSVYBE'
193 >>> ''.join([affine_encipher_letter(l, 3, 5, False) \
194 for l in string.ascii_uppercase])
195 'FILORUXADGJMPSVYBEHKNQTWZC'
197 if letter
in string
.ascii_letters
:
198 if letter
in string
.ascii_uppercase
:
199 alphabet_start
= ord('A')
201 alphabet_start
= ord('a')
202 letter_number
= ord(letter
) - alphabet_start
203 if one_based
: letter_number
+= 1
204 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
205 if one_based
: cipher_number
-= 1
206 return chr(cipher_number
% 26 + alphabet_start
)
210 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
211 """Encipher a letter, given a multiplier and adder
213 >>> ''.join([affine_decipher_letter(l, 3, 5, True) \
214 for l in 'HKNQTWZCFILORUXADGJMPSVYBE'])
215 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
216 >>> ''.join([affine_decipher_letter(l, 3, 5, False) \
217 for l in 'FILORUXADGJMPSVYBEHKNQTWZC'])
218 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
220 if letter
in string
.ascii_letters
:
221 if letter
in string
.ascii_uppercase
:
222 alphabet_start
= ord('A')
224 alphabet_start
= ord('a')
225 cipher_number
= ord(letter
) - alphabet_start
226 if one_based
: cipher_number
+= 1
227 plaintext_number
= ( modular_division_table
[multiplier
]
228 [(cipher_number
- adder
) % 26] )
229 if one_based
: plaintext_number
-= 1
230 return chr(plaintext_number
% 26 + alphabet_start
)
234 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
235 """Encipher a message
237 >>> affine_encipher('hours passed during which jerico tried every ' \
238 'trick he could think of', 15, 22, True)
239 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
241 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
243 return ''.join(enciphered
)
245 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
246 """Decipher a message
248 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
249 'jfaoe ls omytd jlaxe mh', 15, 22, True)
250 'hours passed during which jerico tried every trick he could think of'
252 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
254 return ''.join(enciphered
)
257 def keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=0):
258 """Find the cipher alphabet given a keyword.
259 wrap_alphabet controls how the rest of the alphabet is added
262 1 : from the last letter in the sanitised keyword
263 2 : from the largest letter in the sanitised keyword
265 >>> keyword_cipher_alphabet_of('bayes')
266 'bayescdfghijklmnopqrtuvwxz'
267 >>> keyword_cipher_alphabet_of('bayes', 0)
268 'bayescdfghijklmnopqrtuvwxz'
269 >>> keyword_cipher_alphabet_of('bayes', 1)
270 'bayestuvwxzcdfghijklmnopqr'
271 >>> keyword_cipher_alphabet_of('bayes', 2)
272 'bayeszcdfghijklmnopqrtuvwx'
274 if wrap_alphabet
== 0:
275 cipher_alphabet
= ''.join(deduplicate(sanitise(keyword
) +
276 string
.ascii_lowercase
))
278 if wrap_alphabet
== 1:
279 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
281 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
282 last_keyword_position
= string
.ascii_lowercase
.find(
283 last_keyword_letter
) + 1
284 cipher_alphabet
= ''.join(
285 deduplicate(sanitise(keyword
) +
286 string
.ascii_lowercase
[last_keyword_position
:] +
287 string
.ascii_lowercase
))
288 return cipher_alphabet
291 def keyword_encipher(message
, keyword
, wrap_alphabet
=0):
292 """Enciphers a message with a keyword substitution cipher.
293 wrap_alphabet controls how the rest of the alphabet is added
296 1 : from the last letter in the sanitised keyword
297 2 : from the largest letter in the sanitised keyword
299 >>> keyword_encipher('test message', 'bayes')
301 >>> keyword_encipher('test message', 'bayes', 0)
303 >>> keyword_encipher('test message', 'bayes', 1)
305 >>> keyword_encipher('test message', 'bayes', 2)
308 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
309 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
310 return message
.lower().translate(cipher_translation
)
312 def keyword_decipher(message
, keyword
, wrap_alphabet
=0):
313 """Deciphers a message with a keyword substitution cipher.
314 wrap_alphabet controls how the rest of the alphabet is added
317 1 : from the last letter in the sanitised keyword
318 2 : from the largest letter in the sanitised keyword
320 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
322 >>> keyword_decipher('rsqr ksqqbds', 'bayes', 0)
324 >>> keyword_decipher('lskl dskkbus', 'bayes', 1)
326 >>> keyword_decipher('qspq jsppbcs', 'bayes', 2)
329 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
330 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
331 return message
.lower().translate(cipher_translation
)
333 def scytale_encipher(message
, rows
):
334 """Enciphers using the scytale transposition cipher.
335 Message is padded with spaces to allow all rows to be the same length.
337 >>> scytale_encipher('thequickbrownfox', 3)
339 >>> scytale_encipher('thequickbrownfox', 4)
341 >>> scytale_encipher('thequickbrownfox', 5)
342 'tubn hirf ecoo qkwx '
343 >>> scytale_encipher('thequickbrownfox', 6)
345 >>> scytale_encipher('thequickbrownfox', 7)
346 'tqcrnx hukof eibwo '
348 if len(message
) % rows
!= 0:
349 message
+= ' '*(rows
- len(message
) % rows
)
350 row_length
= round(len(message
) / rows
)
351 slices
= [message
[i
:i
+row_length
]
352 for i
in range(0, len(message
), row_length
)]
353 return ''.join([''.join(r
) for r
in zip_longest(*slices
, fillvalue
='')])
355 def scytale_decipher(message
, rows
):
356 """Deciphers using the scytale transposition cipher.
357 Assumes the message is padded so that all rows are the same length.
359 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
361 >>> scytale_decipher('tubnhirfecooqkwx', 4)
363 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
365 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
367 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
370 cols
= round(len(message
) / rows
)
371 columns
= [message
[i
:i
+rows
] for i
in range(0, cols
* rows
, rows
)]
372 return ''.join([''.join(c
) for c
in zip_longest(*columns
, fillvalue
='')])
375 def transpositions_of(keyword
):
376 """Finds the transpostions given by a keyword. For instance, the keyword
377 'clever' rearranges to 'celrv', so the first column (0) stays first, the
378 second column (1) moves to third, the third column (2) moves to second,
381 If passed a tuple, assume it's already a transposition and just return it.
383 >>> transpositions_of('clever')
385 >>> transpositions_of('fred')
387 >>> transpositions_of((3, 2, 0, 1))
390 if isinstance(keyword
, tuple):
393 key
= deduplicate(keyword
)
394 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
395 return transpositions
397 def column_transposition_encipher(message
, keyword
, fillvalue
=' '):
398 """Enciphers using the column transposition cipher.
399 Message is padded to allow all rows to be the same length.
401 >>> column_transposition_encipher('hellothere', 'clever')
403 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
406 return column_transposition_worker(message
, keyword
, encipher
=True,
409 def column_transposition_decipher(message
, keyword
, fillvalue
=' '):
410 """Deciphers using the column transposition cipher.
411 Message is padded to allow all rows to be the same length.
413 >>> column_transposition_decipher('hleolteher', 'clever')
415 >>> column_transposition_decipher('hleolthre!e!', 'cleverly', fillvalue='?')
418 return column_transposition_worker(message
, keyword
, encipher
=False,
421 def column_transposition_worker(message
, keyword
,
422 encipher
=True, fillvalue
=' '):
423 """Does the actual work of the column transposition cipher.
424 Message is padded with spaces to allow all rows to be the same length.
426 >>> column_transposition_worker('hellothere', 'clever')
428 >>> column_transposition_worker('hellothere', 'clever', encipher=True)
430 >>> column_transposition_worker('hleolteher', 'clever', encipher=False)
433 transpositions
= transpositions_of(keyword
)
434 columns
= every_nth(message
, len(transpositions
), fillvalue
=fillvalue
)
436 transposed_columns
= transpose(columns
, transpositions
)
438 transposed_columns
= untranspose(columns
, transpositions
)
439 return combine_every_nth(transposed_columns
)
441 def vigenere_encipher(message
, keyword
):
444 >>> vigenere_encipher('hello', 'abc')
447 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
448 pairs
= zip(message
, cycle(shifts
))
449 return ''.join([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
451 def vigenere_decipher(message
, keyword
):
454 >>> vigenere_decipher('hfnlp', 'abc')
457 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
458 pairs
= zip(message
, cycle(shifts
))
459 return ''.join([caesar_decipher_letter(l
, k
) for l
, k
in pairs
])
463 if __name__
== "__main__":