5 from itertools
import zip_longest
, cycle
, chain
, count
7 from numpy
import matrix
8 from numpy
import linalg
9 from language_models
import *
17 if letter
in string
.ascii_lowercase
:
18 return ord(letter
) - ord('a')
19 elif letter
in string
.ascii_uppercase
:
20 return ord(letter
) - ord('A')
24 def unpos(number
): return chr(number
% 26 + ord('a'))
27 modular_division_table
= [[0]*26 for _
in range(26)]
31 modular_division_table
[b
][c
] = a
34 def every_nth(text
, n
, fillvalue
=''):
35 """Returns n strings, each of which consists of every nth character,
36 starting with the 0th, 1st, 2nd, ... (n-1)th character
38 >>> every_nth(string.ascii_lowercase, 5)
39 ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty']
40 >>> every_nth(string.ascii_lowercase, 1)
41 ['abcdefghijklmnopqrstuvwxyz']
42 >>> every_nth(string.ascii_lowercase, 26) # doctest: +NORMALIZE_WHITESPACE
43 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
44 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
45 >>> every_nth(string.ascii_lowercase, 5, fillvalue='!')
46 ['afkpuz', 'bglqv!', 'chmrw!', 'dinsx!', 'ejoty!']
48 split_text
= chunks(text
, n
, fillvalue
)
49 return [cat(l
) for l
in zip_longest(*split_text
, fillvalue
=fillvalue
)]
51 def combine_every_nth(split_text
):
52 """Reforms a text split into every_nth strings
54 >>> combine_every_nth(every_nth(string.ascii_lowercase, 5))
55 'abcdefghijklmnopqrstuvwxyz'
56 >>> combine_every_nth(every_nth(string.ascii_lowercase, 1))
57 'abcdefghijklmnopqrstuvwxyz'
58 >>> combine_every_nth(every_nth(string.ascii_lowercase, 26))
59 'abcdefghijklmnopqrstuvwxyz'
62 for l
in zip_longest(*split_text
, fillvalue
='')])
64 def chunks(text
, n
, fillvalue
=None):
65 """Split a text into chunks of n characters
67 >>> chunks('abcdefghi', 3)
69 >>> chunks('abcdefghi', 4)
71 >>> chunks('abcdefghi', 4, fillvalue='!')
72 ['abcd', 'efgh', 'i!!!']
75 padding
= fillvalue
[0] * (n
- len(text
) % n
)
78 return [(text
+padding
)[i
:i
+n
] for i
in range(0, len(text
), n
)]
80 def transpose(items
, transposition
):
81 """Moves items around according to the given transposition
83 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
85 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
87 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
88 [13, 12, 14, 11, 15, 10]
90 transposed
= [''] * len(transposition
)
91 for p
, t
in enumerate(transposition
):
92 transposed
[p
] = items
[t
]
95 def untranspose(items
, transposition
):
98 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
100 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
102 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
103 [10, 11, 12, 13, 14, 15]
105 transposed
= [''] * len(transposition
)
106 for p
, t
in enumerate(transposition
):
107 transposed
[t
] = items
[p
]
110 def deduplicate(text
):
111 return list(collections
.OrderedDict
.fromkeys(text
))
114 def caesar_encipher_letter(accented_letter
, shift
):
115 """Encipher a letter, given a shift amount
117 >>> caesar_encipher_letter('a', 1)
119 >>> caesar_encipher_letter('a', 2)
121 >>> caesar_encipher_letter('b', 2)
123 >>> caesar_encipher_letter('x', 2)
125 >>> caesar_encipher_letter('y', 2)
127 >>> caesar_encipher_letter('z', 2)
129 >>> caesar_encipher_letter('z', -1)
131 >>> caesar_encipher_letter('a', -1)
133 >>> caesar_encipher_letter('A', 1)
135 >>> caesar_encipher_letter('é', 1)
138 # letter = unaccent(accented_letter)
139 # if letter in string.ascii_letters:
140 # if letter in string.ascii_uppercase:
141 # alphabet_start = ord('A')
143 # alphabet_start = ord('a')
144 # return chr(((ord(letter) - alphabet_start + shift) % 26) +
149 letter
= unaccent(accented_letter
)
150 if letter
in string
.ascii_letters
:
151 cipherletter
= unpos(pos(letter
) + shift
)
152 if letter
in string
.ascii_uppercase
:
153 return cipherletter
.upper()
159 def caesar_decipher_letter(letter
, shift
):
160 """Decipher a letter, given a shift amount
162 >>> caesar_decipher_letter('b', 1)
164 >>> caesar_decipher_letter('b', 2)
167 return caesar_encipher_letter(letter
, -shift
)
169 def caesar_encipher(message
, shift
):
170 """Encipher a message with the Caesar cipher of given shift
172 >>> caesar_encipher('abc', 1)
174 >>> caesar_encipher('abc', 2)
176 >>> caesar_encipher('abcxyz', 2)
178 >>> caesar_encipher('ab cx yz', 2)
180 >>> caesar_encipher('Héllo World!', 2)
183 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
184 return cat(enciphered
)
186 def caesar_decipher(message
, shift
):
187 """Decipher a message with the Caesar cipher of given shift
189 >>> caesar_decipher('bcd', 1)
191 >>> caesar_decipher('cde', 2)
193 >>> caesar_decipher('cd ez ab', 2)
195 >>> caesar_decipher('Jgnnq Yqtnf!', 2)
198 return caesar_encipher(message
, -shift
)
200 def affine_encipher_letter(accented_letter
, multiplier
=1, adder
=0, one_based
=True):
201 """Encipher a letter, given a multiplier and adder
203 >>> cat(affine_encipher_letter(l, 3, 5, True) \
204 for l in string.ascii_letters)
205 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE'
206 >>> cat(affine_encipher_letter(l, 3, 5, False) \
207 for l in string.ascii_letters)
208 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC'
210 # letter = unaccent(accented_letter)
211 # if letter in string.ascii_letters:
212 # if letter in string.ascii_uppercase:
213 # alphabet_start = ord('A')
215 # alphabet_start = ord('a')
216 # letter_number = ord(letter) - alphabet_start
217 # if one_based: letter_number += 1
218 # cipher_number = (letter_number * multiplier + adder) % 26
219 # if one_based: cipher_number -= 1
220 # return chr(cipher_number % 26 + alphabet_start)
223 letter
= unaccent(accented_letter
)
224 if letter
in string
.ascii_letters
:
225 letter_number
= pos(letter
)
226 if one_based
: letter_number
+= 1
227 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
228 if one_based
: cipher_number
-= 1
229 if letter
in string
.ascii_uppercase
:
230 return unpos(cipher_number
).upper()
232 return unpos(cipher_number
)
236 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
237 """Encipher a letter, given a multiplier and adder
239 >>> cat(affine_decipher_letter(l, 3, 5, True) \
240 for l in 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE')
241 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
242 >>> cat(affine_decipher_letter(l, 3, 5, False) \
243 for l in 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC')
244 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
246 # if letter in string.ascii_letters:
247 # if letter in string.ascii_uppercase:
248 # alphabet_start = ord('A')
250 # alphabet_start = ord('a')
251 # cipher_number = ord(letter) - alphabet_start
252 # if one_based: cipher_number += 1
253 # plaintext_number = (
254 # modular_division_table[multiplier]
255 # [(cipher_number - adder) % 26])
256 # if one_based: plaintext_number -= 1
257 # return chr(plaintext_number % 26 + alphabet_start)
260 if letter
in string
.ascii_letters
:
261 cipher_number
= pos(letter
)
262 if one_based
: cipher_number
+= 1
264 modular_division_table
[multiplier
]
265 [(cipher_number
- adder
) % 26])
266 if one_based
: plaintext_number
-= 1
267 if letter
in string
.ascii_uppercase
:
268 return unpos(plaintext_number
).upper()
270 return unpos(plaintext_number
)
274 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
275 """Encipher a message
277 >>> affine_encipher('hours passed during which jerico tried every ' \
278 'trick he could think of', 15, 22, True)
279 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
281 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
283 return cat(enciphered
)
285 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
286 """Decipher a message
288 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
289 'jfaoe ls omytd jlaxe mh', 15, 22, True)
290 'hours passed during which jerico tried every trick he could think of'
292 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
294 return cat(enciphered
)
297 class KeywordWrapAlphabet(Enum
):
303 def keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
304 """Find the cipher alphabet given a keyword.
305 wrap_alphabet controls how the rest of the alphabet is added
308 >>> keyword_cipher_alphabet_of('bayes')
309 'bayescdfghijklmnopqrtuvwxz'
310 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_a)
311 'bayescdfghijklmnopqrtuvwxz'
312 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_last)
313 'bayestuvwxzcdfghijklmnopqr'
314 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_largest)
315 'bayeszcdfghijklmnopqrtuvwx'
317 if wrap_alphabet
== KeywordWrapAlphabet
.from_a
:
318 cipher_alphabet
= cat(deduplicate(sanitise(keyword
) +
319 string
.ascii_lowercase
))
321 if wrap_alphabet
== KeywordWrapAlphabet
.from_last
:
322 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
324 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
325 last_keyword_position
= string
.ascii_lowercase
.find(
326 last_keyword_letter
) + 1
327 cipher_alphabet
= cat(
328 deduplicate(sanitise(keyword
) +
329 string
.ascii_lowercase
[last_keyword_position
:] +
330 string
.ascii_lowercase
))
331 return cipher_alphabet
334 def keyword_encipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
335 """Enciphers a message with a keyword substitution cipher.
336 wrap_alphabet controls how the rest of the alphabet is added
339 1 : from the last letter in the sanitised keyword
340 2 : from the largest letter in the sanitised keyword
342 >>> keyword_encipher('test message', 'bayes')
344 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a)
346 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last)
348 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest)
351 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
352 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
353 return unaccent(message
).lower().translate(cipher_translation
)
355 def keyword_decipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
356 """Deciphers a message with a keyword substitution cipher.
357 wrap_alphabet controls how the rest of the alphabet is added
360 1 : from the last letter in the sanitised keyword
361 2 : from the largest letter in the sanitised keyword
363 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
365 >>> keyword_decipher('rsqr ksqqbds', 'bayes', KeywordWrapAlphabet.from_a)
367 >>> keyword_decipher('lskl dskkbus', 'bayes', KeywordWrapAlphabet.from_last)
369 >>> keyword_decipher('qspq jsppbcs', 'bayes', KeywordWrapAlphabet.from_largest)
372 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
373 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
374 return message
.lower().translate(cipher_translation
)
377 def vigenere_encipher(message
, keyword
):
380 >>> vigenere_encipher('hello', 'abc')
383 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
384 pairs
= zip(message
, cycle(shifts
))
385 return cat([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
387 def vigenere_decipher(message
, keyword
):
390 >>> vigenere_decipher('hfnlp', 'abc')
393 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
394 pairs
= zip(message
, cycle(shifts
))
395 return cat([caesar_decipher_letter(l
, k
) for l
, k
in pairs
])
397 beaufort_encipher
=vigenere_decipher
398 beaufort_decipher
=vigenere_encipher
401 def transpositions_of(keyword
):
402 """Finds the transpostions given by a keyword. For instance, the keyword
403 'clever' rearranges to 'celrv', so the first column (0) stays first, the
404 second column (1) moves to third, the third column (2) moves to second,
407 If passed a tuple, assume it's already a transposition and just return it.
409 >>> transpositions_of('clever')
411 >>> transpositions_of('fred')
413 >>> transpositions_of((3, 2, 0, 1))
416 if isinstance(keyword
, tuple):
419 key
= deduplicate(keyword
)
420 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
421 return transpositions
423 def pad(message_len
, group_len
, fillvalue
):
424 padding_length
= group_len
- message_len
% group_len
425 if padding_length
== group_len
: padding_length
= 0
427 for i
in range(padding_length
):
428 if callable(fillvalue
):
429 padding
+= fillvalue()
434 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
435 fillcolumnwise
=False,
436 emptycolumnwise
=False):
437 """Enciphers using the column transposition cipher.
438 Message is padded to allow all rows to be the same length.
440 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
442 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
444 >>> column_transposition_encipher('hellothere', 'abcdef')
446 >>> column_transposition_encipher('hellothere', 'abcde')
448 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
450 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
452 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
454 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
456 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
458 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
460 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
462 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
464 >>> column_transposition_encipher('hellothere', 'cleverly')
466 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
468 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
471 transpositions
= transpositions_of(keyword
)
472 message
+= pad(len(message
), len(transpositions
), fillvalue
)
474 rows
= every_nth(message
, len(message
) // len(transpositions
))
476 rows
= chunks(message
, len(transpositions
))
477 transposed
= [transpose(r
, transpositions
) for r
in rows
]
479 return combine_every_nth(transposed
)
481 return cat(chain(*transposed
))
483 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
484 fillcolumnwise
=False,
485 emptycolumnwise
=False):
486 """Deciphers using the column transposition cipher.
487 Message is padded to allow all rows to be the same length.
489 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
491 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
493 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
495 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
497 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
499 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
501 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
503 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
506 transpositions
= transpositions_of(keyword
)
507 message
+= pad(len(message
), len(transpositions
), fillvalue
)
509 rows
= every_nth(message
, len(message
) // len(transpositions
))
511 rows
= chunks(message
, len(transpositions
))
512 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
514 return combine_every_nth(untransposed
)
516 return cat(chain(*untransposed
))
518 def scytale_encipher(message
, rows
, fillvalue
=' '):
519 """Enciphers using the scytale transposition cipher.
520 Message is padded with spaces to allow all rows to be the same length.
522 >>> scytale_encipher('thequickbrownfox', 3)
524 >>> scytale_encipher('thequickbrownfox', 4)
526 >>> scytale_encipher('thequickbrownfox', 5)
527 'tubn hirf ecoo qkwx '
528 >>> scytale_encipher('thequickbrownfox', 6)
530 >>> scytale_encipher('thequickbrownfox', 7)
531 'tqcrnx hukof eibwo '
533 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
534 # return column_transposition_encipher(message, transpositions,
535 # fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
536 transpositions
= [i
for i
in range(rows
)]
537 return column_transposition_encipher(message
, transpositions
,
538 fillvalue
=fillvalue
, fillcolumnwise
=True, emptycolumnwise
=False)
540 def scytale_decipher(message
, rows
):
541 """Deciphers using the scytale transposition cipher.
542 Assumes the message is padded so that all rows are the same length.
544 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
546 >>> scytale_decipher('tubnhirfecooqkwx', 4)
548 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
550 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
552 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
555 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
556 # return column_transposition_decipher(message, transpositions,
557 # fillcolumnwise=False, emptycolumnwise=True)
558 transpositions
= [i
for i
in range(rows
)]
559 return column_transposition_decipher(message
, transpositions
,
560 fillcolumnwise
=True, emptycolumnwise
=False)
563 def railfence_encipher(message
, height
, fillvalue
=''):
565 Works by splitting the text into sections, then reading across them to
566 generate the rows in the cipher. The rows are then combined to form the
569 Example: the plaintext "hellotherefriends", with a height of four, written
570 out in the railfence as
575 (with the * showing the one character to finish the last section).
576 Each 'section' is two columns, but unfolded. In the example, the first
579 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 2, fillvalue='!')
580 'hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!'
581 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3, fillvalue='!')
582 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!'
583 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5, fillvalue='!')
584 'hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!'
585 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 10, fillvalue='!')
586 'hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!'
587 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3)
588 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece'
589 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5)
590 'hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp'
591 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 7)
592 'haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic'
594 sections
= chunks(message
, (height
- 1) * 2, fillvalue
=fillvalue
)
595 n_sections
= len(sections
)
597 rows
= [cat([s
[0] for s
in sections
])]
598 # process the middle rows of the grid
599 for r
in range(1, height
-1):
600 rows
+= [cat([s
[r
:r
+1] + s
[height
*2-r
-2:height
*2-r
-1] for s
in sections
])]
601 # process the bottom row
602 rows
+= [cat([s
[height
- 1:height
] for s
in sections
])]
603 # rows += [wcat([s[height - 1] for s in sections])]
606 def railfence_decipher(message
, height
, fillvalue
=''):
607 """Railfence decipher.
608 Works by reconstructing the grid used to generate the ciphertext, then
609 unfolding the sections so the text can be concatenated together.
611 Example: given the ciphertext 'hhieterelorfnsled' and a height of 4, first
612 work out that the second row has a character missing, find the rows of the
613 grid, then split the section into its two columns.
615 'hhieterelorfnsled' is split into
620 (spaces added for clarity), which is stored in 'rows'. This is then split
621 into 'down_rows' and 'up_rows':
633 These are then zipped together (after the up_rows are reversed) to recover
636 Most of the procedure is about finding the correct lengths for each row then
637 splitting the ciphertext into those rows.
639 >>> railfence_decipher('hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!', 2).strip('!')
640 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
641 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!', 3).strip('!')
642 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
643 >>> railfence_decipher('hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!', 5).strip('!')
644 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
645 >>> railfence_decipher('hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!', 10).strip('!')
646 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
647 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece', 3)
648 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
649 >>> railfence_decipher('hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp', 5)
650 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
651 >>> railfence_decipher('haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic', 7)
652 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
654 # find the number and size of the sections, including how many characters
655 # are missing for a full grid
656 n_sections
= math
.ceil(len(message
) / ((height
- 1) * 2))
657 padding_to_add
= n_sections
* (height
- 1) * 2 - len(message
)
658 # row_lengths are for the both up rows and down rows
659 row_lengths
= [n_sections
] * (height
- 1) * 2
660 for i
in range((height
- 1) * 2 - 1, (height
- 1) * 2 - (padding_to_add
+ 1), -1):
662 # folded_rows are the combined row lengths in the middle of the railfence
663 folded_row_lengths
= [row_lengths
[0]]
664 for i
in range(1, height
-1):
665 folded_row_lengths
+= [row_lengths
[i
] + row_lengths
[-i
]]
666 folded_row_lengths
+= [row_lengths
[height
- 1]]
667 # find the rows that form the railfence grid
670 for i
in folded_row_lengths
:
671 rows
+= [message
[row_start
:row_start
+ i
]]
673 # split the rows into the 'down_rows' (those that form the first column of
674 # a section) and the 'up_rows' (those that ofrm the second column of a
676 down_rows
= [rows
[0]]
678 for i
in range(1, height
-1):
679 down_rows
+= [cat([c
for n
, c
in enumerate(rows
[i
]) if n
% 2 == 0])]
680 up_rows
+= [cat([c
for n
, c
in enumerate(rows
[i
]) if n
% 2 == 1])]
681 down_rows
+= [rows
[-1]]
683 return cat(c
for r
in zip_longest(*(down_rows
+ up_rows
), fillvalue
='') for c
in r
)
685 def make_cadenus_keycolumn(doubled_letters
= 'vw', start
='a', reverse
=False):
686 """Makes the key column for a Cadenus cipher (the column down between the
689 >>> make_cadenus_keycolumn()['a']
691 >>> make_cadenus_keycolumn()['b']
693 >>> make_cadenus_keycolumn()['c']
695 >>> make_cadenus_keycolumn()['v']
697 >>> make_cadenus_keycolumn()['w']
699 >>> make_cadenus_keycolumn()['z']
701 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['a']
703 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['b']
705 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['c']
707 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['i']
709 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['j']
711 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['v']
713 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['z']
716 index_to_remove
= string
.ascii_lowercase
.find(doubled_letters
[0])
717 short_alphabet
= string
.ascii_lowercase
[:index_to_remove
] + string
.ascii_lowercase
[index_to_remove
+1:]
719 short_alphabet
= cat(reversed(short_alphabet
))
720 start_pos
= short_alphabet
.find(start
)
721 rotated_alphabet
= short_alphabet
[start_pos
:] + short_alphabet
[:start_pos
]
722 keycolumn
= {l
: i
for i
, l
in enumerate(rotated_alphabet
)}
723 keycolumn
[doubled_letters
[0]] = keycolumn
[doubled_letters
[1]]
726 def cadenus_encipher(message
, keyword
, keycolumn
, fillvalue
='a'):
727 """Encipher with the Cadenus cipher
729 >>> cadenus_encipher(sanitise('Whoever has made a voyage up the Hudson ' \
730 'must remember the Kaatskill mountains. ' \
731 'They are a dismembered branch of the great'), \
733 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
734 'antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
735 >>> cadenus_encipher(sanitise('a severe limitation on the usefulness of ' \
736 'the cadenus is that every message must be ' \
737 'a multiple of twenty-five letters long'), \
739 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
740 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
742 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
744 rotated_columns
= [col
[start
:] + col
[:start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
745 rotated_rows
= zip(*rotated_columns
)
746 transpositions
= transpositions_of(keyword
)
747 transposed
= [transpose(r
, transpositions
) for r
in rotated_rows
]
748 return cat(chain(*transposed
))
750 def cadenus_decipher(message
, keyword
, keycolumn
, fillvalue
='a'):
752 >>> cadenus_decipher('antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaa' \
753 'suvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned', \
755 make_cadenus_keycolumn(reverse=True))
756 'whoeverhasmadeavoyageupthehudsonmustrememberthekaatskillmountainstheyareadismemberedbranchofthegreat'
757 >>> cadenus_decipher('systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtof' \
758 'arenuseieeieltarlmentieetogevesitfaisltngeeuvowul', \
760 make_cadenus_keycolumn(reverse=True))
761 'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
763 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
764 transpositions
= transpositions_of(keyword
)
765 untransposed_rows
= [untranspose(r
, transpositions
) for r
in rows
]
766 columns
= zip(*untransposed_rows
)
767 rotated_columns
= [col
[-start
:] + col
[:-start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
768 rotated_rows
= zip(*rotated_columns
)
769 # return rotated_columns
770 return cat(chain(*rotated_rows
))
773 def hill_encipher(matrix
, message_letters
, fillvalue
='a'):
776 >>> hill_encipher(np.matrix([[7,8], [11,11]]), 'hellothere')
778 >>> hill_encipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
783 sanitised_message
= sanitise(message_letters
)
784 if len(sanitised_message
) % n
!= 0:
785 padding
= fillvalue
[0] * (n
- len(sanitised_message
) % n
)
788 message
= [ord(c
) - ord('a') for c
in sanitised_message
+ padding
]
789 message_chunks
= [message
[i
:i
+n
] for i
in range(0, len(message
), n
)]
790 # message_chunks = chunks(message, len(matrix), fillvalue=None)
791 enciphered_chunks
= [((matrix
* np
.matrix(c
).T
).T
).tolist()[0]
792 for c
in message_chunks
]
793 return cat([chr(int(round(l
)) % 26 + ord('a'))
794 for l
in sum(enciphered_chunks
, [])])
796 def hill_decipher(matrix
, message
, fillvalue
='a'):
799 >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
801 >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
805 adjoint
= linalg
.det(matrix
)*linalg
.inv(matrix
)
806 inverse_determinant
= modular_division_table
[int(round(linalg
.det(matrix
))) % 26][1]
807 inverse_matrix
= (inverse_determinant
* adjoint
) % 26
808 return hill_encipher(inverse_matrix
, message
, fillvalue
)
811 # Where each piece of text ends up in the AMSCO transpositon cipher.
812 # 'index' shows where the slice appears in the plaintext, with the slice
813 # from 'start' to 'end'
814 AmscoSlice
= collections
.namedtuple('AmscoSlice', ['index', 'start', 'end'])
816 class AmscoFillStyle(Enum
):
821 def amsco_transposition_positions(message
, keyword
,
823 fillstyle
=AmscoFillStyle
.continuous
,
824 fillcolumnwise
=False,
825 emptycolumnwise
=True):
826 """Creates the grid for the AMSCO transposition cipher. Each element in the
827 grid shows the index of that slice and the start and end positions of the
828 plaintext that go to make it up.
830 >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
831 fillpattern=(1, 2)) # doctest: +NORMALIZE_WHITESPACE
832 [[AmscoSlice(index=3, start=4, end=6),
833 AmscoSlice(index=2, start=3, end=4),
834 AmscoSlice(index=0, start=0, end=1),
835 AmscoSlice(index=1, start=1, end=3),
836 AmscoSlice(index=4, start=6, end=7)],
837 [AmscoSlice(index=8, start=12, end=13),
838 AmscoSlice(index=7, start=10, end=12),
839 AmscoSlice(index=5, start=7, end=9),
840 AmscoSlice(index=6, start=9, end=10),
841 AmscoSlice(index=9, start=13, end=15)],
842 [AmscoSlice(index=13, start=19, end=21),
843 AmscoSlice(index=12, start=18, end=19),
844 AmscoSlice(index=10, start=15, end=16),
845 AmscoSlice(index=11, start=16, end=18),
846 AmscoSlice(index=14, start=21, end=22)],
847 [AmscoSlice(index=18, start=27, end=28),
848 AmscoSlice(index=17, start=25, end=27),
849 AmscoSlice(index=15, start=22, end=24),
850 AmscoSlice(index=16, start=24, end=25),
851 AmscoSlice(index=19, start=28, end=30)]]
853 transpositions
= transpositions_of(keyword
)
854 fill_iterator
= cycle(fillpattern
)
856 message_length
= len(message
)
860 current_fillpattern
= fillpattern
861 while current_position
< message_length
:
863 if fillstyle
== AmscoFillStyle
.same_each_row
:
864 fill_iterator
= cycle(fillpattern
)
865 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
866 fill_iterator
= cycle(current_fillpattern
)
867 for _
in range(len(transpositions
)):
868 index
= next(indices
)
869 gap
= next(fill_iterator
)
870 row
+= [AmscoSlice(index
, current_position
, current_position
+ gap
)]
871 current_position
+= gap
873 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
874 current_fillpattern
= list(reversed(current_fillpattern
))
875 return [transpose(r
, transpositions
) for r
in grid
]
877 def amsco_transposition_encipher(message
, keyword
,
878 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
879 """AMSCO transposition encipher.
881 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
883 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
885 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
887 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
889 >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
890 'etecstthhomoerereenisxip'
891 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
892 'hetcsoeisterereipexthomn'
893 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
894 'hecsoisttererteipexhomen'
895 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
896 'heecisoosttrrtepeixhemen'
897 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
898 'hxtomephescieretoeisnter'
899 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
900 'hxomeiphscerettoisenteer'
902 grid
= amsco_transposition_positions(message
, keyword
,
903 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
904 ct_as_grid
= [[message
[s
.start
:s
.end
] for s
in r
] for r
in grid
]
905 return combine_every_nth(ct_as_grid
)
908 def amsco_transposition_decipher(message
, keyword
,
909 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
910 """AMSCO transposition decipher
912 >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
914 >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
916 >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
918 >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
920 >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
921 'hereissometexttoencipher'
922 >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
923 'hereissometexttoencipher'
924 >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
925 'hereissometexttoencipher'
926 >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
927 'hereissometexttoencipher'
928 >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
929 'hereissometexttoencipher'
930 >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
931 'hereissometexttoencipher'
934 grid
= amsco_transposition_positions(message
, keyword
,
935 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
936 transposed_sections
= [s
for c
in [l
for l
in zip(*grid
)] for s
in c
]
937 plaintext_list
= [''] * len(transposed_sections
)
939 for slice in transposed_sections
:
940 plaintext_list
[slice.index
] = message
[current_pos
:current_pos
-slice.start
+slice.end
][:len(message
[slice.start
:slice.end
])]
941 current_pos
+= len(message
[slice.start
:slice.end
])
942 return cat(plaintext_list
)
945 class PocketEnigma(object):
946 """A pocket enigma machine
947 The wheel is internally represented as a 26-element list self.wheel_map,
948 where wheel_map[i] == j shows that the position i places on from the arrow
949 maps to the position j places on.
951 def __init__(self
, wheel
=1, position
='a'):
952 """initialise the pocket enigma, including which wheel to use and the
953 starting position of the wheel.
955 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
958 The position is the letter pointed to by the arrow on the wheel.
961 [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]
965 self
.wheel1
= [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
966 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
967 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
968 self
.wheel2
= [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
969 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
970 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
972 self
.make_wheel_map(self
.wheel1
)
974 self
.make_wheel_map(self
.wheel2
)
976 self
.validate_wheel_spec(wheel
)
977 self
.make_wheel_map(wheel
)
978 if position
in string
.ascii_lowercase
:
979 self
.position
= ord(position
) - ord('a')
981 self
.position
= position
983 def make_wheel_map(self
, wheel_spec
):
984 """Expands a wheel specification from a list of letter-letter pairs
985 into a full wheel_map.
987 >>> pe.make_wheel_map(pe.wheel2)
988 [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]
990 self
.validate_wheel_spec(wheel_spec
)
991 self
.wheel_map
= [0] * 26
993 self
.wheel_map
[ord(p
[0]) - ord('a')] = ord(p
[1]) - ord('a')
994 self
.wheel_map
[ord(p
[1]) - ord('a')] = ord(p
[0]) - ord('a')
995 return self
.wheel_map
997 def validate_wheel_spec(self
, wheel_spec
):
998 """Validates that a wheel specificaiton will turn into a valid wheel
1001 >>> pe.validate_wheel_spec([])
1002 Traceback (most recent call last):
1004 ValueError: Wheel specification has 0 pairs, requires 13
1005 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
1006 Traceback (most recent call last):
1008 ValueError: Not all mappings in wheel specificationhave two elements
1009 >>> pe.validate_wheel_spec([('a', 'b')]*13)
1010 Traceback (most recent call last):
1012 ValueError: Wheel specification does not contain 26 letters
1014 if len(wheel_spec
) != 13:
1015 raise ValueError("Wheel specification has {} pairs, requires 13".
1016 format(len(wheel_spec
)))
1017 for p
in wheel_spec
:
1019 raise ValueError("Not all mappings in wheel specification"
1020 "have two elements")
1021 if len(set([p
[0] for p
in wheel_spec
] +
1022 [p
[1] for p
in wheel_spec
])) != 26:
1023 raise ValueError("Wheel specification does not contain 26 letters")
1025 def encipher_letter(self
, letter
):
1026 """Enciphers a single letter, by advancing the wheel before looking up
1027 the letter on the wheel.
1029 >>> pe.set_position('f')
1031 >>> pe.encipher_letter('k')
1035 return self
.lookup(letter
)
1036 decipher_letter
= encipher_letter
1038 def lookup(self
, letter
):
1039 """Look up what a letter enciphers to, without turning the wheel.
1041 >>> pe.set_position('f')
1043 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
1044 'udhbfejcpgmokrliwntsayqzvx'
1048 if letter
in string
.ascii_lowercase
:
1050 (self
.wheel_map
[(ord(letter
) - ord('a') - self
.position
) % 26] +
1051 self
.position
) % 26 +
1057 """Advances the wheel one position.
1059 >>> pe.set_position('f')
1064 self
.position
= (self
.position
+ 1) % 26
1065 return self
.position
1067 def encipher(self
, message
, starting_position
=None):
1068 """Enciphers a whole message.
1070 >>> pe.set_position('f')
1072 >>> pe.encipher('helloworld')
1074 >>> pe.set_position('f')
1076 >>> pe.encipher('kjsglcjoqc')
1078 >>> pe.encipher('helloworld', starting_position = 'x')
1081 if starting_position
:
1082 self
.set_position(starting_position
)
1085 transformed
+= self
.encipher_letter(l
)
1089 def set_position(self
, position
):
1090 """Sets the position of the wheel, by specifying the letter the arrow
1093 >>> pe.set_position('a')
1095 >>> pe.set_position('m')
1097 >>> pe.set_position('z')
1100 self
.position
= ord(position
) - ord('a')
1101 return self
.position
1104 if __name__
== "__main__":
1106 doctest
.testmod(extraglobs
={'pe': PocketEnigma(1, 'a')})