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 modular_division_table
= [[0]*26 for _
in range(26)]
21 modular_division_table
[b
][c
] = a
24 def every_nth(text
, n
, fillvalue
=''):
25 """Returns n strings, each of which consists of every nth character,
26 starting with the 0th, 1st, 2nd, ... (n-1)th character
28 >>> every_nth(string.ascii_lowercase, 5)
29 ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty']
30 >>> every_nth(string.ascii_lowercase, 1)
31 ['abcdefghijklmnopqrstuvwxyz']
32 >>> every_nth(string.ascii_lowercase, 26) # doctest: +NORMALIZE_WHITESPACE
33 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
34 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
35 >>> every_nth(string.ascii_lowercase, 5, fillvalue='!')
36 ['afkpuz', 'bglqv!', 'chmrw!', 'dinsx!', 'ejoty!']
38 split_text
= chunks(text
, n
, fillvalue
)
39 return [cat(l
) for l
in zip_longest(*split_text
, fillvalue
=fillvalue
)]
41 def combine_every_nth(split_text
):
42 """Reforms a text split into every_nth strings
44 >>> combine_every_nth(every_nth(string.ascii_lowercase, 5))
45 'abcdefghijklmnopqrstuvwxyz'
46 >>> combine_every_nth(every_nth(string.ascii_lowercase, 1))
47 'abcdefghijklmnopqrstuvwxyz'
48 >>> combine_every_nth(every_nth(string.ascii_lowercase, 26))
49 'abcdefghijklmnopqrstuvwxyz'
52 for l
in zip_longest(*split_text
, fillvalue
='')])
54 def chunks(text
, n
, fillvalue
=None):
55 """Split a text into chunks of n characters
57 >>> chunks('abcdefghi', 3)
59 >>> chunks('abcdefghi', 4)
61 >>> chunks('abcdefghi', 4, fillvalue='!')
62 ['abcd', 'efgh', 'i!!!']
65 padding
= fillvalue
[0] * (n
- len(text
) % n
)
68 return [(text
+padding
)[i
:i
+n
] for i
in range(0, len(text
), n
)]
70 def transpose(items
, transposition
):
71 """Moves items around according to the given transposition
73 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
75 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
77 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
78 [13, 12, 14, 11, 15, 10]
80 transposed
= [''] * len(transposition
)
81 for p
, t
in enumerate(transposition
):
82 transposed
[p
] = items
[t
]
85 def untranspose(items
, transposition
):
88 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
90 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
92 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
93 [10, 11, 12, 13, 14, 15]
95 transposed
= [''] * len(transposition
)
96 for p
, t
in enumerate(transposition
):
97 transposed
[t
] = items
[p
]
100 def deduplicate(text
):
101 return list(collections
.OrderedDict
.fromkeys(text
))
104 def caesar_encipher_letter(accented_letter
, shift
):
105 """Encipher a letter, given a shift amount
107 >>> caesar_encipher_letter('a', 1)
109 >>> caesar_encipher_letter('a', 2)
111 >>> caesar_encipher_letter('b', 2)
113 >>> caesar_encipher_letter('x', 2)
115 >>> caesar_encipher_letter('y', 2)
117 >>> caesar_encipher_letter('z', 2)
119 >>> caesar_encipher_letter('z', -1)
121 >>> caesar_encipher_letter('a', -1)
123 >>> caesar_encipher_letter('A', 1)
125 >>> caesar_encipher_letter('é', 1)
128 letter
= unaccent(accented_letter
)
129 if letter
in string
.ascii_letters
:
130 if letter
in string
.ascii_uppercase
:
131 alphabet_start
= ord('A')
133 alphabet_start
= ord('a')
134 return chr(((ord(letter
) - alphabet_start
+ shift
) % 26) +
139 def caesar_decipher_letter(letter
, shift
):
140 """Decipher a letter, given a shift amount
142 >>> caesar_decipher_letter('b', 1)
144 >>> caesar_decipher_letter('b', 2)
147 return caesar_encipher_letter(letter
, -shift
)
149 def caesar_encipher(message
, shift
):
150 """Encipher a message with the Caesar cipher of given shift
152 >>> caesar_encipher('abc', 1)
154 >>> caesar_encipher('abc', 2)
156 >>> caesar_encipher('abcxyz', 2)
158 >>> caesar_encipher('ab cx yz', 2)
160 >>> caesar_encipher('Héllo World!', 2)
163 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
164 return cat(enciphered
)
166 def caesar_decipher(message
, shift
):
167 """Decipher a message with the Caesar cipher of given shift
169 >>> caesar_decipher('bcd', 1)
171 >>> caesar_decipher('cde', 2)
173 >>> caesar_decipher('cd ez ab', 2)
175 >>> caesar_decipher('Jgnnq Yqtnf!', 2)
178 return caesar_encipher(message
, -shift
)
180 def affine_encipher_letter(accented_letter
, multiplier
=1, adder
=0, one_based
=True):
181 """Encipher a letter, given a multiplier and adder
183 >>> cat([affine_encipher_letter(l, 3, 5, True) \
184 for l in string.ascii_uppercase])
185 'HKNQTWZCFILORUXADGJMPSVYBE'
186 >>> cat([affine_encipher_letter(l, 3, 5, False) \
187 for l in string.ascii_uppercase])
188 'FILORUXADGJMPSVYBEHKNQTWZC'
190 letter
= unaccent(accented_letter
)
191 if letter
in string
.ascii_letters
:
192 if letter
in string
.ascii_uppercase
:
193 alphabet_start
= ord('A')
195 alphabet_start
= ord('a')
196 letter_number
= ord(letter
) - alphabet_start
197 if one_based
: letter_number
+= 1
198 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
199 if one_based
: cipher_number
-= 1
200 return chr(cipher_number
% 26 + alphabet_start
)
204 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
205 """Encipher a letter, given a multiplier and adder
207 >>> cat([affine_decipher_letter(l, 3, 5, True) \
208 for l in 'HKNQTWZCFILORUXADGJMPSVYBE'])
209 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
210 >>> cat([affine_decipher_letter(l, 3, 5, False) \
211 for l in 'FILORUXADGJMPSVYBEHKNQTWZC'])
212 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
214 if letter
in string
.ascii_letters
:
215 if letter
in string
.ascii_uppercase
:
216 alphabet_start
= ord('A')
218 alphabet_start
= ord('a')
219 cipher_number
= ord(letter
) - alphabet_start
220 if one_based
: cipher_number
+= 1
222 modular_division_table
[multiplier
]
223 [(cipher_number
- adder
) % 26])
224 if one_based
: plaintext_number
-= 1
225 return chr(plaintext_number
% 26 + alphabet_start
)
229 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
230 """Encipher a message
232 >>> affine_encipher('hours passed during which jerico tried every ' \
233 'trick he could think of', 15, 22, True)
234 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
236 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
238 return cat(enciphered
)
240 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
241 """Decipher a message
243 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
244 'jfaoe ls omytd jlaxe mh', 15, 22, True)
245 'hours passed during which jerico tried every trick he could think of'
247 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
249 return cat(enciphered
)
252 class KeywordWrapAlphabet(Enum
):
258 def keyword_cipher_alphabet_of(keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
259 """Find the cipher alphabet given a keyword.
260 wrap_alphabet controls how the rest of the alphabet is added
263 >>> keyword_cipher_alphabet_of('bayes')
264 'bayescdfghijklmnopqrtuvwxz'
265 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_a)
266 'bayescdfghijklmnopqrtuvwxz'
267 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_last)
268 'bayestuvwxzcdfghijklmnopqr'
269 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_largest)
270 'bayeszcdfghijklmnopqrtuvwx'
272 if wrap_alphabet
== KeywordWrapAlphabet
.from_a
:
273 cipher_alphabet
= cat(deduplicate(sanitise(keyword
) +
274 string
.ascii_lowercase
))
276 if wrap_alphabet
== KeywordWrapAlphabet
.from_last
:
277 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
279 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
280 last_keyword_position
= string
.ascii_lowercase
.find(
281 last_keyword_letter
) + 1
282 cipher_alphabet
= cat(
283 deduplicate(sanitise(keyword
) +
284 string
.ascii_lowercase
[last_keyword_position
:] +
285 string
.ascii_lowercase
))
286 return cipher_alphabet
289 def keyword_encipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
290 """Enciphers a message with a keyword substitution cipher.
291 wrap_alphabet controls how the rest of the alphabet is added
294 1 : from the last letter in the sanitised keyword
295 2 : from the largest letter in the sanitised keyword
297 >>> keyword_encipher('test message', 'bayes')
299 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a)
301 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last)
303 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest)
306 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
307 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
308 return unaccent(message
).lower().translate(cipher_translation
)
310 def keyword_decipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
311 """Deciphers a message with a keyword substitution cipher.
312 wrap_alphabet controls how the rest of the alphabet is added
315 1 : from the last letter in the sanitised keyword
316 2 : from the largest letter in the sanitised keyword
318 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
320 >>> keyword_decipher('rsqr ksqqbds', 'bayes', KeywordWrapAlphabet.from_a)
322 >>> keyword_decipher('lskl dskkbus', 'bayes', KeywordWrapAlphabet.from_last)
324 >>> keyword_decipher('qspq jsppbcs', 'bayes', KeywordWrapAlphabet.from_largest)
327 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
328 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
329 return message
.lower().translate(cipher_translation
)
332 def vigenere_encipher(message
, keyword
):
335 >>> vigenere_encipher('hello', 'abc')
338 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
339 pairs
= zip(message
, cycle(shifts
))
340 return cat([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
342 def vigenere_decipher(message
, keyword
):
345 >>> vigenere_decipher('hfnlp', 'abc')
348 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
349 pairs
= zip(message
, cycle(shifts
))
350 return cat([caesar_decipher_letter(l
, k
) for l
, k
in pairs
])
352 beaufort_encipher
=vigenere_decipher
353 beaufort_decipher
=vigenere_encipher
356 def transpositions_of(keyword
):
357 """Finds the transpostions given by a keyword. For instance, the keyword
358 'clever' rearranges to 'celrv', so the first column (0) stays first, the
359 second column (1) moves to third, the third column (2) moves to second,
362 If passed a tuple, assume it's already a transposition and just return it.
364 >>> transpositions_of('clever')
366 >>> transpositions_of('fred')
368 >>> transpositions_of((3, 2, 0, 1))
371 if isinstance(keyword
, tuple):
374 key
= deduplicate(keyword
)
375 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
376 return transpositions
378 def pad(message_len
, group_len
, fillvalue
):
379 padding_length
= group_len
- message_len
% group_len
380 if padding_length
== group_len
: padding_length
= 0
382 for i
in range(padding_length
):
383 if callable(fillvalue
):
384 padding
+= fillvalue()
389 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
390 fillcolumnwise
=False,
391 emptycolumnwise
=False):
392 """Enciphers using the column transposition cipher.
393 Message is padded to allow all rows to be the same length.
395 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
397 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
399 >>> column_transposition_encipher('hellothere', 'abcdef')
401 >>> column_transposition_encipher('hellothere', 'abcde')
403 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
405 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
407 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
409 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
411 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
413 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
415 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
417 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
419 >>> column_transposition_encipher('hellothere', 'cleverly')
421 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
423 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
426 transpositions
= transpositions_of(keyword
)
427 message
+= pad(len(message
), len(transpositions
), fillvalue
)
429 rows
= every_nth(message
, len(message
) // len(transpositions
))
431 rows
= chunks(message
, len(transpositions
))
432 transposed
= [transpose(r
, transpositions
) for r
in rows
]
434 return combine_every_nth(transposed
)
436 return cat(chain(*transposed
))
438 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
439 fillcolumnwise
=False,
440 emptycolumnwise
=False):
441 """Deciphers using the column transposition cipher.
442 Message is padded to allow all rows to be the same length.
444 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
446 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
448 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
450 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
452 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
454 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
456 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
458 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
461 transpositions
= transpositions_of(keyword
)
462 message
+= pad(len(message
), len(transpositions
), fillvalue
)
464 rows
= every_nth(message
, len(message
) // len(transpositions
))
466 rows
= chunks(message
, len(transpositions
))
467 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
469 return combine_every_nth(untransposed
)
471 return cat(chain(*untransposed
))
473 def scytale_encipher(message
, rows
, fillvalue
=' '):
474 """Enciphers using the scytale transposition cipher.
475 Message is padded with spaces to allow all rows to be the same length.
477 >>> scytale_encipher('thequickbrownfox', 3)
479 >>> scytale_encipher('thequickbrownfox', 4)
481 >>> scytale_encipher('thequickbrownfox', 5)
482 'tubn hirf ecoo qkwx '
483 >>> scytale_encipher('thequickbrownfox', 6)
485 >>> scytale_encipher('thequickbrownfox', 7)
486 'tqcrnx hukof eibwo '
488 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
489 # return column_transposition_encipher(message, transpositions,
490 # fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
491 transpositions
= [i
for i
in range(rows
)]
492 return column_transposition_encipher(message
, transpositions
,
493 fillvalue
=fillvalue
, fillcolumnwise
=True, emptycolumnwise
=False)
495 def scytale_decipher(message
, rows
):
496 """Deciphers using the scytale transposition cipher.
497 Assumes the message is padded so that all rows are the same length.
499 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
501 >>> scytale_decipher('tubnhirfecooqkwx', 4)
503 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
505 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
507 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
510 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
511 # return column_transposition_decipher(message, transpositions,
512 # fillcolumnwise=False, emptycolumnwise=True)
513 transpositions
= [i
for i
in range(rows
)]
514 return column_transposition_decipher(message
, transpositions
,
515 fillcolumnwise
=True, emptycolumnwise
=False)
518 def railfence_encipher(message
, height
, fillvalue
=''):
520 Works by splitting the text into sections, then reading across them to
521 generate the rows in the cipher. The rows are then combined to form the
524 Example: the plaintext "hellotherefriends", with a height of four, written
525 out in the railfence as
530 (with the * showing the one character to finish the last section).
531 Each 'section' is two columns, but unfolded. In the example, the first
534 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 2, fillvalue='!')
535 'hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!'
536 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3, fillvalue='!')
537 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!'
538 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5, fillvalue='!')
539 'hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!'
540 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 10, fillvalue='!')
541 'hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!'
542 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 3)
543 'horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece'
544 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 5)
545 'hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp'
546 >>> railfence_encipher('hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers', 7)
547 'haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic'
549 sections
= chunks(message
, (height
- 1) * 2, fillvalue
=fillvalue
)
550 n_sections
= len(sections
)
552 rows
= [cat([s
[0] for s
in sections
])]
553 # process the middle rows of the grid
554 for r
in range(1, height
-1):
555 rows
+= [cat([s
[r
:r
+1] + s
[height
*2-r
-2:height
*2-r
-1] for s
in sections
])]
556 # process the bottom row
557 rows
+= [cat([s
[height
- 1:height
] for s
in sections
])]
558 # rows += [wcat([s[height - 1] for s in sections])]
561 def railfence_decipher(message
, height
, fillvalue
=''):
562 """Railfence decipher.
563 Works by reconstructing the grid used to generate the ciphertext, then
564 unfolding the sections so the text can be concatenated together.
566 Example: given the ciphertext 'hhieterelorfnsled' and a height of 4, first
567 work out that the second row has a character missing, find the rows of the
568 grid, then split the section into its two columns.
570 'hhieterelorfnsled' is split into
575 (spaces added for clarity), which is stored in 'rows'. This is then split
576 into 'down_rows' and 'up_rows':
588 These are then zipped together (after the up_rows are reversed) to recover
591 Most of the procedure is about finding the correct lengths for each row then
592 splitting the ciphertext into those rows.
594 >>> railfence_decipher('hlohraateerishsslnpeefetotsigaleccpeselteevsmhatetiiaogicotxfretnrifneihr!', 2).strip('!')
595 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
596 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihr!!lhateihsnefttiaece!', 3).strip('!')
597 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
598 >>> railfence_decipher('hresleogcseeemhetaocofrnrner!!lhateihsnefttiaece!!ltvsatiigitxetifih!!oarspeslp!', 5).strip('!')
599 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
600 >>> railfence_decipher('hepisehagitnr!!lernesge!!lmtocerh!!otiletap!!tseaorii!!hassfolc!!evtitffe!!rahsetec!!eixn!', 10).strip('!')
601 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
602 >>> railfence_decipher('horaersslpeeosglcpselteevsmhatetiiaogicotxfretnrifneihrlhateihsnefttiaece', 3)
603 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
604 >>> railfence_decipher('hresleogcseeemhetaocofrnrnerlhateihsnefttiaeceltvsatiigitxetifihoarspeslp', 5)
605 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
606 >>> railfence_decipher('haspolsevsetgifrifrlatihnettaeelemtiocxernhorersleesgcptehaiaottneihesfic', 7)
607 'hellothereavastmeheartiesthisisalongpieceoftextfortestingrailfenceciphers'
609 # find the number and size of the sections, including how many characters
610 # are missing for a full grid
611 n_sections
= math
.ceil(len(message
) / ((height
- 1) * 2))
612 padding_to_add
= n_sections
* (height
- 1) * 2 - len(message
)
613 # row_lengths are for the both up rows and down rows
614 row_lengths
= [n_sections
] * (height
- 1) * 2
615 for i
in range((height
- 1) * 2 - 1, (height
- 1) * 2 - (padding_to_add
+ 1), -1):
617 # folded_rows are the combined row lengths in the middle of the railfence
618 folded_row_lengths
= [row_lengths
[0]]
619 for i
in range(1, height
-1):
620 folded_row_lengths
+= [row_lengths
[i
] + row_lengths
[-i
]]
621 folded_row_lengths
+= [row_lengths
[height
- 1]]
622 # find the rows that form the railfence grid
625 for i
in folded_row_lengths
:
626 rows
+= [message
[row_start
:row_start
+ i
]]
628 # split the rows into the 'down_rows' (those that form the first column of
629 # a section) and the 'up_rows' (those that ofrm the second column of a
631 down_rows
= [rows
[0]]
633 for i
in range(1, height
-1):
634 down_rows
+= [cat([c
for n
, c
in enumerate(rows
[i
]) if n
% 2 == 0])]
635 up_rows
+= [cat([c
for n
, c
in enumerate(rows
[i
]) if n
% 2 == 1])]
636 down_rows
+= [rows
[-1]]
638 return cat(c
for r
in zip_longest(*(down_rows
+ up_rows
), fillvalue
='') for c
in r
)
640 def make_cadenus_keycolumn(doubled_letters
= 'vw', start
='a', reverse
=False):
641 """Makes the key column for a Cadenus cipher (the column down between the
644 >>> make_cadenus_keycolumn()['a']
646 >>> make_cadenus_keycolumn()['b']
648 >>> make_cadenus_keycolumn()['c']
650 >>> make_cadenus_keycolumn()['v']
652 >>> make_cadenus_keycolumn()['w']
654 >>> make_cadenus_keycolumn()['z']
656 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['a']
658 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['b']
660 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['c']
662 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['i']
664 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['j']
666 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['v']
668 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['z']
671 index_to_remove
= string
.ascii_lowercase
.find(doubled_letters
[0])
672 short_alphabet
= string
.ascii_lowercase
[:index_to_remove
] + string
.ascii_lowercase
[index_to_remove
+1:]
674 short_alphabet
= cat(reversed(short_alphabet
))
675 start_pos
= short_alphabet
.find(start
)
676 rotated_alphabet
= short_alphabet
[start_pos
:] + short_alphabet
[:start_pos
]
677 keycolumn
= {l
: i
for i
, l
in enumerate(rotated_alphabet
)}
678 keycolumn
[doubled_letters
[0]] = keycolumn
[doubled_letters
[1]]
681 def cadenus_encipher(message
, keyword
, keycolumn
, fillvalue
='a'):
682 """Encipher with the Cadenus cipher
684 >>> cadenus_encipher(sanitise('Whoever has made a voyage up the Hudson ' \
685 'must remember the Kaatskill mountains. ' \
686 'They are a dismembered branch of the great'), \
688 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
689 'antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
690 >>> cadenus_encipher(sanitise('a severe limitation on the usefulness of ' \
691 'the cadenus is that every message must be ' \
692 'a multiple of twenty-five letters long'), \
694 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
695 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
697 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
699 rotated_columns
= [col
[start
:] + col
[:start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
700 rotated_rows
= zip(*rotated_columns
)
701 transpositions
= transpositions_of(keyword
)
702 transposed
= [transpose(r
, transpositions
) for r
in rotated_rows
]
703 return cat(chain(*transposed
))
705 def cadenus_decipher(message
, keyword
, keycolumn
, fillvalue
='a'):
707 >>> cadenus_decipher('antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaa' \
708 'suvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned', \
710 make_cadenus_keycolumn(reverse=True))
711 'whoeverhasmadeavoyageupthehudsonmustrememberthekaatskillmountainstheyareadismemberedbranchofthegreat'
712 >>> cadenus_decipher('systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtof' \
713 'arenuseieeieltarlmentieetogevesitfaisltngeeuvowul', \
715 make_cadenus_keycolumn(reverse=True))
716 'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
718 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
719 transpositions
= transpositions_of(keyword
)
720 untransposed_rows
= [untranspose(r
, transpositions
) for r
in rows
]
721 columns
= zip(*untransposed_rows
)
722 rotated_columns
= [col
[-start
:] + col
[:-start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
723 rotated_rows
= zip(*rotated_columns
)
724 # return rotated_columns
725 return cat(chain(*rotated_rows
))
728 def hill_encipher(matrix
, message_letters
, fillvalue
='a'):
731 >>> hill_encipher(np.matrix([[7,8], [11,11]]), 'hellothere')
733 >>> hill_encipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
738 sanitised_message
= sanitise(message_letters
)
739 if len(sanitised_message
) % n
!= 0:
740 padding
= fillvalue
[0] * (n
- len(sanitised_message
) % n
)
743 message
= [ord(c
) - ord('a') for c
in sanitised_message
+ padding
]
744 message_chunks
= [message
[i
:i
+n
] for i
in range(0, len(message
), n
)]
745 # message_chunks = chunks(message, len(matrix), fillvalue=None)
746 enciphered_chunks
= [((matrix
* np
.matrix(c
).T
).T
).tolist()[0]
747 for c
in message_chunks
]
748 return cat([chr(int(round(l
)) % 26 + ord('a'))
749 for l
in sum(enciphered_chunks
, [])])
751 def hill_decipher(matrix
, message
, fillvalue
='a'):
754 >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
756 >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
760 adjoint
= linalg
.det(matrix
)*linalg
.inv(matrix
)
761 inverse_determinant
= modular_division_table
[int(round(linalg
.det(matrix
))) % 26][1]
762 inverse_matrix
= (inverse_determinant
* adjoint
) % 26
763 return hill_encipher(inverse_matrix
, message
, fillvalue
)
766 # Where each piece of text ends up in the AMSCO transpositon cipher.
767 # 'index' shows where the slice appears in the plaintext, with the slice
768 # from 'start' to 'end'
769 AmscoSlice
= collections
.namedtuple('AmscoSlice', ['index', 'start', 'end'])
771 class AmscoFillStyle(Enum
):
776 def amsco_transposition_positions(message
, keyword
,
778 fillstyle
=AmscoFillStyle
.continuous
,
779 fillcolumnwise
=False,
780 emptycolumnwise
=True):
781 """Creates the grid for the AMSCO transposition cipher. Each element in the
782 grid shows the index of that slice and the start and end positions of the
783 plaintext that go to make it up.
785 >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
786 fillpattern=(1, 2)) # doctest: +NORMALIZE_WHITESPACE
787 [[AmscoSlice(index=3, start=4, end=6),
788 AmscoSlice(index=2, start=3, end=4),
789 AmscoSlice(index=0, start=0, end=1),
790 AmscoSlice(index=1, start=1, end=3),
791 AmscoSlice(index=4, start=6, end=7)],
792 [AmscoSlice(index=8, start=12, end=13),
793 AmscoSlice(index=7, start=10, end=12),
794 AmscoSlice(index=5, start=7, end=9),
795 AmscoSlice(index=6, start=9, end=10),
796 AmscoSlice(index=9, start=13, end=15)],
797 [AmscoSlice(index=13, start=19, end=21),
798 AmscoSlice(index=12, start=18, end=19),
799 AmscoSlice(index=10, start=15, end=16),
800 AmscoSlice(index=11, start=16, end=18),
801 AmscoSlice(index=14, start=21, end=22)],
802 [AmscoSlice(index=18, start=27, end=28),
803 AmscoSlice(index=17, start=25, end=27),
804 AmscoSlice(index=15, start=22, end=24),
805 AmscoSlice(index=16, start=24, end=25),
806 AmscoSlice(index=19, start=28, end=30)]]
808 transpositions
= transpositions_of(keyword
)
809 fill_iterator
= cycle(fillpattern
)
811 message_length
= len(message
)
815 current_fillpattern
= fillpattern
816 while current_position
< message_length
:
818 if fillstyle
== AmscoFillStyle
.same_each_row
:
819 fill_iterator
= cycle(fillpattern
)
820 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
821 fill_iterator
= cycle(current_fillpattern
)
822 for _
in range(len(transpositions
)):
823 index
= next(indices
)
824 gap
= next(fill_iterator
)
825 row
+= [AmscoSlice(index
, current_position
, current_position
+ gap
)]
826 current_position
+= gap
828 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
829 current_fillpattern
= list(reversed(current_fillpattern
))
830 return [transpose(r
, transpositions
) for r
in grid
]
832 def amsco_transposition_encipher(message
, keyword
,
833 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
834 """AMSCO transposition encipher.
836 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
838 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
840 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
842 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
844 >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
845 'etecstthhomoerereenisxip'
846 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
847 'hetcsoeisterereipexthomn'
848 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
849 'hecsoisttererteipexhomen'
850 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
851 'heecisoosttrrtepeixhemen'
852 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
853 'hxtomephescieretoeisnter'
854 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
855 'hxomeiphscerettoisenteer'
857 grid
= amsco_transposition_positions(message
, keyword
,
858 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
859 ct_as_grid
= [[message
[s
.start
:s
.end
] for s
in r
] for r
in grid
]
860 return combine_every_nth(ct_as_grid
)
863 def amsco_transposition_decipher(message
, keyword
,
864 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
865 """AMSCO transposition decipher
867 >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
869 >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
871 >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
873 >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
875 >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
876 'hereissometexttoencipher'
877 >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
878 'hereissometexttoencipher'
879 >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
880 'hereissometexttoencipher'
881 >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
882 'hereissometexttoencipher'
883 >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
884 'hereissometexttoencipher'
885 >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
886 'hereissometexttoencipher'
889 grid
= amsco_transposition_positions(message
, keyword
,
890 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
891 transposed_sections
= [s
for c
in [l
for l
in zip(*grid
)] for s
in c
]
892 plaintext_list
= [''] * len(transposed_sections
)
894 for slice in transposed_sections
:
895 plaintext_list
[slice.index
] = message
[current_pos
:current_pos
-slice.start
+slice.end
][:len(message
[slice.start
:slice.end
])]
896 current_pos
+= len(message
[slice.start
:slice.end
])
897 return cat(plaintext_list
)
900 class PocketEnigma(object):
901 """A pocket enigma machine
902 The wheel is internally represented as a 26-element list self.wheel_map,
903 where wheel_map[i] == j shows that the position i places on from the arrow
904 maps to the position j places on.
906 def __init__(self
, wheel
=1, position
='a'):
907 """initialise the pocket enigma, including which wheel to use and the
908 starting position of the wheel.
910 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
913 The position is the letter pointed to by the arrow on the wheel.
916 [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]
920 self
.wheel1
= [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
921 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
922 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
923 self
.wheel2
= [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
924 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
925 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
927 self
.make_wheel_map(self
.wheel1
)
929 self
.make_wheel_map(self
.wheel2
)
931 self
.validate_wheel_spec(wheel
)
932 self
.make_wheel_map(wheel
)
933 if position
in string
.ascii_lowercase
:
934 self
.position
= ord(position
) - ord('a')
936 self
.position
= position
938 def make_wheel_map(self
, wheel_spec
):
939 """Expands a wheel specification from a list of letter-letter pairs
940 into a full wheel_map.
942 >>> pe.make_wheel_map(pe.wheel2)
943 [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]
945 self
.validate_wheel_spec(wheel_spec
)
946 self
.wheel_map
= [0] * 26
948 self
.wheel_map
[ord(p
[0]) - ord('a')] = ord(p
[1]) - ord('a')
949 self
.wheel_map
[ord(p
[1]) - ord('a')] = ord(p
[0]) - ord('a')
950 return self
.wheel_map
952 def validate_wheel_spec(self
, wheel_spec
):
953 """Validates that a wheel specificaiton will turn into a valid wheel
956 >>> pe.validate_wheel_spec([])
957 Traceback (most recent call last):
959 ValueError: Wheel specification has 0 pairs, requires 13
960 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
961 Traceback (most recent call last):
963 ValueError: Not all mappings in wheel specificationhave two elements
964 >>> pe.validate_wheel_spec([('a', 'b')]*13)
965 Traceback (most recent call last):
967 ValueError: Wheel specification does not contain 26 letters
969 if len(wheel_spec
) != 13:
970 raise ValueError("Wheel specification has {} pairs, requires 13".
971 format(len(wheel_spec
)))
974 raise ValueError("Not all mappings in wheel specification"
976 if len(set([p
[0] for p
in wheel_spec
] +
977 [p
[1] for p
in wheel_spec
])) != 26:
978 raise ValueError("Wheel specification does not contain 26 letters")
980 def encipher_letter(self
, letter
):
981 """Enciphers a single letter, by advancing the wheel before looking up
982 the letter on the wheel.
984 >>> pe.set_position('f')
986 >>> pe.encipher_letter('k')
990 return self
.lookup(letter
)
991 decipher_letter
= encipher_letter
993 def lookup(self
, letter
):
994 """Look up what a letter enciphers to, without turning the wheel.
996 >>> pe.set_position('f')
998 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
999 'udhbfejcpgmokrliwntsayqzvx'
1003 if letter
in string
.ascii_lowercase
:
1005 (self
.wheel_map
[(ord(letter
) - ord('a') - self
.position
) % 26] +
1006 self
.position
) % 26 +
1012 """Advances the wheel one position.
1014 >>> pe.set_position('f')
1019 self
.position
= (self
.position
+ 1) % 26
1020 return self
.position
1022 def encipher(self
, message
, starting_position
=None):
1023 """Enciphers a whole message.
1025 >>> pe.set_position('f')
1027 >>> pe.encipher('helloworld')
1029 >>> pe.set_position('f')
1031 >>> pe.encipher('kjsglcjoqc')
1033 >>> pe.encipher('helloworld', starting_position = 'x')
1036 if starting_position
:
1037 self
.set_position(starting_position
)
1040 transformed
+= self
.encipher_letter(l
)
1044 def set_position(self
, position
):
1045 """Sets the position of the wheel, by specifying the letter the arrow
1048 >>> pe.set_position('a')
1050 >>> pe.set_position('m')
1052 >>> pe.set_position('z')
1055 self
.position
= ord(position
) - ord('a')
1056 return self
.position
1059 if __name__
== "__main__":
1061 doctest
.testmod(extraglobs
={'pe': PocketEnigma(1, 'a')})