2eb89f72306e30e4db99e539e36733aa792d13c4
5 from itertools
import zip_longest
, cycle
, chain
, count
7 from numpy
import matrix
8 from numpy
import linalg
9 from language_models
import *
14 from utilities
import *
20 from polybius
import *
21 from column_transposition
import *
22 from railfence
import *
25 def make_cadenus_keycolumn(doubled_letters
= 'vw', start
='a', reverse
=False):
26 """Makes the key column for a Cadenus cipher (the column down between the
29 >>> make_cadenus_keycolumn()['a']
31 >>> make_cadenus_keycolumn()['b']
33 >>> make_cadenus_keycolumn()['c']
35 >>> make_cadenus_keycolumn()['v']
37 >>> make_cadenus_keycolumn()['w']
39 >>> make_cadenus_keycolumn()['z']
41 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['a']
43 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['b']
45 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['c']
47 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['i']
49 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['j']
51 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['v']
53 >>> make_cadenus_keycolumn(doubled_letters='ij', start='b', reverse=True)['z']
56 index_to_remove
= string
.ascii_lowercase
.find(doubled_letters
[0])
57 short_alphabet
= string
.ascii_lowercase
[:index_to_remove
] + string
.ascii_lowercase
[index_to_remove
+1:]
59 short_alphabet
= cat(reversed(short_alphabet
))
60 start_pos
= short_alphabet
.find(start
)
61 rotated_alphabet
= short_alphabet
[start_pos
:] + short_alphabet
[:start_pos
]
62 keycolumn
= {l
: i
for i
, l
in enumerate(rotated_alphabet
)}
63 keycolumn
[doubled_letters
[0]] = keycolumn
[doubled_letters
[1]]
66 def cadenus_encipher(message
, keyword
, keycolumn
, fillvalue
='a'):
67 """Encipher with the Cadenus cipher
69 >>> cadenus_encipher(sanitise('Whoever has made a voyage up the Hudson ' \
70 'must remember the Kaatskill mountains. ' \
71 'They are a dismembered branch of the great'), \
73 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
74 'antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaasuvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned'
75 >>> cadenus_encipher(sanitise('a severe limitation on the usefulness of ' \
76 'the cadenus is that every message must be ' \
77 'a multiple of twenty-five letters long'), \
79 make_cadenus_keycolumn(doubled_letters='vw', start='a', reverse=True))
80 'systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtofarenuseieeieltarlmentieetogevesitfaisltngeeuvowul'
82 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
84 rotated_columns
= [col
[start
:] + col
[:start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
85 rotated_rows
= zip(*rotated_columns
)
86 transpositions
= transpositions_of(keyword
)
87 transposed
= [transpose(r
, transpositions
) for r
in rotated_rows
]
88 return cat(chain(*transposed
))
90 def cadenus_decipher(message
, keyword
, keycolumn
, fillvalue
='a'):
92 >>> cadenus_decipher('antodeleeeuhrsidrbhmhdrrhnimefmthgeaetakseomehetyaa' \
93 'suvoyegrastmmuuaeenabbtpchehtarorikswosmvaleatned', \
95 make_cadenus_keycolumn(reverse=True))
96 'whoeverhasmadeavoyageupthehudsonmustrememberthekaatskillmountainstheyareadismemberedbranchofthegreat'
97 >>> cadenus_decipher('systretomtattlusoatleeesfiyheasdfnmschbhneuvsnpmtof' \
98 'arenuseieeieltarlmentieetogevesitfaisltngeeuvowul', \
100 make_cadenus_keycolumn(reverse=True))
101 'aseverelimitationontheusefulnessofthecadenusisthateverymessagemustbeamultipleoftwentyfiveletterslong'
103 rows
= chunks(message
, len(message
) // 25, fillvalue
=fillvalue
)
104 transpositions
= transpositions_of(keyword
)
105 untransposed_rows
= [untranspose(r
, transpositions
) for r
in rows
]
106 columns
= zip(*untransposed_rows
)
107 rotated_columns
= [col
[-start
:] + col
[:-start
] for start
, col
in zip([keycolumn
[l
] for l
in keyword
], columns
)]
108 rotated_rows
= zip(*rotated_columns
)
109 # return rotated_columns
110 return cat(chain(*rotated_rows
))
113 def hill_encipher(matrix
, message_letters
, fillvalue
='a'):
116 >>> hill_encipher(np.matrix([[7,8], [11,11]]), 'hellothere')
118 >>> hill_encipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
123 sanitised_message
= sanitise(message_letters
)
124 if len(sanitised_message
) % n
!= 0:
125 padding
= fillvalue
[0] * (n
- len(sanitised_message
) % n
)
128 message
= [pos(c
) for c
in sanitised_message
+ padding
]
129 message_chunks
= [message
[i
:i
+n
] for i
in range(0, len(message
), n
)]
130 # message_chunks = chunks(message, len(matrix), fillvalue=None)
131 enciphered_chunks
= [((matrix
* np
.matrix(c
).T
).T
).tolist()[0]
132 for c
in message_chunks
]
133 return cat([unpos(round(l
))
134 for l
in sum(enciphered_chunks
, [])])
136 def hill_decipher(matrix
, message
, fillvalue
='a'):
139 >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
141 >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
145 adjoint
= linalg
.det(matrix
)*linalg
.inv(matrix
)
146 inverse_determinant
= modular_division_table
[int(round(linalg
.det(matrix
))) % 26][1]
147 inverse_matrix
= (inverse_determinant
* adjoint
) % 26
148 return hill_encipher(inverse_matrix
, message
, fillvalue
)
151 # Where each piece of text ends up in the AMSCO transpositon cipher.
152 # 'index' shows where the slice appears in the plaintext, with the slice
153 # from 'start' to 'end'
154 AmscoSlice
= collections
.namedtuple('AmscoSlice', ['index', 'start', 'end'])
156 class AmscoFillStyle(Enum
):
161 def amsco_transposition_positions(message
, keyword
,
163 fillstyle
=AmscoFillStyle
.continuous
,
164 fillcolumnwise
=False,
165 emptycolumnwise
=True):
166 """Creates the grid for the AMSCO transposition cipher. Each element in the
167 grid shows the index of that slice and the start and end positions of the
168 plaintext that go to make it up.
170 >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
171 fillpattern=(1, 2)) # doctest: +NORMALIZE_WHITESPACE
172 [[AmscoSlice(index=3, start=4, end=6),
173 AmscoSlice(index=2, start=3, end=4),
174 AmscoSlice(index=0, start=0, end=1),
175 AmscoSlice(index=1, start=1, end=3),
176 AmscoSlice(index=4, start=6, end=7)],
177 [AmscoSlice(index=8, start=12, end=13),
178 AmscoSlice(index=7, start=10, end=12),
179 AmscoSlice(index=5, start=7, end=9),
180 AmscoSlice(index=6, start=9, end=10),
181 AmscoSlice(index=9, start=13, end=15)],
182 [AmscoSlice(index=13, start=19, end=21),
183 AmscoSlice(index=12, start=18, end=19),
184 AmscoSlice(index=10, start=15, end=16),
185 AmscoSlice(index=11, start=16, end=18),
186 AmscoSlice(index=14, start=21, end=22)],
187 [AmscoSlice(index=18, start=27, end=28),
188 AmscoSlice(index=17, start=25, end=27),
189 AmscoSlice(index=15, start=22, end=24),
190 AmscoSlice(index=16, start=24, end=25),
191 AmscoSlice(index=19, start=28, end=30)]]
193 transpositions
= transpositions_of(keyword
)
194 fill_iterator
= cycle(fillpattern
)
196 message_length
= len(message
)
200 current_fillpattern
= fillpattern
201 while current_position
< message_length
:
203 if fillstyle
== AmscoFillStyle
.same_each_row
:
204 fill_iterator
= cycle(fillpattern
)
205 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
206 fill_iterator
= cycle(current_fillpattern
)
207 for _
in range(len(transpositions
)):
208 index
= next(indices
)
209 gap
= next(fill_iterator
)
210 row
+= [AmscoSlice(index
, current_position
, current_position
+ gap
)]
211 current_position
+= gap
213 if fillstyle
== AmscoFillStyle
.reverse_each_row
:
214 current_fillpattern
= list(reversed(current_fillpattern
))
215 return [transpose(r
, transpositions
) for r
in grid
]
217 def amsco_transposition_encipher(message
, keyword
,
218 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
219 """AMSCO transposition encipher.
221 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
223 >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
225 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
227 >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
229 >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
230 'etecstthhomoerereenisxip'
231 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
232 'hetcsoeisterereipexthomn'
233 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
234 'hecsoisttererteipexhomen'
235 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
236 'heecisoosttrrtepeixhemen'
237 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
238 'hxtomephescieretoeisnter'
239 >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
240 'hxomeiphscerettoisenteer'
242 grid
= amsco_transposition_positions(message
, keyword
,
243 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
244 ct_as_grid
= [[message
[s
.start
:s
.end
] for s
in r
] for r
in grid
]
245 return combine_every_nth(ct_as_grid
)
248 def amsco_transposition_decipher(message
, keyword
,
249 fillpattern
=(1,2), fillstyle
=AmscoFillStyle
.reverse_each_row
):
250 """AMSCO transposition decipher
252 >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
254 >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
256 >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
258 >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
260 >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
261 'hereissometexttoencipher'
262 >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
263 'hereissometexttoencipher'
264 >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
265 'hereissometexttoencipher'
266 >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
267 'hereissometexttoencipher'
268 >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
269 'hereissometexttoencipher'
270 >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
271 'hereissometexttoencipher'
274 grid
= amsco_transposition_positions(message
, keyword
,
275 fillpattern
=fillpattern
, fillstyle
=fillstyle
)
276 transposed_sections
= [s
for c
in [l
for l
in zip(*grid
)] for s
in c
]
277 plaintext_list
= [''] * len(transposed_sections
)
279 for slice in transposed_sections
:
280 plaintext_list
[slice.index
] = message
[current_pos
:current_pos
-slice.start
+slice.end
][:len(message
[slice.start
:slice.end
])]
281 current_pos
+= len(message
[slice.start
:slice.end
])
282 return cat(plaintext_list
)
285 def bifid_grid(keyword
, wrap_alphabet
, letter_mapping
):
286 """Create the grids for a Bifid cipher
288 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
289 if letter_mapping
is None:
290 letter_mapping
= {'j': 'i'}
291 translation
= ''.maketrans(letter_mapping
)
292 cipher_alphabet
= cat(collections
.OrderedDict
.fromkeys(cipher_alphabet
.translate(translation
)))
293 f_grid
= {k
: ((i
// 5) + 1, (i
% 5) + 1)
294 for i
, k
in enumerate(cipher_alphabet
)}
295 r_grid
= {((i
// 5) + 1, (i
% 5) + 1): k
296 for i
, k
in enumerate(cipher_alphabet
)}
297 return translation
, f_grid
, r_grid
299 def bifid_encipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
300 letter_mapping
=None, period
=None, fillvalue
=None):
303 >>> bifid_encipher("indiajelly", 'iguana')
305 >>> bifid_encipher("indiacurry", 'iguana', period=4)
307 >>> bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x')
310 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
312 t_message
= message
.translate(translation
)
313 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
315 chunked_pairs
= [pairs0
[i
:i
+period
] for i
in range(0, len(pairs0
), period
)]
316 if len(chunked_pairs
[-1]) < period
and fillvalue
:
317 chunked_pairs
[-1] += [f_grid
[fillvalue
]] * (period
- len(chunked_pairs
[-1]))
319 chunked_pairs
= [pairs0
]
322 for c
in chunked_pairs
:
323 items
= sum(list(list(i
) for i
in zip(*c
)), [])
324 p
= [(items
[i
], items
[i
+1]) for i
in range(0, len(items
), 2)]
327 return cat(r_grid
[p
] for p
in pairs1
)
330 def bifid_decipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
331 letter_mapping
=None, period
=None, fillvalue
=None):
332 """Decipher with bifid cipher
334 >>> bifid_decipher('ibidonhprm', 'iguana')
336 >>> bifid_decipher("ibnhgaqltm", 'iguana', period=4)
338 >>> bifid_decipher("ibnhgaqltzml", 'iguana', period=4)
341 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
343 t_message
= message
.translate(translation
)
344 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
346 chunked_pairs
= [pairs0
[i
:i
+period
] for i
in range(0, len(pairs0
), period
)]
347 if len(chunked_pairs
[-1]) < period
and fillvalue
:
348 chunked_pairs
[-1] += [f_grid
[fillvalue
]] * (period
- len(chunked_pairs
[-1]))
350 chunked_pairs
= [pairs0
]
353 for c
in chunked_pairs
:
354 items
= [j
for i
in c
for j
in i
]
356 p
= [(items
[i
], items
[i
+gap
]) for i
in range(gap
)]
359 return cat(r_grid
[p
] for p
in pairs1
)
362 def autokey_encipher(message
, keyword
):
363 """Encipher with the autokey cipher
365 >>> autokey_encipher('meetatthefountain', 'kilt')
368 shifts
= [pos(l
) for l
in keyword
+ message
]
369 pairs
= zip(message
, shifts
)
370 return cat([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
372 def autokey_decipher(ciphertext
, keyword
):
373 """Decipher with the autokey cipher
375 >>> autokey_decipher('wmpmmxxaeyhbryoca', 'kilt')
381 plaintext_letter
= caesar_decipher_letter(c
, pos(keys
[0]))
382 plaintext
+= [plaintext_letter
]
383 keys
= keys
[1:] + [plaintext_letter
]
384 return cat(plaintext
)
387 class PocketEnigma(object):
388 """A pocket enigma machine
389 The wheel is internally represented as a 26-element list self.wheel_map,
390 where wheel_map[i] == j shows that the position i places on from the arrow
391 maps to the position j places on.
393 def __init__(self
, wheel
=1, position
='a'):
394 """initialise the pocket enigma, including which wheel to use and the
395 starting position of the wheel.
397 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
400 The position is the letter pointed to by the arrow on the wheel.
403 [25, 4, 23, 10, 1, 7, 9, 5, 12, 6, 3, 17, 8, 14, 13, 21, 19, 11, 20, 16, 18, 15, 24, 2, 22, 0]
407 self
.wheel1
= [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
408 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
409 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
410 self
.wheel2
= [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
411 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
412 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
414 self
.make_wheel_map(self
.wheel1
)
416 self
.make_wheel_map(self
.wheel2
)
418 self
.validate_wheel_spec(wheel
)
419 self
.make_wheel_map(wheel
)
420 if position
in string
.ascii_lowercase
:
421 self
.position
= pos(position
)
423 self
.position
= position
425 def make_wheel_map(self
, wheel_spec
):
426 """Expands a wheel specification from a list of letter-letter pairs
427 into a full wheel_map.
429 >>> pe.make_wheel_map(pe.wheel2)
430 [2, 3, 0, 1, 22, 8, 15, 12, 5, 10, 9, 13, 7, 11, 16, 6, 14, 25, 20, 21, 18, 19, 4, 24, 23, 17]
432 self
.validate_wheel_spec(wheel_spec
)
433 self
.wheel_map
= [0] * 26
435 self
.wheel_map
[pos(p
[0])] = pos(p
[1])
436 self
.wheel_map
[pos(p
[1])] = pos(p
[0])
437 return self
.wheel_map
439 def validate_wheel_spec(self
, wheel_spec
):
440 """Validates that a wheel specificaiton will turn into a valid wheel
443 >>> pe.validate_wheel_spec([])
444 Traceback (most recent call last):
446 ValueError: Wheel specification has 0 pairs, requires 13
447 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
448 Traceback (most recent call last):
450 ValueError: Not all mappings in wheel specificationhave two elements
451 >>> pe.validate_wheel_spec([('a', 'b')]*13)
452 Traceback (most recent call last):
454 ValueError: Wheel specification does not contain 26 letters
456 if len(wheel_spec
) != 13:
457 raise ValueError("Wheel specification has {} pairs, requires 13".
458 format(len(wheel_spec
)))
461 raise ValueError("Not all mappings in wheel specification"
463 if len(set([p
[0] for p
in wheel_spec
] +
464 [p
[1] for p
in wheel_spec
])) != 26:
465 raise ValueError("Wheel specification does not contain 26 letters")
467 def encipher_letter(self
, letter
):
468 """Enciphers a single letter, by advancing the wheel before looking up
469 the letter on the wheel.
471 >>> pe.set_position('f')
473 >>> pe.encipher_letter('k')
477 return self
.lookup(letter
)
478 decipher_letter
= encipher_letter
480 def lookup(self
, letter
):
481 """Look up what a letter enciphers to, without turning the wheel.
483 >>> pe.set_position('f')
485 >>> cat([pe.lookup(l) for l in string.ascii_lowercase])
486 'udhbfejcpgmokrliwntsayqzvx'
490 if letter
in string
.ascii_lowercase
:
492 (self
.wheel_map
[(pos(letter
) - self
.position
) % 26] +
498 """Advances the wheel one position.
500 >>> pe.set_position('f')
505 self
.position
= (self
.position
+ 1) % 26
508 def encipher(self
, message
, starting_position
=None):
509 """Enciphers a whole message.
511 >>> pe.set_position('f')
513 >>> pe.encipher('helloworld')
515 >>> pe.set_position('f')
517 >>> pe.encipher('kjsglcjoqc')
519 >>> pe.encipher('helloworld', starting_position = 'x')
522 if starting_position
:
523 self
.set_position(starting_position
)
526 transformed
+= self
.encipher_letter(l
)
530 def set_position(self
, position
):
531 """Sets the position of the wheel, by specifying the letter the arrow
534 >>> pe.set_position('a')
536 >>> pe.set_position('m')
538 >>> pe.set_position('z')
541 self
.position
= pos(position
)
545 if __name__
== "__main__":
547 doctest
.testmod(extraglobs
={'pe': PocketEnigma(1, 'a')})