Updated for challenge 9
[cipher-tools.git] / cipher / bifid.py
1 import multiprocessing
2 from support.utilities import *
3 from support.language_models import *
4 from cipher.keyword_cipher import KeywordWrapAlphabet, keyword_cipher_alphabet_of
5
6 from logger import logger
7
8 def bifid_grid(keyword, wrap_alphabet, letter_mapping):
9 """Create the grids for a Bifid cipher
10 """
11 cipher_alphabet = keyword_cipher_alphabet_of(keyword, wrap_alphabet)
12 if letter_mapping is None:
13 letter_mapping = {'j': 'i'}
14 translation = ''.maketrans(letter_mapping)
15 cipher_alphabet = cat(collections.OrderedDict.fromkeys(cipher_alphabet.translate(translation)))
16 f_grid = {k: ((i // 5) + 1, (i % 5) + 1)
17 for i, k in enumerate(cipher_alphabet)}
18 r_grid = {((i // 5) + 1, (i % 5) + 1): k
19 for i, k in enumerate(cipher_alphabet)}
20 return translation, f_grid, r_grid
21
22 def bifid_encipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a,
23 letter_mapping=None, period=None, fillvalue=None):
24 """Bifid cipher
25
26 >>> bifid_encipher("indiajelly", 'iguana')
27 'ibidonhprm'
28 >>> bifid_encipher("indiacurry", 'iguana', period=4)
29 'ibnhgaqltm'
30 >>> bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x')
31 'ibnhgaqltzml'
32 """
33 translation, f_grid, r_grid = bifid_grid(keyword, wrap_alphabet, letter_mapping)
34
35 t_message = message.translate(translation)
36 pairs0 = [f_grid[l] for l in sanitise(t_message)]
37 if period:
38 chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
39 if len(chunked_pairs[-1]) < period and fillvalue:
40 chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
41 else:
42 chunked_pairs = [pairs0]
43
44 pairs1 = []
45 for c in chunked_pairs:
46 items = sum(list(list(i) for i in zip(*c)), [])
47 p = [(items[i], items[i+1]) for i in range(0, len(items), 2)]
48 pairs1 += p
49
50 return cat(r_grid[p] for p in pairs1)
51
52
53 def bifid_decipher(message, keyword, wrap_alphabet=KeywordWrapAlphabet.from_a,
54 letter_mapping=None, period=None, fillvalue=None):
55 """Decipher with bifid cipher
56
57 >>> bifid_decipher('ibidonhprm', 'iguana')
58 'indiaielly'
59 >>> bifid_decipher("ibnhgaqltm", 'iguana', period=4)
60 'indiacurry'
61 >>> bifid_decipher("ibnhgaqltzml", 'iguana', period=4)
62 'indiacurryxx'
63 """
64 translation, f_grid, r_grid = bifid_grid(keyword, wrap_alphabet, letter_mapping)
65
66 t_message = message.translate(translation)
67 pairs0 = [f_grid[l] for l in sanitise(t_message)]
68 if period:
69 chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
70 if len(chunked_pairs[-1]) < period and fillvalue:
71 chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
72 else:
73 chunked_pairs = [pairs0]
74
75 pairs1 = []
76 for c in chunked_pairs:
77 items = [j for i in c for j in i]
78 gap = len(c)
79 p = [(items[i], items[i+gap]) for i in range(gap)]
80 pairs1 += p
81
82 return cat(r_grid[p] for p in pairs1)
83
84
85 def bifid_break_mp(message, wordlist=keywords, fitness=Pletters, max_period=10,
86 number_of_solutions=1, chunksize=500):
87 """Breaks a keyword substitution cipher using a dictionary and
88 frequency analysis
89
90 >>> bifid_break_mp(bifid_encipher('this is a test message for the ' \
91 'keyword decipherment', 'elephant', wrap_alphabet=KeywordWrapAlphabet.from_last), \
92 wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
93 (('elephant', <KeywordWrapAlphabet.from_last: 2>, 0), -52.834575011...)
94 >>> bifid_break_mp(bifid_encipher('this is a test message for the ' \
95 'keyword decipherment', 'elephant', wrap_alphabet=KeywordWrapAlphabet.from_last), \
96 wordlist=['cat', 'elephant', 'kangaroo'], \
97 number_of_solutions=2) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
98 [(('elephant', <KeywordWrapAlphabet.from_last: 2>, 0), -52.834575011...),
99 (('elephant', <KeywordWrapAlphabet.from_largest: 3>, 0), -52.834575011...)]
100 """
101 with multiprocessing.Pool() as pool:
102 helper_args = [(message, word, wrap, period, fitness)
103 for word in wordlist
104 for wrap in KeywordWrapAlphabet
105 for period in range(max_period+1)]
106 # Gotcha: the helper function here needs to be defined at the top level
107 # (limitation of Pool.starmap)
108 breaks = pool.starmap(bifid_break_worker, helper_args, chunksize)
109 if number_of_solutions == 1:
110 return max(breaks, key=lambda k: k[1])
111 else:
112 return sorted(breaks, key=lambda k: k[1], reverse=True)[:number_of_solutions]
113
114 def bifid_break_worker(message, keyword, wrap_alphabet, period, fitness):
115 plaintext = bifid_decipher(message, keyword, wrap_alphabet, period=period)
116 fit = fitness(plaintext)
117 logger.debug('Keyword break attempt using key {0} (wrap={1}) gives fit of '
118 '{2} and decrypt starting: {3}'.format(keyword,
119 wrap_alphabet, fit, sanitise(plaintext)[:50]))
120 return (keyword, wrap_alphabet, period), fit
121
122 if __name__ == "__main__":
123 import doctest