-def caesar_break(message, metric=norms.euclidean_distance, target_frequencies=normalised_english_counts, message_frequency_scaling=norms.normalise):
- """Breaks a Caesar cipher using frequency analysis
-
- >>> caesar_break('ibxcsyorsaqcheyklxivoexlevmrimwxsfiqevvmihrsasrxliwyrhecjsppsamrkwleppfmergefifvmhixscsymjcsyqeoixlm') # doctest: +ELLIPSIS
- (4, 0.31863952890183...)
- >>> caesar_break('wxwmaxdgheetgwuxztgptedbgznitgwwhpguxyhkxbmhvvtlbhgteeraxlmhiixweblmxgxwmhmaxybkbgztgwztsxwbgmxgmert') # doctest: +ELLIPSIS
- (19, 0.42152901235832...)
- >>> caesar_break('yltbbqnqnzvguvaxurorgenafsbezqvagbnornfgsbevpnaabjurersvaquvzyvxrnznazlybequrvfohgriraabjtbaruraprur') # doctest: +ELLIPSIS
- (13, 0.316029208075451...)
- """
- sanitised_message = sanitise(message)
- best_shift = 0
- best_fit = float("inf")
- for shift in range(26):
- plaintext = caesar_decipher(sanitised_message, shift)
- frequencies = message_frequency_scaling(letter_frequencies(plaintext))
- fit = metric(target_frequencies, frequencies)
- logger.debug('Caesar break attempt using key {0} gives fit of {1} and decrypt starting: {2}'.format(shift, fit, plaintext[:50]))
- if fit < best_fit:
- best_fit = fit
- best_shift = shift
- logger.info('Caesar break best fit: key {0} gives fit of {1} and decrypt starting: {2}'.format(best_shift, best_fit, caesar_decipher(sanitised_message, best_shift)[:50]))
- return best_shift, best_fit
-
-def affine_break(message, metric=norms.euclidean_distance, target_frequencies=normalised_english_counts, message_frequency_scaling=norms.normalise):
- """Breaks an affine cipher using frequency analysis
-
- >>> affine_break('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh jm bfmibj umis hfsul axubafkjamx. ls kffkxwsd jls ofgbjmwfkiu olfmxmtmwaokttg jlsx ls kffkxwsd jlsi zg tsxwjl. jlsx ls umfjsd jlsi zg hfsqysxog. ls dmmdtsd mx jls bats mh bkbsf. ls bfmctsd kfmyxd jls lyj, mztanamyu xmc jm clm cku tmmeaxw kj lai kxd clm ckuxj.') # doctest: +ELLIPSIS
- ((15, 22, True), 0.23570361818655...)
- """
- sanitised_message = sanitise(message)
- best_multiplier = 0
- best_adder = 0
- best_one_based = True
- best_fit = float("inf")
- for one_based in [True, False]:
- for multiplier in range(1, 26, 2):
- for adder in range(26):
- plaintext = affine_decipher(sanitised_message, multiplier, adder, one_based)
- frequencies = message_frequency_scaling(letter_frequencies(plaintext))
- fit = metric(target_frequencies, frequencies)
- logger.debug('Affine break attempt using key {0}x+{1} ({2}) gives fit of {3} and decrypt starting: {4}'.format(multiplier, adder, one_based, fit, plaintext[:50]))
- if fit < best_fit:
- best_fit = fit
- best_multiplier = multiplier
- best_adder = adder
- best_one_based = one_based
- logger.info('Affine break best fit with key {0}x+{1} ({2}) gives fit of {3} and decrypt starting: {4}'.format(best_multiplier, best_adder, best_one_based, best_fit, affine_decipher(sanitised_message, best_multiplier, best_adder, best_one_based)[:50]))
- return (best_multiplier, best_adder, best_one_based), best_fit
-
-
-def keyword_break(message, wordlist=keywords, metric=norms.euclidean_distance, target_frequencies=normalised_english_counts, message_frequency_scaling=norms.normalise):
- """Breaks a keyword substitution cipher using a dictionary and frequency analysis
-
- >>> keyword_break(keyword_encipher('this is a test message for the keyword decipherment', 'elephant', 1), wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
- (('elephant', 1), 0.41643991598441...)
- """
- best_keyword = ''
- best_wrap_alphabet = True
- best_fit = float("inf")
- for wrap_alphabet in range(3):
- for keyword in wordlist:
- plaintext = keyword_decipher(message, keyword, wrap_alphabet)
- frequencies = message_frequency_scaling(letter_frequencies(plaintext))
- fit = metric(target_frequencies, frequencies)
- logger.debug('Keyword break attempt using key {0} (wrap={1}) gives fit of {2} and decrypt starting: {3}'.format(keyword, wrap_alphabet, fit, sanitise(plaintext)[:50]))
- if fit < best_fit:
- best_fit = fit
- best_keyword = keyword
- best_wrap_alphabet = wrap_alphabet
- logger.info('Keyword break best fit with key {0} (wrap={1}) gives fit of {2} and decrypt starting: {3}'.format(best_keyword, best_wrap_alphabet, best_fit, sanitise(keyword_decipher(message, best_keyword))[:50]))
- return (best_keyword, best_wrap_alphabet), best_fit
+ >>> hill_decipher(np.matrix([[7,8], [11,11]]), 'drjiqzdrvx')
+ 'hellothere'
+ >>> hill_decipher(np.matrix([[6, 24, 1], [13, 16, 10], [20, 17, 15]]), \
+ 'tfjflpznvyac')
+ 'hellothereaa'
+ """
+ adjoint = linalg.det(matrix)*linalg.inv(matrix)
+ inverse_determinant = modular_division_table[int(round(linalg.det(matrix))) % 26][1]
+ inverse_matrix = (inverse_determinant * adjoint) % 26
+ return hill_encipher(inverse_matrix, message, fillvalue)
+
+
+# Where each piece of text ends up in the AMSCO transpositon cipher.
+# 'index' shows where the slice appears in the plaintext, with the slice
+# from 'start' to 'end'
+AmscoSlice = collections.namedtuple('AmscoSlice', ['index', 'start', 'end'])
+
+def amsco_transposition_positions(message, keyword,
+ fillpattern=(1, 2),
+ fillcolumnwise=False,
+ emptycolumnwise=True):
+ """Creates the grid for the AMSCO transposition cipher. Each element in the
+ grid shows the index of that slice and the start and end positions of the
+ plaintext that go to make it up.
+
+ >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
+ fillpattern=(1, 2)) # doctest: +NORMALIZE_WHITESPACE
+ [[AmscoSlice(index=3, start=4, end=6),
+ AmscoSlice(index=2, start=3, end=4),
+ AmscoSlice(index=0, start=0, end=1),
+ AmscoSlice(index=1, start=1, end=3),
+ AmscoSlice(index=4, start=6, end=7)],
+ [AmscoSlice(index=8, start=12, end=13),
+ AmscoSlice(index=7, start=10, end=12),
+ AmscoSlice(index=5, start=7, end=9),
+ AmscoSlice(index=6, start=9, end=10),
+ AmscoSlice(index=9, start=13, end=15)],
+ [AmscoSlice(index=13, start=19, end=21),
+ AmscoSlice(index=12, start=18, end=19),
+ AmscoSlice(index=10, start=15, end=16),
+ AmscoSlice(index=11, start=16, end=18),
+ AmscoSlice(index=14, start=21, end=22)],
+ [AmscoSlice(index=18, start=27, end=28),
+ AmscoSlice(index=17, start=25, end=27),
+ AmscoSlice(index=15, start=22, end=24),
+ AmscoSlice(index=16, start=24, end=25),
+ AmscoSlice(index=19, start=28, end=30)]]
+ """
+ transpositions = transpositions_of(keyword)
+ fill_iterator = cycle(fillpattern)
+ indices = count()
+ message_length = len(message)
+
+ current_position = 0
+ grid = []
+ while current_position < message_length:
+ row = []
+ for _ in range(len(transpositions)):
+ index = next(indices)
+ gap = next(fill_iterator)
+ row += [AmscoSlice(index, current_position, current_position + gap)]
+ current_position += gap
+ grid += [row]
+ return [transpose(r, transpositions) for r in grid]
+
+def amsco_transposition_encipher(message, keyword, fillpattern=(1,2)):
+ """AMSCO transposition encipher.
+
+ >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
+ 'hoteelhler'
+ >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
+ 'hetelhelor'
+ >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
+ 'hotelerelh'
+ >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
+ 'hetelorlhe'
+ >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
+ 'hecsoisttererteipexhomen'
+ >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
+ 'heetcisooestrrepeixthemn'
+ >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
+ 'hxomeiphscerettoisenteer'
+ """
+ grid = amsco_transposition_positions(message, keyword, fillpattern=fillpattern)
+ ct_as_grid = [[message[s.start:s.end] for s in r] for r in grid]
+ return combine_every_nth(ct_as_grid)
+
+
+def amsco_transposition_decipher(message, keyword, fillpattern=(1,2)):
+ """AMSCO transposition decipher
+
+ >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
+ 'hellothere'
+ >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
+ 'hellothere'
+ >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
+ 'hellothere'
+ >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
+ 'hellothere'
+ >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2))
+ 'hereissometexttoencipher'
+ >>> amsco_transposition_decipher('heetcisooestrrepeixthemn', 'cipher', fillpattern=(2, 1))
+ 'hereissometexttoencipher'
+ >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2))
+ 'hereissometexttoencipher'
+ """
+
+ grid = amsco_transposition_positions(message, keyword, fillpattern=fillpattern)
+ transposed_sections = [s for c in [l for l in zip(*grid)] for s in c]
+ plaintext_list = [''] * len(transposed_sections)
+ current_pos = 0
+ for slice in transposed_sections:
+ plaintext_list[slice.index] = message[current_pos:current_pos-slice.start+slice.end][:len(message[slice.start:slice.end])]
+ current_pos += len(message[slice.start:slice.end])
+ return ''.join(plaintext_list)
+
+
+class PocketEnigma(object):
+ """A pocket enigma machine
+ The wheel is internally represented as a 26-element list self.wheel_map,
+ where wheel_map[i] == j shows that the position i places on from the arrow
+ maps to the position j places on.
+ """
+ def __init__(self, wheel=1, position='a'):
+ """initialise the pocket enigma, including which wheel to use and the
+ starting position of the wheel.
+
+ The wheel is either 1 or 2 (the predefined wheels) or a list of letter
+ pairs.
+
+ The position is the letter pointed to by the arrow on the wheel.
+
+ >>> pe.wheel_map
+ [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]
+ >>> pe.position
+ 0
+ """
+ self.wheel1 = [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
+ ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
+ ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
+ self.wheel2 = [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
+ ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
+ ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
+ if wheel == 1:
+ self.make_wheel_map(self.wheel1)
+ elif wheel == 2:
+ self.make_wheel_map(self.wheel2)
+ else:
+ self.validate_wheel_spec(wheel)
+ self.make_wheel_map(wheel)
+ if position in string.ascii_lowercase:
+ self.position = ord(position) - ord('a')
+ else:
+ self.position = position
+
+ def make_wheel_map(self, wheel_spec):
+ """Expands a wheel specification from a list of letter-letter pairs
+ into a full wheel_map.
+
+ >>> pe.make_wheel_map(pe.wheel2)
+ [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]
+ """
+ self.validate_wheel_spec(wheel_spec)
+ self.wheel_map = [0] * 26
+ for p in wheel_spec:
+ self.wheel_map[ord(p[0]) - ord('a')] = ord(p[1]) - ord('a')
+ self.wheel_map[ord(p[1]) - ord('a')] = ord(p[0]) - ord('a')
+ return self.wheel_map
+
+ def validate_wheel_spec(self, wheel_spec):
+ """Validates that a wheel specificaiton will turn into a valid wheel
+ map.
+
+ >>> pe.validate_wheel_spec([])
+ Traceback (most recent call last):
+ ...
+ ValueError: Wheel specification has 0 pairs, requires 13
+ >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
+ Traceback (most recent call last):
+ ...
+ ValueError: Not all mappings in wheel specificationhave two elements
+ >>> pe.validate_wheel_spec([('a', 'b')]*13)
+ Traceback (most recent call last):
+ ...
+ ValueError: Wheel specification does not contain 26 letters
+ """
+ if len(wheel_spec) != 13:
+ raise ValueError("Wheel specification has {} pairs, requires 13".
+ format(len(wheel_spec)))
+ for p in wheel_spec:
+ if len(p) != 2:
+ raise ValueError("Not all mappings in wheel specification"
+ "have two elements")
+ if len(set([p[0] for p in wheel_spec] +
+ [p[1] for p in wheel_spec])) != 26:
+ raise ValueError("Wheel specification does not contain 26 letters")
+
+ def encipher_letter(self, letter):
+ """Enciphers a single letter, by advancing the wheel before looking up
+ the letter on the wheel.
+
+ >>> pe.set_position('f')
+ 5
+ >>> pe.encipher_letter('k')
+ 'h'
+ """
+ self.advance()
+ return self.lookup(letter)
+ decipher_letter = encipher_letter
+
+ def lookup(self, letter):
+ """Look up what a letter enciphers to, without turning the wheel.
+
+ >>> pe.set_position('f')
+ 5
+ >>> ''.join([pe.lookup(l) for l in string.ascii_lowercase])
+ 'udhbfejcpgmokrliwntsayqzvx'
+ >>> pe.lookup('A')
+ ''
+ """
+ if letter in string.ascii_lowercase:
+ return chr(
+ (self.wheel_map[(ord(letter) - ord('a') - self.position) % 26] +
+ self.position) % 26 +
+ ord('a'))
+ else:
+ return ''
+
+ def advance(self):
+ """Advances the wheel one position.
+
+ >>> pe.set_position('f')
+ 5
+ >>> pe.advance()
+ 6
+ """
+ self.position = (self.position + 1) % 26
+ return self.position
+
+ def encipher(self, message, starting_position=None):
+ """Enciphers a whole message.
+
+ >>> pe.set_position('f')
+ 5
+ >>> pe.encipher('helloworld')
+ 'kjsglcjoqc'
+ >>> pe.set_position('f')
+ 5
+ >>> pe.encipher('kjsglcjoqc')
+ 'helloworld'
+ >>> pe.encipher('helloworld', starting_position = 'x')
+ 'egrekthnnf'
+ """
+ if starting_position:
+ self.set_position(starting_position)
+ transformed = ''
+ for l in message:
+ transformed += self.encipher_letter(l)
+ return transformed
+ decipher = encipher
+
+ def set_position(self, position):
+ """Sets the position of the wheel, by specifying the letter the arrow
+ points to.
+
+ >>> pe.set_position('a')
+ 0
+ >>> pe.set_position('m')
+ 12
+ >>> pe.set_position('z')
+ 25
+ """
+ self.position = ord(position) - ord('a')
+ return self.position