1 from utilities
import *
2 from language_models
import *
3 from multiprocessing
import Pool
5 from logger
import logger
7 def transpositions_of(keyword
):
8 """Finds the transpostions given by a keyword. For instance, the keyword
9 'clever' rearranges to 'celrv', so the first column (0) stays first, the
10 second column (1) moves to third, the third column (2) moves to second,
13 If passed a tuple, assume it's already a transposition and just return it.
15 >>> transpositions_of('clever')
17 >>> transpositions_of('fred')
19 >>> transpositions_of((3, 2, 0, 1))
22 if isinstance(keyword
, tuple):
25 key
= deduplicate(keyword
)
26 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
29 def pad(message_len
, group_len
, fillvalue
):
30 padding_length
= group_len
- message_len
% group_len
31 if padding_length
== group_len
: padding_length
= 0
33 for i
in range(padding_length
):
34 if callable(fillvalue
):
35 padding
+= fillvalue()
40 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
42 emptycolumnwise
=False):
43 """Enciphers using the column transposition cipher.
44 Message is padded to allow all rows to be the same length.
46 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
48 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
50 >>> column_transposition_encipher('hellothere', 'abcdef')
52 >>> column_transposition_encipher('hellothere', 'abcde')
54 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
56 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
58 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
60 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
62 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
64 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
66 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
68 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
70 >>> column_transposition_encipher('hellothere', 'cleverly')
72 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
74 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
77 transpositions
= transpositions_of(keyword
)
78 message
+= pad(len(message
), len(transpositions
), fillvalue
)
80 rows
= every_nth(message
, len(message
) // len(transpositions
))
82 rows
= chunks(message
, len(transpositions
))
83 transposed
= [transpose(r
, transpositions
) for r
in rows
]
85 return combine_every_nth(transposed
)
87 return cat(chain(*transposed
))
89 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
91 emptycolumnwise
=False):
92 """Deciphers using the column transposition cipher.
93 Message is padded to allow all rows to be the same length.
95 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
97 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
99 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
101 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
103 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
105 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
107 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
109 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
112 transpositions
= transpositions_of(keyword
)
113 message
+= pad(len(message
), len(transpositions
), fillvalue
)
115 rows
= every_nth(message
, len(message
) // len(transpositions
))
117 rows
= chunks(message
, len(transpositions
))
118 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
120 return combine_every_nth(untransposed
)
122 return cat(chain(*untransposed
))
124 def scytale_encipher(message
, rows
, fillvalue
=' '):
125 """Enciphers using the scytale transposition cipher.
126 Message is padded with spaces to allow all rows to be the same length.
128 >>> scytale_encipher('thequickbrownfox', 3)
130 >>> scytale_encipher('thequickbrownfox', 4)
132 >>> scytale_encipher('thequickbrownfox', 5)
133 'tubn hirf ecoo qkwx '
134 >>> scytale_encipher('thequickbrownfox', 6)
136 >>> scytale_encipher('thequickbrownfox', 7)
137 'tqcrnx hukof eibwo '
139 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
140 # return column_transposition_encipher(message, transpositions,
141 # fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
142 transpositions
= [i
for i
in range(rows
)]
143 return column_transposition_encipher(message
, transpositions
,
144 fillvalue
=fillvalue
, fillcolumnwise
=True, emptycolumnwise
=False)
146 def scytale_decipher(message
, rows
):
147 """Deciphers using the scytale transposition cipher.
148 Assumes the message is padded so that all rows are the same length.
150 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
152 >>> scytale_decipher('tubnhirfecooqkwx', 4)
154 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
156 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
158 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
161 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
162 # return column_transposition_decipher(message, transpositions,
163 # fillcolumnwise=False, emptycolumnwise=True)
164 transpositions
= [i
for i
in range(rows
)]
165 return column_transposition_decipher(message
, transpositions
,
166 fillcolumnwise
=True, emptycolumnwise
=False)
169 def column_transposition_break_mp(message
, translist
=transpositions
,
170 fitness
=Pbigrams
, chunksize
=500):
171 """Breaks a column transposition cipher using a dictionary and
172 n-gram frequency analysis
174 >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
175 "It is a truth universally acknowledged, that a single man in \
176 possession of a good fortune, must be in want of a wife. However \
177 little known the feelings or views of such a man may be on his \
178 first entering a neighbourhood, this truth is so well fixed in \
179 the minds of the surrounding families, that he is considered the \
180 rightful property of some one or other of their daughters."), \
182 translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
183 (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
184 (6, 1, 0, 4, 5, 3, 2): ['keyword']}) # doctest: +ELLIPSIS
185 (((2, 0, 5, 3, 1, 4, 6), False, False), -709.4646722...)
186 >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
187 "It is a truth universally acknowledged, that a single man in \
188 possession of a good fortune, must be in want of a wife. However \
189 little known the feelings or views of such a man may be on his \
190 first entering a neighbourhood, this truth is so well fixed in \
191 the minds of the surrounding families, that he is considered the \
192 rightful property of some one or other of their daughters."), \
194 translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
195 (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
196 (6, 1, 0, 4, 5, 3, 2): ['keyword']}, \
197 fitness=Ptrigrams) # doctest: +ELLIPSIS
198 (((2, 0, 5, 3, 1, 4, 6), False, False), -997.0129085...)
201 helper_args
= [(message
, trans
, fillcolumnwise
, emptycolumnwise
,
203 for trans
in translist
204 for fillcolumnwise
in [True, False]
205 for emptycolumnwise
in [True, False]]
206 # Gotcha: the helper function here needs to be defined at the top level
207 # (limitation of Pool.starmap)
208 breaks
= pool
.starmap(column_transposition_break_worker
,
209 helper_args
, chunksize
)
210 return max(breaks
, key
=lambda k
: k
[1])
211 column_transposition_break
= column_transposition_break_mp
213 def column_transposition_break_worker(message
, transposition
,
214 fillcolumnwise
, emptycolumnwise
, fitness
):
215 plaintext
= column_transposition_decipher(message
, transposition
,
216 fillcolumnwise
=fillcolumnwise
, emptycolumnwise
=emptycolumnwise
)
217 fit
= fitness(sanitise(plaintext
))
218 logger
.debug('Column transposition break attempt using key {0} '
219 'gives fit of {1} and decrypt starting: {2}'.format(
221 sanitise(plaintext
)[:50]))
222 return (transposition
, fillcolumnwise
, emptycolumnwise
), fit
225 def scytale_break_mp(message
, max_key_length
=20,
226 fitness
=Pbigrams
, chunksize
=500):
227 """Breaks a scytale cipher using a range of lengths and
228 n-gram frequency analysis
230 >>> scytale_break_mp(scytale_encipher(sanitise( \
231 "It is a truth universally acknowledged, that a single man in \
232 possession of a good fortune, must be in want of a wife. However \
233 little known the feelings or views of such a man may be on his \
234 first entering a neighbourhood, this truth is so well fixed in \
235 the minds of the surrounding families, that he is considered the \
236 rightful property of some one or other of their daughters."), \
237 5)) # doctest: +ELLIPSIS
239 >>> scytale_break_mp(scytale_encipher(sanitise( \
240 "It is a truth universally acknowledged, that a single man in \
241 possession of a good fortune, must be in want of a wife. However \
242 little known the feelings or views of such a man may be on his \
243 first entering a neighbourhood, this truth is so well fixed in \
244 the minds of the surrounding families, that he is considered the \
245 rightful property of some one or other of their daughters."), \
247 fitness=Ptrigrams) # doctest: +ELLIPSIS
251 helper_args
= [(message
, trans
, False, True, fitness
)
253 [[col
for col
in range(math
.ceil(len(message
)/rows
))]
254 for rows
in range(1,max_key_length
+1)]]
255 # Gotcha: the helper function here needs to be defined at the top level
256 # (limitation of Pool.starmap)
257 breaks
= pool
.starmap(column_transposition_break_worker
,
258 helper_args
, chunksize
)
259 best
= max(breaks
, key
=lambda k
: k
[1])
260 return math
.trunc(len(message
) / len(best
[0][0])), best
[1]
261 scytale_break
= scytale_break_mp