Updated for challenge 9
[cipher-tools.git] / cipher / affine.py
1 from support.utilities import *
2 from support.language_models import *
3 from logger import logger
4
5
6 modular_division_table = {
7 (multiplier, (multiplier * plaintext) % 26): plaintext
8 for plaintext in range(26)
9 for multiplier in range(26)
10 }
11
12
13 def affine_encipher_letter(accented_letter, multiplier=1, adder=0, one_based=True):
14 """Encipher a letter, given a multiplier and adder
15
16 >>> cat(affine_encipher_letter(l, 3, 5, True) \
17 for l in string.ascii_letters)
18 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE'
19 >>> cat(affine_encipher_letter(l, 3, 5, False) \
20 for l in string.ascii_letters)
21 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC'
22 """
23 letter = unaccent(accented_letter)
24 if letter in string.ascii_letters:
25 letter_number = pos(letter)
26 if one_based: letter_number += 1
27 cipher_number = (letter_number * multiplier + adder) % 26
28 if one_based: cipher_number -= 1
29 if letter in string.ascii_uppercase:
30 return unpos(cipher_number).upper()
31 else:
32 return unpos(cipher_number)
33 else:
34 return letter
35
36 def affine_decipher_letter(letter, multiplier=1, adder=0, one_based=True):
37 """Encipher a letter, given a multiplier and adder
38
39 >>> cat(affine_decipher_letter(l, 3, 5, True) \
40 for l in 'hknqtwzcfiloruxadgjmpsvybeHKNQTWZCFILORUXADGJMPSVYBE')
41 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
42 >>> cat(affine_decipher_letter(l, 3, 5, False) \
43 for l in 'filoruxadgjmpsvybehknqtwzcFILORUXADGJMPSVYBEHKNQTWZC')
44 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
45 """
46 if letter in string.ascii_letters:
47 cipher_number = pos(letter)
48 if one_based: cipher_number += 1
49 # plaintext_number = (
50 # modular_division_table[multiplier]
51 # [(cipher_number - adder) % 26])
52 plaintext_number = (
53 modular_division_table[multiplier, (cipher_number - adder) % 26]
54 )
55 if one_based: plaintext_number -= 1
56 if letter in string.ascii_uppercase:
57 return unpos(plaintext_number).upper()
58 else:
59 return unpos(plaintext_number)
60 else:
61 return letter
62
63 def affine_encipher(message, multiplier=1, adder=0, one_based=True):
64 """Encipher a message
65
66 >>> affine_encipher('hours passed during which jerico tried every ' \
67 'trick he could think of', 15, 22, True)
68 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
69 """
70 enciphered = [affine_encipher_letter(l, multiplier, adder, one_based)
71 for l in message]
72 return cat(enciphered)
73
74 def affine_decipher(message, multiplier=1, adder=0, one_based=True):
75 """Decipher a message
76
77 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
78 'jfaoe ls omytd jlaxe mh', 15, 22, True)
79 'hours passed during which jerico tried every trick he could think of'
80 """
81 enciphered = [affine_decipher_letter(l, multiplier, adder, one_based)
82 for l in message]
83 return cat(enciphered)
84
85
86
87 def affine_break(message, fitness=Pletters):
88 """Breaks an affine cipher using frequency analysis
89
90 >>> affine_break('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls ' \
91 'omytd jlaxe mh jm bfmibj umis hfsul axubafkjamx. ls kffkxwsd jls ' \
92 'ofgbjmwfkiu olfmxmtmwaokttg jlsx ls kffkxwsd jlsi zg tsxwjl. jlsx ' \
93 'ls umfjsd jlsi zg hfsqysxog. ls dmmdtsd mx jls bats mh bkbsf. ls ' \
94 'bfmctsd kfmyxd jls lyj, mztanamyu xmc jm clm cku tmmeaxw kj lai ' \
95 'kxd clm ckuxj.') # doctest: +ELLIPSIS
96 ((15, 22, True), -340.601181913...)
97 """
98 sanitised_message = sanitise(message)
99 best_multiplier = 0
100 best_adder = 0
101 best_one_based = True
102 best_fit = float("-inf")
103 for one_based in [True, False]:
104 for multiplier in [x for x in range(1, 26, 2) if x != 13]:
105 for adder in range(26):
106 plaintext = affine_decipher(sanitised_message,
107 multiplier, adder, one_based)
108 fit = fitness(plaintext)
109 logger.debug('Affine break attempt using key {0}x+{1} ({2}) '
110 'gives fit of {3} and decrypt starting: {4}'.
111 format(multiplier, adder, one_based, fit,
112 plaintext[:50]))
113 if fit > best_fit:
114 best_fit = fit
115 best_multiplier = multiplier
116 best_adder = adder
117 best_one_based = one_based
118 logger.info('Affine break best fit with key {0}x+{1} ({2}) gives fit of '
119 '{3} and decrypt starting: {4}'.format(
120 best_multiplier, best_adder, best_one_based, best_fit,
121 affine_decipher(sanitised_message, best_multiplier,
122 best_adder, best_one_based)[:50]))
123 return (best_multiplier, best_adder, best_one_based), best_fit