From 4aaa491aaf5ad5c59665d09b7562e608505da7c4 Mon Sep 17 00:00:00 2001 From: dhhruv <180320107529.ce.dhruv@gmail.com> Date: Mon, 23 Nov 2020 19:34:56 +0530 Subject: [PATCH] Added EncrypC.py and Updated README.md --- EncrypC.py | 470 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- files/EncrypC.ico | Bin 0 -> 165422 bytes files/EncrypC.png | Bin 0 -> 2198 bytes 4 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 EncrypC.py create mode 100644 files/EncrypC.ico create mode 100644 files/EncrypC.png diff --git a/EncrypC.py b/EncrypC.py new file mode 100644 index 0000000..608d12c --- /dev/null +++ b/EncrypC.py @@ -0,0 +1,470 @@ +import os +import sys +import hashlib +import tkinter as tk +from tkinter import filedialog +from tkinter import messagebox +from Cryptodome.Cipher import AES + +class EncryptionTool: + def __init__(self, user_file, user_key, user_salt): + # get the path to input file + self.user_file = user_file + + self.input_file_size = os.path.getsize(self.user_file) + self.chunk_size = 1024 + self.total_chunks = (self.input_file_size // self.chunk_size) + 1 + + # convert the key and salt to bytes + self.user_key = bytes(user_key, "utf-8") + self.user_salt = bytes(user_key[::-1], "utf-8") + + # get the file extension + self.file_extension = self.user_file.split(".")[-1] + + # hash type for hashing key and salt + self.hash_type = "SHA256" + + # encrypted file name + self.encrypt_output_file = ".".join(self.user_file.split(".")[:-1]) \ + + "." + self.file_extension + ".encr" + + # decrypted file name + self.decrypt_output_file = self.user_file[:-5].split(".") + self.decrypt_output_file = ".".join(self.decrypt_output_file[:-1]) \ + + "_decrypted." + self.decrypt_output_file[-1] + + # dictionary to store hashed key and salt + self.hashed_key_salt = dict() + + # hash key and salt into 16 bit hashes + self.hash_key_salt() + + def read_in_chunks(self, file_object, chunk_size=1024): + """Lazy function (generator) to read a file piece by piece. + Default chunk size: 1k. + """ + while True: + data = file_object.read(chunk_size) + if not data: + break + yield data + + def encrypt(self): + # create a cipher object + cipher_object = AES.new( + self.hashed_key_salt["key"], + AES.MODE_CFB, + self.hashed_key_salt["salt"] + ) + + self.abort() # if the output file already exists, remove it first + + input_file = open(self.user_file, "rb") + output_file = open(self.encrypt_output_file, "ab") + done_chunks = 0 + + for piece in self.read_in_chunks(input_file, self.chunk_size): + encrypted_content = cipher_object.encrypt(piece) + output_file.write(encrypted_content) + done_chunks += 1 + yield (done_chunks / self.total_chunks) * 100 + + input_file.close() + output_file.close() + + # clean up the cipher object + del cipher_object + + def decrypt(self): + # exact same as above function except in reverse + cipher_object = AES.new( + self.hashed_key_salt["key"], + AES.MODE_CFB, + self.hashed_key_salt["salt"] + ) + + self.abort() # if the output file already exists, remove it first + + input_file = open(self.user_file, "rb") + output_file = open(self.decrypt_output_file, "xb") + done_chunks = 0 + + for piece in self.read_in_chunks(input_file): + decrypted_content = cipher_object.decrypt(piece) + output_file.write(decrypted_content) + done_chunks += 1 + yield (done_chunks / self.total_chunks) * 100 + + input_file.close() + output_file.close() + + # clean up the cipher object + del cipher_object + + def abort(self): + if os.path.isfile(self.encrypt_output_file): + os.remove(self.encrypt_output_file) + if os.path.isfile(self.decrypt_output_file): + os.remove(self.decrypt_output_file) + + + def hash_key_salt(self): + # --- convert key to hash + # create a new hash object + hasher = hashlib.new(self.hash_type) + hasher.update(self.user_key) + + # turn the output key hash into 32 bytes (256 bits) + self.hashed_key_salt["key"] = bytes(hasher.hexdigest()[:32], "utf-8") + + # clean up hash object + del hasher + + # --- convert salt to hash + # create a new hash object + hasher = hashlib.new(self.hash_type) + hasher.update(self.user_salt) + + # turn the output salt hash into 16 bytes (128 bits) + self.hashed_key_salt["salt"] = bytes(hasher.hexdigest()[:16], "utf-8") + + # clean up hash object + del hasher + +class MainWindow: + """ GUI Wrapper """ + + # configure root directory path relative to this file + THIS_FOLDER_G = "" + if getattr(sys, "frozen", False): + # frozen + THIS_FOLDER_G = os.path.dirname(sys.executable) + else: + # unfrozen + THIS_FOLDER_G = os.path.dirname(os.path.realpath(__file__)) + + def __init__(self, root): + self.root = root + self._cipher = None + self._file_url = tk.StringVar() + self._secret_key = tk.StringVar() + self._salt = tk.StringVar() + self._status = tk.StringVar() + self._status.set("---") + + self.should_cancel = False + + root.title("EncrypC") + root.configure(bg="#eeeeee") + + try: + icon_img = tk.Image( + "photo", + file=self.THIS_FOLDER_G + "/files/EncrypC.png" + ) + root.call( + "wm", + "iconphoto", + root._w, + icon_img + ) + except Exception: + pass + + self.menu_bar = tk.Menu( + root, + bg="#eeeeee", + relief=tk.FLAT + ) + self.menu_bar.add_command( + label="Tutorial", + command=self.show_help_callback + ) + + root.configure( + menu=self.menu_bar + ) + + self.file_entry_label = tk.Label( + root, + text="Enter File Path Or Click SELECT FILE Button", + bg="#eeeeee", + anchor=tk.W + ) + self.file_entry_label.grid( + padx=12, + pady=(8, 0), + ipadx=0, + ipady=1, + row=0, + column=0, + columnspan=4, + sticky=tk.W+tk.E+tk.N+tk.S + ) + + self.file_entry = tk.Entry( + root, + textvariable=self._file_url, + bg="#fff", + exportselection=0, + relief=tk.FLAT + ) + self.file_entry.grid( + padx=15, + pady=6, + ipadx=8, + ipady=8, + row=1, + column=0, + columnspan=4, + sticky=tk.W+tk.E+tk.N+tk.S + ) + + self.select_btn = tk.Button( + root, + text="SELECT FILE", + command=self.selectfile_callback, + width=42, + bg="#1089ff", + fg="#ffffff", + bd=2, + relief=tk.FLAT + ) + self.select_btn.grid( + padx=15, + pady=8, + ipadx=24, + ipady=6, + row=2, + column=0, + columnspan=4, + sticky=tk.W+tk.E+tk.N+tk.S + ) + + self.key_entry_label = tk.Label( + root, + text="Enter Key (To be Remembered while Decryption)", + bg="#eeeeee", + anchor=tk.W + ) + self.key_entry_label.grid( + padx=12, + pady=(8, 0), + ipadx=0, + ipady=1, + row=3, + column=0, + columnspan=4, + sticky=tk.W+tk.E+tk.N+tk.S + ) + + self.key_entry = tk.Entry( + root, + textvariable=self._secret_key, + bg="#fff", + exportselection=0, + relief=tk.FLAT + ) + self.key_entry.grid( + padx=15, + pady=6, + ipadx=8, + ipady=8, + row=4, + column=0, + columnspan=4, + sticky=tk.W+tk.E+tk.N+tk.S + ) + + self.encrypt_btn = tk.Button( + root, + text="ENCRYPT", + command=self.encrypt_callback, + bg="#ed3833", + fg="#ffffff", + bd=2, + relief=tk.FLAT + ) + self.encrypt_btn.grid( + padx=(15, 6), + pady=8, + ipadx=24, + ipady=6, + row=7, + column=0, + columnspan=2, + sticky=tk.W+tk.E+tk.N+tk.S + ) + + self.decrypt_btn = tk.Button( + root, + text="DECRYPT", + command=self.decrypt_callback, + bg="#00bd56", + fg="#ffffff", + bd=2, + relief=tk.FLAT + ) + self.decrypt_btn.grid( + padx=(6, 15), + pady=8, + ipadx=24, + ipady=6, + row=7, + column=2, + columnspan=2, + sticky=tk.W+tk.E+tk.N+tk.S + ) + + self.reset_btn = tk.Button( + root, + text="CLEAR", + command=self.reset_callback, + bg="#aaaaaa", + fg="#ffffff", + bd=2, + relief=tk.FLAT + ) + self.reset_btn.grid( + padx=15, + pady=(4, 12), + ipadx=24, + ipady=6, + row=8, + column=0, + columnspan=4, + sticky=tk.W+tk.E+tk.N+tk.S + ) + + self.status_label = tk.Label( + root, + textvariable=self._status, + bg="#eeeeee", + anchor=tk.W, + justify=tk.LEFT, + relief=tk.FLAT, + wraplength=350 + ) + self.status_label.grid( + padx=12, + pady=(0, 12), + ipadx=0, + ipady=1, + row=9, + column=0, + columnspan=4, + sticky=tk.W+tk.E+tk.N+tk.S + ) + + tk.Grid.columnconfigure(root, 0, weight=1) + tk.Grid.columnconfigure(root, 1, weight=1) + tk.Grid.columnconfigure(root, 2, weight=1) + tk.Grid.columnconfigure(root, 3, weight=1) + + def selectfile_callback(self): + try: + name = filedialog.askopenfile() + self._file_url.set(name.name) + except Exception as e: + self._status.set(e) + self.status_label.update() + + def freeze_controls(self): + self.file_entry.configure(state="disabled") + self.key_entry.configure(state="disabled") + self.select_btn.configure(state="disabled") + self.encrypt_btn.configure(state="disabled") + self.decrypt_btn.configure(state="disabled") + self.reset_btn.configure(text="CANCEL", command=self.cancel_callback, + fg="#ed3833", bg="#fafafa") + self.status_label.update() + + def unfreeze_controls(self): + self.file_entry.configure(state="normal") + self.key_entry.configure(state="normal") + self.select_btn.configure(state="normal") + self.encrypt_btn.configure(state="normal") + self.decrypt_btn.configure(state="normal") + self.reset_btn.configure(text="RESET", command=self.reset_callback, + fg="#ffffff", bg="#aaaaaa") + self.status_label.update() + + def encrypt_callback(self): + self.freeze_controls() + + try: + self._cipher = EncryptionTool( + self._file_url.get(), + self._secret_key.get(), + self._salt.get() + ) + for percentage in self._cipher.encrypt(): + if self.should_cancel: + break + percentage = "{0:.2f}%".format(percentage) + self._status.set(percentage) + self.status_label.update() + self._status.set("File Encrypted...") + if self.should_cancel: + self._cipher.abort() + self._status.set("Cancelled...") + self._cipher = None + self.should_cancel = False + except Exception as e: + self._status.set(e) + + self.unfreeze_controls() + + def decrypt_callback(self): + self.freeze_controls() + + try: + self._cipher = EncryptionTool( + self._file_url.get(), + self._secret_key.get(), + self._salt.get() + ) + for percentage in self._cipher.decrypt(): + if self.should_cancel: + break + percentage = "{0:.2f}%".format(percentage) + self._status.set(percentage) + self.status_label.update() + self._status.set("File Decrypted...") + if self.should_cancel: + self._cipher.abort() + self._status.set("Cancelled...") + self._cipher = None + self.should_cancel = False + except Exception as e: + self._status.set(e) + + self.unfreeze_controls() + + def reset_callback(self): + self._cipher = None + self._file_url.set("") + self._secret_key.set("") + self._salt.set("") + self._status.set("---") + + def cancel_callback(self): + self.should_cancel = True + + def show_help_callback(self): + messagebox.showinfo( + "Tutorial", + """1. Open the Application and Click SELECT FILE Button and select your file e.g. "mydoc.pdf" (OR You can add path manually). +2. Enter your Key (This should be alphanumeric letters). Remember this so you can Decrypt the file later. (Else you'll lose your file permanently) +3. Click ENCRYPT Button to encrypt the file. A new encrypted file with ".kryp" extention e.g. "mydoc.pdf.kryp" will be created in the same directory where the "mydoc.pdf" is. +4. When you want to Decrypt a file you, will select the file with the ".kryp" extention and Enter your Key which you chose at the time of Encryption. Click DECRYPT Button to decrypt. The decrypted file will be of the same name as before with the suffix "_decrypted" e.g. "mydoc_decrypted.pdf". +5. Click CLEAR Button to reset the input fields and status bar. +6. You can also Click CANCEL Button during Encryption/Decryption to stop the process or if it doesn't respond.""" + ) + + +if __name__ == "__main__": + ROOT = tk.Tk() + MAIN_WINDOW = MainWindow(ROOT) + ROOT.mainloop() diff --git a/README.md b/README.md index 645b197..10da2c0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # EncrypC File Encryption Application using Python -![Screenshot](https://raw.githubusercontent.com/ahmednooor/KrypApp/master/screenshot.png) +![encrypc](https://user-images.githubusercontent.com/72680045/99971140-cbbd0580-2dc2-11eb-97ec-e507a9bcbca2.PNG) ## Technologies Used: * Python3 diff --git a/files/EncrypC.ico b/files/EncrypC.ico new file mode 100644 index 0000000000000000000000000000000000000000..b0a0354dc9b7e4c52bde7c8c5f22e7aee4a7a3f6 GIT binary patch literal 165422 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 literal 0 HcmV?d00001