Statement of Completion#78258137
Fun and Puzzles
hard
Breaking Caesar's cipher
Resolution
Activities
In [8]:
import string
In [9]:
letters = list(string.ascii_uppercase)
Activities¶
1. Shift letters
by 5
¶
In [10]:
shift_5 = letters[5:] + letters[:5]
"".join(shift_5)
Out[10]:
'FGHIJKLMNOPQRSTUVWXYZABCDE'
2. Shift letters
by -8
¶
In [12]:
shift_minus_8 = letters[-8:] + letters[:-8]
"".join(shift_minus_8)
Out[12]:
'STUVWXYZABCDEFGHIJKLMNOPQR'
3. Shift letters
by 32
¶
In [14]:
shift = 32 % len(letters)
shift_32 = letters[shift:] + letters[:shift]
"".join(shift_32)
Out[14]:
'GHIJKLMNOPQRSTUVWXYZABCDEF'
4. Write the function shift_letters
¶
In [16]:
def shift_letters(letters, shift):
shift = shift % len(letters)
return letters[shift:] + letters[:shift]
Test it on your own:
In [ ]:
result = shift_letters(letters, 6)
" ".join(result)
# should show:
# 'G H I J K L M N O P Q R S T U V W X Y Z A B C D E F'
5. Write the function encrypt_simple
¶
In [18]:
def encrypt_simple(word, shift):
letters = list(string.ascii_uppercase)
shifted = shift_letters(letters, shift)
mapping = {
l: s for l, s in zip(letters, shifted)
}
return "".join(mapping[char] for char in word)
6. Write the function decrypt_simple
¶
In [20]:
def decrypt_simple(encrypted_word, origina_shift):
return encrypt_simple(encrypted_word, origina_shift * -1)
7. Define the functions encrypt_full
and decrypt_full
¶
In [22]:
def encrypt_full(text, shift):
lower_letters = list(string.ascii_lowercase)
shifted_lower = shift_letters(lower_letters, shift)
mapping_lower = {}
for letter, shifted in zip(lower_letters, shifted_lower):
mapping_lower[letter] = shifted
upper_letters = list(string.ascii_uppercase)
shifted_upper = shift_letters(upper_letters, shift)
mapping_upper = {}
for letter, shifted in zip(upper_letters, shifted_upper):
mapping_upper[letter] = shifted
new_word = ""
for char in text:
if char in mapping_lower:
new_word += mapping_lower[char]
continue
if char in mapping_upper:
new_word += mapping_upper[char]
continue
new_word += char
return new_word
In [23]:
def decrypt_full(encrypted_text, original_shift):
return encrypt_full(encrypted_text, original_shift * -1)
Try it out!¶
Now you can put your cipher to a test. Execute the following line to encrypt any message that you want:
In [ ]:
from IPython.core.display import HTML
Encrypt:¶
In [ ]:
text = input("Enter the text you want to encrypt: ")
shift = int(input("Enter the `shift` value (integer): "))
display(HTML(f"<br>Your encrypted message: <b>{encrypt_full(text, shift)}</b>"))
Decrypt:¶
In [ ]:
text = input("Enter the text you want to decrypt: ")
shift = int(input("Enter the `shift` value (integer): "))
display(HTML(f"<br>Your encrypted message: <b>{decrypt_full(text, shift)}</b>"))
Now it's time to apply all our previous functions and some algorithmic brain power to break the cipher! This is the ultimate challenge for any criptographer.
Implement the function break_cipher
that will break any code encrypted using Caesar's cipher. The general approach to break a cipher is to find known words. For example, if you know a messsage contains the word "DATAWARS", you can look for that pattern within the encrypted message.
Let's pick any random shift to encrypt a cipher, for example, 9
:
In [ ]:
encrypt_full("THIS IS DATAWARS", 9)
The word DATAWARS
was encrypted as MJCJFJAB
. But if you check the "distances" between the words DATAWARS
and MJCJFJAB
you'll find the same pattern:
D => A => T => A => W => A => R => S
23 19 7 22 4 17 1
M => J => C => J => F => J => A => B
23 19 7 22 4 17 1
If the pattern is still not clear to you, try counting the "hops" between "D" and "A" and between "M" and "J" using the following alphabet. Important! The key is to always count "forward".
In [ ]:
" ".join(letters)
In [25]:
def extract_text_pattern(text):
prev_char = None
prev_pos = None
pattern = []
for char in text:
if char not in letters:
prev_char = None
prev_pos = None
continue
char_pos = letters.index(char)
if prev_char != None:
r = char_pos - prev_pos
if r < 0:
r += len(letters)
pattern.append(r)
prev_char = char
prev_pos = char_pos
return pattern
In [26]:
def break_cipher(encrypted_message, known_word="DATAWARS"):
letters = list(string.ascii_uppercase)
known_pattern = extract_text_pattern(known_word)
encrypted_words = encrypted_message.split(" ")
for word in encrypted_words:
word_pattern = extract_text_pattern(word)
if word_pattern == known_pattern:
break
else:
raise ValueError("Can't decrypt message. Pattern not found.")
known_char_pos = letters.index(known_word[0])
found_char_pos = letters.index(word[0])
shift = found_char_pos - known_char_pos
return decrypt_full(encrypted_message, shift)
Here are some test cases that you can use to test your own function:
In [ ]:
encrypted_text = encrypt_full("DATAWARS IS THE BEST DATA SCIENCE PRACTICE PLATFORM IN THE ENTIRE WORLD", 12)
encrypted_text
In [ ]:
break_cipher(encrypted_text, "DATAWARS")
In [ ]:
break_cipher(encrypted_text, "SCIENCE")
In [ ]:
break_cipher(encrypted_text, "PLATFORM")