1 """Enciphering and deciphering using the [Column transposition cipher](https://en.wikipedia.org/wiki/Bifid_cipher).
2 Also attempts to break messages that use a column transpositon cipher.
4 A grid is layed out, with one column for each distinct letter in the keyword.
5 The grid is filled by the plaintext, one letter per cell, either in rows or
6 columns. The columns are rearranged so the keyword's letters are in alphabetical
7 order, then the ciphertext is read from the rearranged grid, either in rows
10 The Scytale cipher is a column cipher with an identity transposition, where the
11 message is written in rows and read in columns.
13 Messages that do not fill the grid are padded with fillvalue. Note that
14 `szyfrow.support.utilities.pad` allows a callable, so that the message can be
15 padded by random letters, for instance by calling
16 `szyfrow.support.language_models.random_english_letter`.
20 import multiprocessing
21 from itertools
import chain
22 from szyfrow
.support
.utilities
import *
23 from szyfrow
.support
.language_models
import *
25 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
27 emptycolumnwise
=False):
28 """Enciphers using the column transposition cipher.
29 Message is padded to allow all rows to be the same length.
31 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
33 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
35 >>> column_transposition_encipher('hellothere', 'abcdef')
37 >>> column_transposition_encipher('hellothere', 'abcde')
39 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
41 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
43 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
45 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
47 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
49 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
51 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
53 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
55 >>> column_transposition_encipher('hellothere', 'cleverly')
57 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
59 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
62 transpositions
= transpositions_of(keyword
)
63 message
+= pad(len(message
), len(transpositions
), fillvalue
)
65 rows
= every_nth(message
, len(message
) // len(transpositions
))
67 rows
= chunks(message
, len(transpositions
))
68 transposed
= [transpose(r
, transpositions
) for r
in rows
]
70 return combine_every_nth(transposed
)
72 return cat(chain(*transposed
))
74 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
76 emptycolumnwise
=False):
77 """Deciphers using the column transposition cipher.
78 Message is padded to allow all rows to be the same length.
80 Note that `fillcolumnwise` and `emptycolumnwise` refer to how the message
81 is enciphered. To decipher a message, the operations are performed as an
82 inverse-empty, then inverse-transposition, then inverse-fill.
84 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
86 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
88 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
90 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
92 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
94 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
96 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
98 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
101 transpositions
= transpositions_of(keyword
)
102 message
+= pad(len(message
), len(transpositions
), fillvalue
)
104 rows
= every_nth(message
, len(message
) // len(transpositions
))
106 rows
= chunks(message
, len(transpositions
))
107 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
109 return combine_every_nth(untransposed
)
111 return cat(chain(*untransposed
))
113 def scytale_encipher(message
, rows
, fillvalue
=' '):
114 """Enciphers using the scytale transposition cipher. `rows` is the
115 circumference of the rod. The message is fitted inot columns so that
118 Message is padded with spaces to allow all rows to be the same length.
120 For ease of implementation, the cipher is performed on the transpose
123 >>> scytale_encipher('thequickbrownfox', 3)
125 >>> scytale_encipher('thequickbrownfox', 4)
127 >>> scytale_encipher('thequickbrownfox', 5)
128 'tubn hirf ecoo qkwx '
129 >>> scytale_encipher('thequickbrownfox', 6)
131 >>> scytale_encipher('thequickbrownfox', 7)
132 'tqcrnx hukof eibwo '
134 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
135 # return column_transposition_encipher(message, transpositions,
136 # fillvalue=fillvalue, fillcolumnwise=False, emptycolumnwise=True)
137 transpositions
= (i
for i
in range(rows
))
138 return column_transposition_encipher(message
, transpositions
,
139 fillvalue
=fillvalue
, fillcolumnwise
=True, emptycolumnwise
=False)
141 def scytale_decipher(message
, rows
):
142 """Deciphers using the scytale transposition cipher.
143 Assumes the message is padded so that all rows are the same length.
145 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
147 >>> scytale_decipher('tubnhirfecooqkwx', 4)
149 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
151 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
153 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
156 # transpositions = [i for i in range(math.ceil(len(message) / rows))]
157 # return column_transposition_decipher(message, transpositions,
158 # fillcolumnwise=False, emptycolumnwise=True)
159 transpositions
= [i
for i
in range(rows
)]
160 return column_transposition_decipher(message
, transpositions
,
161 fillcolumnwise
=True, emptycolumnwise
=False)
164 def column_transposition_break(message
, translist
=None,
165 fitness
=Pbigrams
, chunksize
=500):
166 """Breaks a column transposition cipher using a dictionary and
167 n-gram frequency analysis
169 If `translist` is not specified, use
170 [`szyfrow.support.langauge_models.transpositions`](support/language_models.html#szyfrow.support.language_models.transpositions).
175 >>> column_transposition_break(column_transposition_encipher(sanitise( \
176 "It is a truth universally acknowledged, that a single man in \
177 possession of a good fortune, must be in want of a wife. However \
178 little known the feelings or views of such a man may be on his \
179 first entering a neighbourhood, this truth is so well fixed in \
180 the minds of the surrounding families, that he is considered the \
181 rightful property of some one or other of their daughters."), \
183 translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
184 (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
185 (6, 1, 0, 4, 5, 3, 2): ['keyword']}) # doctest: +ELLIPSIS
186 (((2, 0, 5, 3, 1, 4, 6), False, False), -709.4646722...)
187 >>> column_transposition_break(column_transposition_encipher(sanitise( \
188 "It is a truth universally acknowledged, that a single man in \
189 possession of a good fortune, must be in want of a wife. However \
190 little known the feelings or views of such a man may be on his \
191 first entering a neighbourhood, this truth is so well fixed in \
192 the minds of the surrounding families, that he is considered the \
193 rightful property of some one or other of their daughters."), \
195 translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
196 (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
197 (6, 1, 0, 4, 5, 3, 2): ['keyword']}, \
198 fitness=Ptrigrams) # doctest: +ELLIPSIS
199 (((2, 0, 5, 3, 1, 4, 6), False, False), -997.0129085...)
201 if translist
is None:
202 translist
= transpositions
204 with multiprocessing
.Pool() as pool
:
205 helper_args
= [(message
, trans
, fillcolumnwise
, emptycolumnwise
,
207 for trans
in translist
208 for fillcolumnwise
in [True, False]
209 for emptycolumnwise
in [True, False]]
210 # Gotcha: the helper function here needs to be defined at the top level
211 # (limitation of Pool.starmap)
212 breaks
= pool
.starmap(column_transposition_break_worker
,
213 helper_args
, chunksize
)
214 return max(breaks
, key
=lambda k
: k
[1])
216 def column_transposition_break_worker(message
, transposition
,
217 fillcolumnwise
, emptycolumnwise
, fitness
):
218 plaintext
= column_transposition_decipher(message
, transposition
,
219 fillcolumnwise
=fillcolumnwise
, emptycolumnwise
=emptycolumnwise
)
220 fit
= fitness(sanitise(plaintext
))
221 return (transposition
, fillcolumnwise
, emptycolumnwise
), fit
224 def scytale_break(message
, max_key_length
=20,
225 fitness
=Pbigrams
, chunksize
=500):
226 """Breaks a scytale cipher using a range of lengths and
227 n-gram frequency analysis
229 >>> scytale_break(scytale_encipher(sanitise( \
230 "It is a truth universally acknowledged, that a single man in \
231 possession of a good fortune, must be in want of a wife. However \
232 little known the feelings or views of such a man may be on his \
233 first entering a neighbourhood, this truth is so well fixed in \
234 the minds of the surrounding families, that he is considered the \
235 rightful property of some one or other of their daughters."), \
236 5)) # doctest: +ELLIPSIS
238 >>> scytale_break(scytale_encipher(sanitise( \
239 "It is a truth universally acknowledged, that a single man in \
240 possession of a good fortune, must be in want of a wife. However \
241 little known the feelings or views of such a man may be on his \
242 first entering a neighbourhood, this truth is so well fixed in \
243 the minds of the surrounding families, that he is considered the \
244 rightful property of some one or other of their daughters."), \
246 fitness=Ptrigrams) # doctest: +ELLIPSIS
249 with multiprocessing
.Pool() as pool
:
250 helper_args
= [(message
, trans
, False, True, fitness
)
252 [[col
for col
in range(math
.ceil(len(message
)/rows
))]
253 for rows
in range(1,max_key_length
+1)]]
254 # Gotcha: the helper function here needs to be defined at the top level
255 # (limitation of Pool.starmap)
256 breaks
= pool
.starmap(column_transposition_break_worker
,
257 helper_args
, chunksize
)
258 best
= max(breaks
, key
=lambda k
: k
[1])
259 return math
.trunc(len(message
) / len(best
[0][0])), best
[1]
261 if __name__
== "__main__":