zehadialam / Vigenere-Cipher

A Python implementation of the Vigenère Cipher.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

CSCI 1300 Final Project: Vigenère Cipher

  • Instructor: Dr. Michael E. Cotterell
  • Semester: Spring 2021
  • Author: Zehadi Alam

Introduction

Encryption is a method of securing information through an algorithm, such that it is rendered into a meaningless and unintelligible format. It is a form of secure communication (i.e. unintended individuals are not able to understand the message). A cipher is a particular encryption algorithm. Among the various ciphers that exist, I will focus here on a substitution cipher called the Vigenère cipher. A substitution cipher is an encryption algorithm that involves substituting individual characters of the plaintext (non-encrypted text) with certain other characters to produce the ciphertext (encrypted text) (e.g. the letter "A" can be substituted with the letter "E"). The particular substitution being made is based on a key, which specifies the value for shifting from the original letter (e.g. a right-shift value of 4 when applied to the letter "A", yields the letter "E"). What we have described so far is actually a cipher known as the Caesar cipher. The Vigenère cipher is essentially many Caesar ciphers, in that different shift values are used throughout the plaintext (i.e. one for each letter until repetition of the values are required), instead of the same shift value for every letter.

The Vigenère cipher is no longer a secure encryption algorithm and should not be used for serious encryption. It can still be used for teaching purposes. That is what is often done, as it is taught in cryptography courses during the overview of historical ciphers. Suppose that as a demonstration of the Vigenère cipher, you decide to encrypt a page-length amount of text. Doing this manually would be greatly tedious and tiresome, not to mention take a long time to do.

If you program an algorithm to carry out the task for you, it would be as simple as inputting the text you want to encrypt to the program and have the program output the encrypted text. I will proceed to explain the process of encrypting a plaintext message with the Vigenère cipher, so that one might be able to implement it themselves in their preferred programming language.

Algorithm

Encrypting a message with the Vigenère cipher requires two things:

  • The message to be encrypted
  • A key
The key in this context, refers to any alphabetic sequence of characters. The way that the key is applied in the encryption process will be explained as follows.

Table 1

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

Suppose we want to encrypt the message "Cryptology" with the key "Python"
To do this, we can refer to Table 1 to map each letter in the message and the key with its numeric value, as determined by its position in the alphabet.

"Cryptology" will be converted to: 2 17 24 15 19 14 11 14 6 24
"Python" will be converted to: 15 24 19 7 14 13

After the letter to number conversion, each number associated with the word "Cryptology" will be mapped to each number associated with the word "Python".
If there are fewer numbers of the key than the message, then one must start over with the key numbers after all of them have been mapped to the numbers of message.
In this example, the mapping of the message with the key will look like the following (notice the repetition of the key after the sixth column):

Table 2

2 17 24 15 19 14 11 14 6 24
15 24 19 7 14 13 15 24 19 7

Once the mapping, as shown by Table 2, is complete, each column value of the first row must be added with the column value of the second row. Each of the numbers of the key constitute the "shift value" for each of the numbers of the message.

For example, the first letter of the message is a "C" (numeric equivalent of 2), and the number mapped to it based on the key is 15. That means that the replacement ciphertext character will be 15 places to the right of the position of "C" in the alphabet (right-shift for encryption; left-shift for decryption). Adding 2 and 15 results in 17, which is mapped to "R", as shown in Table 1. Thus, the first character of the ciphertext for "Cryptology" with the key "Python" is "R".

For shift values that will take you past the "edge" of the alphabet, the modulo operation (denoted by "%") needs to be carried out on the sum. The sum % 26 will allow for the sum to "wrap around" the alphabet, so that the number maps on to a letter.

Following the process that we have described so far for the remaining letters, the numeric equivalent of the ciphertext for the word "Cryptology" is 17 15 17 22 7 1 0 12 25 5. When we convert this to the alphabetic ciphertext using Table 1, we get: Rprwhbamzf

To decrypt this ciphertext back into the plaintext, follow the same process carried out on the plaintext for the ciphertext, but use subtraction for the column values, instead of addition.

Implementation

The following code blocks contain my implementation of the Vigenère cipher.

Topics from Modules 1 - 5 that have been incorporated:

Module 1:

  • Values and expressions - Used all throughout the program.
  • Assignment statements - Used all throughout the program.
  • Boolean expressions - Used in every function except the_alphabet()
Module 2:
  • Boolean operators - Used in every function except the_alphabet() and restore_original_format()
  • Decision statements - Used in every function except the_alphabet(), test_convert_letter_to_number(), and test_convert_number_to_letter()
  • Loops - Used in every function except the_alphabet() and main()
  • F-strings - Used in prompt_user() and main()
Module 3:
  • Functions - Used in the first seven code blocks in the Implementation section and the first two code blocks in the Test Cases and Discussion section.
  • Exception handling - Used in the functions convert_letter_to_number(), convert_number_to_letter(), and prompt_user()
  • String methods
    • convert_letter_to_number() uses replace() and upper()
    • restore_original_format() uses isalpha(), isupper(), upper(), and isspace()
    • vigenere_cipher() uses lower()
    • prompt_user() uses isalpha()
    • test_convert_letter_to_number() uses lower()
    • test_convert_number_to_letter() uses join()
Module 4:
  • Lists - Used in every function except restore_original_format() and prompt_user()
    • List comprehension is used in test_convert_letter_to_number() and test_convert_number_to_letter()
Module 5:
  • Dictionaries - Used in vigenere_cipher()
def the_alphabet():
    """Return a list containing the alphabet as individual characters."""
    return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
            's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
def convert_letter_to_number(text):
    """Return a list with the numeric position (starting at zero) in the alphabet for each letter of the inputted argument.
    
    Example: >>> convert_letter_to_number('python')
                 [15, 24, 19, 7, 14, 13]
    """
    try:
        text_no_spaces = text.replace(' ', '') # removing whitespace
        letter_to_number = [0] * len(text_no_spaces) # initialize list with zeros
        alphabet = the_alphabet()
        k = 0
        for i in range(0, len(text)):
            for j in range(0, len(alphabet)):
                if text[i] == alphabet[j] or text[i] == alphabet[j].upper():
                    letter_to_number[k] = j # assigning index of character to list position 'k'
                    k += 1
        return letter_to_number
    except AttributeError:
        print('Invalid input! Must only input alphabetic strings.')
def convert_number_to_letter(letter_to_number):
    """Return a string that is the alphabetic equivalent for a list of numbers.
    
    Example: >>> convert_number_to_letter([15, 24, 19, 7, 14, 13])
                 python
    """
    try:
        for i in range(0, len(letter_to_number)):
            if not isinstance(letter_to_number[i], int): # check if every element is int
                raise TypeError
        message_only_alphabetic = ''
        for i in range(0, len(letter_to_number)):
            for j in range(0, len(the_alphabet())):
                if letter_to_number[i] == j:
                    message_only_alphabetic += the_alphabet()[j] # concatenating alphabetic character
        return message_only_alphabetic
    except TypeError:
        print('Invalid input! Must only input iterables containing data of type int.')
def restore_original_format(original_format, modified_format):
    """Return a string with the format of 'original_format' (i.e. including spaces and punctuation).
    
    Example: >>> restore_original_format('Ice - Cream', 'abcdefgh')
                 Abc - Defgh
    """
    restored_text = ''
    j = 0
    for i in range(0, len(original_format)):
        if original_format[i].isalpha():
            # accounting for letter casing
            if original_format[i].isupper():
                restored_text += modified_format[j].upper()
            else:
                restored_text += modified_format[j]
            j += 1
        elif original_format[i].isspace(): # accounting for spaces in original_format text
            restored_text += ' '
        else:
            restored_text += original_format[i] # accounting for other non-alphabetic characters
    return restored_text
def vigenere_cipher(text, key, process):
    """Return the encrypted or decrypted version of inputted text, based on the process and key specified.
    
    Example: >>> vigenere_cipher('Quantum theory', 'physics', 'encrypt')
                 Fbyfbwe iocgza
                
             >>> vigenere_cipher('Fbyfbwe iocgza', 'physics', 'decrypt')
                 Quantum theory
    """
    text_copy = text # make a copy of text to be used for restoring original format later
    letter_to_number = convert_letter_to_number(text)
    vigenere_cipher_shifts = {} # dictionary for holding shift values
    shift_value = convert_letter_to_number(key)
    for i in range(len(letter_to_number)):
        vigenere_cipher_shifts[i] = shift_value[i % len(key)]
    for i in range(0, len(letter_to_number)):
        if process.lower() == 'encrypt':
            letter_to_number[i] = (letter_to_number[i] + vigenere_cipher_shifts.get(i)) % len(the_alphabet())
        elif process.lower() == 'decrypt':
            letter_to_number[i] = (letter_to_number[i] - vigenere_cipher_shifts.get(i)) % len(the_alphabet())
    message_only_alphabetic = convert_number_to_letter(letter_to_number)
    processed_text = restore_original_format(text_copy, message_only_alphabetic)
    return processed_text
def prompt_user(prompt, prompt_number):
    """Return the answer to a prompt."""
    invalid_response = True
    while invalid_response:
        try:
            if prompt_number == 1:
                answer = input(prompt)
                if answer != '1' and answer != '2' and answer != '3':
                    print()
                    raise ValueError('Invalid input! ')
                else:
                    invalid_response = False
                    return answer
            if prompt_number == 2:
                answer = input(prompt)
                if not answer.isalpha():
                    print()
                    raise ValueError('Invalid input! ')
                else:
                    invalid_response = False
                    return answer
        except ValueError as e:
            print(f'{e}')
def main():
    """Run the program."""
    print('Make your selection from the following options: ', end='\n\n')
    print('[1] - Encrypt a message')
    print('[2] - Decrypt a message')
    print('[3] - Cancel', end='\n\n', flush=True)
    choice = prompt_user('Please enter either 1, 2, or 3: ', 1)
    print()
    option = []
    if choice == '1':
        option = ['encrypt', 'encryption', 'encrypted']
    elif choice == '2':
        option = ['decrypt', 'decryption', 'decrypted']
    elif choice == '3':
        print('Goodbye!')
        return
    print(f'Please enter the message that you would like to {option[0]}: ')
    message = input()
    print()
    key = prompt_user(f'Please enter an alphabetic string for the {option[1]} key: ', 2)
    print()
    print(f'The following is your {option[2]} message: ', end='\n\n')
    print(vigenere_cipher(message, key, f'{option[0]}'))
if __name__ == "__main__":
    main()

Test Cases and Discussion

In my implementation of the Vigenère cipher, I wanted a message that is decrypted to look the same as the original message, so I created the restore_original_format() function. This preserves spaces, punctuation, and other non-alphabetic characters that may have been present in the original message. I have not included an ability to remove these characters, which is a limitation of the program.

The following code blocks contain test cases of the major functions that make up my program. Although test cases are provided for functions that are not the vigenere_cipher() function, those functions are not meant to be called directly. They are meant to be like private utility functions and only valid inputs will ever get entered when they are called by the vigenere_cipher() function. Because these functions are not meant to be called directly, not all of them incorporate exception handling. The restore_original_format() function, for example, will throw an exception if the argument for the second parameter has more alphabetic characters than the first argument. This is not a handled exception, since attempting to handle it led to issues with preserving certain non-alphabetic characters in the original message when using the vigenere_cipher() function to encrypt and decrypt.

Another bug that I was not able to resolve is that when the main function is run, sometimes the prompt appears before the print statements, instead of after. This occurs intermittently and inconsistently, so I was not able to track down the cause of it.

def test_convert_letter_to_number(text):
    """Return None if the assert statement is true.
    
    The result of the function call to convert_letter_to_number(), is compared with a list of ints that 
    is constructed through using the ord() function. The ord() function returns an int that is a representation 
    of a Unicode character. There is a subtraction of 97, so that the lowercase letter 'a' starts at 0, in 
    accordance with the indexing of the list containing the alphabet.
    """
    assert convert_letter_to_number(text) == [ord(letter.lower()) - 97 for letter in text], 'not equal'

test_convert_letter_to_number('Python')
test_convert_letter_to_number('cryptography')
test_convert_letter_to_number('chemistry')
test_convert_letter_to_number('electroencephalography')
test_convert_letter_to_number('mountains')
print('PASS')
def test_convert_number_to_letter(numbers):
    """Return None if the assert statement is true.
    
    The result of the function call to convert_number_to_letter(), is compared with a string that is 
    constructed from the join() function used with a list that is constructed through using the chr() 
    function. The chr() function returns a str from an int that is a representation of a Unicode character. 
    There is an addition of 97, so that 0 is associated with the lowercase letter 'a', in accordance with 
    the indexing of the list containing the alphabet.
    """
    assert convert_number_to_letter(numbers) == ''.join([chr(number + 97) for number in numbers]), 'not equal'
    
test_convert_number_to_letter([15, 24, 19, 7, 14, 13]) # numeric equivalent of 'python'
test_convert_number_to_letter([2, 17, 24, 15, 19, 14, 6, 17, 0, 15, 7, 24]) # numeric equivalent of 'crytography'
test_convert_number_to_letter([2, 7, 4, 12, 8, 18, 19, 17, 24]) # numeric equivalent of 'chemistry'
test_convert_number_to_letter([4, 11, 4, 2, 19, 17, 14, 4, 13, 2, 4, 15, 7, 0, 11, 14, 6, 17, 0, 15, 7, 24]) # numeric equivalent of 'electroencephalography'
test_convert_number_to_letter([12, 14, 20, 13, 19, 0, 8, 13, 18]) # numeric equivalent of 'mountains'
print('PASS')
assert restore_original_format('Ice!!Cream', 'abcdefgh') == 'Abc!!Defgh', 'not equal'
assert restore_original_format('Quantum -theory', 'abcdefghijklm') == 'Abcdefg -hijklm', 'not equal'
assert restore_original_format('CrYpToGrApHy', 'abcdefghijkl') == 'AbCdEfGhIjKl', 'not equal'
assert restore_original_format('  -Python', 'abcdef') == '  -Abcdef', 'not equal'
assert restore_original_format(' P r o g r a m m i n g', 'abcdefghijk') == ' A b c d e f g h i j k', 'not equal'
print('PASS')
# The following test cases are based on the encryption and decryption results from:
# https://www.scopulus.co.uk/tools/vigenerecipher.htm

assert vigenere_cipher('Python', 'program', 'encrypt') == 'Ephnfn', 'not equal'
assert vigenere_cipher('   Cryptography ', 'code', 'encrypt') == '   Efbtvcjvcdkc ', 'not equal'
assert vigenere_cipher('ChEmIsTry', 'science', 'encrypt') == 'UjMqVuXja', 'not equal'
assert vigenere_cipher('Electro - encephalography', 'neuroscience', 'encrypt') == 'Rpythjq - mrpgtueffujcxll', 'not equal'
assert vigenere_cipher('Quantum theory!!!', 'physics', 'encrypt') == 'Fbyfbwe iocgza!!!', 'not equal'

assert vigenere_cipher('Ephnfn', 'program', 'decrypt') == 'Python', 'not equal'
assert vigenere_cipher('   Efbtvcjvcdkc ', 'code', 'decrypt') == '   Cryptography ', 'not equal'
assert vigenere_cipher('UjMqVuXja', 'science', 'decrypt') == 'ChEmIsTry', 'not equal'
assert vigenere_cipher('Rpythjq - mrpgtueffujcxll', 'neuroscience', 'decrypt') == 'Electro - encephalography', 'not equal'
assert vigenere_cipher('Fbyfbwe iocgza!!!', 'physics', 'decrypt') == 'Quantum theory!!!', 'not equal'
print('PASS')

About

A Python implementation of the Vigenère Cipher.


Languages

Language:Python 100.0%