Advertisement
lihkgcode

Secure Protected Disc Creator (SecurDisc 4.0™ Alternative)

Mar 9th, 2025 (edited)
2,915
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 29.63 KB | Software | 0 0
  1. import tkinter as tk
  2. from tkinter import ttk, filedialog, messagebox
  3. import os
  4. import sys
  5. import threading
  6. import shutil
  7. import hashlib
  8. import json
  9. from Crypto.Cipher import AES
  10. from Crypto.Protocol.KDF import PBKDF2
  11. from Crypto.Random import get_random_bytes
  12. from cryptography.hazmat.primitives import serialization
  13. from cryptography.hazmat.backends import default_backend
  14.  
  15. # Configuration
  16. SALT_SIZE = 32
  17. KEY_SIZE = 32  # AES-256
  18. IV_SIZE = 16
  19. ITERATIONS = 100000
  20. CHUNK_SIZE = 64 * 1024  # 64KB to reduce memory usage during large file processing
  21.  
  22. class SecureProtectedDisc:
  23.     def derive_key(self, password: str, salt: bytes) -> bytes:
  24.         return PBKDF2(password.encode(), salt, dkLen=KEY_SIZE, count=ITERATIONS)
  25.  
  26.     def load_key_file(self, key_path: str) -> bytes:
  27.         """Load key from PEM file or raw key file."""
  28.         try:
  29.             # Try to load as PEM private key
  30.             with open(key_path, "rb") as f:
  31.                 private_key = serialization.load_pem_private_key(
  32.                     f.read(),
  33.                     password=None,
  34.                     backend=default_backend()
  35.                 )
  36.             private_bytes = private_key.private_bytes(
  37.                 encoding=serialization.Encoding.DER,
  38.                 format=serialization.PrivateFormat.PKCS8,
  39.                 encryption_algorithm=serialization.NoEncryption()
  40.             )
  41.             return hashlib.sha256(private_bytes).digest()[:KEY_SIZE]
  42.         except:
  43.             # Try as raw key file
  44.             with open(key_path, "rb") as f:
  45.                 key = f.read()
  46.             if len(key) not in (16, 24, 32):
  47.                 raise ValueError("Invalid key size. Must be 16, 24, or 32 bytes")
  48.             return key
  49.  
  50.     def encrypt_with_key(self, input_path: str, output_path: str, key: bytes) -> None:
  51.         """
  52.        Encrypt a file with a given raw key using chunk-based encryption (AES-CBC).
  53.        """
  54.         iv = get_random_bytes(IV_SIZE)
  55.         cipher = AES.new(key, AES.MODE_CBC, iv)
  56.  
  57.         with open(input_path, 'rb') as f_in, open(output_path, 'wb') as f_out:
  58.             # Write IV first
  59.             f_out.write(iv)
  60.             while True:
  61.                 chunk = f_in.read(CHUNK_SIZE)
  62.                 if len(chunk) == 0:
  63.                     # No more data: final padded block
  64.                     padded_block = self._pad(b'')
  65.                     f_out.write(cipher.encrypt(padded_block))
  66.                     break
  67.                 elif len(chunk) < CHUNK_SIZE:
  68.                     # Last (incomplete) chunk: pad then encrypt
  69.                     chunk = self._pad(chunk)
  70.                     f_out.write(cipher.encrypt(chunk))
  71.                     break
  72.                 else:
  73.                     # Full chunk
  74.                     f_out.write(cipher.encrypt(chunk))
  75.  
  76.     def decrypt_with_key(self, input_path: str, output_path: str, key: bytes) -> None:
  77.         """
  78.        Decrypt a file with a given raw key using chunk-based decryption (AES-CBC).
  79.        """
  80.         with open(input_path, 'rb') as f_in:
  81.             # Read IV first
  82.             iv = f_in.read(IV_SIZE)
  83.             cipher = AES.new(key, AES.MODE_CBC, iv)
  84.  
  85.             with open(output_path, 'wb') as f_out:
  86.                 next_chunk = b''
  87.                 while True:
  88.                     chunk = f_in.read(CHUNK_SIZE)
  89.                     if len(chunk) == 0:
  90.                         # Final decrypt, unpad
  91.                         final_block = cipher.decrypt(next_chunk)
  92.                         final_block = self._unpad(final_block)
  93.                         f_out.write(final_block)
  94.                         break
  95.                     decrypted = cipher.decrypt(next_chunk)
  96.                     f_out.write(decrypted)
  97.                     next_chunk = chunk
  98.  
  99.     def encrypt_with_password(self, input_path: str, output_path: str, password: str) -> tuple:
  100.         """
  101.        Encrypt using password-based key derivation, chunk-based approach.
  102.        """
  103.         salt = get_random_bytes(SALT_SIZE)
  104.         key = self.derive_key(password, salt)
  105.         iv = get_random_bytes(IV_SIZE)
  106.         cipher = AES.new(key, AES.MODE_CBC, iv)
  107.  
  108.         with open(input_path, 'rb') as f_in, open(output_path, 'wb') as f_out:
  109.             # Write salt + iv
  110.             f_out.write(salt + iv)
  111.             while True:
  112.                 chunk = f_in.read(CHUNK_SIZE)
  113.                 if len(chunk) == 0:
  114.                     # Final padded block
  115.                     padded_block = self._pad(b'')
  116.                     f_out.write(cipher.encrypt(padded_block))
  117.                     break
  118.                 elif len(chunk) < CHUNK_SIZE:
  119.                     # Last chunk
  120.                     chunk = self._pad(chunk)
  121.                     f_out.write(cipher.encrypt(chunk))
  122.                     break
  123.                 else:
  124.                     # Full chunk
  125.                     f_out.write(cipher.encrypt(chunk))
  126.  
  127.         return salt, iv
  128.  
  129.     def decrypt_with_password(self, input_path: str, output_path: str, password: str) -> bool:
  130.         """
  131.        Decrypt using password-based key derivation, chunk-based approach.
  132.        """
  133.         with open(input_path, 'rb') as f_in:
  134.             # Read salt + iv
  135.             header = f_in.read(SALT_SIZE + IV_SIZE)
  136.             salt = header[:SALT_SIZE]
  137.             iv = header[SALT_SIZE:]
  138.             key = self.derive_key(password, salt)
  139.             cipher = AES.new(key, AES.MODE_CBC, iv)
  140.  
  141.             with open(output_path, 'wb') as f_out:
  142.                 next_chunk = b''
  143.                 while True:
  144.                     chunk = f_in.read(CHUNK_SIZE)
  145.                     if len(chunk) == 0:
  146.                         final_block = cipher.decrypt(next_chunk)
  147.                         final_block = self._unpad(final_block)
  148.                         f_out.write(final_block)
  149.                         break
  150.                     decrypted = cipher.decrypt(next_chunk)
  151.                     f_out.write(decrypted)
  152.                     next_chunk = chunk
  153.         return True
  154.  
  155.     def _pad(self, data: bytes) -> bytes:
  156.         """
  157.        Standard PKCS#7 padding to make data a multiple of AES.block_size
  158.        """
  159.         block_size = AES.block_size
  160.         padding_length = block_size - (len(data) % block_size)
  161.         return data + bytes([padding_length] * padding_length)
  162.  
  163.     @staticmethod
  164.     def _unpad(data: bytes) -> bytes:
  165.         """
  166.        Remove PKCS#7 padding.
  167.        """
  168.         padding_length = data[-1]
  169.         return data[:-padding_length]
  170.  
  171.     def create_checksum(self, file_path: str) -> str:
  172.         """
  173.        Create SHA-256 checksum in a chunk-based manner.
  174.        """
  175.         sha256 = hashlib.sha256()
  176.         with open(file_path, 'rb') as f:
  177.             while True:
  178.                 chunk = f.read(4096)
  179.                 if not chunk:
  180.                     break
  181.                 sha256.update(chunk)
  182.         return sha256.hexdigest()
  183.  
  184. class SecureProtectedDiscGUI(tk.Tk):
  185.     def __init__(self):
  186.         super().__init__()
  187.         self.title("Secure Protected Disc Creator")
  188.         self.geometry("900x600")
  189.         self.configure(padx=20, pady=20)
  190.        
  191.         self.spd = SecureProtectedDisc()
  192.         self.key_type = tk.StringVar(value="password")
  193.         self.mode = tk.StringVar(value="encrypt")
  194.         self.file_paths = []
  195.         self.output_dir = ""
  196.         self.password = ""
  197.         self.confirm_password = ""
  198.         self.key_file = ""
  199.        
  200.         self.create_widgets()
  201.  
  202.     def create_widgets(self):
  203.         # --- Mode Selection ---
  204.         ttk.Label(self, text="Mode:").grid(row=0, column=0, sticky="w")
  205.         encrypt_radio = ttk.Radiobutton(self, text="Encrypt", variable=self.mode, value="encrypt", command=self.update_ui)
  206.         decrypt_radio = ttk.Radiobutton(self, text="Decrypt", variable=self.mode, value="decrypt", command=self.update_ui)
  207.         encrypt_radio.grid(row=0, column=1, sticky="w")
  208.         decrypt_radio.grid(row=0, column=2, sticky="w")
  209.  
  210.         # --- Key Type Selection ---
  211.         ttk.Label(self, text="Encryption Type:").grid(row=1, column=0, sticky="w")
  212.         ttk.Radiobutton(self, text="Password", variable=self.key_type, value="password", command=self.update_key_ui).grid(row=1, column=1, sticky="w")
  213.         ttk.Radiobutton(self, text="Key File", variable=self.key_type, value="keyfile", command=self.update_key_ui).grid(row=1, column=2, sticky="w")
  214.  
  215.         # --- Password Fields ---
  216.         self.pw_label = ttk.Label(self, text="Password:")
  217.         self.pw_label.grid(row=2, column=0, sticky="w", pady=(10,0))
  218.         self.pw_entry = ttk.Entry(self, show="*", width=80)
  219.         self.pw_entry.grid(row=3, column=0, columnspan=3, padx=5)
  220.  
  221.         self.confirm_pw_label = ttk.Label(self, text="Confirm Password:")
  222.         self.confirm_pw_label.grid(row=4, column=0, sticky="w", pady=(10,0))
  223.         self.confirm_pw_entry = ttk.Entry(self, show="*", width=80)
  224.         self.confirm_pw_entry.grid(row=5, column=0, columnspan=3, padx=5)
  225.  
  226.         # --- Key File Fields ---
  227.         self.key_file_frame = ttk.Frame(self)
  228.         self.key_file_frame.grid(row=6, column=0, columnspan=4, sticky="ew")
  229.         self.key_file_button = ttk.Button(self.key_file_frame, text="Select Key File", command=self.select_key_file)
  230.         self.key_file_button.pack(side=tk.LEFT)
  231.         self.key_file_label = ttk.Label(self.key_file_frame, text="No key file selected")
  232.         self.key_file_label.pack(side=tk.LEFT, padx=5)
  233.  
  234.         # --- File Selection ---
  235.         ttk.Label(self, text="Select Files:").grid(row=7, column=0, sticky="w", pady=(10,0))
  236.         self.file_list = tk.Listbox(self, width=80, height=5, selectmode=tk.EXTENDED)
  237.         self.file_list.grid(row=8, column=0, columnspan=4, padx=5, pady=5)
  238.        
  239.         btn_frame = ttk.Frame(self)
  240.         btn_frame.grid(row=9, column=0, columnspan=4, pady=5)
  241.         self.add_button = ttk.Button(btn_frame, text="Add Files", command=self.add_files)
  242.         self.add_button.pack(side=tk.LEFT)
  243.  
  244.         self.remove_button = ttk.Button(btn_frame, text="Remove Selected", command=self.remove_files)
  245.         self.remove_button.pack(side=tk.LEFT, padx=10)
  246.  
  247.         # 1) Button to remove all entries from the file list
  248.         self.remove_all_button = ttk.Button(btn_frame, text="Clear All", command=self.clear_all_files)
  249.         self.remove_all_button.pack(side=tk.LEFT, padx=10)
  250.  
  251.         # --- Output Directory ---
  252.         ttk.Label(self, text="Output Directory:").grid(row=10, column=0, sticky="w", pady=(10,0))
  253.         self.output_dir_entry = tk.Entry(self, width=80)
  254.         self.output_dir_entry.grid(row=11, column=0, columnspan=3, padx=5)
  255.         self.browse_button = ttk.Button(self, text="Browse", command=self.browse_output_dir)
  256.         self.browse_button.grid(row=11, column=3, padx=5)
  257.  
  258.         # --- Progress & Status ---
  259.         self.progress = ttk.Progressbar(self, orient=tk.HORIZONTAL, length=760, mode='determinate')
  260.         self.progress.grid(row=12, column=0, columnspan=4, pady=20)
  261.         self.status = ttk.Label(self, text="Ready")
  262.         self.status.grid(row=13, column=0, columnspan=4)
  263.  
  264.         # --- Action Buttons ---
  265.         btn_frame2 = ttk.Frame(self)
  266.         btn_frame2.grid(row=14, column=0, columnspan=4, pady=20)
  267.         self.start_button = ttk.Button(btn_frame2, text="Start", command=self.start_process)
  268.         self.start_button.pack(side=tk.LEFT)
  269.  
  270.         self.exit_button = ttk.Button(btn_frame2, text="Exit", command=self.destroy)
  271.         self.exit_button.pack(side=tk.LEFT, padx=10)
  272.  
  273.         self.update_key_ui()
  274.  
  275.     def update_ui(self):
  276.         """
  277.        Called whenever the Mode RadioButton changes (Encrypt or Decrypt).
  278.        We also add the logic to automatically add all .DAT files in the same
  279.        directory as this script if 'Decrypt' is selected.
  280.        """
  281.         is_encrypt = self.mode.get() == "encrypt"
  282.         if is_encrypt:
  283.             self.add_button.config(text="Add Files")
  284.         else:
  285.             self.add_button.config(text="Select Meta File/Encrypted Files")
  286.             # ----------------- AUTO ADD ALL *.DAT FILES -----------------
  287.             # Clear the list box first to avoid duplicates
  288.             self.file_list.delete(0, tk.END)
  289.            
  290.             # Get the directory of this script
  291.             script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
  292.            
  293.             # Find all *.DAT files in the script directory and add them
  294.             for f in os.listdir(script_dir):
  295.                 if f.lower().endswith(".dat"):
  296.                     full_path = os.path.join(script_dir, f)
  297.                     self.file_list.insert(tk.END, full_path)
  298.             # -----------------------------------------------------------
  299.  
  300.     def update_key_ui(self):
  301.         if self.key_type.get() == "password":
  302.             self.pw_label.config(state="normal")
  303.             self.pw_entry.config(state="normal")
  304.             self.confirm_pw_label.config(state="normal")
  305.             self.confirm_pw_entry.config(state="normal")
  306.             self.key_file_frame.grid_remove()
  307.         else:
  308.             self.pw_label.config(state="disabled")
  309.             self.pw_entry.config(state="disabled")
  310.             self.confirm_pw_label.config(state="disabled")
  311.             self.confirm_pw_entry.config(state="disabled")
  312.             self.key_file_frame.grid()
  313.  
  314.     def select_key_file(self):
  315.         key_path = filedialog.askopenfilename(
  316.             title="Select Key File",
  317.             filetypes=[("PEM Files", "*.pem"), ("All Files", "*.*")]
  318.         )
  319.         if key_path:
  320.             self.key_file = key_path
  321.             self.key_file_label.config(text=os.path.basename(key_path))
  322.  
  323.     def add_files(self):
  324.         if self.mode.get() == "encrypt":
  325.             files = filedialog.askopenfilenames()
  326.             if files:
  327.                 for f in files:
  328.                     if f not in self.file_list.get(0, tk.END):
  329.                         self.file_list.insert(tk.END, f)
  330.         else:
  331.             # Allow selection of either ENCRYPTED_META.DAT or encrypted files
  332.             files = filedialog.askopenfilenames(
  333.                 title="Select ENCRYPTED_META.DAT and/or Encrypted Files",
  334.                 filetypes=[("Meta File", "*.DAT"), ("Encrypted Files", "*.DAT"), ("All Files", "*.*")]
  335.             )
  336.             if files:
  337.                 for f in files:
  338.                     if f not in self.file_list.get(0, tk.END):
  339.                         self.file_list.insert(tk.END, f)
  340.  
  341.     def remove_files(self):
  342.         selected = self.file_list.curselection()
  343.         for index in reversed(selected):
  344.             self.file_list.delete(index)
  345.  
  346.     def clear_all_files(self):
  347.         """Remove all items from the file list."""
  348.         self.file_list.delete(0, tk.END)
  349.  
  350.     def browse_output_dir(self):
  351.         directory = filedialog.askdirectory()
  352.         self.output_dir_entry.delete(0, tk.END)
  353.         self.output_dir_entry.insert(0, directory)
  354.  
  355.     def start_process(self):
  356.         """Disable the Start button, then start encryption/decryption in a thread."""
  357.         if not self.validate_inputs():
  358.             return
  359.  
  360.         # Disable the start button until done
  361.         self.start_button.config(state="disabled")
  362.  
  363.         self.output_dir = self.output_dir_entry.get()
  364.         items = self.file_list.get(0, tk.END)
  365.        
  366.         try:
  367.             if self.key_type.get() == "password":
  368.                 if self.pw_entry.get() != self.confirm_pw_entry.get():
  369.                     messagebox.showerror("Error", "Passwords do not match!")
  370.                     self.start_button.config(state="normal")
  371.                     return
  372.                 self.password = self.pw_entry.get()
  373.                 threading.Thread(target=self.process_password_based, args=(items,)).start()
  374.             else:
  375.                 if not self.key_file:
  376.                     messagebox.showerror("Error", "Please select a key file!")
  377.                     self.start_button.config(state="normal")
  378.                     return
  379.                 threading.Thread(target=self.process_keyfile_based, args=(items,)).start()
  380.         except Exception as e:
  381.             messagebox.showerror("Error", f"Initialization failed: {str(e)}")
  382.             # Re-enable start button on failure
  383.             self.start_button.config(state="normal")
  384.  
  385.     def validate_inputs(self):
  386.         if self.file_list.size() == 0:
  387.             messagebox.showerror("Error", "Please select at least one file/directory!")
  388.             return False
  389.         if self.key_type.get() == "password":
  390.             if len(self.pw_entry.get()) < 8 and self.mode.get() == "encrypt":
  391.                 # Only enforce password length check in encryption mode
  392.                 messagebox.showerror("Error", "Password must be at least 8 characters!")
  393.                 return False
  394.         if not self.output_dir_entry.get():
  395.              messagebox.showerror("Error", "Please select output directory!")
  396.              return False
  397.         return True
  398.  
  399.     def update_progress(self, value):
  400.         self.progress['value'] = value
  401.         self.update_idletasks()
  402.  
  403.     #
  404.     # --------------- Encryption Process (Password-based) ---------------
  405.     #
  406.     def process_password_based(self, items):
  407.         try:
  408.             os.makedirs(self.output_dir, exist_ok=True)
  409.             total_items = len(items)
  410.            
  411.             if self.mode.get() == "encrypt":
  412.                 meta_data = {"files": []}
  413.                 for idx, file_path in enumerate(items, 1):
  414.                     self.status.config(text=f"Encrypting file {idx}/{total_items}")
  415.                     self.update_progress((idx-1)*80/total_items)
  416.    
  417.                     # Compute original file checksum
  418.                     original_checksum = self.spd.create_checksum(file_path)
  419.  
  420.                     # Encrypt file
  421.                     enc_name = f"ENCRYPTED{idx}.DAT"
  422.                     enc_path = os.path.join(self.output_dir, enc_name)
  423.                     self.spd.encrypt_with_password(file_path, enc_path, self.password)
  424.  
  425.                     # Add info to metadata
  426.                     meta_data["files"].append({
  427.                         "original_name": os.path.basename(file_path),
  428.                         "encrypted_name": enc_name,
  429.                         "size": os.path.getsize(file_path),
  430.                         "original_checksum": original_checksum
  431.                     })
  432.                
  433.                 # Encrypt metadata
  434.                 self.status.config(text="Creating metadata...")
  435.                 temp_meta_path = os.path.join(self.output_dir, "temp_metadata.json")
  436.                 with open(temp_meta_path, 'w') as f:
  437.                     json.dump(meta_data, f)
  438.    
  439.                 enc_meta_path = os.path.join(self.output_dir, "ENCRYPTED_META.DAT")
  440.                 self.spd.encrypt_with_password(temp_meta_path, enc_meta_path, self.password)
  441.                 os.remove(temp_meta_path)
  442.    
  443.                 # Create checksum for the encrypted metadata
  444.                 checksum = self.spd.create_checksum(enc_meta_path)
  445.                 with open(os.path.join(self.output_dir, "CHECKSUM.TXT"), "w") as f:
  446.                     f.write(checksum)
  447.    
  448.                 # Copy this script to output
  449.                 script_copy = os.path.join(self.output_dir, "SECPROTDISC_GUI.PY")
  450.                 shutil.copy(sys.argv[0], script_copy)
  451.    
  452.                 self.update_progress(100)
  453.                 self.status.config(text=f"Files ready in: {self.output_dir}")
  454.                 messagebox.showinfo("Success", "Disc files prepared successfully!\nBurn all files to a disc.")
  455.             else:
  456.                 self.decrypt_with_metadata(items, self.password)
  457.         except Exception as e:
  458.             messagebox.showerror("Error", f"Process failed: {str(e)}")
  459.         finally:
  460.             # Re-enable start button whether success or fail
  461.             self.start_button.config(state="normal")
  462.             self.progress['value'] = 0
  463.             self.status.config(text="Ready")
  464.  
  465.     #
  466.     # --------------- Encryption Process (Key File-based) ---------------
  467.     #
  468.     def process_keyfile_based(self, items):
  469.         try:
  470.             key = self.spd.load_key_file(self.key_file)
  471.             os.makedirs(self.output_dir, exist_ok=True)
  472.  
  473.             if self.mode.get() == "encrypt":
  474.                 meta_data = {"files": []}
  475.                 for idx, file_path in enumerate(items, 1):
  476.                     output_path = os.path.join(self.output_dir, f"ENCRYPTED{idx}.DAT")
  477.  
  478.                     # Compute original file checksum
  479.                     original_checksum = self.spd.create_checksum(file_path)
  480.  
  481.                     self.spd.encrypt_with_key(file_path, output_path, key)
  482.                     meta_data["files"].append({
  483.                         "original_name": os.path.basename(file_path),
  484.                         "encrypted_name": f"ENCRYPTED{idx}.DAT",
  485.                         "size": os.path.getsize(file_path),
  486.                         "original_checksum": original_checksum
  487.                     })
  488.  
  489.                 # Encrypt metadata
  490.                 temp_meta_path = os.path.join(self.output_dir, "temp_metadata.json")
  491.                 with open(temp_meta_path, 'w') as f:
  492.                     json.dump(meta_data, f)
  493.  
  494.                 enc_meta_path = os.path.join(self.output_dir, "ENCRYPTED_META.DAT")
  495.                 self.spd.encrypt_with_key(temp_meta_path, enc_meta_path, key)
  496.                 os.remove(temp_meta_path)
  497.  
  498.                 # Create checksum
  499.                 checksum = self.spd.create_checksum(enc_meta_path)
  500.                 with open(os.path.join(self.output_dir, "CHECKSUM.TXT"), "w") as f:
  501.                     f.write(checksum)
  502.  
  503.                 # Copy the script
  504.                 script_copy = os.path.join(self.output_dir, "SECPROTDISC_GUI.PY")
  505.                 shutil.copy(sys.argv[0], script_copy)
  506.  
  507.                 messagebox.showinfo("Success", "Files encrypted successfully!")
  508.             else:
  509.                 self.decrypt_with_metadata_key(items, key)
  510.         except Exception as e:
  511.             messagebox.showerror("Error", f"Key-based process failed: {str(e)}")
  512.         finally:
  513.             # Re-enable start button
  514.             self.start_button.config(state="normal")
  515.             self.progress['value'] = 0
  516.             self.status.config(text="Ready")
  517.  
  518.     #
  519.     # --------------- Decryption (Password-based) ---------------
  520.     #
  521.     def decrypt_with_metadata(self, items, password):
  522.         try:
  523.             self.status.config(text="Decrypting with metadata...")
  524.             meta_file_path = next((item for item in items if os.path.basename(item) == "ENCRYPTED_META.DAT"), None)
  525.             if not meta_file_path:
  526.                 raise FileNotFoundError("ENCRYPTED_META.DAT not found in selected files.")
  527.  
  528.             # 1) Decrypt the metadata to a temporary file
  529.             temp_meta_path = os.path.join(self.output_dir, "temp_metadata.json")
  530.             self.spd.decrypt_with_password(meta_file_path, temp_meta_path, password)
  531.  
  532.             # 2) Load the metadata
  533.             with open(temp_meta_path, 'r') as f:
  534.                 meta_data = json.load(f)
  535.             os.remove(temp_meta_path)
  536.  
  537.             # 3) Let user choose which file(s) to decrypt
  538.             selected_files = self.choose_files_to_decrypt(meta_data["files"])
  539.             if not selected_files:
  540.                 # If user cancelled or selected nothing, just return
  541.                 return
  542.  
  543.             total_files = len(selected_files)
  544.             file_paths = {os.path.basename(item): item for item in items}
  545.  
  546.             for idx, file_info in enumerate(selected_files, 1):
  547.                 self.status.config(text=f"Decrypting file {idx}/{total_files}")
  548.                 self.update_progress(idx * 90 / total_files)
  549.                
  550.                 enc_name = file_info["encrypted_name"]
  551.                 if enc_name not in file_paths:
  552.                     messagebox.showerror(
  553.                         "Error",
  554.                         f"Encrypted file {enc_name} not selected.\nPlease select all encrypted files and ENCRYPTED_META.DAT."
  555.                     )
  556.                     return
  557.                    
  558.                 enc_path = file_paths[enc_name]
  559.                 output_path = os.path.join(self.output_dir, file_info["original_name"])
  560.  
  561.                 # Decrypt
  562.                 self.spd.decrypt_with_password(enc_path, output_path, password)
  563.  
  564.                 # Checksum verify
  565.                 decrypted_checksum = self.spd.create_checksum(output_path)
  566.                 if decrypted_checksum != file_info["original_checksum"]:
  567.                     messagebox.showerror(
  568.                         "Checksum Mismatch",
  569.                         f"Decrypted file {file_info['original_name']} does not match the original checksum!"
  570.                     )
  571.                     return
  572.  
  573.             self.update_progress(100)
  574.             messagebox.showinfo("Success", f"Files decrypted to:\n{self.output_dir}")
  575.         except FileNotFoundError as e:
  576.             messagebox.showerror("Error", str(e))
  577.         except Exception as e:
  578.             messagebox.showerror("Error", f"Decryption failed: {str(e)}")
  579.         finally:
  580.             self.update_progress(0)
  581.             self.status.config(text="")
  582.  
  583.     #
  584.     # --------------- Decryption (Key-based) ---------------
  585.     #
  586.     def decrypt_with_metadata_key(self, items, key):
  587.         try:
  588.             self.status.config(text="Decrypting with metadata...")
  589.             meta_file_path = next((item for item in items if os.path.basename(item) == "ENCRYPTED_META.DAT"), None)
  590.             if not meta_file_path:
  591.                 raise FileNotFoundError("ENCRYPTED_META.DAT not found in selected files.")
  592.  
  593.             # 1) Decrypt the metadata
  594.             temp_meta_path = os.path.join(self.output_dir, "temp_metadata.json")
  595.             self.spd.decrypt_with_key(meta_file_path, temp_meta_path, key)
  596.  
  597.             # 2) Load
  598.             with open(temp_meta_path, 'r') as f:
  599.                 meta_data = json.load(f)
  600.             os.remove(temp_meta_path)
  601.  
  602.             # 3) Let user choose which file(s) to decrypt
  603.             selected_files = self.choose_files_to_decrypt(meta_data["files"])
  604.             if not selected_files:
  605.                 return
  606.  
  607.             total_files = len(selected_files)
  608.             file_paths = {os.path.basename(item): item for item in items}
  609.  
  610.             for idx, file_info in enumerate(selected_files, 1):
  611.                 self.status.config(text=f"Decrypting file {idx}/{total_files}")
  612.                 self.update_progress(idx * 90 / total_files)
  613.                
  614.                 enc_name = file_info["encrypted_name"]
  615.                 if enc_name not in file_paths:
  616.                     messagebox.showerror(
  617.                         "Error",
  618.                         f"Encrypted file {enc_name} not selected.\nPlease select all encrypted files and ENCRYPTED_META.DAT."
  619.                     )
  620.                     return
  621.                    
  622.                 enc_path = file_paths[enc_name]
  623.                 output_path = os.path.join(self.output_dir, file_info["original_name"])
  624.  
  625.                 # Decrypt
  626.                 self.spd.decrypt_with_key(enc_path, output_path, key)
  627.  
  628.                 # Checksum verify
  629.                 decrypted_checksum = self.spd.create_checksum(output_path)
  630.                 if decrypted_checksum != file_info["original_checksum"]:
  631.                     messagebox.showerror(
  632.                         "Checksum Mismatch",
  633.                         f"Decrypted file {file_info['original_name']} does not match the original checksum!"
  634.                     )
  635.                     return
  636.  
  637.             self.update_progress(100)
  638.             messagebox.showinfo("Success", f"Files decrypted to:\n{self.output_dir}")
  639.         except FileNotFoundError as e:
  640.             messagebox.showerror("Error", str(e))
  641.         except Exception as e:
  642.             messagebox.showerror("Error", f"Decryption failed: {str(e)}")
  643.         finally:
  644.             self.update_progress(0)
  645.             self.status.config(text="")
  646.  
  647.     #
  648.     # --------------- Popup for Selecting Which Files to Decrypt ---------------
  649.     #
  650.     def choose_files_to_decrypt(self, files_meta):
  651.         """
  652.        Presents a popup GUI to let the user choose which files from the metadata
  653.        are to be decrypted. Returns a list of the selected file metadata objects.
  654.        """
  655.         window = tk.Toplevel(self)
  656.         window.title("Select Files to Decrypt")
  657.         window.geometry("400x300")
  658.         window.grab_set()  # Makes this window modal
  659.  
  660.         lbl = ttk.Label(window, text="Choose which files you want to decrypt:")
  661.         lbl.pack(pady=5)
  662.  
  663.         # Scrollable list of files with multiple selection
  664.         listbox = tk.Listbox(window, height=10, selectmode=tk.MULTIPLE)
  665.         listbox.pack(expand=True, fill=tk.BOTH, padx=10, pady=5)
  666.  
  667.         # Populate with original_name from each metadata entry
  668.         for i, f_meta in enumerate(files_meta):
  669.             listbox.insert(tk.END, f_meta["original_name"])
  670.  
  671.         chosen_files = []
  672.  
  673.         def on_ok():
  674.             """Callback to finalize user selection."""
  675.             selected_indices = listbox.curselection()
  676.             for idx in selected_indices:
  677.                 chosen_files.append(files_meta[idx])
  678.             window.destroy()
  679.  
  680.         def on_cancel():
  681.             # No files selected
  682.             window.destroy()
  683.  
  684.         # Buttons
  685.         btn_frame = ttk.Frame(window)
  686.         btn_frame.pack(pady=5)
  687.  
  688.         ok_btn = ttk.Button(btn_frame, text="OK", command=on_ok)
  689.         ok_btn.pack(side=tk.LEFT, padx=5)
  690.  
  691.         cancel_btn = ttk.Button(btn_frame, text="Cancel", command=on_cancel)
  692.         cancel_btn.pack(side=tk.LEFT, padx=5)
  693.  
  694.         # Wait for user to close the dialog
  695.         self.wait_window(window)
  696.         return chosen_files
  697.  
  698. if __name__ == "__main__":
  699.     app = SecureProtectedDiscGUI()
  700.     app.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement