1 """A set of ciphers with implementations for both enciphering and deciphering
2 them. See cipherbreak for automatic breaking of these ciphers
8 from itertools
import zip_longest
, cycle
, chain
9 from language_models
import unaccent
, sanitise
12 modular_division_table
= [[0]*26 for _
in range(26)]
16 modular_division_table
[b
][c
] = a
19 def deduplicate(text
):
20 """If a string contains duplicate letters, remove all but the first. Retain
21 the order of the letters.
23 >>> deduplicate('cat')
25 >>> deduplicate('happy')
27 >>> deduplicate('cattca')
30 return list(collections
.OrderedDict
.fromkeys(text
))
33 def caesar_encipher_letter(accented_letter
, shift
):
34 """Encipher a letter, given a shift amount
36 >>> caesar_encipher_letter('a', 1)
38 >>> caesar_encipher_letter('a', 2)
40 >>> caesar_encipher_letter('b', 2)
42 >>> caesar_encipher_letter('x', 2)
44 >>> caesar_encipher_letter('y', 2)
46 >>> caesar_encipher_letter('z', 2)
48 >>> caesar_encipher_letter('z', -1)
50 >>> caesar_encipher_letter('a', -1)
52 >>> caesar_encipher_letter('A', 1)
54 >>> caesar_encipher_letter('é', 1)
57 letter
= unaccent(accented_letter
)
58 if letter
in string
.ascii_letters
:
59 if letter
in string
.ascii_uppercase
:
60 alphabet_start
= ord('A')
62 alphabet_start
= ord('a')
63 return chr(((ord(letter
) - alphabet_start
+ shift
) % 26) +
68 def caesar_decipher_letter(letter
, shift
):
69 """Decipher a letter, given a shift amount
71 >>> caesar_decipher_letter('b', 1)
73 >>> caesar_decipher_letter('b', 2)
76 return caesar_encipher_letter(letter
, -shift
)
78 def caesar_encipher(message
, shift
):
79 """Encipher a message with the Caesar cipher of given shift
81 >>> caesar_encipher('abc', 1)
83 >>> caesar_encipher('abc', 2)
85 >>> caesar_encipher('abcxyz', 2)
87 >>> caesar_encipher('ab cx yz', 2)
89 >>> caesar_encipher('Héllo World!', 2)
92 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
93 return ''.join(enciphered
)
95 def caesar_decipher(message
, shift
):
96 """Decipher a message with the Caesar cipher of given shift
98 >>> caesar_decipher('bcd', 1)
100 >>> caesar_decipher('cde', 2)
102 >>> caesar_decipher('cd ez ab', 2)
104 >>> caesar_decipher('Jgnnq Yqtnf!', 2)
107 return caesar_encipher(message
, -shift
)
109 def affine_encipher_letter(accented_letter
, multiplier
=1, adder
=0,
111 """Encipher a letter, given a multiplier and adder
112 >>> ''.join([affine_encipher_letter(l, 3, 5, True) \
113 for l in string.ascii_uppercase])
114 'HKNQTWZCFILORUXADGJMPSVYBE'
115 >>> ''.join([affine_encipher_letter(l, 3, 5, False) \
116 for l in string.ascii_uppercase])
117 'FILORUXADGJMPSVYBEHKNQTWZC'
119 letter
= unaccent(accented_letter
)
120 if letter
in string
.ascii_letters
:
121 if letter
in string
.ascii_uppercase
:
122 alphabet_start
= ord('A')
124 alphabet_start
= ord('a')
125 letter_number
= ord(letter
) - alphabet_start
126 if one_based
: letter_number
+= 1
127 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
128 if one_based
: cipher_number
-= 1
129 return chr(cipher_number
% 26 + alphabet_start
)
133 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
134 """Encipher a letter, given a multiplier and adder
136 >>> ''.join([affine_decipher_letter(l, 3, 5, True) \
137 for l in 'HKNQTWZCFILORUXADGJMPSVYBE'])
138 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
139 >>> ''.join([affine_decipher_letter(l, 3, 5, False) \
140 for l in 'FILORUXADGJMPSVYBEHKNQTWZC'])
141 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
143 if letter
in string
.ascii_letters
:
144 if letter
in string
.ascii_uppercase
:
145 alphabet_start
= ord('A')
147 alphabet_start
= ord('a')
148 cipher_number
= ord(letter
) - alphabet_start
149 if one_based
: cipher_number
+= 1
151 modular_division_table
[multiplier
]
152 [(cipher_number
- adder
) % 26]
154 if one_based
: plaintext_number
-= 1
155 return chr(plaintext_number
% 26 + alphabet_start
)
159 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
160 """Encipher a message
162 >>> affine_encipher('hours passed during which jerico tried every ' \
163 'trick he could think of', 15, 22, True)
164 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
166 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
168 return ''.join(enciphered
)
170 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
171 """Decipher a message
173 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
174 'jfaoe ls omytd jlaxe mh', 15, 22, True)
175 'hours passed during which jerico tried every trick he could think of'
177 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
179 return ''.join(enciphered
)
182 class KeywordWrapAlphabet(Enum
):
183 """Ways of wrapping the alphabet for keyword-based substitution ciphers."""
189 def keyword_cipher_alphabet_of(keyword
,
190 wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
191 """Find the cipher alphabet given a keyword.
192 wrap_alphabet controls how the rest of the alphabet is added
195 >>> keyword_cipher_alphabet_of('bayes')
196 'bayescdfghijklmnopqrtuvwxz'
197 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_a)
198 'bayescdfghijklmnopqrtuvwxz'
199 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_last)
200 'bayestuvwxzcdfghijklmnopqr'
201 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_largest)
202 'bayeszcdfghijklmnopqrtuvwx'
204 if wrap_alphabet
== KeywordWrapAlphabet
.from_a
:
205 cipher_alphabet
= ''.join(deduplicate(sanitise(keyword
) +
206 string
.ascii_lowercase
))
208 if wrap_alphabet
== KeywordWrapAlphabet
.from_last
:
209 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
211 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
212 last_keyword_position
= string
.ascii_lowercase
.find(
213 last_keyword_letter
) + 1
214 cipher_alphabet
= ''.join(
215 deduplicate(sanitise(keyword
) +
216 string
.ascii_lowercase
[last_keyword_position
:] +
217 string
.ascii_lowercase
))
218 return cipher_alphabet
221 def keyword_encipher(message
, keyword
,
222 wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
223 """Enciphers a message with a keyword substitution cipher.
224 wrap_alphabet controls how the rest of the alphabet is added
227 1 : from the last letter in the sanitised keyword
228 2 : from the largest letter in the sanitised keyword
230 >>> keyword_encipher('test message', 'bayes')
232 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a)
234 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last)
236 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest)
239 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
240 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
241 return unaccent(message
).lower().translate(cipher_translation
)
243 def keyword_decipher(message
, keyword
,
244 wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
245 """Deciphers a message with a keyword substitution cipher.
246 wrap_alphabet controls how the rest of the alphabet is added
249 1 : from the last letter in the sanitised keyword
250 2 : from the largest letter in the sanitised keyword
252 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
254 >>> keyword_decipher('rsqr ksqqbds', 'bayes', KeywordWrapAlphabet.from_a)
256 >>> keyword_decipher('lskl dskkbus', 'bayes', KeywordWrapAlphabet.from_last)
258 >>> keyword_decipher('qspq jsppbcs', 'bayes', KeywordWrapAlphabet.from_largest)
261 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
262 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
263 return message
.lower().translate(cipher_translation
)
265 if __name__
== "__main__":