1 """A set of ciphers with implementations for both enciphering and deciphering
2 them. See cipherbreak for automatic breaking of these ciphers
8 from language_models
import unaccent
, sanitise
11 modular_division_table
= [[0]*26 for _
in range(26)]
15 modular_division_table
[b
][c
] = a
18 def deduplicate(text
):
19 """If a string contains duplicate letters, remove all but the first. Retain
20 the order of the letters.
22 >>> deduplicate('cat')
24 >>> deduplicate('happy')
26 >>> deduplicate('cattca')
29 return list(collections
.OrderedDict
.fromkeys(text
))
32 def caesar_encipher_letter(accented_letter
, shift
):
33 """Encipher a letter, given a shift amount
35 >>> caesar_encipher_letter('a', 1)
37 >>> caesar_encipher_letter('a', 2)
39 >>> caesar_encipher_letter('b', 2)
41 >>> caesar_encipher_letter('x', 2)
43 >>> caesar_encipher_letter('y', 2)
45 >>> caesar_encipher_letter('z', 2)
47 >>> caesar_encipher_letter('z', -1)
49 >>> caesar_encipher_letter('a', -1)
51 >>> caesar_encipher_letter('A', 1)
53 >>> caesar_encipher_letter('é', 1)
56 letter
= unaccent(accented_letter
)
57 if letter
in string
.ascii_letters
:
58 if letter
in string
.ascii_uppercase
:
59 alphabet_start
= ord('A')
61 alphabet_start
= ord('a')
62 return chr(((ord(letter
) - alphabet_start
+ shift
) % 26) +
67 def caesar_decipher_letter(letter
, shift
):
68 """Decipher a letter, given a shift amount
70 >>> caesar_decipher_letter('b', 1)
72 >>> caesar_decipher_letter('b', 2)
75 return caesar_encipher_letter(letter
, -shift
)
77 def caesar_encipher(message
, shift
):
78 """Encipher a message with the Caesar cipher of given shift
80 >>> caesar_encipher('abc', 1)
82 >>> caesar_encipher('abc', 2)
84 >>> caesar_encipher('abcxyz', 2)
86 >>> caesar_encipher('ab cx yz', 2)
88 >>> caesar_encipher('Héllo World!', 2)
91 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
92 return ''.join(enciphered
)
94 def caesar_decipher(message
, shift
):
95 """Decipher a message with the Caesar cipher of given shift
97 >>> caesar_decipher('bcd', 1)
99 >>> caesar_decipher('cde', 2)
101 >>> caesar_decipher('cd ez ab', 2)
103 >>> caesar_decipher('Jgnnq Yqtnf!', 2)
106 return caesar_encipher(message
, -shift
)
108 def affine_encipher_letter(accented_letter
, multiplier
=1, adder
=0,
110 """Encipher a letter, given a multiplier and adder
111 >>> ''.join([affine_encipher_letter(l, 3, 5, True) \
112 for l in string.ascii_uppercase])
113 'HKNQTWZCFILORUXADGJMPSVYBE'
114 >>> ''.join([affine_encipher_letter(l, 3, 5, False) \
115 for l in string.ascii_uppercase])
116 'FILORUXADGJMPSVYBEHKNQTWZC'
118 letter
= unaccent(accented_letter
)
119 if letter
in string
.ascii_letters
:
120 if letter
in string
.ascii_uppercase
:
121 alphabet_start
= ord('A')
123 alphabet_start
= ord('a')
124 letter_number
= ord(letter
) - alphabet_start
125 if one_based
: letter_number
+= 1
126 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
127 if one_based
: cipher_number
-= 1
128 return chr(cipher_number
% 26 + alphabet_start
)
132 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
133 """Encipher a letter, given a multiplier and adder
135 >>> ''.join([affine_decipher_letter(l, 3, 5, True) \
136 for l in 'HKNQTWZCFILORUXADGJMPSVYBE'])
137 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
138 >>> ''.join([affine_decipher_letter(l, 3, 5, False) \
139 for l in 'FILORUXADGJMPSVYBEHKNQTWZC'])
140 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
142 if letter
in string
.ascii_letters
:
143 if letter
in string
.ascii_uppercase
:
144 alphabet_start
= ord('A')
146 alphabet_start
= ord('a')
147 cipher_number
= ord(letter
) - alphabet_start
148 if one_based
: cipher_number
+= 1
150 modular_division_table
[multiplier
]
151 [(cipher_number
- adder
) % 26]
153 if one_based
: plaintext_number
-= 1
154 return chr(plaintext_number
% 26 + alphabet_start
)
158 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
159 """Encipher a message
161 >>> affine_encipher('hours passed during which jerico tried every ' \
162 'trick he could think of', 15, 22, True)
163 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
165 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
167 return ''.join(enciphered
)
169 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
170 """Decipher a message
172 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
173 'jfaoe ls omytd jlaxe mh', 15, 22, True)
174 'hours passed during which jerico tried every trick he could think of'
176 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
178 return ''.join(enciphered
)
181 class KeywordWrapAlphabet(Enum
):
182 """Ways of wrapping the alphabet for keyword-based substitution ciphers."""
188 def keyword_cipher_alphabet_of(keyword
,
189 wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
190 """Find the cipher alphabet given a keyword.
191 wrap_alphabet controls how the rest of the alphabet is added
194 >>> keyword_cipher_alphabet_of('bayes')
195 'bayescdfghijklmnopqrtuvwxz'
196 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_a)
197 'bayescdfghijklmnopqrtuvwxz'
198 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_last)
199 'bayestuvwxzcdfghijklmnopqr'
200 >>> keyword_cipher_alphabet_of('bayes', KeywordWrapAlphabet.from_largest)
201 'bayeszcdfghijklmnopqrtuvwx'
203 if wrap_alphabet
== KeywordWrapAlphabet
.from_a
:
204 cipher_alphabet
= ''.join(deduplicate(sanitise(keyword
) +
205 string
.ascii_lowercase
))
207 if wrap_alphabet
== KeywordWrapAlphabet
.from_last
:
208 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
210 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
211 last_keyword_position
= string
.ascii_lowercase
.find(
212 last_keyword_letter
) + 1
213 cipher_alphabet
= ''.join(
214 deduplicate(sanitise(keyword
) +
215 string
.ascii_lowercase
[last_keyword_position
:] +
216 string
.ascii_lowercase
))
217 return cipher_alphabet
220 def keyword_encipher(message
, keyword
,
221 wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
222 """Enciphers a message with a keyword substitution cipher.
223 wrap_alphabet controls how the rest of the alphabet is added
226 1 : from the last letter in the sanitised keyword
227 2 : from the largest letter in the sanitised keyword
229 >>> keyword_encipher('test message', 'bayes')
231 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_a)
233 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_last)
235 >>> keyword_encipher('test message', 'bayes', KeywordWrapAlphabet.from_largest)
238 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
239 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
240 return unaccent(message
).lower().translate(cipher_translation
)
242 def keyword_decipher(message
, keyword
,
243 wrap_alphabet
=KeywordWrapAlphabet
.from_a
):
244 """Deciphers a message with a keyword substitution cipher.
245 wrap_alphabet controls how the rest of the alphabet is added
248 1 : from the last letter in the sanitised keyword
249 2 : from the largest letter in the sanitised keyword
251 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
253 >>> keyword_decipher('rsqr ksqqbds', 'bayes', KeywordWrapAlphabet.from_a)
255 >>> keyword_decipher('lskl dskkbus', 'bayes', KeywordWrapAlphabet.from_last)
257 >>> keyword_decipher('qspq jsppbcs', 'bayes', KeywordWrapAlphabet.from_largest)
260 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
261 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
262 return message
.lower().translate(cipher_translation
)
264 if __name__
== "__main__":