1 """A set of functions to break the ciphers give in ciphers.py.
10 from itertools
import starmap
11 from segment
import segment
12 from multiprocessing
import Pool
14 import matplotlib
.pyplot
as plt
16 logger
= logging
.getLogger(__name__
)
17 logger
.addHandler(logging
.FileHandler('cipher.log'))
18 logger
.setLevel(logging
.WARNING
)
19 #logger.setLevel(logging.INFO)
20 #logger.setLevel(logging.DEBUG)
23 from language_models
import *
28 # c5a = open('2012/5a.ciphertext', 'r').read()
29 # timeit.timeit('keyword_break(c5a)', setup='gc.enable() ; from __main__ import c5a ; from cipher import keyword_break', number=1)
30 # timeit.repeat('keyword_break_mp(c5a, chunksize=500)', setup='gc.enable() ; from __main__ import c5a ; from cipher import keyword_break_mp', repeat=5, number=1)
32 transpositions
= collections
.defaultdict(list)
34 transpositions
[transpositions_of(word
)] += [word
]
36 def frequencies(text
):
37 """Count the number of occurrences of each character in text
39 >>> sorted(frequencies('abcdefabc').items())
40 [('a', 2), ('b', 2), ('c', 2), ('d', 1), ('e', 1), ('f', 1)]
41 >>> sorted(frequencies('the quick brown fox jumped over the lazy ' \
42 'dog').items()) # doctest: +NORMALIZE_WHITESPACE
43 [(' ', 8), ('a', 1), ('b', 1), ('c', 1), ('d', 2), ('e', 4), ('f', 1),
44 ('g', 1), ('h', 2), ('i', 1), ('j', 1), ('k', 1), ('l', 1), ('m', 1),
45 ('n', 1), ('o', 4), ('p', 1), ('q', 1), ('r', 2), ('t', 2), ('u', 2),
46 ('v', 1), ('w', 1), ('x', 1), ('y', 1), ('z', 1)]
47 >>> sorted(frequencies('The Quick BROWN fox jumped! over... the ' \
48 '(9lazy) DOG').items()) # doctest: +NORMALIZE_WHITESPACE
49 [(' ', 8), ('!', 1), ('(', 1), (')', 1), ('.', 3), ('9', 1), ('B', 1),
50 ('D', 1), ('G', 1), ('N', 1), ('O', 2), ('Q', 1), ('R', 1), ('T', 1),
51 ('W', 1), ('a', 1), ('c', 1), ('d', 1), ('e', 4), ('f', 1), ('h', 2),
52 ('i', 1), ('j', 1), ('k', 1), ('l', 1), ('m', 1), ('o', 2), ('p', 1),
53 ('r', 1), ('t', 1), ('u', 2), ('v', 1), ('x', 1), ('y', 1), ('z', 1)]
54 >>> sorted(frequencies(sanitise('The Quick BROWN fox jumped! over... '\
55 'the (9lazy) DOG')).items()) # doctest: +NORMALIZE_WHITESPACE
56 [('a', 1), ('b', 1), ('c', 1), ('d', 2), ('e', 4), ('f', 1), ('g', 1),
57 ('h', 2), ('i', 1), ('j', 1), ('k', 1), ('l', 1), ('m', 1), ('n', 1),
58 ('o', 4), ('p', 1), ('q', 1), ('r', 2), ('t', 2), ('u', 2), ('v', 1),
59 ('w', 1), ('x', 1), ('y', 1), ('z', 1)]
60 >>> frequencies('abcdefabcdef')['x']
63 return collections
.Counter(c
for c
in text
)
66 def caesar_break(message
, fitness
=Pletters
):
67 """Breaks a Caesar cipher using frequency analysis
69 >>> caesar_break('ibxcsyorsaqcheyklxivoexlevmrimwxsfiqevvmihrsasrxliwyrh' \
70 'ecjsppsamrkwleppfmergefifvmhixscsymjcsyqeoixlm') # doctest: +ELLIPSIS
71 (4, -130.849989015...)
72 >>> caesar_break('wxwmaxdgheetgwuxztgptedbgznitgwwhpguxyhkxbmhvvtlbhgtee' \
73 'raxlmhiixweblmxgxwmhmaxybkbgztgwztsxwbgmxgmert') # doctest: +ELLIPSIS
74 (19, -128.82410410...)
75 >>> caesar_break('yltbbqnqnzvguvaxurorgenafsbezqvagbnornfgsbevpnaabjurer' \
76 'svaquvzyvxrnznazlybequrvfohgriraabjtbaruraprur') # doctest: +ELLIPSIS
77 (13, -126.25403935...)
79 sanitised_message
= sanitise(message
)
81 best_fit
= float('-inf')
82 for shift
in range(26):
83 plaintext
= caesar_decipher(sanitised_message
, shift
)
84 fit
= fitness(plaintext
)
85 logger
.debug('Caesar break attempt using key {0} gives fit of {1} '
86 'and decrypt starting: {2}'.format(shift
, fit
,
91 logger
.info('Caesar break best fit: key {0} gives fit of {1} and '
92 'decrypt starting: {2}'.format(best_shift
, best_fit
,
93 caesar_decipher(sanitised_message
, best_shift
)[:50]))
94 return best_shift
, best_fit
96 def affine_break(message
, fitness
=Pletters
):
97 """Breaks an affine cipher using frequency analysis
99 >>> affine_break('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls ' \
100 'omytd jlaxe mh jm bfmibj umis hfsul axubafkjamx. ls kffkxwsd jls ' \
101 'ofgbjmwfkiu olfmxmtmwaokttg jlsx ls kffkxwsd jlsi zg tsxwjl. jlsx ' \
102 'ls umfjsd jlsi zg hfsqysxog. ls dmmdtsd mx jls bats mh bkbsf. ls ' \
103 'bfmctsd kfmyxd jls lyj, mztanamyu xmc jm clm cku tmmeaxw kj lai ' \
104 'kxd clm ckuxj.') # doctest: +ELLIPSIS
105 ((15, 22, True), -340.601181913...)
107 sanitised_message
= sanitise(message
)
110 best_one_based
= True
111 best_fit
= float("-inf")
112 for one_based
in [True, False]:
113 for multiplier
in [x
for x
in range(1, 26, 2) if x
!= 13]:
114 for adder
in range(26):
115 plaintext
= affine_decipher(sanitised_message
,
116 multiplier
, adder
, one_based
)
117 fit
= fitness(plaintext
)
118 logger
.debug('Affine break attempt using key {0}x+{1} ({2}) '
119 'gives fit of {3} and decrypt starting: {4}'.
120 format(multiplier
, adder
, one_based
, fit
,
124 best_multiplier
= multiplier
126 best_one_based
= one_based
127 logger
.info('Affine break best fit with key {0}x+{1} ({2}) gives fit of '
128 '{3} and decrypt starting: {4}'.format(
129 best_multiplier
, best_adder
, best_one_based
, best_fit
,
130 affine_decipher(sanitised_message
, best_multiplier
,
131 best_adder
, best_one_based
)[:50]))
132 return (best_multiplier
, best_adder
, best_one_based
), best_fit
134 def keyword_break(message
, wordlist
=keywords
, fitness
=Pletters
):
135 """Breaks a keyword substitution cipher using a dictionary and
138 >>> keyword_break(keyword_encipher('this is a test message for the ' \
139 'keyword decipherment', 'elephant', KeywordWrapAlphabet.from_last), \
140 wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
141 (('elephant', <KeywordWrapAlphabet.from_last: 2>), -52.834575011...)
144 best_wrap_alphabet
= True
145 best_fit
= float("-inf")
146 for wrap_alphabet
in KeywordWrapAlphabet
:
147 for keyword
in wordlist
:
148 plaintext
= keyword_decipher(message
, keyword
, wrap_alphabet
)
149 fit
= fitness(plaintext
)
150 logger
.debug('Keyword break attempt using key {0} (wrap={1}) '
151 'gives fit of {2} and decrypt starting: {3}'.format(
152 keyword
, wrap_alphabet
, fit
,
153 sanitise(plaintext
)[:50]))
156 best_keyword
= keyword
157 best_wrap_alphabet
= wrap_alphabet
158 logger
.info('Keyword break best fit with key {0} (wrap={1}) gives fit of '
159 '{2} and decrypt starting: {3}'.format(best_keyword
,
160 best_wrap_alphabet
, best_fit
, sanitise(
161 keyword_decipher(message
, best_keyword
,
162 best_wrap_alphabet
))[:50]))
163 return (best_keyword
, best_wrap_alphabet
), best_fit
165 def keyword_break_mp(message
, wordlist
=keywords
, fitness
=Pletters
,
166 number_of_solutions
=1, chunksize
=500):
167 """Breaks a keyword substitution cipher using a dictionary and
170 >>> keyword_break_mp(keyword_encipher('this is a test message for the ' \
171 'keyword decipherment', 'elephant', KeywordWrapAlphabet.from_last), \
172 wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
173 (('elephant', <KeywordWrapAlphabet.from_last: 2>), -52.834575011...)
174 >>> keyword_break_mp(keyword_encipher('this is a test message for the ' \
175 'keyword decipherment', 'elephant', KeywordWrapAlphabet.from_last), \
176 wordlist=['cat', 'elephant', 'kangaroo'], \
177 number_of_solutions=2) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
178 [(('elephant', <KeywordWrapAlphabet.from_last: 2>), -52.834575011...),
179 (('elephant', <KeywordWrapAlphabet.from_largest: 3>), -52.834575011...)]
182 helper_args
= [(message
, word
, wrap
, fitness
)
184 for wrap
in KeywordWrapAlphabet
]
185 # Gotcha: the helper function here needs to be defined at the top level
186 # (limitation of Pool.starmap)
187 breaks
= pool
.starmap(keyword_break_worker
, helper_args
, chunksize
)
188 if number_of_solutions
== 1:
189 return max(breaks
, key
=lambda k
: k
[1])
191 return sorted(breaks
, key
=lambda k
: k
[1], reverse
=True)[:number_of_solutions
]
193 def keyword_break_worker(message
, keyword
, wrap_alphabet
, fitness
):
194 plaintext
= keyword_decipher(message
, keyword
, wrap_alphabet
)
195 fit
= fitness(plaintext
)
196 logger
.debug('Keyword break attempt using key {0} (wrap={1}) gives fit of '
197 '{2} and decrypt starting: {3}'.format(keyword
,
198 wrap_alphabet
, fit
, sanitise(plaintext
)[:50]))
199 return (keyword
, wrap_alphabet
), fit
201 def monoalphabetic_break_hillclimbing(message
, max_iterations
=10000000,
202 alphabet
=None, fitness
=Pletters
):
203 ciphertext
= unaccent(message
).lower()
205 alphabet
= list(string
.ascii_lowercase
)
206 random
.shuffle(alphabet
)
207 alphabet
= ''.join(alphabet
)
208 return monoalphabetic_break_hillclimbing_worker(ciphertext
, alphabet
,
209 max_iterations
, fitness
)
211 def monoalphabetic_break_hillclimbing_mp(message
, workers
=10,
212 max_iterations
= 10000000, alphabet
=None, fitness
=Pletters
, chunksize
=1):
214 ciphertext
= unaccent(message
).lower()
215 for i
in range(workers
):
217 this_alphabet
= alphabet
219 this_alphabet
= list(string
.ascii_lowercase
)
220 random
.shuffle(this_alphabet
)
221 this_alphabet
= ''.join(this_alphabet
)
222 worker_args
.append((ciphertext
, this_alphabet
, max_iterations
, fitness
))
224 breaks
= pool
.starmap(monoalphabetic_break_hillclimbing_worker
,
225 worker_args
, chunksize
)
226 return max(breaks
, key
=lambda k
: k
[1])
228 def monoalphabetic_break_hillclimbing_worker(message
, alphabet
,
229 max_iterations
, fitness
):
230 def swap(letters
, i
, j
):
236 return (letters
[:i
] + letters
[j
] + letters
[i
+1:j
] + letters
[i
] +
238 best_alphabet
= alphabet
239 best_fitness
= float('-inf')
240 for i
in range(max_iterations
):
241 alphabet
= swap(alphabet
, random
.randrange(26), random
.randrange(26))
242 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, alphabet
)
243 plaintext
= message
.translate(cipher_translation
)
244 if fitness(plaintext
) > best_fitness
:
245 best_fitness
= fitness(plaintext
)
246 best_alphabet
= alphabet
247 print(i
, best_alphabet
, best_fitness
, plaintext
)
248 return best_alphabet
, best_fitness
251 def vigenere_keyword_break_mp(message
, wordlist
=keywords
, fitness
=Pletters
,
253 """Breaks a vigenere cipher using a dictionary and frequency analysis.
255 >>> vigenere_keyword_break_mp(vigenere_encipher(sanitise('this is a test ' \
256 'message for the vigenere decipherment'), 'cat'), \
257 wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
258 ('cat', -52.947271216...)
261 helper_args
= [(message
, word
, fitness
)
262 for word
in wordlist
]
263 # Gotcha: the helper function here needs to be defined at the top level
264 # (limitation of Pool.starmap)
265 breaks
= pool
.starmap(vigenere_keyword_break_worker
, helper_args
,
267 return max(breaks
, key
=lambda k
: k
[1])
268 vigenere_keyword_break
= vigenere_keyword_break_mp
270 def vigenere_keyword_break_worker(message
, keyword
, fitness
):
271 plaintext
= vigenere_decipher(message
, keyword
)
272 fit
= fitness(plaintext
)
273 logger
.debug('Vigenere keyword break attempt using key {0} gives fit of '
274 '{1} and decrypt starting: {2}'.format(keyword
,
275 fit
, sanitise(plaintext
)[:50]))
279 def vigenere_frequency_break(message
, max_key_length
=20, fitness
=Pletters
):
280 """Breaks a Vigenere cipher with frequency analysis
282 >>> vigenere_frequency_break(vigenere_encipher(sanitise("It is time to " \
283 "run. She is ready and so am I. I stole Daniel's pocketbook this " \
284 "afternoon when he left his jacket hanging on the easel in the " \
285 "attic. I jump every time I hear a footstep on the stairs, " \
286 "certain that the theft has been discovered and that I will " \
287 "be caught. The SS officer visits less often now that he is " \
288 "sure"), 'florence')) # doctest: +ELLIPSIS
289 ('florence', -307.5473096791...)
291 def worker(message
, key_length
, fitness
):
292 splits
= every_nth(sanitised_message
, key_length
)
293 key
= ''.join([chr(caesar_break(s
)[0] + ord('a')) for s
in splits
])
294 plaintext
= vigenere_decipher(message
, key
)
295 fit
= fitness(plaintext
)
297 sanitised_message
= sanitise(message
)
298 results
= starmap(worker
, [(sanitised_message
, i
, fitness
)
299 for i
in range(1, max_key_length
+1)])
300 return max(results
, key
=lambda k
: k
[1])
303 def beaufort_frequency_break(message
, max_key_length
=20, fitness
=Pletters
):
304 """Breaks a Beaufort cipher with frequency analysis
306 >>> beaufort_frequency_break(beaufort_encipher(sanitise("It is time to " \
307 "run. She is ready and so am I. I stole Daniel's pocketbook this " \
308 "afternoon when he left his jacket hanging on the easel in the " \
309 "attic. I jump every time I hear a footstep on the stairs, " \
310 "certain that the theft has been discovered and that I will " \
311 "be caught. The SS officer visits less often now " \
312 "that he is sure"), 'florence')) # doctest: +ELLIPSIS
313 ('florence', -307.5473096791...)
315 def worker(message
, key_length
, fitness
):
316 splits
= every_nth(sanitised_message
, key_length
)
317 key
= ''.join([chr(-caesar_break(s
)[0] % 26 + ord('a'))
319 plaintext
= beaufort_decipher(message
, key
)
320 fit
= fitness(plaintext
)
322 sanitised_message
= sanitise(message
)
323 results
= starmap(worker
, [(sanitised_message
, i
, fitness
)
324 for i
in range(1, max_key_length
+1)])
325 return max(results
, key
=lambda k
: k
[1])
328 def column_transposition_break_mp(message
, translist
=transpositions
,
329 fitness
=Pbigrams
, chunksize
=500):
330 """Breaks a column transposition cipher using a dictionary and
331 n-gram frequency analysis
333 >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
334 "It is a truth universally acknowledged, that a single man in \
335 possession of a good fortune, must be in want of a wife. However \
336 little known the feelings or views of such a man may be on his \
337 first entering a neighbourhood, this truth is so well fixed in \
338 the minds of the surrounding families, that he is considered the \
339 rightful property of some one or other of their daughters."), \
341 translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
342 (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
343 (6, 1, 0, 4, 5, 3, 2): ['keyword']}) # doctest: +ELLIPSIS
344 (((2, 0, 5, 3, 1, 4, 6), False, False), -709.4646722...)
345 >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
346 "It is a truth universally acknowledged, that a single man in \
347 possession of a good fortune, must be in want of a wife. However \
348 little known the feelings or views of such a man may be on his \
349 first entering a neighbourhood, this truth is so well fixed in \
350 the minds of the surrounding families, that he is considered the \
351 rightful property of some one or other of their daughters."), \
353 translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
354 (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
355 (6, 1, 0, 4, 5, 3, 2): ['keyword']}, \
356 fitness=Ptrigrams) # doctest: +ELLIPSIS
357 (((2, 0, 5, 3, 1, 4, 6), False, False), -997.0129085...)
360 helper_args
= [(message
, trans
, fillcolumnwise
, emptycolumnwise
,
362 for trans
in translist
.keys()
363 for fillcolumnwise
in [True, False]
364 for emptycolumnwise
in [True, False]]
365 # Gotcha: the helper function here needs to be defined at the top level
366 # (limitation of Pool.starmap)
367 breaks
= pool
.starmap(column_transposition_break_worker
,
368 helper_args
, chunksize
)
369 return max(breaks
, key
=lambda k
: k
[1])
370 column_transposition_break
= column_transposition_break_mp
372 def column_transposition_break_worker(message
, transposition
,
373 fillcolumnwise
, emptycolumnwise
, fitness
):
374 plaintext
= column_transposition_decipher(message
, transposition
,
375 fillcolumnwise
=fillcolumnwise
, emptycolumnwise
=emptycolumnwise
)
376 fit
= fitness(sanitise(plaintext
))
377 logger
.debug('Column transposition break attempt using key {0} '
378 'gives fit of {1} and decrypt starting: {2}'.format(
380 sanitise(plaintext
)[:50]))
381 return (transposition
, fillcolumnwise
, emptycolumnwise
), fit
384 def scytale_break_mp(message
, max_key_length
=20,
385 fitness
=Pbigrams
, chunksize
=500):
386 """Breaks a scytale cipher using a range of lengths and
387 n-gram frequency analysis
389 >>> scytale_break_mp(scytale_encipher(sanitise( \
390 "It is a truth universally acknowledged, that a single man in \
391 possession of a good fortune, must be in want of a wife. However \
392 little known the feelings or views of such a man may be on his \
393 first entering a neighbourhood, this truth is so well fixed in \
394 the minds of the surrounding families, that he is considered the \
395 rightful property of some one or other of their daughters."), \
396 5)) # doctest: +ELLIPSIS
398 >>> scytale_break_mp(scytale_encipher(sanitise( \
399 "It is a truth universally acknowledged, that a single man in \
400 possession of a good fortune, must be in want of a wife. However \
401 little known the feelings or views of such a man may be on his \
402 first entering a neighbourhood, this truth is so well fixed in \
403 the minds of the surrounding families, that he is considered the \
404 rightful property of some one or other of their daughters."), \
406 fitness=Ptrigrams) # doctest: +ELLIPSIS
410 helper_args
= [(message
, trans
, False, True, fitness
)
412 [[col
for col
in range(math
.ceil(len(message
)/rows
))]
413 for rows
in range(1,max_key_length
+1)]]
414 # Gotcha: the helper function here needs to be defined at the top level
415 # (limitation of Pool.starmap)
416 breaks
= pool
.starmap(column_transposition_break_worker
,
417 helper_args
, chunksize
)
418 best
= max(breaks
, key
=lambda k
: k
[1])
419 return math
.trunc(len(message
) / len(best
[0][0])), best
[1]
420 scytale_break
= scytale_break_mp
423 def railfence_break(message
, max_key_length
=20,
424 fitness
=Pletters
, chunksize
=500):
425 """Breaks a hill cipher using a matrix of given rank and letter frequencies
430 sanitised_message
= sanitise(message
)
431 results
= starmap(worker
, [(sanitised_message
, i
, fitness
)
432 for i
in range(2, max_key_length
+1)])
433 return max(results
, key
=lambda k
: k
[1])
436 def railfence_break(message
, max_key_length
=20,
437 fitness
=Pbigrams
, chunksize
=500):
438 """Breaks a railfence cipher using a range of lengths and
439 n-gram frequency analysis
441 >>> railfence_break(railfence_encipher(sanitise( \
442 "It is a truth universally acknowledged, that a single man in \
443 possession of a good fortune, must be in want of a wife. However \
444 little known the feelings or views of such a man may be on his \
445 first entering a neighbourhood, this truth is so well fixed in \
446 the minds of the surrounding families, that he is considered the \
447 rightful property of some one or other of their daughters."), \
448 7)) # doctest: +ELLIPSIS
449 (7, -709.46467226...)
450 >>> railfence_break(railfence_encipher(sanitise( \
451 "It is a truth universally acknowledged, that a single man in \
452 possession of a good fortune, must be in want of a wife. However \
453 little known the feelings or views of such a man may be on his \
454 first entering a neighbourhood, this truth is so well fixed in \
455 the minds of the surrounding families, that he is considered the \
456 rightful property of some one or other of their daughters."), \
458 fitness=Ptrigrams) # doctest: +ELLIPSIS
461 def worker(message
, height
, fitness
):
462 plaintext
= railfence_decipher(message
, height
)
463 fit
= fitness(plaintext
)
466 sanitised_message
= sanitise(message
)
467 results
= starmap(worker
, [(sanitised_message
, i
, fitness
)
468 for i
in range(2, max_key_length
+1)])
469 return max(results
, key
=lambda k
: k
[1])
471 def amsco_break(message
, translist
=transpositions
, patterns
= [(1, 2), (2, 1)],
472 fillstyles
= [AmscoFillStyle
.continuous
,
473 AmscoFillStyle
.same_each_row
,
474 AmscoFillStyle
.reverse_each_row
],
477 """Breaks an AMSCO transposition cipher using a dictionary and
478 n-gram frequency analysis
480 >>> amsco_break(amsco_transposition_encipher(sanitise( \
481 "It is a truth universally acknowledged, that a single man in \
482 possession of a good fortune, must be in want of a wife. However \
483 little known the feelings or views of such a man may be on his \
484 first entering a neighbourhood, this truth is so well fixed in \
485 the minds of the surrounding families, that he is considered the \
486 rightful property of some one or other of their daughters."), \
488 translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
489 (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
490 (6, 1, 0, 4, 5, 3, 2): ['keyword']}, \
491 patterns=[(1, 2)]) # doctest: +ELLIPSIS
492 (((2, 0, 5, 3, 1, 4, 6), (1, 2)), -709.4646722...)
493 >>> amsco_break(amsco_transposition_encipher(sanitise( \
494 "It is a truth universally acknowledged, that a single man in \
495 possession of a good fortune, must be in want of a wife. However \
496 little known the feelings or views of such a man may be on his \
497 first entering a neighbourhood, this truth is so well fixed in \
498 the minds of the surrounding families, that he is considered the \
499 rightful property of some one or other of their daughters."), \
500 'encipher', fillpattern=(2, 1)), \
501 translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
502 (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
503 (6, 1, 0, 4, 5, 3, 2): ['keyword']}, \
504 patterns=[(1, 2), (2, 1)], fitness=Ptrigrams) # doctest: +ELLIPSIS
505 (((2, 0, 5, 3, 1, 4, 6), (2, 1)), -997.0129085...)
508 helper_args
= [(message
, trans
, pattern
, fillstyle
, fitness
)
509 for trans
in translist
.keys()
510 for pattern
in patterns
511 for fillstyle
in fillstyles
]
512 # Gotcha: the helper function here needs to be defined at the top level
513 # (limitation of Pool.starmap)
514 breaks
= pool
.starmap(amsco_break_worker
, helper_args
, chunksize
)
515 return max(breaks
, key
=lambda k
: k
[1])
517 def amsco_break_worker(message
, transposition
,
518 pattern
, fillstyle
, fitness
):
519 plaintext
= amsco_transposition_decipher(message
, transposition
,
520 fillpattern
=pattern
, fillstyle
=fillstyle
)
521 fit
= fitness(sanitise(plaintext
))
522 logger
.debug('AMSCO transposition break attempt using key {0} and pattern'
523 '{1} ({2}) gives fit of {3} and decrypt starting: '
525 transposition
, pattern
, fillstyle
, fit
,
526 sanitise(plaintext
)[:50]))
527 return (transposition
, pattern
, fillstyle
), fit
530 def hill_break(message
, matrix_size
=2, fitness
=Pletters
,
531 number_of_solutions
=1, chunksize
=500):
533 all_matrices
= [np
.matrix(list(m
))
534 for m
in itertools
.product([list(r
)
535 for r
in itertools
.product(range(26), repeat
=matrix_size
)],
537 valid_matrices
= [m
for m
, d
in
538 zip(all_matrices
, (int(round(linalg
.det(m
))) for m
in all_matrices
))
543 helper_args
= [(message
, matrix
, fitness
)
544 for matrix
in valid_matrices
]
545 # Gotcha: the helper function here needs to be defined at the top level
546 # (limitation of Pool.starmap)
547 breaks
= pool
.starmap(hill_break_worker
, helper_args
, chunksize
)
548 if number_of_solutions
== 1:
549 return max(breaks
, key
=lambda k
: k
[1])
551 return sorted(breaks
, key
=lambda k
: k
[1], reverse
=True)[:number_of_solutions
]
553 def hill_break_worker(message
, matrix
, fitness
):
554 plaintext
= hill_decipher(matrix
, message
)
555 fit
= fitness(plaintext
)
556 logger
.debug('Hill cipher break attempt using key {0} gives fit of '
557 '{1} and decrypt starting: {2}'.format(matrix
,
558 fit
, sanitise(plaintext
)[:50]))
562 def pocket_enigma_break_by_crib(message
, wheel_spec
, crib
, crib_position
):
563 """Break a pocket enigma using a crib (some plaintext that's expected to
564 be in a certain position). Returns a list of possible starting wheel
565 positions that could produce the crib.
567 >>> pocket_enigma_break_by_crib('kzpjlzmoga', 1, 'h', 0)
569 >>> pocket_enigma_break_by_crib('kzpjlzmoga', 1, 'he', 0)
571 >>> pocket_enigma_break_by_crib('kzpjlzmoga', 1, 'll', 2)
573 >>> pocket_enigma_break_by_crib('kzpjlzmoga', 1, 'l', 2)
575 >>> pocket_enigma_break_by_crib('kzpjlzmoga', 1, 'l', 3)
577 >>> pocket_enigma_break_by_crib('aaaaa', 1, 'l', 3)
580 pe
= PocketEnigma(wheel
=wheel_spec
)
581 possible_positions
= []
582 for p
in string
.ascii_lowercase
:
584 plaintext
= pe
.decipher(message
)
585 if plaintext
[crib_position
:crib_position
+len(crib
)] == crib
:
586 possible_positions
+= [p
]
587 return possible_positions
590 def plot_frequency_histogram(freqs
, sort_key
=None):
591 x
= range(len(freqs
.keys()))
592 y
= [freqs
[l
] for l
in sorted(freqs
.keys(), key
=sort_key
)]
594 ax
= f
.add_axes([0.1, 0.1, 0.9, 0.9])
595 ax
.bar(x
, y
, align
='center')
597 ax
.set_xticklabels(sorted(freqs
.keys(), key
=sort_key
))
601 if __name__
== "__main__":