I have written a pair of programs in Python that can be used to encrypt, decrypt, and crack Caesar and Vigenere Ciphered text. I am fairly new to Python and I wrote these programs largely to try and test myself on what I had learned so far with a practical problem, although I cannot claim that every line of these programs is my own. I would appreciate feedback related to any aspect of my program: Efficiency of my algorithms, efficiency of my implementation, quality of my Python, overlooked features, bugs, etc.
I have included a sample of text I used for testing below.
vigenere.py
#!/usr/bin/python3
"""
vigenere.py - Vigenere tool, can use statistical analysis to guess keys of varying length for enciphered text
Options:
--encrypt - enable encryption mode
--decrypt - enable decryption mode
--preserve-spacing - preserve the spacing of the input in the output
--key - specify the encryption key
--spacing - specify the output spacing
--guess - attempt to guess the encryption key by statistical analysis
Todo:
- Implement n-gram analysis to improve accuracy of key guesses
- Perform frequency analysis of deciphered text to improve accuracy of key guesses
Future:
- Add support for multiple languages
- Include standard deviations for each letter frequency
- Get better numbers for frequency analysis
"""
import argparse
import re
import string
from itertools import cycle
def buildSubStrings(string, seperation): # Build all substrings required to analyse the polyalphabetic cipher
return [string[i::seperation] for i in range(seperation)]
def frequencyAnalysis(string): # Normalised frequency analysis
freq = [0] * 26
for c in string:
freq[ord(c) - ord('A')] += 1
total = sum(freq)
for i in range(0, len(freq)):
freq[i] /= (float(total) / 100)
return freq
def initialiseParser():
parser = argparse.ArgumentParser(description = "Encrypt or decrpyt a string using the Caesar Cipher")
parser.add_argument("--encrypt", "--enc", "-e", help = "encryption mode (default)", action = "store_true")
parser.add_argument("--decrypt", "--dec", "-d", help = "decryption mode", action = "store_true")
parser.add_argument("--preserve-spacing", "--preserve", "-p", help = "use same spacing as the input text", action = "store_true", dest = "preserveSpacing")
parser.add_argument("--key", "-k", help = "encryption key for vigenere cipher", type = str)
parser.add_argument("--spacing", "-s", help = "specify the spacing in output", type = int)
parser.add_argument("--guess", "-g", help = "Attempt to guess the most likely key value", action = "store_true")
return parser
def scoreCalculator(frequencyAnalysis, shift): # Calculates a weighted score for a given shift value
englishFrequencies = [ 8.167, 1.492, 2.782,
4.253, 12.702, 2.228,
2.015, 6.094, 6.966,
0.153, 0.772, 4.025,
2.406, 6.749, 7.507,
1.929, 0.095, 5.987,
6.327, 9.056, 2.758,
0.978, 2.360, 0.150,
1.974, 0.074 ]
score = 0
for index in range(0, 26):
shiftIndex = (index + shift) % 26
score += abs(frequencyAnalysis[index] - englishFrequencies[shiftIndex])
return score / 26
def shiftCalculator(frequencyAnalysis): # Calculates the most likely shift value for a substring by comparing weighted scores of different shift values
bestGuess = ''
bestGuessScore = float('inf')
for shift in range(1, 27):
score = scoreCalculator(frequencyAnalysis, shift)
if score < bestGuessScore:
bestGuessScore = score
bestGuess = chr(ord('Z') - shift + 1)
return bestGuess
def stringPrepare(string, preserveSpacing): # Strip all non alphabetic characters from a string and convert to upper case
if preserveSpacing == True:
regex = '[^A-Z\s]'
else:
regex = '[^A-Z]'
return re.compile(regex).sub('', string).upper()
def vigenere(plaintext, key, encrypt):
alphabet = string.ascii_uppercase
output = ''
shift = 1
if encrypt == False:
shift = -1
for x, y in zip(stringPrepare(plaintext, False).upper(), cycle(key.upper())):
output += alphabet[(alphabet.index(x) + alphabet.index(y) * shift) % 26]
return output
def main():
parser = initialiseParser()
args = parser.parse_args()
rawText = stringPrepare(str.upper(input('')), True)
strippedText = stringPrepare(rawText, False)
if args.decrypt or args.encrypt:
if(args.key != None):
output = vigenere(strippedText, args.key, args.encrypt)
else:
print("Error: No key given!")
elif args.guess:
maxGuess = 30 if len(strippedText) > 30 else len(strippedText)
keyList = list()
for guess in range(2, maxGuess):
substringList = buildSubStrings(strippedText, guess)
frequencyAnalysisList = list()
key = ''
for subString in substringList:
frequencyAnalysisList.append(frequencyAnalysis(subString))
for frequency in frequencyAnalysisList:
key += shiftCalculator(frequency)
keyList.append(key)
bestGuess = ''
bestGuessScore = float('inf')
for key in keyList:
score = scoreCalculator(frequencyAnalysis(str.upper(vigenere(strippedText, key, False))), 0)
if score < bestGuessScore:
bestGuessScore = score
bestGuess = key
print("Best key guess: %s\nAttepting decryption..." % bestGuess)
output = vigenere(strippedText, bestGuess, False)
if args.preserveSpacing:
for x in range(0, len(rawText)):
if rawText[x] == ' ':
output = output[:x] + ' ' + output[x:] # Reinsert the stripped spaces back into the output
elif args.spacing:
if args.spacing > 0:
output = ' '.join([output[i:i + args.spacing] for i in range(0, len(output), args.spacing)])
print(output)
if __name__ == "__main__":
main()
caesar.py
#!/usr/bin/python3
"""
caesar.py - Caesar Cipher tool, can use statistical analysis to guess the shift value of Caesar Ciphered text
Options:
--bruteforce - attempt to bruteforce the shift value
--encrypt - enable encryption mode
--decrypt - enable decryption mode
--preserve-spacing - preserve the spacing of the input in the output
--shift - specify the shift value
--spacing - specify the output spacing
--guess - attempt to guess the shift value by statistical analysis
Todo:
- Implement n-gram analysis to improve accuracy of key guesses
Future:
- Add support for multiple languages
- Include standard deviations for each letter frequency
- Get better numbers for frequency analysis
"""
import argparse
import re
def caesar(string, shift):
return "".join(chr(((ord(char) - 65 + shift) % 26) + 65) if not char.isspace() else " " for char in string)
def frequencyAnalysis(string): # Normalised frequency analysis
freq = [0] * 26
for c in string:
if c.isalpha():
freq[ord(c) - ord('A')] += 1
total = sum(freq)
for i in range(0, len(freq)):
freq[i] /= (float(total) / 100)
return freq
def initialiseParser():
parser = argparse.ArgumentParser(description = "Encrypt or decrpyt a string using the Caesar Cipher")
parser.add_argument("--bruteforce", "--brute", "-b", help = "bruteforce mode", action = "store_true")
parser.add_argument("--encrypt", "--enc", "-e", help = "encryption mode (default)", action = "store_true")
parser.add_argument("--decrypt", "--dec", "-d", help = "decryption mode", action = "store_true")
parser.add_argument("--preserve-spacing", "--preserve", "-p", help = "use same spacing as original string", action = "store_true")
parser.add_argument("--shift", "-s", help = "value for the Caesar shift", type = int, choices = range(1, 26))
parser.add_argument("--spacing", "-x", help = "specify the spacing in output", type = int)
parser.add_argument("--guess", "-g", help = "use statistical analysis to guess the most likely shift value", action = "store_true")
return parser
def shiftScoreCalculator(frequencyAnalysis, shift): # Calculates a weighted score for a given shift value
englishFrequencies = [ 8.167, 1.492, 2.782,
4.253, 12.702, 2.228,
2.015, 6.094, 6.966,
0.153, 0.772, 4.025,
2.406, 6.749, 7.507,
1.929, 0.095, 5.987,
6.327, 9.056, 2.758,
0.978, 2.360, 0.150,
1.974, 0.074 ]
score = 0
for index in range(0, 26):
shiftIndex = (index + shift) % 26
score += abs(frequencyAnalysis[index] - englishFrequencies[shiftIndex])
return score / 26
def shiftCalculator(frequencyAnalysis): # Calculates the most likely shift value for a substring by comparing weighted scores of different shift values
bestGuess = ''
bestGuessVal = float('inf')
for shift in range(1, 27):
score = shiftScoreCalculator(frequencyAnalysis, shift)
if score < bestGuessVal:
bestGuessVal = score
bestGuess = 26 - shift
return bestGuess
def main():
parser = initialiseParser()
args = parser.parse_args()
if args.bruteforce:
bruteforce = True
else:
bruteforce = False
shift = args.shift
if args.decrypt:
shift = -shift
if args.preserve_spacing:
regex = '[^A-Z\s]'
else:
regex = '[^A-Z]'
string = re.compile(regex).sub('', input().upper())
if args.spacing:
string = ' '.join([string[i:i + args.spacing] for i in range(0, len(string), args.spacing)])
if args.guess:
shift = shiftCalculator(frequencyAnalysis(string))
print("Best shift value guess: %d (%c)\nAttempting decryption...\n%s" % (shift, chr(shift + ord('A') - 1), caesar(string, -shift)))
return
if bruteforce:
for shift in range(1, 26):
print("%d:\t%s" %(shift, caesar(string, -shift)))
else:
print(caesar(string, shift))
if __name__ == "__main__":
main()
rainbow-passage.txt
When the sunlight strikes raindrops in the air, they act as a prism and form a rainbow. The rainbow is a division of white light into many beautiful colors. These take the shape of a long round arch, with its path high above, and its two ends apparently beyond the horizon. There is , according to legend, a boiling pot of gold at one end. People look, but no one ever finds it. When a man looks for something beyond his reach, his friends say he is looking for the pot of gold at the end of the rainbow. Throughout the centuries people have explained the rainbow in various ways. Some have accepted it as a miracle without physical explanation. To the Hebrews it was a token that there would be no more universal floods. The Greeks used to imagine that it was a sign from the gods to foretell war or heavy rain. The Norsemen considered the rainbow as a bridge over which the gods passed from earth to their home in the sky. Others have tried to explain the phenomenon physically. Aristotle thought that the rainbow was caused by reflection of the sun’s rays by the rain. Since then physicists have found that it is not reflection, but refraction by the raindrops which causes the rainbows. Many complicated ideas about the rainbow have been formed. The difference in the rainbow depends considerably upon the size of the drops, and the width of the colored band increases as the size of the drops increases. The actual primary rainbow observed is said to be the effect of super-imposition of a number of bows. If the red of the second bow falls upon the green of the first, the result is to give a bow with an abnormally wide yellow band, since red and green light when mixed form yellow. This is a very common type of bow, one showing mainly red and yellow, with little or no green or blue
Example Vigenere Encryption
./vigenere.py -e -k RAINBOW < rainbow-passage.txt
Output
NHMAUVAJUVYJUDKSBEJYAJRIVORNFPAVOHDVAQEUVAPAKGBGWGRQFNOJUFWENONRIVOPKPYEZNJBXFWQFBREMIAVPBKWWPVUSHZGPGJBPFMIAZPARUBVGIHTOTBSGPYEARUOGVTPRTVWGEWSBZKEGZBVBZRRKUXWPYIBFQOPYHQTIOXFVMNOREKSBJPSJUSICQONVNBYZPAPOVQUVAYOZVACJKHMEFWORCKBSREEGBBMSCVNLNCCECIVTQCPFFOBMRWKOVRFBZGEWCMSHFOSOVHJFOVRFJAIFQAEGEKWPROOIRNTBPYOWOZFPAAKHQAHPAPOVQIWOIEIPIVEJFZVFBZJSILISEJLWBLWJXFWEUVAGOBBGUKCDIGUVAVNLBGHDVRIVOPKNTPEPICYOCGUVATEVGVFEVSXRPDHVHIIFSTGLIVOSZKHMEBWJSOEVOJWIIWHTKWPSABNSDRVMNDQAGTMQJHWJAUVSOYCEEVUVKLTXUZGETATRYDHRNIGJCJKOBUFVASRMJTWPNAANUCGVNBUBHPYEZRXCQCDJROCIFRMHOWRVRANMTHFOLFUVAXRMRLGQJELGPWIRGQAFHDRTQGXOORSQTOTNFMBUFUKUSBBGCNVTMYMKWIOZUFORPRIVOHDVNWETSIVNKBOGEUEZREHDVRIVOPKNAANCFEUGMBWSNNHQPIHDVGWQTDWJSMQGFKDEIEUVPFTPRJFDFMMVOHDVSSLPHDVRAUBJAKRQREHKVXXYBWJKHMCISJFMMAPBLYYAVDOHCYIEJGPFTTRUVKLGPGUVWKTPRSOEEBWJXOOTACFFRXPRMSMSYKIWAPTPYEAHOGNRYAOZHDVRIVOGEECMGISJGHGFJQEJTAUBJAWOCAEHDRTQGJGJFTZRGZATTQBOPQKRMSSOYKIWACMPYEZNJBZIOXFXVETHKNVGAJTPRSOEEBWJTAWEYKBNDHZCIGFREUEIFBPKLTBUFFWZNJBXVWMEJRFBBFRUREHDVDQSGSNVNKRJBPYEZNJBXFWLRQSJUSKBOGEUEZNCZULPWAUVAJIHRPTPYELEPDORNLGISSZDBUPTPYEKBMCNVDJNOREECZRBGAJAAGISOZZMBGHDVDZBQGEECZRBGAJTPRBQPLATCSWIRRGEBWJSOEBCGAIVMQJGORILGPPAKHMRGTATTWSTILVRQZQCOZTQBOCBRNCZCSNFFJBXGEWTPRSSZFFBUFGATOVQCCSWATYTILFNBUFUNVEVBGHDVFQETHPYEZRTIHKIAGPUEMEIOPKSZTPNOOXEOZZBZHPWQQFMACLWJCOJUSQADSNVDIAEUNVEVYJUDKWPROAEOELSPFIPETYPKPYIAVTORVRGPPAIFNBLQSKWBWJPBAJHWJJBCDAQAMMNVDIAEMACLWJXWPYLQGUZAFRVBHFAVNWECZQV
Example Vigenere Cracking
./vigenere -g < ciphertext.txt
Output
Best key guess: RAINBOW Attepting decryption... WHENTHESUNLIGHTSTRIKESRAINDROPSINTHEAIRTHEYACTASAPRISMANDFORMARAINBOWTHERAINBOWISADIVISIONOFWHITELIGHTINTOMANYBEAUTIFULCOLORSTHESETAKETHESHAPEOFALONGROUNDARCHWITHITSPATHHIGHABOVEANDITSTWOENDSAPPARENTLYBEYONDTHEHORIZONTHEREISACCORDINGTOLEGENDABOILINGPOTOFGOLDATONEENDPEOPLELOOKBUTNOONEEVERFINDSITWHENAMANLOOKSFORSOMETHINGBEYONDHISREACHHISFRIENDSSAYHEISLOOKINGFORTHEPOTOFGOLDATTHEENDOFTHERAINBOWTHROUGHOUTTHECENTURIESPEOPLEHAVEEXPLAINEDTHERAINBOWINVARIOUSWAYSSOMEHAVEACCEPTEDITASAMIRACLEWITHOUTPHYSICALEXPLANATIONTOTHEHEBREWSITWASATOKENTHATTHEREWOULDBENOMOREUNIVERSALFLOODSTHEGREEKSUSEDTOIMAGINETHATITWASASIGNFROMTHEGODSTOFORETELLWARORHEAVYRAINTHENORSEMENCONSIDEREDTHERAINBOWASABRIDGEOVERWHICHTHEGODSPASSEDFROMEARTHTOTHEIRHOMEINTHESKYOTHERSHAVETRIEDTOEXPLAINTHEPHENOMENONPHYSICALLYARISTOTLETHOUGHTTHATTHERAINBOWWASCAUSEDBYREFLECTIONOFTHESUNSRAYSBYTHERAINSINCETHENPHYSICISTSHAVEFOUNDTHATITISNOTREFLECTIONBUTREFRACTIONBYTHERAINDROPSWHICHCAUSESTHERAINBOWSMANYCOMPLICATEDIDEASABOUTTHERAINBOWHAVEBEENFORMEDTHEDIFFERENCEINTHERAINBOWDEPENDSCONSIDERABLYUPONTHESIZEOFTHEDROPSANDTHEWIDTHOFTHECOLOREDBANDINCREASESASTHESIZEOFTHEDROPSINCREASESTHEACTUALPRIMARYRAINBOWOBSERVEDISSAIDTOBETHEEFFECTOFSUPERIMPOSITIONOFANUMBEROFBOWSIFTHEREDOFTHESECONDBOWFALLSUPONTHEGREENOFTHEFIRSTTHERESULTISTOGIVEABOWWITHANABNORMALLYWIDEYELLOWBANDSINCEREDANDGREENLIGHTWHENMIXEDFORMYELLOWTHISISAVERYCOMMONTYPEOFBOWONESHOWINGMAINLYREDANDYELLOWWITHLITTLEORNOGREENORBLUE