Skip to content

added submission for mcm2018 #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions mcm2018-assign1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# encryption_suite

Matt Mahowald
October 2015

Encryption Suite provides the user with numerous tools to encrypt and decrypt messages,
all prompted from a command line interface.

The source code is in crypto.py, and the remaining files are for testing.
1 change: 1 addition & 0 deletions mcm2018-assign1/caesar-cipher.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SBWKRQ
1 change: 1 addition & 0 deletions mcm2018-assign1/caesar-plain.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PYTHON
333 changes: 333 additions & 0 deletions mcm2018-assign1/crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
"""
File: crypto.py
Name: Matt Mahowald
Date: October 9th 2015
Description: crypto.py implements a cryptography suite that allows
the user to input a message or a filename and then either encrypt
or decrypt the message
"""
# Import to check if a file exists
import os.path

NUM_ALPHA_LETTERS = 26
CAESAR_OFFSET = 3
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job with these constants!


def format_text(text):
# Formats all text to upper case and only alpha characters
to_encrypt = ""
for ch in text:
if(ch.isalpha()):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parentheses aren't necessary in Python!

if ch.isalpha():
  to_encrypt += ch

would be sufficient

to_encrypt += ch
return to_encrypt.upper()

def encrypt_caesar(plaintext):
"""
Encrypts plaintext using a Caesar cipher.
Creates a string to which each character offset by CAESAR_OFFSET
is appended, unless the character's ASCII value + CAESAR_OFFSET
is greater thah the ASCII value of 'Z', in which case the func
subtracts by NUM_ALPHA_LETTERS
"""
plaintext = format_text(plaintext)
print("Encrypting {0} using Caesar cipher".format(plaintext))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job using the format operator

new_string = ""
for letter in plaintext:
if(letter.isalpha()):
if((ord(letter) + CAESAR_OFFSET) <= ord('Z')):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a nit: would be nice to also abstract out 'Z' from the logic here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concise way to solve this problem with some more pythonic constructs is to zip together two strings that are offset by 3 characters from each other.

new_string += chr((ord(letter) + CAESAR_OFFSET))
else:
new_string += chr((ord(letter) + CAESAR_OFFSET - NUM_ALPHA_LETTERS))
return new_string

def decrypt_caesar(ciphertext):
"""
Decrypts a ciphertext using a Caesar cipher.
Reverses the operation of the Caesar encryption, subtracting
CAESAR_OFFSET from each character, wrapping around the end of
the alphabet.
"""
ciphertext = format_text(ciphertext)
print("Decrypting {0} using Caesar cipher".format(ciphertext))
new_string = ""
for letter in ciphertext:
if(letter.isalpha()):
if((ord(letter) - CAESAR_OFFSET) >= ord('A')):
new_string += chr((ord(letter) - CAESAR_OFFSET))
else:
new_string += chr((ord(letter) - CAESAR_OFFSET + NUM_ALPHA_LETTERS))
return new_string
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This decrypt method is pretty clean - might be arguably more concise if you decided to a less space-efficient approach by using a map by following the note above


def encrypt_vigenere(plaintext, keyword):
"""
Encrypts plaintext using a Vigenere cipher with a keyword.
Accepts a keyword to align with the text to be encoded, adding the
distance between 'A' and each letter of the keyword to each character's
ASCII value.
"""
plaintext = format_text(plaintext)
keyword = format_text(keyword)
if(keyword == ""): return plaintext
print("Encrypting {0} using Vigenere cipher with key {1}".format(plaintext, keyword))
plaintext = plaintext.upper()
new_string = ""
for i in range(len(plaintext)):
key_char = keyword[i % len(keyword)]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely done here!

new_chr = chr(ord(plaintext[i]) + ord(key_char) - ord('A'))
while(ord(new_chr) < ord('A') or ord(new_chr) > ord('Z')):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if you would want to subtract NUM_ALPHA_LETTERS in the case is < ord('A')

new_chr = chr(ord(new_chr) - NUM_ALPHA_LETTERS)
new_string += new_chr
return new_string

def decrypt_vigenere(ciphertext, keyword):
"""
Decrypts ciphertext using a Vigenere cipher with a keyword.
Accepts a keyword to align with the text to be decoded, subtracting the
distance between 'A' and each letter of the keyword to each character's
ASCII value.
"""
keyword = format_text(keyword)
ciphertext = format_text(ciphertext)
if(keyword == ""): return ciphertext
print("Decrypting {0} using Vigenere cipher with key {1}".format(ciphertext, keyword))
new_string = ""
for i in range(len(ciphertext)):
key_char = keyword[i % len(keyword)]
new_chr = chr(ord(ciphertext[i]) + ord('A') - ord(key_char))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would suggest using python's enumerate utility to grab the index i and the value ciphertext[i] all at once

for i, val in enumerate(ciphertext)

if(not new_chr.isalpha()):
new_chr = chr(ord(new_chr) + NUM_ALPHA_LETTERS)
new_string += new_chr
return new_string

def encrypt_railfence(plaintext, num_rails):
"""
Encrypts plaintext using a railfence cipher.
Traverses the string as if it were a zig zag, adding each
letter to its "rail", represented by a list of strings to
designate each row of the railfence

WEAREDISCOVEREDFLEEATONCE
becomes
W E C R L T E
E R D S O E E F E A O C
A I V D E N
WECRLTEERDSOEEFEAOCAIVDEN
"""
plaintext = format_text(plaintext)
print("Encrypting {0} using railfence cipher with {1} rails".format(plaintext, num_rails))
rails =[""] * num_rails
row = 0
d_row = -1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: would be nice to have a more descriptive variable name

for ch in plaintext:
if(not ch.isalpha()): continue
if(row == (num_rails - 1) or row == 0):
d_row *= -1
rails[row] = rails[row] + ch
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+= to be consistent with your other lines

row += d_row
return "".join(rails)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job with the join operator here!

# http://codegolf.stackexchange.com/questions/10544/rail-fence-cipher
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for citing your source, your solution is actually fairly different which is cool




def decrypt_railfence(ciphertext, num_rails):
"""
Decrypts ciphertext using a railfence cipher.
Identifies the different rails, then strings them together
one letter at a time, traversing the path of the rail.
"""
ciphertext = format_text(ciphertext)
print("Decrypting {0} using railfence cipher with {1} rails".format(ciphertext, num_rails))
railfence_constant = (num_rails - 1) * 2
l = []
rail_length = len(ciphertext) // railfence_constant
leftover = len(ciphertext) % railfence_constant
letters_used = 0
# Walks through string, breaking up the cipher into the "rails"
for i in range(railfence_constant):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're not really using i in this case - the python convention for ignoring a variable is to do:

for _ in range(railfence_constant)

if(leftover > 0):
l.append(ciphertext[letters_used: letters_used + rail_length + 1])
letters_used += rail_length + 1
leftover -= 1
else:
l.append(ciphertext[letters_used: letters_used + rail_length])
letters_used += rail_length
secret_message = ""
# Combines the rail pairs except for the edge case rails
for i in range(num_rails):
if(i == 0 or i == num_rails - 1):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can actually slice out these cases by looping over range(num_rails)[1:-1]

continue
l[i] = l[i] + l[i + 1]
l.remove(l[i + 1])
row = 0
d_row = -1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm maybe a more descriptive variable name than d_row

# Walks through rail position, combining rails into the secret message
for i in ciphertext:
if(row == (num_rails - 1) or row == 0):
d_row *= -1
secret_message += l[row][0]
l[row] = l[row][1:]
row += d_row
return secret_message

def read_from_file(filename):
"""
Reads and returns content from a file.
Returns a string representation of the file, could be troublesome
for larger files. Probably should revise to deal with this case
entirely separatelt in the program.
"""
raw_text = ""
with open(filename, 'r') as f:
lines = f.readlines()
for line in lines:
for ch in line:
if(ch.isalpha()):
raw_text += ch
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can actually do nested for loops within list comprehensions
raw_text = "".join([ch if ch.isalpha() for ch in line for line in lines])

return raw_text


def write_to_file(filename, content):
# Writes passed in content to any file, existing or not.
print("Writing content to {0}...".format(filename))
with open(filename, 'w+') as f:
f.write(content)

def get_keyword():
# Prompts for a keyword, assuring the keyword is not an empty string
word = input("Passkey? ").upper()
while(word == ""):
input("Please enter any sequence of characters: ").upper()
return word

def get_nrails():
'''
Prompts user for an integer, error checking to insure input is of
the correct type.
'''
user_input = input("Enter an integer: ")
try:
val = int(user_input)
except ValueError:
print("That's not an int!")
# http://stackoverflow.com/questions/5424716/python-how-to-check-if-input-is-a-number
return val

def transform_text(raw_text, operation, tool):
"""
Operates the requested encryption/decryption using
the proper tool.
"""
if(operation == 'E'):
if (tool == 'C'):
return encrypt_caesar(raw_text)
elif(tool == 'V'):
return encrypt_vigenere(raw_text, get_keyword())
elif(tool == 'R'):
return encrypt_railfence(raw_text, get_nrails())
elif(operation == 'D'):
if (tool == 'C'):
return decrypt_caesar(raw_text)
elif(tool == 'V'):
return decrypt_vigenere(raw_text, get_keyword())
elif(tool == 'R'):
return decrypt_railfence(raw_text, get_nrails())
else:
print("We're sorry, an error has occurred")

def prompt_for_filename():
# Assumes user will enter a valid filename
return input('Filename? ')

def run_suite():
"""
Runs a single iteration of the cryptography suite.

Asks the user for input text from a string or file, whether to encrypt
or decrypt, what tool to use, and where to show the output.
"""
print("*Input*")
raw_text = "" # the to-be-encrypted/decrypted text
if(type_is_file()):
filename = prompt_for_filename()
while(not os.path.exists(filename)):
filename = prompt_for_filename()
raw_text = read_from_file(filename).upper()
else:
raw_text = input("Enter the string to encrypt or decrypt: ").upper()

print("*Transform*")
operation = transformation_type()
tool = encryption_method()
message = transform_text(raw_text, operation, tool)
print("...")

print("*Output*")
if(type_is_file()):
write_to_file(prompt_for_filename(), message)
else:
print(message)

def encryption_method():
# Requests, returns, and error checks for the method of en/decryption
"""
Asks the user how they would like to encrypt or decrypt.
Responses that begin with a `C` return 'C'. (case-insensitively)
Responses that begin with a `V` return 'V'. (case-insensitively)
Responses that begin with a `R` return 'R'. (case-insensitively)
All other responses (including '') cause a reprompt.
"""
input_type = input("(C)aesar, (V)igenere, or (R)ailfence? ").upper()
while not input_type or input_type[0] not in ['C', 'V', 'R']:
input_type = input("Please enter either 'C', 'V', or 'R': ").upper()
return input_type[0]

def transformation_type():
"""
Asks the user whether they would like to encrypt or decrypt.
Responses that begin with a `E` return 'E'. (case-insensitively)
Responses that begin with a `D` return 'D'. (case-insensitively)
All other responses (including '') cause a reprompt.
"""
input_type = input("(E)ncrypt or (D)ecrypt? ").upper()
while not input_type or input_type[0] not in ['E', 'D']:
input_type = input("Please enter either 'E' or 'D': ").upper()
return input_type[0]

def type_is_file():
"""
Asks the user whether they would like to use a string or a file.
Responses that begin with a `F` return True. (case-insensitively)
Responses that begin with a `S` return False. (case-insensitively)
All other responses (including '') cause a reprompt.
"""
selection = input("(F)ile or (S)tring? ").upper()
while not selection or selection[0] not in ['F', 'S']:
selection = input("Please enter either 'F' or 'S': ").upper()
return selection[0] == 'F'

# Do not modify code beneath this point.
def should_continue():
"""
Asks the user whether they would like to continue.
Responses that begin with a `Y` return True. (case-insensitively)
Responses that begin with a `N` return False. (case-insensitively)
All other responses (including '') cause a reprompt.
"""
choice = input("Again (Y/N)? ").upper()
while not choice or choice[0] not in ['Y', 'N']:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool concise check here!

choice = input("Please enter either 'Y' or 'N'. Again (Y/N)? ").upper()
return choice[0] == 'Y'


def main():
"""Harness for the Cryptography Suite"""
print("Welcome to the Cryptography Suite!")
run_suite()
while should_continue():
run_suite()
print("Goodbye!")


if __name__ == '__main__':
"""This block is run if and only if the Python script is invoked from the
command line."""
main()
14 changes: 14 additions & 0 deletions mcm2018-assign1/feedback.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Name: Matt Mahowald
1) How long did this assignment take you to complete?
3-4 hours

2) What has been the best part of the class so far?
I've really enjoyed the class style. Good lectures and the labs are fun, as well as a 106-series feel to the work so far.

3) What can we do to make this class more enjoyable for you?
Class has been great! No suggestions.

4) What types of assignments would excite you in this class?
I'd really like to see what applications of python make it exceptional. Like flipping the string with [::-1] is super cool, what else can python do like that.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thanks for the tip, we'll definitely look for more Python idiosyncrasies that make developer life much more enjoyable!


Check output.txt for a secret message to be decrypted. Use the Vigenere decryption method with a keyword that is on the CS Ninety Two SI website. Or look no further than this document...
1 change: 1 addition & 0 deletions mcm2018-assign1/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VZNVXCHSYKFLMCUUQAKTLTSSKWOWPTNWL
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh! I wrote a quick script using your decrypt method to run through all the words on our website, but none of them gave anything readable...perhaps I'm not catching the "ninety two" hint...
@sredmond - you should look into this!

1 change: 1 addition & 0 deletions mcm2018-assign1/railfence-cipher.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WECRLTEERDSOEEFEAOCAIVDEN
1 change: 1 addition & 0 deletions mcm2018-assign1/railfence-plain.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WE ARE DISCOVERED. FLEE AT ONCE
Loading