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