CS303E Project 2: Spring 2024

Instructor: Dr. Bill Young
Due Date: Monday, April 8, 2024 at 11:59pm

A Substitution Cipher Class

In cryptography, a Substitution Cipher is one of the simplest and most widely known encryption techniques. Each symbol in the alphabet is replaced by another symbol in the same (or different) alphabet. For example, consider the following picture:

Notice that the top line is simply the standard English alphabet. The second line is the "key" for the encryption because it tells you how to transform each letter. For this assignment, the alphabet and the key should both be strings of length 26, containing only lowercase letters. When encrypting a text (the "plaintext"), you take each letter in the text, find it in the top line, note the corresponding letter in the key, and write that down. The resulting text is the "ciphertext." Nonletters are not changed.

When decrypting, you simply reverse the roles of the two lines. I.e., for each letter in the ciphertext, find it's position in the key, note the corresponding letter in the alphabet, and write that down. Voila! You get the plaintext back.

For this all to work, the key must be a permutation of the alphabet. That is, it should have exactly the same letters as the alphabet, but in scrambled order. By the way, your code must work for any legal key, not just the ones in the pictures above.

Please note that this project is mainly about classes and strings. Do not turn strings into lists and manipulate them as lists.

Assignment:

Your task in this project is to implement a substitution cipher application. There are two main parts to this: the SubstitutionCipher class itself, and a main() driver routine which calls it. You must use a class for the substitution cipher itself.

The SubstitutionCipher class

Here are the specific requirements for your SubstitutionCipher class:
  1. Implement your substitution cipher as a class, with the key as a private data member. When you create a SubstitutionCipher object you can pass in the key, which is just a string of length 26 containing all lowercase letters in the English alphabet in arbitrary order. If the key is not supplied, default to a randomly generated key. If you do pass in a key, validate that the key is legal.

  2. You should have at least the following methods in your class: Of course, the definitions of these will need the self parameter. See the Program Structure section below.

  3. When encrypting or decrypting preserve case. That is, corresponding plaintext and ciphertext letters should have the same case (uppercase or lowercase). If the character is a nonletter, just return it unchanged.

  4. You can have additional methods in the class if needed. You can also define auxiliary functions outside the class. For example, you should define the function makeRandomKey(); I've given you some code for this below. There's no reason for this to be a class method; it should be a function at the top level but can be called within the class definition.
Hints: It might be a good idea to define the functionality of encryption and decryption as top level functions initially. I.e., make sure that you understand how everything works before you try to put it into the framework of a class. You'll probably write several auxiliary functions, most of which probably can stay outside the class. In particular, notice that encryption and decryption are really the same operation with respect to two keys (strings). If you're clever, you can define the encryptText and decryptText methods as two calls to the same auxiliary function with the parameters swapped.

The main() Driver Function:

In addition to the class described above, you will define a main function that makes use of the class. This function should do the following:
  1. Create a new SubtitutionCipher object. (Below let's assume it's called cipher.) The initial key should be randomly generated.

  2. In a loop, accept from the user any of the following commands: help, getkey, changekey, encrypt, decrypt, quit. Case doesn't matter for these commands. (Hint: lowercase the string the user types and always compare to that.) Execute each command as entered. Execution of some of them may involve a nested loop.

  3. Commands behave as follows:

Possible Program Structure

Below is a possible structure for your program. You don't have to follow this exactly. But you must have the SubstitutionCipher class with at least the indicated methods, and a main routine.

import random

# A global constant defining the alphabet. 
LCLETTERS = "abcdefghijklmnopqrstuvwxyz"

# You are welcome to use the following two auxiliary functions, or 
# define your own.   You don't need to understand this code at this
# point in the semester. 

def isLegalKey( key ):
    # A key is legal if it has length 26 and contains all letters.
    # from LCLETTERS.
    return ( len(key) == 26 and all( [ ch in key for ch in LCLETTERS ] ) )

def makeRandomKey():
    # A legal random key is a permutation of LCLETTERS.
    lst = list( LCLETTERS )  # Turn string into list of letters
    random.shuffle( lst )    # Shuffle the list randomly
    return ''.join( lst )    # Assemble them back into a string

# There may be some additional auxiliary functions defined here.
# I had several others, mainly used in encrypt and decrypt. 

class SubstitutionCipher:
    def __init__ (self, key = None ):
        """Create an instance of the cipher with stored key, which
        defaults to random."""
        if key == None:
           self.__key = makeRandomKey()
        # otherwise, a key was passed in
           ...

    # Note that these are the required methods, but you may define
    # additional methods if you need them.  (I didn't need any.)

    def getKey( self ):
        """Getter for the stored key."""

    def setKey( self, newKey ):
        """Setter for the stored key.  Check that it's a legal
        key."""

    def encryptText( self, plaintext ):
        """Return the plaintext encrypted with respect to the stored key."""

    def decryptText( self, ciphertext ):
        """Return the ciphertext decrypted with respect to the stored
        key."""

def main():
     """ This implements the top level command loop.  It
    creates an instance of the SubstitutionCipher class and allows the user
    to invoke within a loop the following commands: help, getKey, changeKey,
    encrypt, decrypt, quit."""

main()
Note: I updated the code above from an earlier version where I had the __init__ method as:
    def __init__ (self, key = makeRandomKey() ): 
That probably works for this project, but it's not ideal. The issue is that the default value is computed when the method is compiled. That means that makeRandomKey is run once at compile time, not each time __init__ runs. So if you happen to create multiple instances of the class, they'd all have the same initial randomly defined key. In this project, you're probably only creating one instance of the class, so it probably wouldn't matter, but it's not good programming. Thanks to Sam for pointing out this issue.

Expected Output:

Below is a sample output for this program. You should match this, except that your keys will be different. Note that this is run from the command line. You can run yours from your IDE, but the TAs should be able to run it from the command line. Each level is indented two spaces from the level above it (except for some indentation in the help message).


> python Project2.py
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): hELp
The following commands are available:
  help: show this message;
  getkey: display the current key;
  changekey: the user may provide a new legal key or enter 
      random to generate a key randomly, or quit to end this 
      command and leave the key unchanged;
  encrypt: receive a text from the user, encrypt it using the 
      current key, and print the result;
  decrypt: receive a text from the user, decrypt it using the 
      current key, and print the result;
  quit: exit the command loop;
  anything else is an error.
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): getKEY
  Current cipher key: bapldjgktyisxrnuvwomqhzecf
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): changekey
  Enter a valid cipher key, 'random' for a random key, or 'quit' to quit: ljljljljljljl
    Illegal key entered. Try again!
  Enter a valid cipher key, 'random' for a random key, or 'quit' to quit: RANdom
    New cipher key: rpqusjznvetfdiybclaomgwxkh
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): getKEY
  Current cipher key: rpqusjznvetfdiybclaomgwxkh
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): chaNGEkey
  Enter a valid cipher key, 'random' for a random key, or 'quit' to quit: bapldjgktyisxrnuvwomqhzecf
    New cipher key: bapldjgktyisxrnuvwomqhzecf
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): getkey
  Current cipher key: bapldjgktyisxrnuvwomqhzecf
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): ChANGekey
  Enter a valid cipher key, 'random' for a random key, or 'quit' to quit: QUIT
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): getkey
  Current cipher key: bapldjgktyisxrnuvwomqhzecf
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): enCrypt
  Enter a text to encrypt: Now is the time for all good folks to come to the aid of their country!
    The encrypted text is: Rnz to mkd mtxd jnw bss gnnl jnsio mn pnxd mn mkd btl nj mkdtw pnqrmwc!
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): deCRYpt
  Enter a text to decrypt: Rnz to mkd mtxd jnw bss gnnl jnsio mn pnxd mn mkd btl nj mkdtw pnqrmwc!
    The decrypted text is: Now is the time for all good folks to come to the aid of their country!
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): argleBargle
  Command not recognized. Try again!
Enter a command (help, getKey, changeKey, encrypt, decrypt, quit): quit
Thanks for visiting!

Note, that if you comment out the call to main(), you should also be able to use your SubstitutionCipher class directly. It might be a good idea to get that running before you build your top level driver routine. Here's what that would look like:

> python
>>> from Project2 import *
>>> cipher = SubstitutionCipher()
>>> cipher.getKey()
'cmaukidjtvlnpgbehowxzryfqs'
>>> text = "Now is the time for all good men to come to the aid of their country."
>>> text
'Now is the time for all good men to come to the aid of their country.'
>>> cipher.encryptText( text )
'Gby tw xjk xtpk ibo cnn dbbu pkg xb abpk xb xjk ctu bi xjkto abzgxoq.'
>>> ciphertext = cipher.encryptText( text )
>>> ciphertext
'Gby tw xjk xtpk ibo cnn dbbu pkg xb abpk xb xjk ctu bi xjkto abzgxoq.'
>>> cipher.decryptText( ciphertext )
'Now is the time for all good men to come to the aid of their country.'
>>> key = makeRandomKey()
>>> key
'cglnypqdsaxjvrwfmhbtkeuoiz'
>>> isLegalKey( key )
True
>>> cipher2 = SubstitutionCipher("abcdfgh")
Key entered is not legal
>>> cipher2 = SubstitutionCipher( key )
>>> cipher2.getKey()
'cglnypqdsaxjvrwfmhbtkeuoiz'
>>> cipher2.encryptText("ABCdef")
'CGLnyp'
>>> cipher2.decryptText("CGLnyp")
'ABCdef'
>>> 

Turning in the Assignment:

The program should be in a file named Project2.py. Submit the file via Canvas before the deadline shown at the top of this page. Submit it to the assignment project2 under the assignments sections by uploading your Python file. Make sure that you following good coding style and use comments.

Your file must compile and run before submission. It must also contain a header with the following format:

# File: Project2.py
# Student: 
# UT EID:
# Course Name: CS303E
# 
# Date Created:
# Description of Program: 

Programming tips

Use functions: At this point in the semester, it shouldn't be necessary to say that you should use functions to code this. Below are some of the functions I defined in my implementation. You don't have to define exactly these functions, but you should use functional abstraction to build an understandable and maintainable program.
    isLegalKey( key ):                       # supplied for you
    makeRandomKey():                         # supplied for you
    keyIndex( ch, key ):                     # return index of ch in the key,
                                             #    if it's there
    convertLCCharacter( ch, key1, key2 ):    # convert lowercase character
    convertUCCharacter( ch, key1, key2 ):    # convert uppercase character
    convertText( text, key1, key2 ):         # put it together to convert
                                             #    a text

Add robustness: I always find it a good idea to make any system I code as robust and user-friendly as possible. For example, I find it annoying when using a system to have to worry about the case of input commands. Why not make commands case insensitive so that "EnCRypt" works as well as "encrypt"? It's easy to do that; just lowercase the command entered by the user (using comm.lower()) and then compare that to the lowercase version. If they match, accept the command; otherwise, reject it.

Remember: if the command is stored in variable comm, comm.lower() doesn't change comm. You'd want to do something like:

   commLower = comm.lower()
if you need to preserve comm, to print an error message, for example. If you don't need to preserve comm, you can just do this:
   comm = comm.lower()