Simple Substitution Cipher
The Simple Substitution Cipher substitutes one letter for another. Since there are 26 possible substitutions for the letter A, 25 possible substitutions for B, 24 for C, and so on, the total number of possible keys is 26 × 25 × 24 × 23 × … × 1, or 403,291,461,126,605,635,584,000,000 keys! That’s far too many keys for even a supercomputer to brute force, so the code-breaking method used in Project 7, “Caesar Hacker,” can’t be used against the simple cipher. Unfortunately, devious attackers can take advantage of known weakness to break the code. If you’d like to learn more about ciphers and code breaking, you can read my book Cracking Codes with Python (No Starch Press, 2018; https://nostarch.com/crackingcodes/).
simple_substitution_cipher.py
1"""Simple Substitution Cipher, by Al Sweigart al@inventwithpython.com
2A simple substitution cipher has a one-to-one translation for each
3symbol in the plaintext and each symbol in the ciphertext.
4More info at: https://en.wikipedia.org/wiki/Substitution_cipher
5This code is available at https://nostarch.com/big-book-small-python-programming
6Tags: short, cryptography, math"""
7
8import random
9
10try:
11 import pyperclip # pyperclip copies text to the clipboard.
12except ImportError:
13 pass # If pyperclip is not installed, do nothing. It's no big deal.
14
15# Every possible symbol that can be encrypted/decrypted:
16LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
17
18def main():
19 print('''Simple Substitution Cipher, by Al Sweigart
20A simple substitution cipher has a one-to-one translation for each
21symbol in the plaintext and each symbol in the ciphertext.''')
22
23 # Let the user specify if they are encrypting or decrypting:
24 while True: # Keep asking until the user enters e or d.
25 print('Do you want to (e)ncrypt or (d)ecrypt?')
26 response = input('> ').lower()
27 if response.startswith('e'):
28 myMode = 'encrypt'
29 break
30 elif response.startswith('d'):
31 myMode = 'decrypt'
32 break
33 print('Please enter the letter e or d.')
34
35 # Let the user specify the key to use:
36 while True: # Keep asking until the user enters a valid key.
37 print('Please specify the key to use.')
38 if myMode == 'encrypt':
39 print('Or enter RANDOM to have one generated for you.')
40 response = input('> ').upper()
41 if response == 'RANDOM':
42 myKey = generateRandomKey()
43 print('The key is {}. KEEP THIS SECRET!'.format(myKey))
44 break
45 else:
46 if checkKey(response):
47 myKey = response
48 break
49
50 # Let the user specify the message to encrypt/decrypt:
51 print('Enter the message to {}.'.format(myMode))
52 myMessage = input('> ')
53
54 # Perform the encryption/decryption:
55 if myMode == 'encrypt':
56 translated = encryptMessage(myMessage, myKey)
57 elif myMode == 'decrypt':
58 translated = decryptMessage(myMessage, myKey)
59
60 # Display the results:
61 print('The %sed message is:' % (myMode))
62 print(translated)
63
64 try:
65 pyperclip.copy(translated)
66 print('Full %sed text copied to clipboard.' % (myMode))
67 except:
68 pass # Do nothing if pyperclip wasn't installed.
69
70
71def checkKey(key):
72 """Return True if key is valid. Otherwise return False."""
73 keyList = list(key)
74 lettersList = list(LETTERS)
75 keyList.sort()
76 lettersList.sort()
77 if keyList != lettersList:
78 print('There is an error in the key or symbol set.')
79 return False
80 return True
81
82
83def encryptMessage(message, key):
84 """Encrypt the message using the key."""
85 return translateMessage(message, key, 'encrypt')
86
87
88def decryptMessage(message, key):
89 """Decrypt the message using the key."""
90 return translateMessage(message, key, 'decrypt')
91
92
93def translateMessage(message, key, mode):
94 """Encrypt or decrypt the message using the key."""
95 translated = ''
96 charsA = LETTERS
97 charsB = key
98 if mode == 'decrypt':
99 # For decrypting, we can use the same code as encrypting. We
100 # just need to swap where the key and LETTERS strings are used.
101 charsA, charsB = charsB, charsA
102
103 # Loop through each symbol in the message:
104 for symbol in message:
105 if symbol.upper() in charsA:
106 # Encrypt/decrypt the symbol:
107 symIndex = charsA.find(symbol.upper())
108 if symbol.isupper():
109 translated += charsB[symIndex].upper()
110 else:
111 translated += charsB[symIndex].lower()
112 else:
113 # The symbol is not in LETTERS, just add it unchanged.
114 translated += symbol
115
116 return translated
117
118
119def generateRandomKey():
120 """Generate and return a random encryption key."""
121 key = list(LETTERS) # Get a list from the LETTERS string.
122 random.shuffle(key) # Randomly shuffle the list.
123 return ''.join(key) # Get a string from the list.
124
125
126# If this program was run (instead of imported), run the program:
127if __name__ == '__main__':
128 main()