166 lines
7.3 KiB
Python
Executable File
166 lines
7.3 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
################################################
|
|
### example program for VTI class
|
|
### caesar cipher encryption tool
|
|
### licensed under WTFPL, feel free to steal
|
|
### http://www.wtfpl.net/about/
|
|
################################################
|
|
|
|
|
|
from collections import OrderedDict, Counter
|
|
from itertools import islice, cycle
|
|
import re
|
|
import argparse
|
|
|
|
class EncryptionTools(object):
|
|
def __init__(self):
|
|
self.characters = ["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 make_cipher(self, shift):
|
|
alphabet = OrderedDict() #ordered dict is used to allow for shifting and to remember character positions
|
|
for char in self.characters:
|
|
alphabet[char] = char #first an unshifted alphabet dict is created
|
|
|
|
shift %= len(alphabet) #mod shift value by alphabet length to keep shift in range of alphabet in case of shift input as number >26 or negative number
|
|
cipher_alphabet = OrderedDict(
|
|
(k, v)
|
|
for k, v in zip(alphabet.keys(), islice(cycle(alphabet.values()), shift, None)) #build cipher dict by shifting values of alphabet dict
|
|
)
|
|
|
|
return cipher_alphabet
|
|
|
|
def make_reverse_cipher(self, alphabet):
|
|
inverse_alphabet = OrderedDict()
|
|
for key, value in alphabet.items():
|
|
inverse_alphabet[value] = key #keys and values are swapped to create inverse alphabet
|
|
|
|
return inverse_alphabet
|
|
|
|
def encrypt_text(self, text, shift):
|
|
alphabet = self.make_cipher(shift)
|
|
text = re.sub(r"[^A-Z]", "", text.upper()) #strip everything that is not uppercase letters
|
|
encrypted_text = []
|
|
|
|
for letter in text: #convert text to uppercase only
|
|
encrypted_text.append(alphabet.get(letter, letter)) #replace each letter with the corresponding cipher letter from the shifted alphabet
|
|
|
|
return str("".join(encrypted_text)) #since the string was processed letter by letter, it needs to be joined back up
|
|
|
|
def custom_dict_encrypt(self, text, custom_dict): #functionally the same, uses user entered dictionary as cipher
|
|
alphabet = custom_dict
|
|
text = re.sub(r"[^A-Z]", "", text.upper())
|
|
encrypted_text = []
|
|
|
|
for letter in text.upper():
|
|
encrypted_text.append(alphabet.get(letter, letter))
|
|
|
|
return str("".join(encrypted_text))
|
|
|
|
#It is possible to use the same function for both encryption and decryption of text just by shifting the alphabet a negative amount
|
|
#since such input is sanitized in the cipher making function.
|
|
#However, to avoid any confusion, here is a separate decryption function (and separate decryption mode).
|
|
|
|
def decrypt_text(self, text, shift):
|
|
inverse_alphabet = self.make_reverse_cipher(self.make_cipher(shift))
|
|
text = re.sub(r"[^A-Z]", "", text.upper())
|
|
decrypted_text = []
|
|
|
|
for letter in text.upper():
|
|
decrypted_text.append(inverse_alphabet.get(letter, letter))
|
|
|
|
return str("".join(decrypted_text))
|
|
|
|
def guess_rot(self, text):
|
|
text = re.sub(r"[^A-Z]", "", text.upper())
|
|
most_common_letters = Counter(text.upper()).most_common(1) #find the most common ciphertext character, this is very likely E in english texts
|
|
#assuming the most frequent character in ciphertext to indeed represent plaintext E, calculate its offset from plaintext alphabet E.
|
|
#if the assumption is correct, we get the encoding offset
|
|
#guessed offset is calculated with mod 26 to keep the resulting integer positive in case of large offsets
|
|
possible_rot = (self.characters.index(str(most_common_letters[0][0])) - 4) % len(self.characters)
|
|
|
|
return possible_rot
|
|
|
|
def parse_custom_dict(self, key):
|
|
inp_raw = key.strip("\"").replace(" ", "") #strip enclosing quotations and remove whitespace
|
|
|
|
try:
|
|
inp_pairs = inp_raw.split(",") #split input into "key:value" strings
|
|
inp_kvpairs = []
|
|
|
|
for i in inp_pairs:
|
|
inp_kvpairs.append(inp_pairs[inp_pairs.index(i)].split(":")) #split "key:value" strings into separate ["key", "value"] pairs
|
|
except Exception as e:
|
|
print("Problem processing dictionary input.\n%s" % e)
|
|
return
|
|
|
|
try:
|
|
custom_dict = OrderedDict(
|
|
(k.upper(), v.upper()) #convert custom dictionary keys and values to uppercase
|
|
for k, v in inp_kvpairs) #assign keys and values
|
|
|
|
filtered_dict = OrderedDict()
|
|
duplicates = []
|
|
#filter out duplicate values. Duplicate keys are not a problem, each reassignment only changes the value
|
|
for k, v in custom_dict.items():
|
|
if k.isalpha() == 0 or v.isalpha() == 0:
|
|
print("Only alphabetic characters are accepted for custom dictionary")
|
|
return
|
|
if v not in filtered_dict.values():
|
|
filtered_dict[k] = v
|
|
else:
|
|
duplicates.append(k)
|
|
|
|
#warn user if duplicates are detected but proceed anyway
|
|
if len(duplicates) > 0:
|
|
print("Warning, some keys have duplicate values. They will not be used. Duplicates: %s" % str(duplicates))
|
|
|
|
return(filtered_dict)
|
|
except Exception as e:
|
|
print("Problem parsing custom dictionary. Please check your input.\n%s" % e)
|
|
return
|
|
|
|
def run_encryption(text, shift):
|
|
print(enc_tools.encrypt_text(text, shift))
|
|
return 0
|
|
|
|
def run_custom_encryption(text, key):
|
|
custom_key = enc_tools.parse_custom_dict(key)
|
|
if custom_key is not None:
|
|
print(enc_tools.custom_dict_encrypt(text, custom_key))
|
|
return 0
|
|
else:
|
|
print("Encryption failed\n")
|
|
return 1
|
|
|
|
def run_decryption(text, shift):
|
|
print(enc_tools.decrypt_text(text, shift))
|
|
return 0
|
|
|
|
def run_freq_analysis_decryption(text):
|
|
print("Trying to decrypt using frequency analysis. This method works best on large ciphertexts.")
|
|
shift = enc_tools.guess_rot(text)
|
|
print("Text possibly encrypted using rot%s\n" % str(shift))
|
|
print(enc_tools.decrypt_text(text, shift))
|
|
return 0
|
|
|
|
enc_tools = EncryptionTools()
|
|
argparser = argparse.ArgumentParser(description="Caesar cipher encoding/decoding tool.")
|
|
argparser.add_argument("-s", "--shift", type=int, default=13, help="Use custom alphabet shift (default is rot13)")
|
|
argparser.add_argument("-k", "--key", help="Use custom alphabet, input as \"A:N, B:O, ...\" with substitution for each character")
|
|
argparser.add_argument("-d", "--decrypt", action="store_const", const="decrypt", help="Set mode to decode")
|
|
argparser.add_argument("-f", "--force", action="store_const", const="force", help="Try to guess original encryption")
|
|
argparser.add_argument("text", type=str, help="Text to be encoded or decoded")
|
|
|
|
args = argparser.parse_args()
|
|
|
|
if args.force and not args.decrypt:
|
|
print("Frequency analysis can only be used to decrypt text")
|
|
elif args.key:
|
|
run_custom_encryption(args.text, args.key)
|
|
elif args.decrypt and args.force:
|
|
run_freq_analysis_decryption(args.text)
|
|
elif args.decrypt:
|
|
run_decryption(args.text, args.shift)
|
|
else:
|
|
run_encryption(args.text, args.shift) |