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 chunks(text
, n
, fillvalue
=None):
84 """Split a text into chunks of n characters
86 >>> chunks('abcdefghi', 3)
88 >>> chunks('abcdefghi', 4)
90 >>> chunks('abcdefghi', 4, fillvalue='!')
91 ['abcd', 'efgh', 'i!!!']
94 padding
= fillvalue
[0] * (n
- len(text
) % n
)
97 return [(text
+padding
)[i
:i
+n
] for i
in range(0, len(text
), n
)]
99 def transpose(items
, transposition
):
100 """Moves items around according to the given transposition
102 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
104 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
106 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
107 [13, 12, 14, 11, 15, 10]
109 transposed
= [''] * len(transposition
)
110 for p
, t
in enumerate(transposition
):
111 transposed
[p
] = items
[t
]
114 def untranspose(items
, transposition
):
115 """Undoes a transpose
117 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
119 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
121 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
122 [10, 11, 12, 13, 14, 15]
124 transposed
= [''] * len(transposition
)
125 for p
, t
in enumerate(transposition
):
126 transposed
[t
] = items
[p
]
129 def deduplicate(text
):
130 return list(collections
.OrderedDict
.fromkeys(text
))
133 def caesar_encipher_letter(letter
, shift
):
134 """Encipher a letter, given a shift amount
136 >>> caesar_encipher_letter('a', 1)
138 >>> caesar_encipher_letter('a', 2)
140 >>> caesar_encipher_letter('b', 2)
142 >>> caesar_encipher_letter('x', 2)
144 >>> caesar_encipher_letter('y', 2)
146 >>> caesar_encipher_letter('z', 2)
148 >>> caesar_encipher_letter('z', -1)
150 >>> caesar_encipher_letter('a', -1)
153 if letter
in string
.ascii_letters
:
154 if letter
in string
.ascii_uppercase
:
155 alphabet_start
= ord('A')
157 alphabet_start
= ord('a')
158 return chr(((ord(letter
) - alphabet_start
+ shift
) % 26) +
163 def caesar_decipher_letter(letter
, shift
):
164 """Decipher a letter, given a shift amount
166 >>> caesar_decipher_letter('b', 1)
168 >>> caesar_decipher_letter('b', 2)
171 return caesar_encipher_letter(letter
, -shift
)
173 def caesar_encipher(message
, shift
):
174 """Encipher a message with the Caesar cipher of given shift
176 >>> caesar_encipher('abc', 1)
178 >>> caesar_encipher('abc', 2)
180 >>> caesar_encipher('abcxyz', 2)
182 >>> caesar_encipher('ab cx yz', 2)
185 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
186 return ''.join(enciphered
)
188 def caesar_decipher(message
, shift
):
189 """Encipher 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)
198 return caesar_encipher(message
, -shift
)
200 def affine_encipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
201 """Encipher a letter, given a multiplier and adder
203 >>> ''.join([affine_encipher_letter(l, 3, 5, True) \
204 for l in string.ascii_uppercase])
205 'HKNQTWZCFILORUXADGJMPSVYBE'
206 >>> ''.join([affine_encipher_letter(l, 3, 5, False) \
207 for l in string.ascii_uppercase])
208 'FILORUXADGJMPSVYBEHKNQTWZC'
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 letter_number
= ord(letter
) - alphabet_start
216 if one_based
: letter_number
+= 1
217 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
218 if one_based
: cipher_number
-= 1
219 return chr(cipher_number
% 26 + alphabet_start
)
223 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
224 """Encipher a letter, given a multiplier and adder
226 >>> ''.join([affine_decipher_letter(l, 3, 5, True) \
227 for l in 'HKNQTWZCFILORUXADGJMPSVYBE'])
228 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
229 >>> ''.join([affine_decipher_letter(l, 3, 5, False) \
230 for l in 'FILORUXADGJMPSVYBEHKNQTWZC'])
231 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
233 if letter
in string
.ascii_letters
:
234 if letter
in string
.ascii_uppercase
:
235 alphabet_start
= ord('A')
237 alphabet_start
= ord('a')
238 cipher_number
= ord(letter
) - alphabet_start
239 if one_based
: cipher_number
+= 1
240 plaintext_number
= ( modular_division_table
[multiplier
]
241 [(cipher_number
- adder
) % 26] )
242 if one_based
: plaintext_number
-= 1
243 return chr(plaintext_number
% 26 + alphabet_start
)
247 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
248 """Encipher a message
250 >>> affine_encipher('hours passed during which jerico tried every ' \
251 'trick he could think of', 15, 22, True)
252 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
254 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
256 return ''.join(enciphered
)
258 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
259 """Decipher a message
261 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
262 'jfaoe ls omytd jlaxe mh', 15, 22, True)
263 'hours passed during which jerico tried every trick he could think of'
265 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
267 return ''.join(enciphered
)
270 def keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=0):
271 """Find the cipher alphabet given a keyword.
272 wrap_alphabet controls how the rest of the alphabet is added
275 1 : from the last letter in the sanitised keyword
276 2 : from the largest letter in the sanitised keyword
278 >>> keyword_cipher_alphabet_of('bayes')
279 'bayescdfghijklmnopqrtuvwxz'
280 >>> keyword_cipher_alphabet_of('bayes', 0)
281 'bayescdfghijklmnopqrtuvwxz'
282 >>> keyword_cipher_alphabet_of('bayes', 1)
283 'bayestuvwxzcdfghijklmnopqr'
284 >>> keyword_cipher_alphabet_of('bayes', 2)
285 'bayeszcdfghijklmnopqrtuvwx'
287 if wrap_alphabet
== 0:
288 cipher_alphabet
= ''.join(deduplicate(sanitise(keyword
) +
289 string
.ascii_lowercase
))
291 if wrap_alphabet
== 1:
292 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
294 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
295 last_keyword_position
= string
.ascii_lowercase
.find(
296 last_keyword_letter
) + 1
297 cipher_alphabet
= ''.join(
298 deduplicate(sanitise(keyword
) +
299 string
.ascii_lowercase
[last_keyword_position
:] +
300 string
.ascii_lowercase
))
301 return cipher_alphabet
304 def keyword_encipher(message
, keyword
, wrap_alphabet
=0):
305 """Enciphers a message with a keyword substitution cipher.
306 wrap_alphabet controls how the rest of the alphabet is added
309 1 : from the last letter in the sanitised keyword
310 2 : from the largest letter in the sanitised keyword
312 >>> keyword_encipher('test message', 'bayes')
314 >>> keyword_encipher('test message', 'bayes', 0)
316 >>> keyword_encipher('test message', 'bayes', 1)
318 >>> keyword_encipher('test message', 'bayes', 2)
321 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
322 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
323 return message
.lower().translate(cipher_translation
)
325 def keyword_decipher(message
, keyword
, wrap_alphabet
=0):
326 """Deciphers a message with a keyword substitution cipher.
327 wrap_alphabet controls how the rest of the alphabet is added
330 1 : from the last letter in the sanitised keyword
331 2 : from the largest letter in the sanitised keyword
333 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
335 >>> keyword_decipher('rsqr ksqqbds', 'bayes', 0)
337 >>> keyword_decipher('lskl dskkbus', 'bayes', 1)
339 >>> keyword_decipher('qspq jsppbcs', 'bayes', 2)
342 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
343 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
344 return message
.lower().translate(cipher_translation
)
346 def scytale_encipher(message
, rows
):
347 """Enciphers using the scytale transposition cipher.
348 Message is padded with spaces to allow all rows to be the same length.
350 >>> scytale_encipher('thequickbrownfox', 3)
352 >>> scytale_encipher('thequickbrownfox', 4)
354 >>> scytale_encipher('thequickbrownfox', 5)
355 'tubn hirf ecoo qkwx '
356 >>> scytale_encipher('thequickbrownfox', 6)
358 >>> scytale_encipher('thequickbrownfox', 7)
359 'tqcrnx hukof eibwo '
361 if len(message
) % rows
!= 0:
362 message
+= ' '*(rows
- len(message
) % rows
)
363 row_length
= round(len(message
) / rows
)
364 slices
= [message
[i
:i
+row_length
]
365 for i
in range(0, len(message
), row_length
)]
366 return ''.join([''.join(r
) for r
in zip_longest(*slices
, fillvalue
='')])
368 def scytale_decipher(message
, rows
):
369 """Deciphers using the scytale transposition cipher.
370 Assumes the message is padded so that all rows are the same length.
372 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
374 >>> scytale_decipher('tubnhirfecooqkwx', 4)
376 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
378 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
380 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
383 cols
= round(len(message
) / rows
)
384 columns
= [message
[i
:i
+rows
] for i
in range(0, cols
* rows
, rows
)]
385 return ''.join([''.join(c
) for c
in zip_longest(*columns
, fillvalue
='')])
388 def transpositions_of(keyword
):
389 """Finds the transpostions given by a keyword. For instance, the keyword
390 'clever' rearranges to 'celrv', so the first column (0) stays first, the
391 second column (1) moves to third, the third column (2) moves to second,
394 If passed a tuple, assume it's already a transposition and just return it.
396 >>> transpositions_of('clever')
398 >>> transpositions_of('fred')
400 >>> transpositions_of((3, 2, 0, 1))
403 if isinstance(keyword
, tuple):
406 key
= deduplicate(keyword
)
407 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
408 return transpositions
410 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
412 """Enciphers using the column transposition cipher.
413 Message is padded to allow all rows to be the same length.
415 >>> column_transposition_encipher('hellothere', 'clever')
417 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
419 >>> column_transposition_encipher('hellothere', 'clever', columnwise=True)
422 transpositions
= transpositions_of(keyword
)
423 columns
= every_nth(message
, len(transpositions
), fillvalue
=fillvalue
)
424 transposed_columns
= transpose(columns
, transpositions
)
426 return ''.join(transposed_columns
)
428 return combine_every_nth(transposed_columns
)
430 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
432 """Deciphers using the column transposition cipher.
433 Message is padded to allow all rows to be the same length.
435 >>> column_transposition_decipher('hleolteher', 'clever')
437 >>> column_transposition_decipher('hleolthre!e!', 'cleverly', fillvalue='?')
439 >>> column_transposition_decipher('htleehoelr', 'clever', columnwise=True)
442 transpositions
= transpositions_of(keyword
)
444 columns
= chunks(message
, int(len(message
) / len(transpositions
)))
446 columns
= every_nth(message
, len(transpositions
), fillvalue
=fillvalue
)
447 untransposed_columns
= untranspose(columns
, transpositions
)
448 return combine_every_nth(untransposed_columns
)
451 def vigenere_encipher(message
, keyword
):
454 >>> vigenere_encipher('hello', 'abc')
457 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
458 pairs
= zip(message
, cycle(shifts
))
459 return ''.join([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
461 def vigenere_decipher(message
, keyword
):
464 >>> vigenere_decipher('hfnlp', 'abc')
467 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
468 pairs
= zip(message
, cycle(shifts
))
469 return ''.join([caesar_decipher_letter(l
, k
) for l
, k
in pairs
])
471 beaufort_encipher
=vigenere_decipher
472 beaufort_decipher
=vigenere_encipher
475 if __name__
== "__main__":