Simple Substitution Cipher

Tags: short, cryptography, math

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()

https://inventwithpython.com/bigbookpython/project66.html