import tkinter as tk from tkinter import ttk, filedialog, messagebox import os import sys import threading import shutil import hashlib import json from Crypto.Cipher import AES from Crypto.Protocol.KDF import PBKDF2 from Crypto.Random import get_random_bytes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend # Configuration SALT_SIZE = 32 KEY_SIZE = 32 # AES-256 IV_SIZE = 16 ITERATIONS = 100000 CHUNK_SIZE = 64 * 1024 # 64KB to reduce memory usage during large file processing class SecureProtectedDisc: def derive_key(self, password: str, salt: bytes) -> bytes: return PBKDF2(password.encode(), salt, dkLen=KEY_SIZE, count=ITERATIONS) def load_key_file(self, key_path: str) -> bytes: """Load key from PEM file or raw key file.""" try: # Try to load as PEM private key with open(key_path, "rb") as f: private_key = serialization.load_pem_private_key( f.read(), password=None, backend=default_backend() ) private_bytes = private_key.private_bytes( encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() ) return hashlib.sha256(private_bytes).digest()[:KEY_SIZE] except: # Try as raw key file with open(key_path, "rb") as f: key = f.read() if len(key) not in (16, 24, 32): raise ValueError("Invalid key size. Must be 16, 24, or 32 bytes") return key def encrypt_with_key(self, input_path: str, output_path: str, key: bytes) -> None: """ Encrypt a file with a given raw key using chunk-based encryption (AES-CBC). """ iv = get_random_bytes(IV_SIZE) cipher = AES.new(key, AES.MODE_CBC, iv) with open(input_path, 'rb') as f_in, open(output_path, 'wb') as f_out: # Write IV first f_out.write(iv) while True: chunk = f_in.read(CHUNK_SIZE) if len(chunk) == 0: # No more data: final padded block padded_block = self._pad(b'') f_out.write(cipher.encrypt(padded_block)) break elif len(chunk) < CHUNK_SIZE: # Last (incomplete) chunk: pad then encrypt chunk = self._pad(chunk) f_out.write(cipher.encrypt(chunk)) break else: # Full chunk f_out.write(cipher.encrypt(chunk)) def decrypt_with_key(self, input_path: str, output_path: str, key: bytes) -> None: """ Decrypt a file with a given raw key using chunk-based decryption (AES-CBC). """ with open(input_path, 'rb') as f_in: # Read IV first iv = f_in.read(IV_SIZE) cipher = AES.new(key, AES.MODE_CBC, iv) with open(output_path, 'wb') as f_out: next_chunk = b'' while True: chunk = f_in.read(CHUNK_SIZE) if len(chunk) == 0: # Final decrypt, unpad final_block = cipher.decrypt(next_chunk) final_block = self._unpad(final_block) f_out.write(final_block) break decrypted = cipher.decrypt(next_chunk) f_out.write(decrypted) next_chunk = chunk def encrypt_with_password(self, input_path: str, output_path: str, password: str) -> tuple: """ Encrypt using password-based key derivation, chunk-based approach. """ salt = get_random_bytes(SALT_SIZE) key = self.derive_key(password, salt) iv = get_random_bytes(IV_SIZE) cipher = AES.new(key, AES.MODE_CBC, iv) with open(input_path, 'rb') as f_in, open(output_path, 'wb') as f_out: # Write salt + iv f_out.write(salt + iv) while True: chunk = f_in.read(CHUNK_SIZE) if len(chunk) == 0: # Final padded block padded_block = self._pad(b'') f_out.write(cipher.encrypt(padded_block)) break elif len(chunk) < CHUNK_SIZE: # Last chunk chunk = self._pad(chunk) f_out.write(cipher.encrypt(chunk)) break else: # Full chunk f_out.write(cipher.encrypt(chunk)) return salt, iv def decrypt_with_password(self, input_path: str, output_path: str, password: str) -> bool: """ Decrypt using password-based key derivation, chunk-based approach. """ with open(input_path, 'rb') as f_in: # Read salt + iv header = f_in.read(SALT_SIZE + IV_SIZE) salt = header[:SALT_SIZE] iv = header[SALT_SIZE:] key = self.derive_key(password, salt) cipher = AES.new(key, AES.MODE_CBC, iv) with open(output_path, 'wb') as f_out: next_chunk = b'' while True: chunk = f_in.read(CHUNK_SIZE) if len(chunk) == 0: final_block = cipher.decrypt(next_chunk) final_block = self._unpad(final_block) f_out.write(final_block) break decrypted = cipher.decrypt(next_chunk) f_out.write(decrypted) next_chunk = chunk return True def _pad(self, data: bytes) -> bytes: """ Standard PKCS#7 padding to make data a multiple of AES.block_size """ block_size = AES.block_size padding_length = block_size - (len(data) % block_size) return data + bytes([padding_length] * padding_length) @staticmethod def _unpad(data: bytes) -> bytes: """ Remove PKCS#7 padding. """ padding_length = data[-1] return data[:-padding_length] def create_checksum(self, file_path: str) -> str: """ Create SHA-256 checksum in a chunk-based manner. """ sha256 = hashlib.sha256() with open(file_path, 'rb') as f: while True: chunk = f.read(4096) if not chunk: break sha256.update(chunk) return sha256.hexdigest() class SecureProtectedDiscGUI(tk.Tk): def __init__(self): super().__init__() self.title("Secure Protected Disc Creator") self.geometry("900x600") self.configure(padx=20, pady=20) self.spd = SecureProtectedDisc() self.key_type = tk.StringVar(value="password") self.mode = tk.StringVar(value="encrypt") self.file_paths = [] self.output_dir = "" self.password = "" self.confirm_password = "" self.key_file = "" self.create_widgets() def create_widgets(self): # --- Mode Selection --- ttk.Label(self, text="Mode:").grid(row=0, column=0, sticky="w") encrypt_radio = ttk.Radiobutton(self, text="Encrypt", variable=self.mode, value="encrypt", command=self.update_ui) decrypt_radio = ttk.Radiobutton(self, text="Decrypt", variable=self.mode, value="decrypt", command=self.update_ui) encrypt_radio.grid(row=0, column=1, sticky="w") decrypt_radio.grid(row=0, column=2, sticky="w") # --- Key Type Selection --- ttk.Label(self, text="Encryption Type:").grid(row=1, column=0, sticky="w") ttk.Radiobutton(self, text="Password", variable=self.key_type, value="password", command=self.update_key_ui).grid(row=1, column=1, sticky="w") ttk.Radiobutton(self, text="Key File", variable=self.key_type, value="keyfile", command=self.update_key_ui).grid(row=1, column=2, sticky="w") # --- Password Fields --- self.pw_label = ttk.Label(self, text="Password:") self.pw_label.grid(row=2, column=0, sticky="w", pady=(10,0)) self.pw_entry = ttk.Entry(self, show="*", width=80) self.pw_entry.grid(row=3, column=0, columnspan=3, padx=5) self.confirm_pw_label = ttk.Label(self, text="Confirm Password:") self.confirm_pw_label.grid(row=4, column=0, sticky="w", pady=(10,0)) self.confirm_pw_entry = ttk.Entry(self, show="*", width=80) self.confirm_pw_entry.grid(row=5, column=0, columnspan=3, padx=5) # --- Key File Fields --- self.key_file_frame = ttk.Frame(self) self.key_file_frame.grid(row=6, column=0, columnspan=4, sticky="ew") self.key_file_button = ttk.Button(self.key_file_frame, text="Select Key File", command=self.select_key_file) self.key_file_button.pack(side=tk.LEFT) self.key_file_label = ttk.Label(self.key_file_frame, text="No key file selected") self.key_file_label.pack(side=tk.LEFT, padx=5) # --- File Selection --- ttk.Label(self, text="Select Files:").grid(row=7, column=0, sticky="w", pady=(10,0)) self.file_list = tk.Listbox(self, width=80, height=5, selectmode=tk.EXTENDED) self.file_list.grid(row=8, column=0, columnspan=4, padx=5, pady=5) btn_frame = ttk.Frame(self) btn_frame.grid(row=9, column=0, columnspan=4, pady=5) self.add_button = ttk.Button(btn_frame, text="Add Files", command=self.add_files) self.add_button.pack(side=tk.LEFT) self.remove_button = ttk.Button(btn_frame, text="Remove Selected", command=self.remove_files) self.remove_button.pack(side=tk.LEFT, padx=10) # 1) Button to remove all entries from the file list self.remove_all_button = ttk.Button(btn_frame, text="Clear All", command=self.clear_all_files) self.remove_all_button.pack(side=tk.LEFT, padx=10) # --- Output Directory --- ttk.Label(self, text="Output Directory:").grid(row=10, column=0, sticky="w", pady=(10,0)) self.output_dir_entry = tk.Entry(self, width=80) self.output_dir_entry.grid(row=11, column=0, columnspan=3, padx=5) self.browse_button = ttk.Button(self, text="Browse", command=self.browse_output_dir) self.browse_button.grid(row=11, column=3, padx=5) # --- Progress & Status --- self.progress = ttk.Progressbar(self, orient=tk.HORIZONTAL, length=760, mode='determinate') self.progress.grid(row=12, column=0, columnspan=4, pady=20) self.status = ttk.Label(self, text="Ready") self.status.grid(row=13, column=0, columnspan=4) # --- Action Buttons --- btn_frame2 = ttk.Frame(self) btn_frame2.grid(row=14, column=0, columnspan=4, pady=20) self.start_button = ttk.Button(btn_frame2, text="Start", command=self.start_process) self.start_button.pack(side=tk.LEFT) self.exit_button = ttk.Button(btn_frame2, text="Exit", command=self.destroy) self.exit_button.pack(side=tk.LEFT, padx=10) self.update_key_ui() def update_ui(self): """ Called whenever the Mode RadioButton changes (Encrypt or Decrypt). We also add the logic to automatically add all .DAT files in the same directory as this script if 'Decrypt' is selected. """ is_encrypt = self.mode.get() == "encrypt" if is_encrypt: self.add_button.config(text="Add Files") else: self.add_button.config(text="Select Meta File/Encrypted Files") # ----------------- AUTO ADD ALL *.DAT FILES ----------------- # Clear the list box first to avoid duplicates self.file_list.delete(0, tk.END) # Get the directory of this script script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) # Find all *.DAT files in the script directory and add them for f in os.listdir(script_dir): if f.lower().endswith(".dat"): full_path = os.path.join(script_dir, f) self.file_list.insert(tk.END, full_path) # ----------------------------------------------------------- def update_key_ui(self): if self.key_type.get() == "password": self.pw_label.config(state="normal") self.pw_entry.config(state="normal") self.confirm_pw_label.config(state="normal") self.confirm_pw_entry.config(state="normal") self.key_file_frame.grid_remove() else: self.pw_label.config(state="disabled") self.pw_entry.config(state="disabled") self.confirm_pw_label.config(state="disabled") self.confirm_pw_entry.config(state="disabled") self.key_file_frame.grid() def select_key_file(self): key_path = filedialog.askopenfilename( title="Select Key File", filetypes=[("PEM Files", "*.pem"), ("All Files", "*.*")] ) if key_path: self.key_file = key_path self.key_file_label.config(text=os.path.basename(key_path)) def add_files(self): if self.mode.get() == "encrypt": files = filedialog.askopenfilenames() if files: for f in files: if f not in self.file_list.get(0, tk.END): self.file_list.insert(tk.END, f) else: # Allow selection of either ENCRYPTED_META.DAT or encrypted files files = filedialog.askopenfilenames( title="Select ENCRYPTED_META.DAT and/or Encrypted Files", filetypes=[("Meta File", "*.DAT"), ("Encrypted Files", "*.DAT"), ("All Files", "*.*")] ) if files: for f in files: if f not in self.file_list.get(0, tk.END): self.file_list.insert(tk.END, f) def remove_files(self): selected = self.file_list.curselection() for index in reversed(selected): self.file_list.delete(index) def clear_all_files(self): """Remove all items from the file list.""" self.file_list.delete(0, tk.END) def browse_output_dir(self): directory = filedialog.askdirectory() self.output_dir_entry.delete(0, tk.END) self.output_dir_entry.insert(0, directory) def start_process(self): """Disable the Start button, then start encryption/decryption in a thread.""" if not self.validate_inputs(): return # Disable the start button until done self.start_button.config(state="disabled") self.output_dir = self.output_dir_entry.get() items = self.file_list.get(0, tk.END) try: if self.key_type.get() == "password": if self.pw_entry.get() != self.confirm_pw_entry.get(): messagebox.showerror("Error", "Passwords do not match!") self.start_button.config(state="normal") return self.password = self.pw_entry.get() threading.Thread(target=self.process_password_based, args=(items,)).start() else: if not self.key_file: messagebox.showerror("Error", "Please select a key file!") self.start_button.config(state="normal") return threading.Thread(target=self.process_keyfile_based, args=(items,)).start() except Exception as e: messagebox.showerror("Error", f"Initialization failed: {str(e)}") # Re-enable start button on failure self.start_button.config(state="normal") def validate_inputs(self): if self.file_list.size() == 0: messagebox.showerror("Error", "Please select at least one file/directory!") return False if self.key_type.get() == "password": if len(self.pw_entry.get()) < 8 and self.mode.get() == "encrypt": # Only enforce password length check in encryption mode messagebox.showerror("Error", "Password must be at least 8 characters!") return False if not self.output_dir_entry.get(): messagebox.showerror("Error", "Please select output directory!") return False return True def update_progress(self, value): self.progress['value'] = value self.update_idletasks() # # --------------- Encryption Process (Password-based) --------------- # def process_password_based(self, items): try: os.makedirs(self.output_dir, exist_ok=True) total_items = len(items) if self.mode.get() == "encrypt": meta_data = {"files": []} for idx, file_path in enumerate(items, 1): self.status.config(text=f"Encrypting file {idx}/{total_items}") self.update_progress((idx-1)*80/total_items) # Compute original file checksum original_checksum = self.spd.create_checksum(file_path) # Encrypt file enc_name = f"ENCRYPTED{idx}.DAT" enc_path = os.path.join(self.output_dir, enc_name) self.spd.encrypt_with_password(file_path, enc_path, self.password) # Add info to metadata meta_data["files"].append({ "original_name": os.path.basename(file_path), "encrypted_name": enc_name, "size": os.path.getsize(file_path), "original_checksum": original_checksum }) # Encrypt metadata self.status.config(text="Creating metadata...") temp_meta_path = os.path.join(self.output_dir, "temp_metadata.json") with open(temp_meta_path, 'w') as f: json.dump(meta_data, f) enc_meta_path = os.path.join(self.output_dir, "ENCRYPTED_META.DAT") self.spd.encrypt_with_password(temp_meta_path, enc_meta_path, self.password) os.remove(temp_meta_path) # Create checksum for the encrypted metadata checksum = self.spd.create_checksum(enc_meta_path) with open(os.path.join(self.output_dir, "CHECKSUM.TXT"), "w") as f: f.write(checksum) # Copy this script to output script_copy = os.path.join(self.output_dir, "SECPROTDISC_GUI.PY") shutil.copy(sys.argv[0], script_copy) self.update_progress(100) self.status.config(text=f"Files ready in: {self.output_dir}") messagebox.showinfo("Success", "Disc files prepared successfully!\nBurn all files to a disc.") else: self.decrypt_with_metadata(items, self.password) except Exception as e: messagebox.showerror("Error", f"Process failed: {str(e)}") finally: # Re-enable start button whether success or fail self.start_button.config(state="normal") self.progress['value'] = 0 self.status.config(text="Ready") # # --------------- Encryption Process (Key File-based) --------------- # def process_keyfile_based(self, items): try: key = self.spd.load_key_file(self.key_file) os.makedirs(self.output_dir, exist_ok=True) if self.mode.get() == "encrypt": meta_data = {"files": []} for idx, file_path in enumerate(items, 1): output_path = os.path.join(self.output_dir, f"ENCRYPTED{idx}.DAT") # Compute original file checksum original_checksum = self.spd.create_checksum(file_path) self.spd.encrypt_with_key(file_path, output_path, key) meta_data["files"].append({ "original_name": os.path.basename(file_path), "encrypted_name": f"ENCRYPTED{idx}.DAT", "size": os.path.getsize(file_path), "original_checksum": original_checksum }) # Encrypt metadata temp_meta_path = os.path.join(self.output_dir, "temp_metadata.json") with open(temp_meta_path, 'w') as f: json.dump(meta_data, f) enc_meta_path = os.path.join(self.output_dir, "ENCRYPTED_META.DAT") self.spd.encrypt_with_key(temp_meta_path, enc_meta_path, key) os.remove(temp_meta_path) # Create checksum checksum = self.spd.create_checksum(enc_meta_path) with open(os.path.join(self.output_dir, "CHECKSUM.TXT"), "w") as f: f.write(checksum) # Copy the script script_copy = os.path.join(self.output_dir, "SECPROTDISC_GUI.PY") shutil.copy(sys.argv[0], script_copy) messagebox.showinfo("Success", "Files encrypted successfully!") else: self.decrypt_with_metadata_key(items, key) except Exception as e: messagebox.showerror("Error", f"Key-based process failed: {str(e)}") finally: # Re-enable start button self.start_button.config(state="normal") self.progress['value'] = 0 self.status.config(text="Ready") # # --------------- Decryption (Password-based) --------------- # def decrypt_with_metadata(self, items, password): try: self.status.config(text="Decrypting with metadata...") meta_file_path = next((item for item in items if os.path.basename(item) == "ENCRYPTED_META.DAT"), None) if not meta_file_path: raise FileNotFoundError("ENCRYPTED_META.DAT not found in selected files.") # 1) Decrypt the metadata to a temporary file temp_meta_path = os.path.join(self.output_dir, "temp_metadata.json") self.spd.decrypt_with_password(meta_file_path, temp_meta_path, password) # 2) Load the metadata with open(temp_meta_path, 'r') as f: meta_data = json.load(f) os.remove(temp_meta_path) # 3) Let user choose which file(s) to decrypt selected_files = self.choose_files_to_decrypt(meta_data["files"]) if not selected_files: # If user cancelled or selected nothing, just return return total_files = len(selected_files) file_paths = {os.path.basename(item): item for item in items} for idx, file_info in enumerate(selected_files, 1): self.status.config(text=f"Decrypting file {idx}/{total_files}") self.update_progress(idx * 90 / total_files) enc_name = file_info["encrypted_name"] if enc_name not in file_paths: messagebox.showerror( "Error", f"Encrypted file {enc_name} not selected.\nPlease select all encrypted files and ENCRYPTED_META.DAT." ) return enc_path = file_paths[enc_name] output_path = os.path.join(self.output_dir, file_info["original_name"]) # Decrypt self.spd.decrypt_with_password(enc_path, output_path, password) # Checksum verify decrypted_checksum = self.spd.create_checksum(output_path) if decrypted_checksum != file_info["original_checksum"]: messagebox.showerror( "Checksum Mismatch", f"Decrypted file {file_info['original_name']} does not match the original checksum!" ) return self.update_progress(100) messagebox.showinfo("Success", f"Files decrypted to:\n{self.output_dir}") except FileNotFoundError as e: messagebox.showerror("Error", str(e)) except Exception as e: messagebox.showerror("Error", f"Decryption failed: {str(e)}") finally: self.update_progress(0) self.status.config(text="") # # --------------- Decryption (Key-based) --------------- # def decrypt_with_metadata_key(self, items, key): try: self.status.config(text="Decrypting with metadata...") meta_file_path = next((item for item in items if os.path.basename(item) == "ENCRYPTED_META.DAT"), None) if not meta_file_path: raise FileNotFoundError("ENCRYPTED_META.DAT not found in selected files.") # 1) Decrypt the metadata temp_meta_path = os.path.join(self.output_dir, "temp_metadata.json") self.spd.decrypt_with_key(meta_file_path, temp_meta_path, key) # 2) Load with open(temp_meta_path, 'r') as f: meta_data = json.load(f) os.remove(temp_meta_path) # 3) Let user choose which file(s) to decrypt selected_files = self.choose_files_to_decrypt(meta_data["files"]) if not selected_files: return total_files = len(selected_files) file_paths = {os.path.basename(item): item for item in items} for idx, file_info in enumerate(selected_files, 1): self.status.config(text=f"Decrypting file {idx}/{total_files}") self.update_progress(idx * 90 / total_files) enc_name = file_info["encrypted_name"] if enc_name not in file_paths: messagebox.showerror( "Error", f"Encrypted file {enc_name} not selected.\nPlease select all encrypted files and ENCRYPTED_META.DAT." ) return enc_path = file_paths[enc_name] output_path = os.path.join(self.output_dir, file_info["original_name"]) # Decrypt self.spd.decrypt_with_key(enc_path, output_path, key) # Checksum verify decrypted_checksum = self.spd.create_checksum(output_path) if decrypted_checksum != file_info["original_checksum"]: messagebox.showerror( "Checksum Mismatch", f"Decrypted file {file_info['original_name']} does not match the original checksum!" ) return self.update_progress(100) messagebox.showinfo("Success", f"Files decrypted to:\n{self.output_dir}") except FileNotFoundError as e: messagebox.showerror("Error", str(e)) except Exception as e: messagebox.showerror("Error", f"Decryption failed: {str(e)}") finally: self.update_progress(0) self.status.config(text="") # # --------------- Popup for Selecting Which Files to Decrypt --------------- # def choose_files_to_decrypt(self, files_meta): """ Presents a popup GUI to let the user choose which files from the metadata are to be decrypted. Returns a list of the selected file metadata objects. """ window = tk.Toplevel(self) window.title("Select Files to Decrypt") window.geometry("400x300") window.grab_set() # Makes this window modal lbl = ttk.Label(window, text="Choose which files you want to decrypt:") lbl.pack(pady=5) # Scrollable list of files with multiple selection listbox = tk.Listbox(window, height=10, selectmode=tk.MULTIPLE) listbox.pack(expand=True, fill=tk.BOTH, padx=10, pady=5) # Populate with original_name from each metadata entry for i, f_meta in enumerate(files_meta): listbox.insert(tk.END, f_meta["original_name"]) chosen_files = [] def on_ok(): """Callback to finalize user selection.""" selected_indices = listbox.curselection() for idx in selected_indices: chosen_files.append(files_meta[idx]) window.destroy() def on_cancel(): # No files selected window.destroy() # Buttons btn_frame = ttk.Frame(window) btn_frame.pack(pady=5) ok_btn = ttk.Button(btn_frame, text="OK", command=on_ok) ok_btn.pack(side=tk.LEFT, padx=5) cancel_btn = ttk.Button(btn_frame, text="Cancel", command=on_cancel) cancel_btn.pack(side=tk.LEFT, padx=5) # Wait for user to close the dialog self.wait_window(window) return chosen_files if __name__ == "__main__": app = SecureProtectedDiscGUI() app.mainloop()