diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88fde26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +icon: \ No newline at end of file diff --git a/App logo.png b/App logo.png deleted file mode 100644 index 1edb5ed..0000000 Binary files a/App logo.png and /dev/null differ diff --git a/README.md b/README.md index 076f789..4aa1882 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,120 @@ # πŸ”’ Secure File Encryptor/Decryptor πŸ›‘οΈ -| Section | Content | -|---------|---------| -| **🌟 Description** | A secure GUI tool for file encryption/decryption using military-grade AES-GCM encryption | -| **✨ Features** | | -| πŸ” Encryption | AES-GCM 256-bit encryption (NSA-approved) | -| πŸ”“ Decryption | Authenticated decryption with tamper detection | -| πŸ“ Large Files | Supports huge files up to 10GB (chunked processing) | -| πŸ–₯️ GUI | Beautiful PyQt5 interface with dark theme | -| πŸ”‘ Security | PBKDF2-HMAC-SHA256 with 600,000 iterations | -| **πŸ“¦ Requirements** | | -| 🐍 Python Version | 3.6+ (Recommended: 3.8+) | -| πŸ’» System | Windows/macOS/Linux | -| πŸ“š Dependencies | `PyQt5`, `cryptography`, `qt_material` | -| βš™οΈ Install Command | `pip install PyQt5 cryptography qt_material` | -| **πŸ“‹ Usage Guide** | | -| πŸ”’ Encryption | 1. Click "Select File"
2. Set output path (.encrypted)
3. Enter password + confirmation
4. Click "Start Encryption"
5. Wait for completion βœ… | -| πŸ”“ Decryption | 1. Click "Select Encrypted File"
2. Set output path
3. Enter original password
4. Click "Start Decryption"
5. Get your original file back βœ… | -| ⚠️ Important | - Never lose your password!
- Keep backups of important files
- Cancel operations using window close | -| **βš™οΈ Technical Specs** | | -| πŸ› οΈ Algorithm | AES-GCM (Authenticated Encryption) | -| πŸ”‘ Key Size | 256-bit (Military Grade) | -| πŸ”„ Iterations | 600,000 (NIST Recommended) | -| 🧩 Chunk Size | 1MB (Optimal Performance) | -| πŸ§‚ Salt Size | 16 bytes | -| πŸ”’ Nonce Size | 12 bytes | -| **πŸ” Security Notes** | - πŸ”„ Cryptographically secure RNG
- βœ… Automatic integrity verification
- 🧹 Cleanup on failure
- πŸ›‘οΈ Protection against common attacks | -| **πŸ“œ License** | MIT License - Free for everyone | -| **πŸ“Έ Preview** | ![App Screenshot](https://github.com/logand166/Encryptor/blob/main/Screenshot.jpg?raw=true) | -| **β˜• Support** | [Buy Me a Coffee](https://www.buymeacoffee.com/logand) | +A secure and user-friendly GUI tool for file encryption and decryption using military-grade AES-GCM encryption. + +--- + +## 🌟 Description + +Secure File Encryptor/Decryptor is designed to protect your sensitive files with state-of-the-art encryption. It ensures data integrity and security while offering a simple and intuitive interface. This project also introduces hard disk encryption and a robust key recovery mechanism for enhanced usability and security. + +--- + +## ✨ Features + +- **πŸ” Encryption**: AES-GCM 256-bit encryption (NSA-approved). +- **πŸ”“ Decryption**: Authenticated decryption with tamper detection. +- **πŸ“ Large File Support**: Handles files up to 10GB with chunked processing. +- **πŸ–₯️ GUI**: Beautiful PyQt5 interface with a dark theme. +- **πŸ”‘ Security**: PBKDF2-HMAC-SHA256 with 600,000 iterations for password hashing. +- **πŸ’Ύ Hard Disk Encryption**: Encrypt entire drives with ease. +- **πŸ”‘ Key Recovery**: Recover encryption keys using security questions, seed phrases, or a hardware token (PicoKey). + +--- + +## πŸ“¦ Requirements + +- **🐍 Python Version**: 3.10+ (Recommended: 3.10.16). +- **πŸ’» Supported Systems**: Windows, macOS, Linux. +- **πŸ“š Dependencies**: + - `PyQt5` + - `cryptography` + - `qt_material` + - `mnemonic==0.20` + - `Unidecode==1.3.6` + - `pyserial` + - `pyqtspinner` + +### Installation + +Run the following command to install the required dependencies: + +```bash +pip install PyQt5 cryptography qt_material mnemonic==0.20 Unidecode==1.3.6 pyserial pyqtspinner +``` + +--- + +## πŸ“‹ Usage Guide + +### πŸ”’ Encryption + +1. Click **"Select File or Folder"**. +2. Set the output path (e.g., `.encrypted`). +3. Enter a password and confirm it. +4. Optionally enable the key recovery mechanism. +5. Click **"Start Encryption"**. +6. Wait for the process to complete βœ…. + +### πŸ”“ Decryption + +1. Click **"Select Encrypted File/Folder"**. +2. Set the output path for the decrypted file. +3. Enter the original password. +4. Click **"Start Decryption"**. +5. Retrieve your original file βœ…. + +### πŸ”‘ Key Recovery + +1. Navigate to the **"Recovery"** tab. +2. Choose a recovery method (security questions, seed phrase, or hardware token). +3. Follow the on-screen instructions to recover your encryption key. + +### ⚠️ Important Notes + +- Never lose your password or recovery credentials! +- Keep backups of important files. +- Cancel operations by closing the application window. + +--- + +## βš™οΈ Technical Specifications + +- **πŸ› οΈ Algorithm**: AES-GCM (Authenticated Encryption). +- **πŸ”‘ Key Size**: 256-bit (Military Grade). +- **πŸ”„ Iterations**: 600,000 (NIST Recommended). +- **🧩 Chunk Size**: 1MB (Optimal Performance). +- **πŸ§‚ Salt Size**: 16 bytes. +- **πŸ”’ Nonce Size**: 12 bytes. +- **πŸ”‘ Key Recovery**: Securely stores recovery keys using encryption. + +--- + +## πŸ” Security Notes + +- πŸ”„ Cryptographically secure random number generation. +- βœ… Automatic integrity verification. +- 🧹 Cleanup on failure. +- πŸ›‘οΈ Protection against common attacks. +- πŸ”‘ Recovery options include hashed security answers, encrypted seed phrases, and hardware tokens. + +--- + +## πŸ“œ License + +This project is licensed under the **MIT License**. Free for everyone to use and modify. + +--- + +## πŸ“Έ Preview + +![App Screenshot](./screenshot.png) + +--- + +## β˜• Support + +If you find this project helpful, consider supporting me: + +[Buy Me a Coffee](https://www.buymeacoffee.com/logand) + \ No newline at end of file diff --git a/Screenshot.jpg b/Screenshot.jpg deleted file mode 100644 index 08389d2..0000000 Binary files a/Screenshot.jpg and /dev/null differ diff --git a/Screenshot2.jpg b/Screenshot2.jpg deleted file mode 100644 index adefc31..0000000 Binary files a/Screenshot2.jpg and /dev/null differ diff --git a/app-logo.ico b/app-logo.ico new file mode 100644 index 0000000..bd4617e Binary files /dev/null and b/app-logo.ico differ diff --git a/app-logo.png b/app-logo.png new file mode 100644 index 0000000..844d1ab Binary files /dev/null and b/app-logo.png differ diff --git a/crypto.py b/crypto.py new file mode 100644 index 0000000..3963049 --- /dev/null +++ b/crypto.py @@ -0,0 +1,802 @@ +import ast +import hashlib +import os +import secrets +import time + +import serial +import serial.tools.list_ports + + +from PyQt5.QtCore import QThread, pyqtSignal, QMutex + +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.exceptions import InvalidTag + +from utilities import generate_seed_phrase, log_activity + +# Constants +CHUNK_SIZE = 1024 * 1024 # 1MB chunks +SALT_SIZE = 16 +NONCE_SIZE = 12 +KEY_SIZE = 32 +ITERATIONS = 600_000 +MAX_FILE_SIZE = 10 * 1024 * 1024 * 1024 # 10GB + + +class CryptoManager: + """ + CryptoManager + + A utility class for secure file encryption and decryption using AES-GCM with PBKDF2 key derivation. + This class provides methods to encrypt and decrypt files with chunk-based processing, ensuring + data integrity and confidentiality. + + Methods: + derive_key(password: str, salt: bytes) -> bytes: + Derives a cryptographic key from a password and salt using PBKDF2 with HMAC-SHA256. + + encrypt_file(src_path: str, dest_path: str, password: str, progress_callback=None) -> None: + Encrypts a file securely using AES-GCM with a unique nonce per chunk. Supports progress tracking. + + decrypt_file(src_path: str, dest_path: str, password: str, progress_callback=None) -> None: + Decrypts a file securely using AES-GCM with chunk validation. Supports progress tracking. + + Constants (not defined in this snippet but required): + SALT_SIZE: int + The size of the salt in bytes. + KEY_SIZE: int + The size of the derived key in bytes. + ITERATIONS: int + The number of iterations for PBKDF2 key derivation. + CHUNK_SIZE: int + The size of each chunk to be processed in bytes. + NONCE_SIZE: int + The size of the nonce in bytes. + MAX_FILE_SIZE: int + The maximum allowed file size for encryption in bytes. + + Exceptions: + ValueError: + Raised for invalid input parameters or failed operations. + FileNotFoundError: + Raised if the source file does not exist. + RuntimeError: + Raised for general encryption or decryption failures. + InvalidTag: + Raised during decryption if authentication fails (e.g., incorrect password or tampered file). + """ + + @staticmethod + def derive_key(password: str, salt: bytes) -> bytes: + """Secure key derivation with error handling + Args: + password (str): The password to derive the key from. + salt (bytes): The salt used for key derivation. + Returns: + bytes: The derived key. + """ + if not password: + raise ValueError("Password cannot be empty") + if len(salt) != SALT_SIZE: + raise ValueError("Invalid salt size") + + try: + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=KEY_SIZE, + salt=salt, + iterations=ITERATIONS, + ) + return kdf.derive(password.encode("utf-8")) + except Exception as e: + raise ValueError(f"Key derivation failed: {str(e)}") + + @staticmethod + def encrypt_file( + src_path: str, dest_path: str, password: str, progress_callback=None + ) -> None: + """Secure file encryption with chunk processing using unique nonce per chunk + Args: + src_path (str): The path to the source file to encrypt. + dest_path (str): The path to save the encrypted file. + password (str): The password for encryption. + progress_callback (callable, optional): A callback function to report progress. + Raises: + FileNotFoundError: If the source file does not exist. + ValueError: If the file is too large or if the password is invalid. + RuntimeError: If encryption fails. + """ + if not os.path.exists(src_path): + raise FileNotFoundError(f"Source file not found: {src_path}") + + salt = secrets.token_bytes(SALT_SIZE) + key = CryptoManager.derive_key(password, salt) + + file_size = os.path.getsize(src_path) + if file_size > MAX_FILE_SIZE: + raise ValueError( + f"File too large ({file_size/1024/1024:.2f}MB > {MAX_FILE_SIZE/1024/1024}MB" + ) + + try: + with open(src_path, "rb") as fin, open(dest_path, "wb") as fout: + # Write salt first + fout.write(salt) + + # Calculate total chunks + total_chunks = (file_size + CHUNK_SIZE - 1) // CHUNK_SIZE + bytes_processed = 0 + + for chunk_num in range(total_chunks): + chunk = fin.read(CHUNK_SIZE) + if not chunk: + break + + # Generate unique nonce for each chunk + nonce = secrets.token_bytes(NONCE_SIZE) + aesgcm = AESGCM(key) + + # Include chunk number in additional data to prevent reordering + additional_data = f"chunk_{chunk_num}_of_{total_chunks}".encode() + + encrypted_chunk = aesgcm.encrypt(nonce, chunk, additional_data) + + # Write nonce followed by encrypted chunk + fout.write(nonce + encrypted_chunk) + + bytes_processed += len(chunk) + if progress_callback: + progress = int((bytes_processed / file_size) * 100) + progress_callback(progress) + + except Exception as e: + if os.path.exists(dest_path): + try: + os.remove(dest_path) + except: + pass + raise RuntimeError(f"Encryption failed: {str(e)}") + + @staticmethod + def decrypt_file( + src_path: str, dest_path: str, password: str, progress_callback=None + ) -> None: + """Secure file decryption with chunk validation + Args: + src_path (str): The path to the encrypted file. + dest_path (str): The path to save the decrypted file. + password (str): The password for decryption. + progress_callback (callable, optional): A callback function to report progress. + Raises: + FileNotFoundError: If the source file does not exist. + ValueError: If the file structure is invalid or if the password is incorrect. + RuntimeError: If decryption fails. + """ + if not os.path.exists(src_path): + raise FileNotFoundError(f"Source file not found: {src_path}") + + try: + with open(src_path, "rb") as fin: + # Read salt + salt = fin.read(SALT_SIZE) + if len(salt) != SALT_SIZE: + raise ValueError("Invalid salt size") + + key = CryptoManager.derive_key(password, salt) + + # Get remaining file size + remaining_size = os.path.getsize(src_path) - SALT_SIZE + if remaining_size <= 0: + raise ValueError("Invalid file structure") + + # Calculate total chunks + total_chunks = 0 + while True: + # Read nonce + nonce = fin.read(NONCE_SIZE) + if len(nonce) == 0: + break # End of file + if len(nonce) != NONCE_SIZE: + raise ValueError("Invalid nonce size") + + # Read encrypted chunk (data + tag) + encrypted_chunk = fin.read(CHUNK_SIZE + 16) + if not encrypted_chunk: + break + + total_chunks += 1 + + # Reset file pointer + fin.seek(SALT_SIZE) + bytes_processed = SALT_SIZE + + with open(dest_path, "wb") as fout: + for chunk_num in range(total_chunks): + # Read nonce + nonce = fin.read(NONCE_SIZE) + if len(nonce) != NONCE_SIZE: + raise ValueError("Invalid nonce size") + + # Read encrypted chunk + encrypted_chunk = fin.read(CHUNK_SIZE + 16) + if not encrypted_chunk: + break + + aesgcm = AESGCM(key) + additional_data = ( + f"chunk_{chunk_num}_of_{total_chunks}".encode() + ) + + try: + decrypted_chunk = aesgcm.decrypt( + nonce, encrypted_chunk, additional_data + ) + fout.write(decrypted_chunk) + except InvalidTag: + raise ValueError( + f"Chunk {chunk_num} authentication failed - possible tampering" + ) + + bytes_processed += len(encrypted_chunk) + NONCE_SIZE + if progress_callback: + progress = int( + (bytes_processed / (remaining_size + SALT_SIZE)) * 100 + ) + progress_callback(progress) + + except InvalidTag: + if os.path.exists(dest_path): + try: + os.remove(dest_path) + except: + pass + raise ValueError("Incorrect password or corrupted file") + except Exception as e: + if os.path.exists(dest_path): + try: + os.remove(dest_path) + except: + pass + raise RuntimeError(f"Decryption failed: {str(e)}") + + +class CryptoWorker(QThread): + """ + CryptoWorker + A worker thread for performing encryption and decryption operations. + This class inherits from QThread and emits signals to update the progress, + status, and completion of the operation. + It handles the encryption and decryption of files using the CryptoManager class. + It also manages the deletion of original files if requested. + It provides a thread-safe way to perform long-running operations without blocking the GUI. + It emits signals to update the progress, status, and completion of the operation. + It also handles errors and exceptions that may occur during the operation. + It provides a way to stop the operation gracefully. + + Args: + QThread (QThread): Inherits from QThread to run the encryption/decryption in a separate thread. + Attributes: + operation (str): The operation to perform ("encrypt" or "decrypt"). + args (tuple): The arguments for the operation (source path, destination path, password). + _is_running (bool): Flag to indicate if the thread is running. + mutex (QMutex): Mutex for thread safety. + delete_original (bool): Flag to indicate if the original file should be deleted after encryption/decryption. + Signals: + progress_updated (int): Signal emitted to update the progress of the operation. + status_updated (str): Signal emitted to update the status of the operation. + operation_completed (bool, str): Signal emitted when the operation is completed. + error_occurred (str): Signal emitted when an error occurs during the operation. + delete_original_requested (str): Signal emitted when a request to delete the original file is made. + Methods: + set_delete_original(delete: bool) -> None: + Sets the flag to indicate if the original file should be deleted. + stop() -> None: + Stops the operation gracefully. + run() -> None: + Runs the encryption/decryption operation in a separate thread. + """ + + progress_updated = pyqtSignal(int) + status_updated = pyqtSignal(str) + operation_completed = pyqtSignal(bool, str) + error_occurred = pyqtSignal(str) + delete_original_requested = pyqtSignal(str) + + def __init__(self, operation, *args): + super().__init__() + self.operation = operation + self.args = args + self._is_running = True + self.mutex = QMutex() + self.delete_original = False + + def set_delete_original(self, delete): + """Set the flag to indicate if the original file should be deleted.""" + self.delete_original = delete + + def stop(self): + """Stop the operation gracefully.""" + with self.mutex: + self._is_running = False + + def run(self): + """Run the encryption/decryption operation in a separate thread.""" + try: + if not self._is_running: + return + + if self.operation == "encrypt": + src_path, dest_path, password = self.args + CryptoManager.encrypt_file( + src_path, + dest_path, + password, + progress_callback=self.progress_updated.emit, + ) + + if self.delete_original: + try: + os.remove(src_path) + self.delete_original_requested.emit(src_path) + except Exception as e: + self.error_occurred.emit( + f"Failed to delete original file: {str(e)}" + ) + + elif self.operation == "decrypt": + src_path, dest_path, password = self.args + CryptoManager.decrypt_file( + src_path, + dest_path, + password, + progress_callback=self.progress_updated.emit, + ) + + if self.delete_original: + try: + os.remove(src_path) + self.delete_original_requested.emit(src_path) + except Exception as e: + self.error_occurred.emit( + f"Failed to delete original file: {str(e)}" + ) + + self.operation_completed.emit(True, "Operation completed successfully") + + except Exception as e: + self.error_occurred.emit(f"Error: {str(e)}") + self.operation_completed.emit(False, str(e)) + + +class DriveCrypto(QThread): + """ + DriveCrypto + A worker thread for performing encryption and decryption operations on a drive. + This class inherits from QThread and emits signals to update the progress, + status, and completion of the operation. + It handles the encryption and decryption of files using the CryptoManager class. + It also manages the deletion of original files if requested. + It provides a thread-safe way to perform long-running operations without blocking the GUI. + It emits signals to update the progress, status, and completion of the operation. + It also handles errors and exceptions that may occur during the operation. + + Args: + QThread (QThread): Inherits from QThread to run the encryption/decryption in a separate thread. + Attributes: + drive_path (str): The path to the drive to encrypt/decrypt. + operation (str): The operation to perform ("encrypt" or "decrypt"). + password (str): The password for encryption/decryption. + delete_original (bool): Flag to indicate if the original file should be deleted after encryption/decryption. + directory_structure (dict): Dictionary to store the directory structure of the drive. + file_structure (dict): Dictionary to store the file structure of the drive. + _is_running (bool): Flag to indicate if the thread is running. + mutex (QMutex): Mutex for thread safety. + Signals: + result_ready (int): Signal emitted when the operation is completed. + progress_updated (int): Signal emitted to update the progress of the operation. + status_updated (str): Signal emitted to update the status of the operation. + operation_completed (bool, str): Signal emitted when the operation is completed. + error_occurred (str): Signal emitted when an error occurs during the operation. + delete_original_requested (str): Signal emitted when a request to delete the original file is made. + Methods: + stop() -> None: + Stops the operation gracefully. + get_directory_structure() -> None: + Gets the directory structure of the drive. + visualize_directory_structure_as_string() -> str: + Visualizes the directory structure as a string. + visualize_directory_structure_as_single_line_string() -> str: + Visualizes the directory structure as a single line string. + run() -> None: + Runs the encryption/decryption operation in a separate thread. + """ + + result_ready = pyqtSignal(int) + progress_updated = pyqtSignal(int) + status_updated = pyqtSignal(str) + operation_completed = pyqtSignal(bool, str) + error_occurred = pyqtSignal(str) + delete_original_requested = pyqtSignal(str) + + def __init__( + self, + drive_path: str, + operation: str, + password: str, + delete_original: bool = False, + ): + super().__init__() + if not os.path.isdir(drive_path): + raise ValueError(f"Invalid drive path: {drive_path}") + self.drive_path = drive_path + self.operation = operation + self.password = password + self.delete_original = delete_original + self.directory_structure = {} + self.file_structure = {} + self._is_running = True + self.mutex = QMutex() + self.get_directory_structure() + + def stop(self): + """Stop the operation gracefully.""" + with self.mutex: + self._is_running = False + + def get_directory_structure(self) -> None: + """Get the directory structure of the drive.""" + for root, dirs, files in os.walk(self.drive_path): + relative_path = os.path.relpath(root, self.drive_path) + self.directory_structure[relative_path] = dirs + for file in files: + file_path = os.path.join(relative_path, file) + self.file_structure[file_path] = os.path.getsize( + os.path.join(root, file) + ) + + def visualize_directory_structure_as_string(self) -> str: + """Visualize the directory structure as a string""" + structure_str = "" + for dir_path, dirs in self.directory_structure.items(): + structure_str += f"Directory: {dir_path}\n" + for file_path, size in self.file_structure.items(): + if os.path.dirname(file_path) == dir_path: + structure_str += ( + f" File: {os.path.basename(file_path)} - Size: {size} bytes\n" + ) + return structure_str + + def visualize_directory_structure_as_single_line_string(self) -> str: + """Visualize the directory structure as a string""" + structure_str = "" + for dir_path, dirs in self.directory_structure.items(): + structure_str += f"Directory: {dir_path};" + for file_path, size in self.file_structure.items(): + if os.path.dirname(file_path) == dir_path: + structure_str += ( + f" File: {os.path.basename(file_path)} - Size: {size} bytes;" + ) + return structure_str + + def run(self): + """Run the encryption/decryption operation in a separate thread.""" + try: + print("Starting encryption/decryption process...") + if not self._is_running: + print("Process stopped by user.") + return + + print("Directory structure:") + log_activity( + "directory-structure", + self.drive_path, + self.visualize_directory_structure_as_single_line_string(), + ) + if self.operation == "encrypt": + print("Encrypting files...") + print(self.file_structure) + for file_path, size in self.file_structure.items(): + print(f"Processing file: {file_path} - Size: {size} bytes") + if ( + not file_path.endswith(".enc") + and not file_path.endswith(".key") + and not file_path.endswith(".log") + ): + src_path = os.path.join(self.drive_path, file_path) + dest_path = os.path.join(self.drive_path, f"{file_path}.enc") + self.status_updated.emit(f"Encrypting {file_path}") + print(f"Encrypting {src_path} to {dest_path}") + CryptoManager.encrypt_file( + src_path, + dest_path, + self.password, + progress_callback=self.progress_updated.emit, + ) + if self.delete_original: + try: + os.remove(src_path) + self.delete_original_requested.emit(src_path) + except Exception as e: + self.error_occurred.emit( + f"Failed to delete {src_path}: {str(e)}" + ) + + elif self.operation == "decrypt": + for file_path, size in self.file_structure.items(): + if ( + file_path.endswith(".enc") + and not file_path.endswith(".key") + and not file_path.endswith(".log") + ): + src_path = os.path.join(self.drive_path, file_path) + dest_path = os.path.join(self.drive_path, file_path[:-4]) + self.status_updated.emit(f"Decrypting {file_path}") + CryptoManager.decrypt_file( + src_path, + dest_path, + self.password, + progress_callback=self.progress_updated.emit, + ) + if self.delete_original: + try: + os.remove(src_path) + self.delete_original_requested.emit(src_path) + except Exception as e: + self.error_occurred.emit( + f"Failed to delete {src_path}: {str(e)}" + ) + + self.operation_completed.emit(True, "Operation completed successfully") + self.result_ready.emit(True) + + except Exception as e: + self.error_occurred.emit(f"Error: {str(e)}") + self.operation_completed.emit(False, str(e)) + self.result_ready.emit(False) + + +class PasswordRecovery: + def __init__(self, path: str, key: str = None): + if not os.path.isdir(path): + path = os.path.dirname(path) + self.drive_path: str = path + self.key: str = key + self.recovery_key: str = None + self.strategy = None + + def setup_key_recovery( + self, strategy: str, recovery_key: str, additional_info: dict = {} + ) -> None: + """ + Setup key recovery strategy + Args: + strategy (str): The recovery strategy to use (e.g., "seed_phrase", "security_questions + recovery_key (str): The recovery key to use. + additional_info (dict): Additional information for the recovery strategy. + """ + + self.strategy = strategy + + if self.strategy == "seed_phrase": + self.recovery_key = recovery_key + elif self.strategy == "security_questions": + self.recovery_key = recovery_key + questions_path = os.path.join(self.drive_path, "security.questions") + with open(questions_path, "w") as f: + for question in additional_info.items(): + f.write(f"{question}\n") + print(f"Security questions saved to {questions_path}") + elif self.strategy == "hardware_token": + # self.recovery_keys = [seed_phrase for _, seed_phrase in additional_info] + self.recovery_key = recovery_key + + def encrypt_recovery_key(self) -> None: + """ + Encrypt the recovery key + """ + if not self.recovery_key: + raise ValueError("Recovery key not set") + + # Encrypt the recovery key with the password + salt = secrets.token_bytes(SALT_SIZE) + key = CryptoManager.derive_key(self.recovery_key, salt) + + # Encrypt the recovery key + aesgcm = AESGCM(key) + nonce = secrets.token_bytes(NONCE_SIZE) + encrypted_key = aesgcm.encrypt(nonce, self.key.encode(), None) + + # Save the encrypted key to a file + encrypted_key_path = os.path.join(self.drive_path, "encrypted.key") + with open(encrypted_key_path, "wb") as f: + f.write(salt + nonce + encrypted_key) + print(f"Encrypted recovery key saved to {encrypted_key_path}") + return encrypted_key_path + + def decrypt_recovery_key(self) -> str: + """ + Decrypt the recovery key + """ + # if self.strategy == "hardware_token": + # return self.decrypt_against_multiple() + + encrypted_key_path = os.path.join(self.drive_path, "encrypted.key") + if not os.path.exists(encrypted_key_path): + raise FileNotFoundError(f"Encrypted key not found: {encrypted_key_path}") + + with open(encrypted_key_path, "rb") as f: + data = f.read() + salt = data[:SALT_SIZE] + nonce = data[SALT_SIZE : SALT_SIZE + NONCE_SIZE] + encrypted_key = data[SALT_SIZE + NONCE_SIZE :] + + # Derive the key from the password + key = CryptoManager.derive_key(self.recovery_key, salt) + + # Decrypt the recovery key + aesgcm = AESGCM(key) + decrypted_key = aesgcm.decrypt(nonce, encrypted_key, None) + print(f"Decrypted recovery key: {decrypted_key.decode()}") + return decrypted_key.decode() + + def decrypt_against_multiple(self): + """ + Decrypt the recovery key against multiple hardware tokens + """ + for recovery_key in self.recovery_keys: + try: + encrypted_key_path = os.path.join(self.drive_path, "encrypted.key") + if not os.path.exists(encrypted_key_path): + raise FileNotFoundError( + f"Encrypted key not found: {encrypted_key_path}" + ) + + with open(encrypted_key_path, "rb") as f: + data = f.read() + salt = data[:SALT_SIZE] + nonce = data[SALT_SIZE : SALT_SIZE + NONCE_SIZE] + encrypted_key = data[SALT_SIZE + NONCE_SIZE :] + + if self.strategy == "hardware_token": + self.decrypt_against_multiple() + + # Derive the key from the password + key = CryptoManager.derive_key(self.recovery_key, salt) + + # Decrypt the recovery key + aesgcm = AESGCM(key) + decrypted_key = aesgcm.decrypt(nonce, encrypted_key, None) + print(f"Decrypted recovery key: {decrypted_key.decode()}") + return decrypted_key.decode() + except Exception as e: + print(e) + return None + + +class HardwareToken: + """ + HardwareToken + A class for managing a hardware token (e.g., Pico Key) for secure storage of seed phrases. + This class provides methods to connect to the token, check available space, + write and retrieve seed phrases, and clear the token's data. + It uses the serial library to communicate with the token over a serial port. + It provides a way to securely store and retrieve seed phrases using the hardware token. + """ + def __init__(self): + self.ser = None + self.token_port = None + self.token_name = "No - Token" + + def find_token_port(self): + """Find the token's serial port.""" + ports = serial.tools.list_ports.comports() + for port in ports: + if ( + "Board in FS mode" in port.description + or "MicroPython" in port.description + or "Board CDC" in port.description + ): + return port.device + raise Exception("Token not found - is it plugged in?") + + def connect(self): + """Connect to the token.""" + try: + self.token_port = self.find_token_port() + self.ser = serial.Serial(self.token_port, baudrate=115200, timeout=4) + print(f"Connected to token on {self.token_port}") + + # Send commands and receive responses + self.ser.write(b"search:\n") + response: str = self.ser.readline().decode().strip() + if response.find("Key") == -1: + raise Exception("Valid Hardware Token not found - is it plugged in?") + else: + self.token_name = response + except Exception as e: + print("Error:", e) + self.disconnect() + raise + + def has_space(self) -> bool: + """Check if the hardware token has enough space for storing data.""" + if not self.ser or not self.ser.is_open: + raise Exception("Token is not connected") + self.ser.write(b"check:space\n") + response = self.ser.readline().decode().strip() + if response == "OK": + return True + elif response == "NO_SPACE": + return False + else: + raise Exception(f"Unexpected response from token: {response}") + + def get_space(self) -> int: + """Retrieve the available space on the hardware token.""" + if not self.ser or not self.ser.is_open: + raise Exception("Token is not connected") + self.ser.write(b"get:space\n") + response = self.ser.readline().decode().strip() + if response.startswith("SPACE:"): + try: + return int(response.split("SPACE:")[1]) + except ValueError: + raise Exception(f"Invalid space value received from token: {response}") + else: + raise Exception(f"Failed to retrieve space from token: {response}") + + def write_seed_phrase_to_token(self, seed_phrase: str) -> None: + """Write a seed phrase to the hardware token.""" + if not self.ser or not self.ser.is_open: + raise Exception("Token is not connected") + if not self.has_space(): + raise Exception("Token does not have enough space to store the seed phrase") + self.ser.write(f"add:{seed_phrase}\n".encode()) + response = self.ser.readline().decode().strip() + if response != "OK": + raise Exception(f"Failed to write seed phrase to token: {response}") + + def get_seed_phrase_from_token(self) -> dict: + """Retrieve the seed phrase from the hardware token.""" + if not self.ser or not self.ser.is_open: + raise Exception("Token is not connected") + self.ser.write(b"get\n") + response = self.ser.readline().decode().strip() + if response.startswith("SEED:"): + list_seed_phrases = ast.literal_eval(response.split("SEED:")[1]) + dict_seed_phrases = { + f"Seed {i+1}": phrase for i, phrase in enumerate(list_seed_phrases) + } + return dict_seed_phrases + else: + raise Exception(f"Failed to retrieve seed phrase from token: {response}") + + def empty_token(self): + """Erase all data stored on the hardware token.""" + if not self.ser or not self.ser.is_open: + raise Exception("Token is not connected") + self.ser.write(b"clear\n") + response = self.ser.readline().decode().strip() + if response != "OK": + raise Exception(f"Failed to clear token: {response}") + print("Token successfully cleared") + + def disconnect(self): + """Disconnect from the token.""" + if self.ser and self.ser.is_open: + self.ser.close() + print(f"Disconnected from {self.token_name}") + + +if __name__ == "__main__": + token = HardwareToken() + try: + token.connect() + token.empty_token() + except Exception as e: + print(f"Error: {e}") + finally: + token.disconnect() diff --git a/encryptor(v2.0).py b/encryptor(v2.0).py deleted file mode 100644 index 498e933..0000000 --- a/encryptor(v2.0).py +++ /dev/null @@ -1,617 +0,0 @@ -#!/usr/bin/env python3 - -""" -Secure File Encryptor/Decryptor -- Uses AES-GCM with unique nonce per chunk -- Includes chunk sequence validation -- Password strength checking -- Progress reporting -- Secure file handling -""" - -import os -import sys -import secrets -import re -from functools import partial -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QTabWidget, - QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, - QLabel, QFileDialog, QTextEdit, QProgressBar, QMessageBox, QCheckBox) -from PyQt5.QtCore import QThread, pyqtSignal, QMutex, QCoreApplication, Qt -from PyQt5.QtGui import QIcon, QColor -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from cryptography.hazmat.primitives.ciphers.aead import AESGCM -from cryptography.exceptions import InvalidTag -from qt_material import apply_stylesheet - -# Constants -CHUNK_SIZE = 1024 * 1024 # 1MB chunks -SALT_SIZE = 16 -NONCE_SIZE = 12 -KEY_SIZE = 32 -ITERATIONS = 600_000 -MAX_FILE_SIZE = 10 * 1024 * 1024 * 1024 # 10GB - -class PasswordStrengthMeter: - @staticmethod - def calculate_strength(password): - if not password: - return 0 - - strength = 0 - - # Length check - length = len(password) - if length >= 8: - strength += 1 - if length >= 12: - strength += 1 - if length >= 16: - strength += 1 - - # Character diversity - if re.search(r'[A-Z]', password): - strength += 1 - if re.search(r'[a-z]', password): - strength += 1 - if re.search(r'[0-9]', password): - strength += 1 - if re.search(r'[^A-Za-z0-9]', password): - strength += 1 - - # Normalize to 0-100 range - max_possible = 7 # 3 for length + 4 for diversity - return int((strength / max_possible) * 100) - - @staticmethod - def get_strength_color(strength): - if strength < 30: - return QColor(255, 0, 0) # Red - elif strength < 70: - return QColor(255, 255, 0) # Yellow - else: - return QColor(0, 255, 0) # Green - -class CryptoManager: - @staticmethod - def derive_key(password: str, salt: bytes) -> bytes: - """Secure key derivation with error handling""" - if not password: - raise ValueError("Password cannot be empty") - if len(salt) != SALT_SIZE: - raise ValueError("Invalid salt size") - - try: - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - length=KEY_SIZE, - salt=salt, - iterations=ITERATIONS, - ) - return kdf.derive(password.encode('utf-8')) - except Exception as e: - raise ValueError(f"Key derivation failed: {str(e)}") - - @staticmethod - def encrypt_file(src_path: str, dest_path: str, password: str, progress_callback=None) -> None: - """Secure file encryption with chunk processing using unique nonce per chunk""" - if not os.path.exists(src_path): - raise FileNotFoundError(f"Source file not found: {src_path}") - - salt = secrets.token_bytes(SALT_SIZE) - key = CryptoManager.derive_key(password, salt) - - file_size = os.path.getsize(src_path) - if file_size > MAX_FILE_SIZE: - raise ValueError(f"File too large ({file_size/1024/1024:.2f}MB > {MAX_FILE_SIZE/1024/1024}MB") - - try: - with open(src_path, 'rb') as fin, open(dest_path, 'wb') as fout: - # Write salt first - fout.write(salt) - - # Calculate total chunks - total_chunks = (file_size + CHUNK_SIZE - 1) // CHUNK_SIZE - bytes_processed = 0 - - for chunk_num in range(total_chunks): - chunk = fin.read(CHUNK_SIZE) - if not chunk: - break - - # Generate unique nonce for each chunk - nonce = secrets.token_bytes(NONCE_SIZE) - aesgcm = AESGCM(key) - - # Include chunk number in additional data to prevent reordering - additional_data = f"chunk_{chunk_num}_of_{total_chunks}".encode() - - encrypted_chunk = aesgcm.encrypt(nonce, chunk, additional_data) - - # Write nonce followed by encrypted chunk - fout.write(nonce + encrypted_chunk) - - bytes_processed += len(chunk) - if progress_callback: - progress = int((bytes_processed / file_size) * 100) - progress_callback(progress) - - except Exception as e: - if os.path.exists(dest_path): - try: - os.remove(dest_path) - except: - pass - raise RuntimeError(f"Encryption failed: {str(e)}") - - @staticmethod - def decrypt_file(src_path: str, dest_path: str, password: str, progress_callback=None) -> None: - """Secure file decryption with chunk validation""" - if not os.path.exists(src_path): - raise FileNotFoundError(f"Source file not found: {src_path}") - - try: - with open(src_path, 'rb') as fin: - # Read salt - salt = fin.read(SALT_SIZE) - if len(salt) != SALT_SIZE: - raise ValueError("Invalid salt size") - - key = CryptoManager.derive_key(password, salt) - - # Get remaining file size - remaining_size = os.path.getsize(src_path) - SALT_SIZE - if remaining_size <= 0: - raise ValueError("Invalid file structure") - - # Calculate total chunks - total_chunks = 0 - while True: - # Read nonce - nonce = fin.read(NONCE_SIZE) - if len(nonce) == 0: - break # End of file - if len(nonce) != NONCE_SIZE: - raise ValueError("Invalid nonce size") - - # Read encrypted chunk (data + tag) - encrypted_chunk = fin.read(CHUNK_SIZE + 16) - if not encrypted_chunk: - break - - total_chunks += 1 - - # Reset file pointer - fin.seek(SALT_SIZE) - bytes_processed = SALT_SIZE - - with open(dest_path, 'wb') as fout: - for chunk_num in range(total_chunks): - # Read nonce - nonce = fin.read(NONCE_SIZE) - if len(nonce) != NONCE_SIZE: - raise ValueError("Invalid nonce size") - - # Read encrypted chunk - encrypted_chunk = fin.read(CHUNK_SIZE + 16) - if not encrypted_chunk: - break - - aesgcm = AESGCM(key) - additional_data = f"chunk_{chunk_num}_of_{total_chunks}".encode() - - try: - decrypted_chunk = aesgcm.decrypt(nonce, encrypted_chunk, additional_data) - fout.write(decrypted_chunk) - except InvalidTag: - raise ValueError(f"Chunk {chunk_num} authentication failed - possible tampering") - - bytes_processed += len(encrypted_chunk) + NONCE_SIZE - if progress_callback: - progress = int((bytes_processed / (remaining_size + SALT_SIZE)) * 100) - progress_callback(progress) - - except InvalidTag: - if os.path.exists(dest_path): - try: - os.remove(dest_path) - except: - pass - raise ValueError("Incorrect password or corrupted file") - except Exception as e: - if os.path.exists(dest_path): - try: - os.remove(dest_path) - except: - pass - raise RuntimeError(f"Decryption failed: {str(e)}") - -class CryptoWorker(QThread): - progress_updated = pyqtSignal(int) - status_updated = pyqtSignal(str) - operation_completed = pyqtSignal(bool, str) - error_occurred = pyqtSignal(str) - delete_original_requested = pyqtSignal(str) - - def __init__(self, operation, *args): - super().__init__() - self.operation = operation - self.args = args - self._is_running = True - self.mutex = QMutex() - self.delete_original = False - - def set_delete_original(self, delete): - self.delete_original = delete - - def stop(self): - with self.mutex: - self._is_running = False - - def run(self): - try: - if not self._is_running: - return - - if self.operation == 'encrypt': - src_path, dest_path, password = self.args - CryptoManager.encrypt_file( - src_path, dest_path, password, - progress_callback=self.progress_updated.emit - ) - - if self.delete_original: - try: - os.remove(src_path) - self.delete_original_requested.emit(src_path) - except Exception as e: - self.error_occurred.emit(f"Failed to delete original file: {str(e)}") - - elif self.operation == 'decrypt': - src_path, dest_path, password = self.args - CryptoManager.decrypt_file( - src_path, dest_path, password, - progress_callback=self.progress_updated.emit - ) - - if self.delete_original: - try: - os.remove(src_path) - self.delete_original_requested.emit(src_path) - except Exception as e: - self.error_occurred.emit(f"Failed to delete original file: {str(e)}") - - self.operation_completed.emit(True, "Operation completed successfully") - - except Exception as e: - self.error_occurred.emit(f"Error: {str(e)}") - self.operation_completed.emit(False, str(e)) - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - - self.setWindowTitle("Secure File Cryptor") - self.setGeometry(100, 100, 800, 600) - self.setWindowIcon(self.load_icon()) - - self.setWindowFlags( - self.windowFlags() | - Qt.WindowSystemMenuHint | - Qt.WindowMinMaxButtonsHint | - Qt.WindowCloseButtonHint - ) - - self.init_ui() - self.worker = None - self.current_operation = None - - def load_icon(self): - if getattr(sys, 'frozen', False): - base_path = sys._MEIPASS - else: - base_path = os.path.abspath(".") - - icon_path = os.path.join(base_path, 'main_icon.ico') - - if os.path.exists(icon_path): - return QIcon(icon_path) - else: - return QIcon() - - def init_ui(self): - self.tabs = QTabWidget() - self.encrypt_tab = self.create_encrypt_tab() - self.decrypt_tab = self.create_decrypt_tab() - self.tabs.addTab(self.encrypt_tab, "Encrypt") - self.tabs.addTab(self.decrypt_tab, "Decrypt") - self.setCentralWidget(self.tabs) - - def create_encrypt_tab(self): - tab = QWidget() - layout = QVBoxLayout() - - # File selection - self.encrypt_file_line = QLineEdit() - self.encrypt_file_btn = QPushButton("Select File") - self.encrypt_file_btn.clicked.connect(partial(self.select_files, self.encrypt_file_line, False)) - - # Output path - self.encrypt_output_line = QLineEdit() - self.encrypt_output_btn = QPushButton("Select Output Path") - self.encrypt_output_btn.clicked.connect(partial(self.select_output_file, self.encrypt_output_line, "encrypted")) - - # Password fields - self.encrypt_password = QLineEdit() - self.encrypt_password.setEchoMode(QLineEdit.Password) - self.encrypt_password.textChanged.connect(self.update_password_strength) - - self.encrypt_confirm = QLineEdit() - self.encrypt_confirm.setEchoMode(QLineEdit.Password) - - # Show password checkbox - self.show_password_checkbox = QCheckBox("Show Password") - self.show_password_checkbox.stateChanged.connect(self.toggle_password_visibility) - - # Delete original file checkbox - self.delete_original_checkbox = QCheckBox("Delete original file after encryption") - - # Password strength meter - self.password_strength_label = QLabel("Password Strength:") - self.password_strength_meter = QProgressBar() - self.password_strength_meter.setRange(0, 100) - self.password_strength_meter.setTextVisible(False) - - # Progress - self.encrypt_progress = QProgressBar() - self.encrypt_log = QTextEdit() - self.encrypt_log.setReadOnly(True) - - # Buttons - self.encrypt_btn = QPushButton("Start Encryption") - self.encrypt_btn.clicked.connect(partial(self.start_operation, 'encrypt')) - - # Layout organization - file_layout = QHBoxLayout() - file_layout.addWidget(self.encrypt_file_line) - file_layout.addWidget(self.encrypt_file_btn) - - output_layout = QHBoxLayout() - output_layout.addWidget(self.encrypt_output_line) - output_layout.addWidget(self.encrypt_output_btn) - - password_layout = QVBoxLayout() - - password_row1 = QHBoxLayout() - password_row1.addWidget(QLabel("Password:")) - password_row1.addWidget(self.encrypt_password) - password_row1.addWidget(QLabel("Confirm:")) - password_row1.addWidget(self.encrypt_confirm) - - password_row2 = QHBoxLayout() - password_row2.addWidget(self.show_password_checkbox) - password_row2.addWidget(self.delete_original_checkbox) - - password_layout.addLayout(password_row1) - password_layout.addLayout(password_row2) - - strength_layout = QHBoxLayout() - strength_layout.addWidget(self.password_strength_label) - strength_layout.addWidget(self.password_strength_meter) - password_layout.addLayout(strength_layout) - - layout.addLayout(file_layout) - layout.addLayout(output_layout) - layout.addLayout(password_layout) - layout.addWidget(self.encrypt_btn) - layout.addWidget(self.encrypt_progress) - layout.addWidget(self.encrypt_log) - - tab.setLayout(layout) - return tab - - def create_decrypt_tab(self): - tab = QWidget() - layout = QVBoxLayout() - - # File selection - self.decrypt_file_line = QLineEdit() - self.decrypt_file_btn = QPushButton("Select File") - self.decrypt_file_btn.clicked.connect(partial(self.select_files, self.decrypt_file_line, False)) - - # Output path - self.decrypt_output_line = QLineEdit() - self.decrypt_output_btn = QPushButton("Select Output Path") - self.decrypt_output_btn.clicked.connect(partial(self.select_output_file, self.decrypt_output_line, "decrypted")) - - # Password field - self.decrypt_password = QLineEdit() - self.decrypt_password.setEchoMode(QLineEdit.Password) - - # Show password checkbox - self.show_password_checkbox_decrypt = QCheckBox("Show Password") - self.show_password_checkbox_decrypt.stateChanged.connect(self.toggle_password_visibility_decrypt) - - # Delete original file checkbox - self.delete_original_checkbox_decrypt = QCheckBox("Delete encrypted file after decryption") - - # Progress - self.decrypt_progress = QProgressBar() - self.decrypt_log = QTextEdit() - self.decrypt_log.setReadOnly(True) - - # Buttons - self.decrypt_btn = QPushButton("Start Decryption") - self.decrypt_btn.clicked.connect(partial(self.start_operation, 'decrypt')) - - # Layout organization - file_layout = QHBoxLayout() - file_layout.addWidget(self.decrypt_file_line) - file_layout.addWidget(self.decrypt_file_btn) - - output_layout = QHBoxLayout() - output_layout.addWidget(self.decrypt_output_line) - output_layout.addWidget(self.decrypt_output_btn) - - password_layout = QHBoxLayout() - password_layout.addWidget(QLabel("Password:")) - password_layout.addWidget(self.decrypt_password) - password_layout.addWidget(self.show_password_checkbox_decrypt) - password_layout.addWidget(self.delete_original_checkbox_decrypt) - - layout.addLayout(file_layout) - layout.addLayout(output_layout) - layout.addLayout(password_layout) - layout.addWidget(self.decrypt_btn) - layout.addWidget(self.decrypt_progress) - layout.addWidget(self.decrypt_log) - - tab.setLayout(layout) - return tab - - def toggle_password_visibility(self, state): - if state == Qt.Checked: - self.encrypt_password.setEchoMode(QLineEdit.Normal) - self.encrypt_confirm.setEchoMode(QLineEdit.Normal) - else: - self.encrypt_password.setEchoMode(QLineEdit.Password) - self.encrypt_confirm.setEchoMode(QLineEdit.Password) - - def toggle_password_visibility_decrypt(self, state): - if state == Qt.Checked: - self.decrypt_password.setEchoMode(QLineEdit.Normal) - else: - self.decrypt_password.setEchoMode(QLineEdit.Password) - - def update_password_strength(self): - password = self.encrypt_password.text() - strength = PasswordStrengthMeter.calculate_strength(password) - color = PasswordStrengthMeter.get_strength_color(strength) - - self.password_strength_meter.setValue(strength) - self.password_strength_meter.setStyleSheet( - f"QProgressBar::chunk {{ background-color: {color.name()}; }}" - ) - - def select_files(self, line_edit, multi): - if multi: - files, _ = QFileDialog.getOpenFileNames(self, "Select Files") - if files: - line_edit.setText(";".join(files)) - else: - file, _ = QFileDialog.getOpenFileName(self, "Select File") - if file: - line_edit.setText(file) - - def select_output_file(self, line_edit, default_suffix): - file, _ = QFileDialog.getSaveFileName( - self, - "Select Output File", - "", - f"Encrypted Files (*.{default_suffix})" - ) - if file: - line_edit.setText(file) - - def start_operation(self, operation): - if self.worker and self.worker.isRunning(): - QMessageBox.warning(self, "Warning", "Another operation is in progress") - return - - try: - if operation == 'encrypt': - file_path = self.encrypt_file_line.text() - output_path = self.encrypt_output_line.text() - password = self.encrypt_password.text() - confirm = self.encrypt_confirm.text() - - if not file_path: - raise ValueError("Please select a file to encrypt") - if not output_path: - raise ValueError("Please select output path") - if password != confirm: - raise ValueError("Passwords do not match") - if not password: - raise ValueError("Password cannot be empty") - - # Add .encrypted extension if not present - if not output_path.endswith('.encrypted'): - output_path += '.encrypted' - - self.worker = CryptoWorker(operation, file_path, output_path, password) - self.worker.set_delete_original(self.delete_original_checkbox.isChecked()) - self.encrypt_log.append(f"Starting encryption of {file_path}...") - - elif operation == 'decrypt': - file_path = self.decrypt_file_line.text() - output_path = self.decrypt_output_line.text() - password = self.decrypt_password.text() - - if not file_path: - raise ValueError("Please select a file to decrypt") - if not output_path: - raise ValueError("Please select output path") - if not password: - raise ValueError("Password cannot be empty") - - self.worker = CryptoWorker(operation, file_path, output_path, password) - self.worker.set_delete_original(self.delete_original_checkbox_decrypt.isChecked()) - self.decrypt_log.append(f"Starting decryption of {file_path}...") - - self.setup_worker_connections(operation) - self.worker.start() - - except Exception as e: - self.show_error(str(e)) - - def setup_worker_connections(self, operation): - if operation == 'encrypt': - log = self.encrypt_log - progress = self.encrypt_progress - btn = self.encrypt_btn - else: - log = self.decrypt_log - progress = self.decrypt_progress - btn = self.decrypt_btn - - btn.setEnabled(False) - progress.setValue(0) - self.worker.progress_updated.connect(progress.setValue) - self.worker.status_updated.connect(log.append) - self.worker.operation_completed.connect( - lambda success, msg: self.on_operation_complete(success, msg, btn, log) - ) - self.worker.error_occurred.connect( - lambda err: self.show_error(err, log) - ) - self.worker.delete_original_requested.connect( - lambda path: log.append(f"Original file deleted: {path}")) - - def on_operation_complete(self, success, message, btn, log): - btn.setEnabled(True) - if success: - log.append("Operation completed successfully") - QMessageBox.information(self, "Success", message) - else: - log.append(f"Operation failed: {message}") - QMessageBox.critical(self, "Error", message) - - def show_error(self, message, log=None): - if log: - log.append(f"Error: {message}") - QMessageBox.critical(self, "Error", message) - - def closeEvent(self, event): - if self.worker and self.worker.isRunning(): - self.worker.stop() - self.worker.wait(2000) # Wait up to 2 seconds for clean exit - event.accept() - -if __name__ == "__main__": - QCoreApplication.setAttribute(Qt.AA_DisableWindowContextHelpButton) - app = QApplication(sys.argv) - apply_stylesheet(app, theme='dark_teal.xml') - window = MainWindow() - window.show() - sys.exit(app.exec_()) diff --git a/encryptor.py b/encryptor.py deleted file mode 100644 index ec80e9c..0000000 --- a/encryptor.py +++ /dev/null @@ -1,417 +0,0 @@ -#!/usr/bin/env python3 -""" -Secure File Encryptor/Decryptor - Fixed Version -""" - -import os -import sys -import secrets -from functools import partial -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QTabWidget, - QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, - QLabel, QFileDialog, QTextEdit, QProgressBar, QMessageBox) -from PyQt5.QtCore import QThread, pyqtSignal, QMutex, QCoreApplication, Qt -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from cryptography.hazmat.primitives.ciphers.aead import AESGCM -from cryptography.exceptions import InvalidTag -from qt_material import apply_stylesheet - -# Constants -CHUNK_SIZE = 1024 * 1024 # 1MB chunks -SALT_SIZE = 16 -NONCE_SIZE = 12 -KEY_SIZE = 32 -ITERATIONS = 600_000 -MAX_FILE_SIZE = 10 * 1024 * 1024 * 1024 # 10GB - -class CryptoManager: - @staticmethod - def derive_key(password: str, salt: bytes) -> bytes: - """Secure key derivation with error handling""" - if not password: - raise ValueError("Password cannot be empty") - if len(salt) != SALT_SIZE: - raise ValueError("Invalid salt size") - - try: - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - length=KEY_SIZE, - salt=salt, - iterations=ITERATIONS, - ) - return kdf.derive(password.encode('utf-8')) - except Exception as e: - raise ValueError(f"Key derivation failed: {str(e)}") - - @staticmethod - def encrypt_file(src_path: str, dest_path: str, password: str, progress_callback=None) -> None: - """Safe file encryption with chunk processing""" - if not os.path.exists(src_path): - raise FileNotFoundError(f"Source file not found: {src_path}") - - salt = secrets.token_bytes(SALT_SIZE) - key = CryptoManager.derive_key(password, salt) - aesgcm = AESGCM(key) - nonce = secrets.token_bytes(NONCE_SIZE) - - file_size = os.path.getsize(src_path) - if file_size > MAX_FILE_SIZE: - raise ValueError(f"File too large ({file_size/1024/1024:.2f}MB > {MAX_FILE_SIZE/1024/1024}MB)") - - try: - with open(src_path, 'rb') as fin, open(dest_path, 'wb') as fout: - fout.write(salt + nonce) - total_chunks = (file_size + CHUNK_SIZE - 1) // CHUNK_SIZE - bytes_processed = 0 - - for chunk_num in range(total_chunks): - chunk = fin.read(CHUNK_SIZE) - if not chunk: - break - - encrypted_chunk = aesgcm.encrypt(nonce, chunk, None) - fout.write(encrypted_chunk) - - bytes_processed += len(chunk) - if progress_callback: - progress = int((bytes_processed / file_size) * 100) - progress_callback(progress) - - except Exception as e: - if os.path.exists(dest_path): - try: - os.remove(dest_path) - except: - pass - raise RuntimeError(f"Encryption failed: {str(e)}") - - @staticmethod - def decrypt_file(src_path: str, dest_path: str, password: str, progress_callback=None) -> None: - """Safe file decryption with chunk processing""" - if not os.path.exists(src_path): - raise FileNotFoundError(f"Source file not found: {src_path}") - - try: - with open(src_path, 'rb') as fin: - salt = fin.read(SALT_SIZE) - nonce = fin.read(NONCE_SIZE) - - if len(salt) != SALT_SIZE or len(nonce) != NONCE_SIZE: - raise ValueError("Invalid file format - missing salt or nonce") - - key = CryptoManager.derive_key(password, salt) - aesgcm = AESGCM(key) - - file_size = os.path.getsize(src_path) - SALT_SIZE - NONCE_SIZE - if file_size <= 0: - raise ValueError("Invalid file structure") - - with open(dest_path, 'wb') as fout: - total_chunks = (file_size + CHUNK_SIZE - 1) // CHUNK_SIZE - bytes_processed = 0 - - for _ in range(total_chunks): - chunk = fin.read(CHUNK_SIZE + 16) # Account for GCM tag - if not chunk: - break - - decrypted_chunk = aesgcm.decrypt(nonce, chunk, None) - fout.write(decrypted_chunk) - - bytes_processed += len(chunk) - if progress_callback: - progress = int((bytes_processed / file_size) * 100) - progress_callback(progress) - - except InvalidTag: - if os.path.exists(dest_path): - try: - os.remove(dest_path) - except: - pass - raise ValueError("Incorrect password or corrupted file") - except Exception as e: - if os.path.exists(dest_path): - try: - os.remove(dest_path) - except: - pass - raise RuntimeError(f"Decryption failed: {str(e)}") - -class CryptoWorker(QThread): - progress_updated = pyqtSignal(int) - status_updated = pyqtSignal(str) - operation_completed = pyqtSignal(bool, str) - error_occurred = pyqtSignal(str) - - def __init__(self, operation, *args): - super().__init__() - self.operation = operation - self.args = args - self._is_running = True - self.mutex = QMutex() - - def stop(self): - with self.mutex: - self._is_running = False - - def run(self): - try: - if not self._is_running: - return - - if self.operation == 'encrypt': - CryptoManager.encrypt_file( - *self.args, - progress_callback=self.progress_updated.emit - ) - elif self.operation == 'decrypt': - CryptoManager.decrypt_file( - *self.args, - progress_callback=self.progress_updated.emit - ) - - self.operation_completed.emit(True, "Operation completed successfully") - - except Exception as e: - self.error_occurred.emit(f"Error: {str(e)}") - self.operation_completed.emit(False, str(e)) - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - self.worker = None - self.current_operation = None - self.init_ui() - self.setWindowTitle("Secure File Cryptor") - self.setGeometry(100, 100, 800, 600) - - def init_ui(self): - self.tabs = QTabWidget() - self.encrypt_tab = self.create_encrypt_tab() - self.decrypt_tab = self.create_decrypt_tab() - self.tabs.addTab(self.encrypt_tab, "Encrypt") - self.tabs.addTab(self.decrypt_tab, "Decrypt") - self.setCentralWidget(self.tabs) - - def create_encrypt_tab(self): - tab = QWidget() - layout = QVBoxLayout() - - # File selection - self.encrypt_file_line = QLineEdit() - self.encrypt_file_btn = QPushButton("Select File") - self.encrypt_file_btn.clicked.connect(partial(self.select_files, self.encrypt_file_line, False)) - - # Output path - self.encrypt_output_line = QLineEdit() - self.encrypt_output_btn = QPushButton("Select Output Path") - self.encrypt_output_btn.clicked.connect(partial(self.select_output_file, self.encrypt_output_line, "encrypted")) - - # Password fields - self.encrypt_password = QLineEdit() - self.encrypt_password.setEchoMode(QLineEdit.Password) - self.encrypt_confirm = QLineEdit() - self.encrypt_confirm.setEchoMode(QLineEdit.Password) - - # Progress - self.encrypt_progress = QProgressBar() - self.encrypt_log = QTextEdit() - self.encrypt_log.setReadOnly(True) - - # Buttons - self.encrypt_btn = QPushButton("Start Encryption") - self.encrypt_btn.clicked.connect(partial(self.start_operation, 'encrypt')) - - # Layout organization - file_layout = QHBoxLayout() - file_layout.addWidget(self.encrypt_file_line) - file_layout.addWidget(self.encrypt_file_btn) - - output_layout = QHBoxLayout() - output_layout.addWidget(self.encrypt_output_line) - output_layout.addWidget(self.encrypt_output_btn) - - password_layout = QHBoxLayout() - password_layout.addWidget(QLabel("Password:")) - password_layout.addWidget(self.encrypt_password) - password_layout.addWidget(QLabel("Confirm:")) - password_layout.addWidget(self.encrypt_confirm) - - layout.addLayout(file_layout) - layout.addLayout(output_layout) - layout.addLayout(password_layout) - layout.addWidget(self.encrypt_btn) - layout.addWidget(self.encrypt_progress) - layout.addWidget(self.encrypt_log) - - tab.setLayout(layout) - return tab - - def create_decrypt_tab(self): - tab = QWidget() - layout = QVBoxLayout() - - # File selection - self.decrypt_file_line = QLineEdit() - self.decrypt_file_btn = QPushButton("Select File") - self.decrypt_file_btn.clicked.connect(partial(self.select_files, self.decrypt_file_line, False)) - - # Output path - self.decrypt_output_line = QLineEdit() - self.decrypt_output_btn = QPushButton("Select Output Path") - self.decrypt_output_btn.clicked.connect(partial(self.select_output_file, self.decrypt_output_line, "decrypted")) - - # Password field - self.decrypt_password = QLineEdit() - self.decrypt_password.setEchoMode(QLineEdit.Password) - - # Progress - self.decrypt_progress = QProgressBar() - self.decrypt_log = QTextEdit() - self.decrypt_log.setReadOnly(True) - - # Buttons - self.decrypt_btn = QPushButton("Start Decryption") - self.decrypt_btn.clicked.connect(partial(self.start_operation, 'decrypt')) - - # Layout organization - file_layout = QHBoxLayout() - file_layout.addWidget(self.decrypt_file_line) - file_layout.addWidget(self.decrypt_file_btn) - - output_layout = QHBoxLayout() - output_layout.addWidget(self.decrypt_output_line) - output_layout.addWidget(self.decrypt_output_btn) - - password_layout = QHBoxLayout() - password_layout.addWidget(QLabel("Password:")) - password_layout.addWidget(self.decrypt_password) - - layout.addLayout(file_layout) - layout.addLayout(output_layout) - layout.addLayout(password_layout) - layout.addWidget(self.decrypt_btn) - layout.addWidget(self.decrypt_progress) - layout.addWidget(self.decrypt_log) - - tab.setLayout(layout) - return tab - - # File selection methods - def select_files(self, line_edit, multi): - if multi: - files, _ = QFileDialog.getOpenFileNames(self, "Select Files") - if files: - line_edit.setText(";".join(files)) - else: - file, _ = QFileDialog.getOpenFileName(self, "Select File") - if file: - line_edit.setText(file) - - def select_output_file(self, line_edit, default_suffix): - file, _ = QFileDialog.getSaveFileName( - self, - "Select Output File", - "", - f"Encrypted Files (*.{default_suffix})" - ) - if file: - line_edit.setText(file) - - # Operation handling - def start_operation(self, operation): - if self.worker and self.worker.isRunning(): - QMessageBox.warning(self, "Warning", "Another operation is in progress") - return - - try: - if operation == 'encrypt': - file_path = self.encrypt_file_line.text() - output_path = self.encrypt_output_line.text() - password = self.encrypt_password.text() - confirm = self.encrypt_confirm.text() - - if not file_path: - raise ValueError("Please select a file to encrypt") - if not output_path: - raise ValueError("Please select output path") - if password != confirm: - raise ValueError("Passwords do not match") - if not password: - raise ValueError("Password cannot be empty") - - self.worker = CryptoWorker(operation, file_path, output_path, password) - self.encrypt_log.append(f"Starting encryption of {file_path}...") - - elif operation == 'decrypt': - file_path = self.decrypt_file_line.text() - output_path = self.decrypt_output_line.text() - password = self.decrypt_password.text() - - if not file_path: - raise ValueError("Please select a file to decrypt") - if not output_path: - raise ValueError("Please select output path") - if not password: - raise ValueError("Password cannot be empty") - - self.worker = CryptoWorker(operation, file_path, output_path, password) - self.decrypt_log.append(f"Starting decryption of {file_path}...") - - self.setup_worker_connections(operation) - self.worker.start() - - except Exception as e: - self.show_error(str(e)) - - def setup_worker_connections(self, operation): - if operation == 'encrypt': - log = self.encrypt_log - progress = self.encrypt_progress - btn = self.encrypt_btn - else: - log = self.decrypt_log - progress = self.decrypt_progress - btn = self.decrypt_btn - - btn.setEnabled(False) - progress.setValue(0) - self.worker.progress_updated.connect(progress.setValue) - self.worker.status_updated.connect(log.append) - self.worker.operation_completed.connect( - lambda success, msg: self.on_operation_complete(success, msg, btn, log) - ) - self.worker.error_occurred.connect( - lambda err: self.show_error(err, log) - ) - - def on_operation_complete(self, success, message, btn, log): - btn.setEnabled(True) - if success: - log.append("Operation completed successfully") - QMessageBox.information(self, "Success", message) - else: - log.append(f"Operation failed: {message}") - QMessageBox.critical(self, "Error", message) - - def show_error(self, message, log=None): - if log: - log.append(f"Error: {message}") - QMessageBox.critical(self, "Error", message) - - def closeEvent(self, event): - if self.worker and self.worker.isRunning(): - self.worker.stop() - self.worker.wait(2000) # Wait up to 2 seconds for clean exit - event.accept() - -if __name__ == "__main__": - QCoreApplication.setAttribute(Qt.AA_DisableWindowContextHelpButton) - app = QApplication(sys.argv) - apply_stylesheet(app, theme='dark_teal.xml') - window = MainWindow() - window.show() - sys.exit(app.exec_()) \ No newline at end of file diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..31172af --- /dev/null +++ b/gui.py @@ -0,0 +1,1575 @@ +from functools import partial +import re +import time +from PyQt5.QtWidgets import ( + QMainWindow, + QWidget, + QTabWidget, + QVBoxLayout, + QHBoxLayout, + QLineEdit, + QPushButton, + QLabel, + QFileDialog, + QTextEdit, + QProgressBar, + QMessageBox, + QCheckBox, + QRadioButton, + QComboBox, + QDialog, +) +from PyQt5.QtGui import QIcon +import sys +import os +from PyQt5.QtCore import Qt + +from crypto import CryptoWorker, DriveCrypto, HardwareToken, PasswordRecovery +from utilities import PasswordStrengthMeter, convert_to_multi_line, log_activity + +from utilities import generate_seed_phrase + +import hashlib + +from pyqtspinner.spinner import WaitingSpinner +from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView, QDateEdit +from PyQt5.QtCore import QDate + + +class MainWindow(QMainWindow): + """MainWindow Class + This class represents the main window of the Encryptor application. It provides a graphical user interface (GUI) + for encrypting, decrypting, and recovering files or folders. The application also includes features for setting + up key recovery options, managing activity logs, and utilizing hardware tokens for secure operations. + Attributes: + worker (CryptoWorker): The worker thread for encryption or decryption operations. + current_operation (str): The current operation being performed (e.g., "encrypt", "decrypt"). + encrypt_type (str): The type of encryption (e.g., "file", "folder"). + hardware_token (HardwareToken): The hardware token instance for secure operations. + hardware_token_seed_phrase (str): The seed phrase stored in the hardware token. + recovery_hardware_token_seed_phrases (dict): Seed phrases retrieved from the hardware token. + spinner (WaitingSpinner): A spinner widget to indicate ongoing operations. + operation_thread (DriveCrypto): The thread for folder-level encryption or decryption operations. + security_questions (dict): A dictionary of security questions and their respective hashes. + security_questions_text (list): A list of security questions loaded from a file. + Methods: + __init__(): Initializes the main window and its components. + load_icon(): Loads the application icon from the current directory. + load_security_questions(): Loads security questions from a file for encryption setup. + load_security_questions_for_recovery(): Loads security questions for account recovery. + init_ui(): Initializes the user interface, including tabs for encryption, decryption, recovery, and activity logs. + create_encrypt_tab(): Creates the "Encrypt" tab with file selection, password input, and recovery options. + create_decrypt_tab(): Creates the "Decrypt" tab with file selection and password input. + create_recovery_tab(): Creates the "Recover Key" tab for password recovery and re-encryption. + create_activity_log_tab(): Creates the "Activity Log" tab for viewing and filtering activity logs. + load_activity_log(): Loads the activity log from a selected file. + filter_logs(): Filters the activity log based on activity type and date range. + populate_log_table(data): Populates the activity log table with filtered data. + handle_cell_click(row, column): Handles clicks on the activity log table cells. + show_directory_structure_popup(details): Displays a popup with directory structure details. + toggle_new_password_visibility(state): Toggles the visibility of new password fields. + toggle_password_visibility(state): Toggles the visibility of password fields in the "Encrypt" tab. + toggle_password_visibility_decrypt(state): Toggles the visibility of password fields in the "Decrypt" tab. + recover_password(): Handles the password recovery process based on selected recovery options. + update_password_strength(): Updates the password strength meter for the encryption password. + update_new_password_strength(): Updates the password strength meter for the new password. + select_files(line_edit, multi): Opens a file dialog to select files for encryption or decryption. + select_folder(line_edit, multi): Opens a folder dialog to select a directory for encryption or decryption. + select_output_file(line_edit, default_suffix): Opens a file dialog to select an output file for encryption. + folder_operation(driveCrypto, operation): Performs folder-level encryption or decryption operations. + recovery_folder_operation(driveCrypto, old_password, new_password): Handles folder recovery and re-encryption. + save_recovery_stuff(operation): Saves recovery information based on selected recovery options. + recover_key(): Recovers the encryption key using the selected recovery method. + on_complete(): Stops the spinner when an operation is complete. + start_operation(operation): Starts the encryption or decryption operation. + setup_operation_thread(operation): Sets up the thread for folder-level operations. + setup_worker_connections(operation): Sets up connections for the worker thread. + on_operation_complete(success, message, btn, log, type): Handles the completion of an operation. + show_error(message, log=None): Displays an error message in a dialog and optionally logs it. + closeEvent(event): Handles the close event to ensure clean termination of threads. + toggle_recovery_section(): Toggles the visibility of the key recovery options section. + generate_seed_phrase(): Generates a random seed phrase for recovery. + register_hardware_token(): Registers a hardware token for secure operations. + toggle_decrypt_recovery_section(): Toggles the visibility of the decrypt recovery options section. + verify_hardware_token(): Verifies the hardware token during decryption. + """ + def __init__(self): + super().__init__() + + self.setWindowTitle("Encryptor") + self.setGeometry(100, 100, 1280, 720) + self.setWindowIcon(self.load_icon()) + + self.setWindowFlags( + self.windowFlags() + | Qt.WindowSystemMenuHint + | Qt.WindowMinMaxButtonsHint + | Qt.WindowCloseButtonHint + ) + + self.init_ui() + self.worker = None + self.current_operation = None + self.encrypt_type = None + + self.hardware_token = None + self.hardware_token_seed_phrase = None + self.recovery_hardware_token_seed_phrases = {} + + self.spinner = WaitingSpinner(self, color="white") + self.operation_thread = None + + def load_icon(self): + """ + Loads an icon from the current path. + + The icon name should be app-logo.ico + + Returns: + QIcon: Instance of QIcon + """ + base_path = os.path.dirname(os.path.abspath(__file__)) + + icon_path = os.path.join(base_path, "app-logo.ico") + + if os.path.exists(icon_path): + return QIcon(icon_path) + else: + return QIcon() + + def load_security_questions(self): + """ + Loads a database of questions from a file named 'security.questions'. + + The questions are then loaded into a dictionary with their respective hashes. + """ + self.security_questions = [] + try: + with open("security.questions", "r") as f: + self.security_questions_text = [line.strip() for line in f.readlines()] + + # create a dictionary with hashes of the questions as key and questions as values + self.security_questions = { + hashlib.sha256(question.encode()).hexdigest(): question + for question in self.security_questions_text + } + + except FileNotFoundError: + QMessageBox.warning( + self, + "Error", + "Security questions file not found. Please create a file named 'security.questions' with your questions.", + ) + return + except Exception as e: + QMessageBox.critical( + self, + "Error", + f"An error occurred while loading security questions: {str(e)}", + ) + return + if len(self.security_questions) < 2: + QMessageBox.warning( + self, + "Error", + "Please provide at least two security questions in the 'security.questions' file.", + ) + return + + def load_security_questions_for_recovery(self): + """ + Loads security questions for account recovery from a specified file or folder. + + This function retrieves the path to a recovery file or folder, determines the directory, + and attempts to load security questions from a file named "security.questions" in that directory. + It validates the file's content to ensure it contains at least two security questions with valid hashes. + + Raises: + ValueError: If the security questions file contains fewer than two questions or if the questions + do not contain valid hashes. + FileNotFoundError: If the "security.questions" file is not found in the specified directory. + Exception: For any other errors encountered while loading the security questions. + """ + recovery_path = self.recovery_drive_line.text().strip() + if not recovery_path: + QMessageBox.warning( + self, "Warning", "Please select a file or folder for recovery." + ) + return + + # Determine the directory of the selected file or folder + if os.path.isfile(recovery_path): + directory = os.path.dirname(recovery_path) + else: + directory = recovery_path + + # Construct the path to the security questions file + questions_file_path = os.path.join(directory, "security.questions") + + # Load security questions from the file + try: + with open(questions_file_path, "r") as f: + questions = [line.strip() for line in f.readlines()] + if len(questions) < 2: + raise ValueError( + "The security questions file must contain at least two questions." + ) + + question_hashes = [] + + for question in questions: + match = re.search(r"'([a-fA-F0-9]{64})'", question) + if match: + extracted_hash = match.group(1) + question_hashes.append(extracted_hash) + else: + print("No valid hash found in the string.") + + if len(question_hashes) < 2: + raise ValueError( + "The security questions file must contain at least two questions." + ) + + self.recovery_question1.setText( + self.security_questions.get(question_hashes[0]) + ) + self.recovery_question2.setText( + self.security_questions.get(question_hashes[1]) + ) + + except FileNotFoundError: + QMessageBox.warning( + self, "Warning", f"Security questions file not found in {directory}." + ) + except Exception as e: + QMessageBox.critical( + self, + "Error", + f"An error occurred while loading security questions: {str(e)}", + ) + + def init_ui(self): + """ + Initializes the UI for the App + """ + self.tabs = QTabWidget() + self.encrypt_tab = self.create_encrypt_tab() + self.decrypt_tab = self.create_decrypt_tab() + self.recovery_tab = self.create_recovery_tab() + self.activity_log_tab = self.create_activity_log_tab() + self.tabs.addTab(self.encrypt_tab, "Encrypt") + self.tabs.addTab(self.decrypt_tab, "Decrypt") + self.tabs.addTab(self.recovery_tab, "Recover Key") + self.tabs.addTab(self.activity_log_tab, "Activity Log") + self.setCentralWidget(self.tabs) + + def create_encrypt_tab(self): + """ + Creates an encrypt tab for the UI with appropriate elements + + Returns: + QWidget: The UI for the Encrypt Tab + """ + + tab = QWidget() + layout = QVBoxLayout() + + # File selection (existing code remains the same) + self.encrypt_file_line = QLineEdit() + self.encrypt_file_btn = QPushButton("Select File") + self.encrypt_file_btn.clicked.connect( + partial(self.select_files, self.encrypt_file_line, False) + ) + self.encrypt_directory_btn = QPushButton("Select Directory") + self.encrypt_directory_btn.clicked.connect( + partial(self.select_folder, self.encrypt_file_line, True) + ) + + # Password fields (existing code remains the same) + self.encrypt_password = QLineEdit() + self.encrypt_password.setEchoMode(QLineEdit.Password) + self.encrypt_password.textChanged.connect(self.update_password_strength) + + self.encrypt_confirm = QLineEdit() + self.encrypt_confirm.setEchoMode(QLineEdit.Password) + + # Show password checkbox (existing code remains the same) + self.show_password_checkbox = QCheckBox("Show Password") + self.show_password_checkbox.stateChanged.connect( + self.toggle_password_visibility + ) + + # Delete original file checkbox (existing code remains the same) + self.delete_original_checkbox = QCheckBox( + "Delete original file after encryption" + ) + + # Password strength meter (existing code remains the same) + self.password_strength_label = QLabel("Password Strength:") + self.password_strength_meter = QProgressBar() + self.password_strength_meter.setRange(0, 100) + self.password_strength_meter.setTextVisible(False) + + # ===== NEW KEY RECOVERY SECTION ===== + self.recovery_section = QWidget() + self.recovery_section.setVisible(False) # Start collapsed + recovery_layout = QVBoxLayout() + + # Seed phrase recovery + self.seed_phrase_radio_btn = QRadioButton("Enable Seed Phrase Recovery") + self.seed_phrase_text = QTextEdit() + self.seed_phrase_text.setReadOnly(True) + self.seed_phrase_text.setMaximumHeight(60) + self.generate_seed_btn = QPushButton("Generate 12-word Phrase") + self.generate_seed_btn.clicked.connect(self.generate_seed_phrase) + + # Security questions + self.security_questions_radio_btn = QRadioButton("Enable Security Questions") + self.security_questions_widget = QWidget() + sq_layout = QVBoxLayout() + + self.load_security_questions() + + # Dropdown for selecting questions + self.question1 = QComboBox() + self.question1.addItems(self.security_questions.values()) + self.answer1 = QLineEdit() + self.answer1.setPlaceholderText("Answer for Question 1") + self.answer1.setEchoMode(QLineEdit.Password) + + self.question2 = QComboBox() + self.question2.addItems(self.security_questions.values()) + self.answer2 = QLineEdit() + self.answer2.setPlaceholderText("Answer for Question 2") + self.answer2.setEchoMode(QLineEdit.Password) + + sq_layout.addWidget(self.question1) + sq_layout.addWidget(self.answer1) + sq_layout.addWidget(self.question2) + sq_layout.addWidget(self.answer2) + self.security_questions_widget.setLayout(sq_layout) + + # Hardware token + self.hardware_token_radio_btn = QRadioButton( + "Enable Hardware Token (e.g., YubiKey)" + ) + self.register_token_btn = QPushButton("Register Device") + self.register_token_btn.clicked.connect(self.register_hardware_token) + + # Add to recovery layout + recovery_layout.addWidget(self.seed_phrase_radio_btn) + recovery_layout.addWidget(self.seed_phrase_text) + recovery_layout.addWidget(self.generate_seed_btn) + recovery_layout.addWidget(self.security_questions_radio_btn) + recovery_layout.addWidget(self.security_questions_widget) + recovery_layout.addWidget(self.hardware_token_radio_btn) + recovery_layout.addWidget(self.register_token_btn) + self.recovery_section.setLayout(recovery_layout) + + # Toggle button for the section + self.toggle_recovery_btn = QPushButton("β–Ό Set up Key Recovery Options") + self.toggle_recovery_btn.setCheckable(True) + self.toggle_recovery_btn.setChecked(False) + self.toggle_recovery_btn.setStyleSheet("text-align: left;") + self.toggle_recovery_btn.clicked.connect(self.toggle_recovery_section) + + # Progress (existing code remains the same) + self.encrypt_progress = QProgressBar() + self.encrypt_log = QTextEdit() + self.encrypt_log.setReadOnly(True) + + # Buttons (existing code remains the same) + self.encrypt_btn = QPushButton("Start Encryption") + self.encrypt_btn.clicked.connect(partial(self.start_operation, "encrypt")) + + # Layout organization (modified to include recovery section) + file_layout = QHBoxLayout() + file_layout.addWidget(self.encrypt_file_line) + file_layout.addWidget(self.encrypt_file_btn) + file_layout.addWidget(self.encrypt_directory_btn) + + password_layout = QVBoxLayout() + + password_row1 = QHBoxLayout() + password_row1.addWidget(QLabel("Password:")) + password_row1.addWidget(self.encrypt_password) + password_row1.addWidget(QLabel("Confirm:")) + password_row1.addWidget(self.encrypt_confirm) + + password_row2 = QHBoxLayout() + password_row2.addWidget(self.show_password_checkbox) + password_row2.addWidget(self.delete_original_checkbox) + + password_layout.addLayout(password_row1) + password_layout.addLayout(password_row2) + + strength_layout = QHBoxLayout() + strength_layout.addWidget(self.password_strength_label) + strength_layout.addWidget(self.password_strength_meter) + password_layout.addLayout(strength_layout) + + layout.addLayout(file_layout) + layout.addLayout(password_layout) + layout.addWidget(self.toggle_recovery_btn) # Add the toggle button + layout.addWidget(self.recovery_section) # Add the recovery section + layout.addWidget(self.encrypt_btn) + layout.addWidget(self.encrypt_progress) + layout.addWidget(self.encrypt_log) + + tab.setLayout(layout) + return tab + + def create_decrypt_tab(self): + """ + Creates an decrypt tab for the UI with appropriate elements + + Returns: + QWidget: The UI for the Decrypt Tab + """ + + tab = QWidget() + layout = QVBoxLayout() + + # File selection + self.decrypt_file_line = QLineEdit() + self.decrypt_file_btn = QPushButton("Select File") + self.decrypt_file_btn.clicked.connect( + partial(self.select_files, self.decrypt_file_line, False) + ) + + self.decrypt_directory_btn = QPushButton("Select Directory") + self.decrypt_directory_btn.clicked.connect( + partial(self.select_folder, self.decrypt_file_line, True) + ) + + # Password field + self.decrypt_password = QLineEdit() + self.decrypt_password.setEchoMode(QLineEdit.Password) + + # Show password checkbox + self.show_password_checkbox_decrypt = QCheckBox("Show Password") + self.show_password_checkbox_decrypt.stateChanged.connect( + self.toggle_password_visibility_decrypt + ) + + # Delete original file checkbox + self.delete_original_checkbox_decrypt = QCheckBox( + "Delete encrypted file after decryption" + ) + + # Progress + self.decrypt_progress = QProgressBar() + self.decrypt_log = QTextEdit() + self.decrypt_log.setReadOnly(True) + + # Buttons + self.decrypt_btn = QPushButton("Start Decryption") + self.decrypt_btn.clicked.connect(partial(self.start_operation, "decrypt")) + + # Layout organization + file_layout = QHBoxLayout() + file_layout.addWidget(self.decrypt_file_line) + file_layout.addWidget(self.decrypt_file_btn) + file_layout.addWidget(self.decrypt_directory_btn) + + password_layout = QHBoxLayout() + password_layout.addWidget(QLabel("Password:")) + password_layout.addWidget(self.decrypt_password) + password_layout.addWidget(self.show_password_checkbox_decrypt) + password_layout.addWidget(self.delete_original_checkbox_decrypt) + + layout.addLayout(file_layout) + layout.addLayout(password_layout) + layout.addWidget(self.decrypt_btn) + layout.addWidget(self.decrypt_progress) + layout.addWidget(self.decrypt_log) + + tab.setLayout(layout) + return tab + + def create_recovery_tab(self): + """ + Creates an recovery tab for the UI with appropriate elements + + Returns: + QWidget: The UI for the Recovery Tab + """ + + tab = QWidget() + layout = QVBoxLayout() + + # Recovery key file selection + self.recovery_key_line = QLineEdit() + self.recovery_key_btn = QPushButton("Select Recovery Key File") + self.recovery_key_btn.clicked.connect( + partial(self.select_files, self.recovery_key_line, False) + ) + + # Drive or file selection for re-encryption/recovery + self.recovery_drive_line = QLineEdit() + self.recovery_file_btn = QPushButton("Select File") + self.recovery_file_btn.clicked.connect( + partial(self.select_files, self.recovery_drive_line, True) + ) + self.recovery_drive_btn = QPushButton("Select Directory") + self.recovery_drive_btn.clicked.connect( + partial(self.select_folder, self.recovery_drive_line, True) + ) + + # Recovery options + self.recovery_seed_phrase_radio_btn = QRadioButton("Use Seed Phrase") + self.recovery_seed_phrase_text = QTextEdit() + self.recovery_seed_phrase_text.setPlaceholderText("Enter your seed phrase here") + self.recovery_seed_phrase_text.setMaximumHeight(60) + + self.recovery_security_questions_radio_btn = QRadioButton( + "Use Security Questions" + ) + self.recovery_security_questions_radio_btn.toggled.connect( + self.load_security_questions_for_recovery + ) + self.recovery_question1 = QLineEdit(placeholderText="Question 1") + self.recovery_question1.setReadOnly(True) + self.recovery_answer1 = QLineEdit(placeholderText="Answer") + self.recovery_question2 = QLineEdit(placeholderText="Question 2") + self.recovery_question1.setReadOnly(True) + self.recovery_answer2 = QLineEdit(placeholderText="Answer") + + self.recovery_hardware_token_radio_btn = QRadioButton("Use Hardware Token") + self.verify_token_btn = QPushButton("Verify Hardware Token") + self.verify_token_btn.clicked.connect(self.verify_hardware_token) + + # New password fields + self.new_password = QLineEdit() + self.new_password.setEchoMode(QLineEdit.Password) + self.new_password.textChanged.connect(self.update_new_password_strength) + + self.confirm_new_password = QLineEdit() + self.confirm_new_password.setEchoMode(QLineEdit.Password) + + self.show_new_password_checkbox = QCheckBox("Show Password") + self.show_new_password_checkbox.stateChanged.connect( + self.toggle_new_password_visibility + ) + + # Password strength meter + self.new_password_strength_label = QLabel("Password Strength:") + self.new_password_strength_meter = QProgressBar() + self.new_password_strength_meter.setRange(0, 100) + self.new_password_strength_meter.setTextVisible(False) + + # Recover button + self.recover_btn = QPushButton("Recover Key") + self.recover_btn.clicked.connect(self.recover_password) + + # Layout organization + recovery_key_layout = QHBoxLayout() + recovery_key_layout.addWidget(self.recovery_key_line) + recovery_key_layout.addWidget(self.recovery_key_btn) + + recovery_drive_layout = QHBoxLayout() + recovery_drive_layout.addWidget(self.recovery_drive_line) + recovery_drive_layout.addWidget(self.recovery_file_btn) + recovery_drive_layout.addWidget(self.recovery_drive_btn) + + security_questions_layout = QVBoxLayout() + security_questions_layout.addWidget(self.recovery_question1) + security_questions_layout.addWidget(self.recovery_answer1) + security_questions_layout.addWidget(self.recovery_question2) + security_questions_layout.addWidget(self.recovery_answer2) + + password_layout = QVBoxLayout() + password_row1 = QHBoxLayout() + password_row1.addWidget(QLabel("New Password:")) + password_row1.addWidget(self.new_password) + password_row1.addWidget(QLabel("Confirm:")) + password_row1.addWidget(self.confirm_new_password) + + password_row2 = QHBoxLayout() + password_row2.addWidget(self.show_new_password_checkbox) + + password_layout.addLayout(password_row1) + password_layout.addLayout(password_row2) + + strength_layout = QHBoxLayout() + strength_layout.addWidget(self.new_password_strength_label) + strength_layout.addWidget(self.new_password_strength_meter) + password_layout.addLayout(strength_layout) + + layout.addLayout(recovery_key_layout) + layout.addLayout(recovery_drive_layout) + layout.addWidget(self.recovery_seed_phrase_radio_btn) + layout.addWidget(self.recovery_seed_phrase_text) + layout.addWidget(self.recovery_security_questions_radio_btn) + layout.addLayout(security_questions_layout) + layout.addWidget(self.recovery_hardware_token_radio_btn) + layout.addWidget(self.verify_token_btn) + layout.addLayout(password_layout) + layout.addWidget(self.recover_btn) + + tab.setLayout(layout) + return tab + + def create_activity_log_tab(self): + """ + Creates an activity log tab for the UI with appropriate elements + + Returns: + QWidget: The UI for the Activity Log Tab + """ + + tab = QWidget() + layout = QVBoxLayout() + + # Activity log file selection + self.activity_log_file_line = QLineEdit() + self.activity_log_file_btn = QPushButton("Select Log File") + self.activity_log_file_btn.clicked.connect( + partial(self.select_files, self.activity_log_file_line, False) + ) + self.activity_log_file_line.setPlaceholderText("Select a log file to view") + + # Filters + filter_layout = QHBoxLayout() + + # Activity type filter + self.activity_type_filter = QComboBox() + self.activity_type_filter.addItem("All") + self.activity_type_filter.addItem("encrypt") + self.activity_type_filter.addItem("decrypt") + self.activity_type_filter.addItem("recover") + self.activity_type_filter.addItem("error") + self.activity_type_filter.addItem("directory-structure") + self.activity_type_filter.addItem("success") + self.activity_type_filter.addItem("unknown") + self.activity_type_filter.currentIndexChanged.connect(self.filter_logs) + + # Date range filter + self.start_date_filter = QDateEdit() + self.start_date_filter.setCalendarPopup(True) + self.start_date_filter.setDate(QDate.currentDate().addMonths(-1)) + self.start_date_filter.dateChanged.connect(self.filter_logs) + + self.end_date_filter = QDateEdit() + self.end_date_filter.setCalendarPopup(True) + self.end_date_filter.setDate(QDate.currentDate()) + self.end_date_filter.dateChanged.connect(self.filter_logs) + + filter_layout.addWidget(QLabel("Activity Type:")) + filter_layout.addWidget(self.activity_type_filter) + filter_layout.addWidget(QLabel("Start Date:")) + filter_layout.addWidget(self.start_date_filter) + filter_layout.addWidget(QLabel("End Date:")) + filter_layout.addWidget(self.end_date_filter) + + # Activity log table + self.activity_log_table = QTableWidget() + self.activity_log_table.setColumnCount(3) + self.activity_log_table.setHorizontalHeaderLabels( + ["Date/Time", "Activity Type", "Details"] + ) + self.activity_log_table.horizontalHeader().setSectionResizeMode( + QHeaderView.Stretch + ) + self.activity_log_table.setEditTriggers(QTableWidget.NoEditTriggers) + self.activity_log_table.cellClicked.connect(self.handle_cell_click) + + # Load log button + self.activity_log_btn = QPushButton("Load Log") + self.activity_log_btn.clicked.connect(self.load_activity_log) + + # Layout organization + file_layout = QHBoxLayout() + file_layout.addWidget(self.activity_log_file_line) + file_layout.addWidget(self.activity_log_file_btn) + file_layout.addWidget(self.activity_log_btn) + + layout.addLayout(file_layout) + layout.addLayout(filter_layout) + layout.addWidget(self.activity_log_table) + + tab.setLayout(layout) + return tab + + def load_activity_log(self): + """ + Loads the activity log from the selected file and populates the table. + """ + log_file_path = self.activity_log_file_line.text() + if not log_file_path.endswith(".log"): + QMessageBox.warning(self, "Warning", "Please select a .log file.") + return + if not os.path.exists(log_file_path): + QMessageBox.warning(self, "Warning", "Log file does not exist.") + return + + self.activity_log_data = [] + try: + with open(log_file_path, "r") as f: + for line in f.readlines(): + parts = line.strip().split(",") + if len(parts) == 3: + self.activity_log_data.append(parts) + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to load log file: {str(e)}") + return + + self.filter_logs() + + def filter_logs(self): + """ + Filters the activity log based on the selected activity type and date range. + """ + if not hasattr(self, "activity_log_data"): + return + + filtered_data = [] + selected_type = self.activity_type_filter.currentText() + start_date = self.start_date_filter.date().toPyDate() + end_date = self.end_date_filter.date().toPyDate() + + for entry in self.activity_log_data: + date_time, activity_type, details = entry + entry_date = QDate.fromString( + date_time.split(" ")[0], "yyyy-MM-dd" + ).toPyDate() + + if selected_type != "All" and activity_type != selected_type: + continue + if not (start_date <= entry_date <= end_date): + continue + + filtered_data.append(entry) + + self.populate_log_table(filtered_data) + + def populate_log_table(self, data): + """ + Populates the activity log table with the given data. + + Args: + data (list): List of log entries to display. + """ + self.activity_log_table.setRowCount(len(data)) + for row, entry in enumerate(data): + for col, value in enumerate(entry): + item = QTableWidgetItem(value) + if col == 2 and entry[1] == "directory-structure": + # Make the Details cell clickable for directory-structure logs + item.setForeground(Qt.blue) + item.setFlags(item.flags() | Qt.ItemIsSelectable) + self.activity_log_table.setItem(row, col, item) + + def handle_cell_click(self, row, column): + """ + Handles clicks on the activity log table cells. + + Args: + row (int): The row of the clicked cell. + column (int): The column of the clicked cell. + """ + if column == 2: # Details column + activity_type = self.activity_log_table.item(row, 1).text() + if activity_type == "directory-structure": + details = convert_to_multi_line( + self.activity_log_table.item(row, column).text() + ) + self.show_directory_structure_popup(details) + + def show_directory_structure_popup(self, details): + """ + Displays a popup with the full directory structure in a scrollable view. + + Args: + details (str): The directory structure details to display. + """ + dialog = QDialog(self) + dialog.setWindowTitle("Directory Structure Details") + dialog.setMinimumSize(600, 400) + + layout = QVBoxLayout() + text_edit = QTextEdit() + text_edit.setReadOnly(True) + text_edit.setPlainText(details) + + layout.addWidget(text_edit) + dialog.setLayout(layout) + dialog.exec_() + + def toggle_new_password_visibility(self, state): + """ + Toggles the visibility of the new password and confirmation fields + based on the given state. + Args: + state (Qt.CheckState): The state of the checkbox, where + Qt.Checked shows the passwords and + other states hide them. + """ + + if state == Qt.Checked: + self.new_password.setEchoMode(QLineEdit.Normal) + self.confirm_new_password.setEchoMode(QLineEdit.Normal) + else: + self.new_password.setEchoMode(QLineEdit.Password) + self.confirm_new_password.setEchoMode(QLineEdit.Password) + + def toggle_password_visibility(self, state): + """ + Toggles the visibility of password fields based on the given state. + + Args: + state (Qt.CheckState): The state of the checkbox, where Qt.Checked + shows the passwords and other states hide them. + """ + if state == Qt.Checked: + self.encrypt_password.setEchoMode(QLineEdit.Normal) + self.encrypt_confirm.setEchoMode(QLineEdit.Normal) + else: + self.encrypt_password.setEchoMode(QLineEdit.Password) + self.encrypt_confirm.setEchoMode(QLineEdit.Password) + + def toggle_password_visibility_decrypt(self, state): + """ + Toggles the visibility of the password in the decrypt password field. + + Args: + state (Qt.CheckState): The state of the checkbox, where Qt.Checked + makes the password visible and any other state + hides it. + """ + if state == Qt.Checked: + self.decrypt_password.setEchoMode(QLineEdit.Normal) + else: + self.decrypt_password.setEchoMode(QLineEdit.Password) + + def recover_password(self): + """ + Handles the password recovery process based on the selected recovery method. + It validates the input fields, retrieves the necessary information, + and performs the recovery operation. + + Raises: + ValueError: If any of the required fields are empty or invalid. + ValueError: If the new password and confirmation do not match. + Exception: If the recovery method is not selected or if the hardware token + ValueError: If the seed phrase is empty or if the security questions and answers are not filled. + ValueError: If the recovery key file is not selected. + """ + try: + if self.recovery_seed_phrase_radio_btn.isChecked(): + seed_phrase = self.recovery_seed_phrase_text.toPlainText().strip() + if not seed_phrase: + raise ValueError("Seed phrase cannot be empty") + + if self.recovery_security_questions_radio_btn.isChecked(): + question1 = self.recovery_question1.text().strip() + answer1 = self.recovery_answer1.text().strip() + question2 = self.recovery_question2.text().strip() + answer2 = self.recovery_answer2.text().strip() + if not (question1 and answer1 and question2 and answer2): + raise ValueError( + "All security questions and answers must be filled" + ) + + if self.recovery_hardware_token_radio_btn.isChecked(): + if len(self.recovery_hardware_token_seed_phrases) < 1: + raise Exception("Seed Phrases not Retrieved From Hardware Token") + + # Validate new password + new_password = self.new_password.text() + confirm_password = self.confirm_new_password.text() + if not new_password: + raise ValueError("New password cannot be empty") + if new_password != confirm_password: + raise ValueError("Passwords do not match") + + self.recover_key() + + QMessageBox.information( + self, + "Success", + "Password recovery successful. Your new password has been set.", + ) + + except Exception as e: + self.show_error(str(e)) + + def update_password_strength(self): + """ + Updates the password strength meter based on the current password input. + It calculates the strength of the password and sets the progress bar value + and color accordingly. + """ + password = self.encrypt_password.text() + strength = PasswordStrengthMeter.calculate_strength(password) + color = PasswordStrengthMeter.get_strength_color(strength) + + self.password_strength_meter.setValue(strength) + self.password_strength_meter.setStyleSheet( + f"QProgressBar::chunk {{ background-color: {color.name()}; }}" + ) + + def update_new_password_strength(self): + """ + Updates the new password strength meter based on the current new password input. + It calculates the strength of the new password and sets the progress bar value + and color accordingly. + """ + + password = self.new_password.text() + strength = PasswordStrengthMeter.calculate_strength(password) + color = PasswordStrengthMeter.get_strength_color(strength) + + self.new_password_strength_meter.setValue(strength) + self.new_password_strength_meter.setStyleSheet( + f"QProgressBar::chunk {{ background-color: {color.name()}; }}" + ) + + def select_files(self, line_edit, multi): + """ + Opens a file dialog to select files or folders and sets the selected path in the line edit. + Args: + line_edit (QLineEdit): The line edit to set the selected path. + multi (bool): If True, allows multiple file selection; otherwise, single file selection. + """ + if multi: + files, _ = QFileDialog.getOpenFileNames(self, "Select Files") + if files: + line_edit.setText(";".join(files)) + else: + file, _ = QFileDialog.getOpenFileName(self, "Select File") + if file: + line_edit.setText(file) + + self.encrypt_type = "file" if not multi else "files" + + def select_folder(self, line_edit, multi): + """ + Opens a folder dialog to select a folder and sets the selected path in the line edit. + Args: + line_edit (QLineEdit): The line edit to set the selected path. + multi (bool): If True, allows multiple folder selection; otherwise, single folder selection. + """ + folder = QFileDialog.getExistingDirectory(self, "Select Folder") + if folder: + line_edit.setText(folder) + + self.encrypt_type = "folder" if folder else None + + def select_output_file(self, line_edit, default_suffix): + """ + Opens a file dialog to select an output file and sets the selected path in the line edit. + Args: + line_edit (QLineEdit): The line edit to set the selected path. + default_suffix (str): The default file suffix for the output file. + """ + + file, _ = QFileDialog.getSaveFileName( + self, "Select Output File", "", f"Encrypted Files (*.{default_suffix})" + ) + if file: + line_edit.setText(file) + + def folder_operation(self, driveCrypto: DriveCrypto, operation): + """ + Performs the encryption or decryption operation on the selected folder. + Args: + driveCrypto (DriveCrypto): The DriveCrypto instance to perform the operation. + operation (str): The operation to perform ("encrypt" or "decrypt"). + """ + if operation == "encrypt": + self.encrypt_log.append( + driveCrypto.visualize_directory_structure_as_string() + ) + self.encrypt_log.append( + f"Starting encryption of folder: {self.encrypt_file_line.text()}..." + ) + self.encrypt_log.append( + f"Encrypted files will be saved in: {self.encrypt_file_line.text()}" + ) + self.spinner.start() + driveCrypto.type = "encrypt" + driveCrypto.encrypt_log = self.encrypt_log + # driveCrypto.encrypt(self.encrypt_password.text(), self.encrypt_log) + self.operation_thread = driveCrypto + self.operation_thread.result_ready.connect(self.on_complete) + self.operation_thread.start() + # self.encrypt_log.append( + # f"Encryption of folder {self.encrypt_file_line.text()} completed." + # ) + elif operation == "decrypt": + self.decrypt_log.append( + driveCrypto.visualize_directory_structure_as_string() + ) + self.decrypt_log.append( + f"Starting decryption of folder: {self.decrypt_file_line.text()}..." + ) + self.decrypt_log.append( + f"Decrypted files will be saved in: {self.decrypt_file_line.text()}" + ) + driveCrypto.decrypt(self.decrypt_password.text(), self.decrypt_log) + self.decrypt_log.append( + f"Decryption of folder {self.decrypt_file_line.text()} completed." + ) + + def recovery_folder_operation( + self, driveCrypto: DriveCrypto, old_password, new_password + ): + """ + Performs the recovery operation on the selected folder. + Args: + driveCrypto (DriveCrypto): The DriveCrypto instance to perform the operation. + old_password (str): The old password for the folder. + new_password (str): The new password for the folder. + """ + self.encrypt_log.append( + f"Old password for {self.recovery_drive_line.text()} is: {old_password}" + ) + self.encrypt_log.append(driveCrypto.visualize_directory_structure_as_string()) + self.encrypt_log.append( + f"Starting recovery of folder: {self.recovery_drive_line.text()}..." + ) + driveCrypto.decrypt(self.new_password.text()) + self.encrypt_log.append( + f"Decryption of folder {self.recovery_drive_line.text()} completed." + ) + self.encrypt_log.append( + f"New password for {self.recovery_drive_line.text()} is: {new_password}" + ) + self.encrypt_log.append( + f"Re-encryption of folder {self.recovery_drive_line.text()} completed." + ) + driveCrypto.encrypt(new_password) + self.encrypt_log.append( + f"Re-encryption of folder {self.recovery_drive_line.text()} completed." + ) + + def save_recovery_stuff(self, operation): + """ + Saves the recovery information based on the selected method (seed phrase, security questions, or hardware token). + Args: + operation (str): The operation to perform ("encrypt" or "decrypt"). + """ + if operation == "decrypt": + return + + password = self.encrypt_password.text() + password_recovery = PasswordRecovery(self.encrypt_file_line.text(), password) + if self.seed_phrase_radio_btn.isChecked(): + password_recovery.setup_key_recovery( + "seed_phrase", self.seed_phrase_text.toPlainText() + ) + elif self.security_questions_radio_btn.isChecked(): + + if self.question1.currentText() == self.question2.currentText(): + raise Exception("Please don't select same questions!") + + if self.answer1.text() == "" or self.answer2.text() == "": + raise Exception("Please don't leave answers empty") + + questions = { + "question1": hashlib.sha256( + self.question1.currentText().encode() + ).hexdigest(), + "question2": hashlib.sha256( + self.question2.currentText().encode() + ).hexdigest(), + } + recovery_key = self.answer1.text() + ";" + self.answer2.text() + password_recovery.setup_key_recovery( + "security_questions", recovery_key, questions + ) + elif self.hardware_token_radio_btn.isChecked(): + password_recovery.setup_key_recovery( + "hardware_token", self.hardware_token_seed_phrase + ) + + password_recovery.encrypt_recovery_key() + self.encrypt_log.append("Key recovery information has been saved successfully.") + self.encrypt_log.append( + f"Recovery information for {self.encrypt_file_line.text()} has been saved." + ) + + def recover_key(self): + """ + Handles the recovery of the encryption key based on the selected recovery method. + It validates the input fields, retrieves the necessary information, + and performs the recovery operation. + """ + if self.new_password.text() != self.confirm_new_password.text(): + raise ValueError("Passwords do not match") + if not self.new_password.text(): + raise ValueError("Password cannot be empty") + + password_recovery = PasswordRecovery( + self.recovery_drive_line.text(), self.new_password.text() + ) + if self.recovery_seed_phrase_radio_btn.isChecked(): + seed_phrase = self.recovery_seed_phrase_text.toPlainText().strip() + if not seed_phrase: + raise ValueError("Seed phrase cannot be empty") + + password_recovery.setup_key_recovery("seed_phrase", seed_phrase) + elif self.recovery_security_questions_radio_btn.isChecked(): + questions = { + "question1": hashlib.sha256( + self.recovery_question1.text().encode() + ).hexdigest(), + "question2": hashlib.sha256( + self.recovery_question2.text().encode() + ).hexdigest(), + } + recovery_key = ( + self.recovery_answer1.text() + ";" + self.recovery_answer2.text() + ) + password_recovery.setup_key_recovery( + "security_questions", recovery_key, questions + ) + + elif self.recovery_hardware_token_radio_btn.isChecked(): + password_recovery.setup_key_recovery( + "hardware_token", + self.recovery_hardware_token_seed_phrases.popitem()[1], + self.recovery_hardware_token_seed_phrases, + ) + + old_password = None + try: + old_password = password_recovery.decrypt_recovery_key() + password_recovery.encrypt_recovery_key() + except Exception as e: + self.show_error(f"Failed key: {str(e)}") + return + + # show in log that new password is set + self.encrypt_log.append( + f"Old password for {self.recovery_drive_line.text()} is: {old_password}" + ) + self.encrypt_log.append( + f"New password for {self.recovery_drive_line.text()} is: {self.new_password.text()}" + ) + + log_activity( + "recover", + self.recovery_drive_line.text(), + f"Old password: {old_password} set to a New password", + ) + + if self.encrypt_type == "folder": + driveCrypto = DriveCrypto((self.recovery_drive_line.text()), True) + + self.recovery_folder_operation(driveCrypto) + return + else: + file_path = self.recovery_drive_line.text() + new_password = self.new_password.text() + output_path = self.recovery_drive_line.text()[:-4] + + self.worker = CryptoWorker("decrypt", file_path, output_path, old_password) + self.worker.set_delete_original(True) + self.encrypt_log.append(f"Starting encryption of {file_path}...") + + self.setup_worker_connections("decrypt") + self.worker.start() + + self.worker.wait() + + file_path, output_path = output_path, file_path + + self.worker = CryptoWorker("encrypt", file_path, output_path, new_password) + self.worker.set_delete_original(True) + self.encrypt_log.append(f"Starting decryption of {file_path}...") + + self.setup_worker_connections("encrypt") + self.worker.start() + + def on_complete(self): + """ + Handles the completion of the operation and stops the spinner. + """ + self.spinner.stop() + + def start_operation(self, operation): + """ + Starts the encryption or decryption operation based on the selected operation. + Args: + operation (str): The operation to perform ("encrypt" or "decrypt"). + """ + if self.worker and self.worker.isRunning(): + QMessageBox.warning(self, "Warning", "Another operation is in progress") + return + if self.operation_thread and self.operation_thread.isRunning(): + QMessageBox.warning(self, "Warning", "Another operation is in progress") + return + + if self.encrypt_type is None: + QMessageBox.information( + self, "Info", "Please select a file or folder to encrypt" + ) + return + + log_activity(operation, self.encrypt_file_line.text()) + + try: + if self.recovery_section.isVisible() and ( + self.seed_phrase_radio_btn.isChecked() + or self.security_questions_radio_btn.isChecked() + or self.hardware_token_radio_btn.isChecked() + ): + self.save_recovery_stuff(operation) + + if self.encrypt_type == "folder": + + if not (operation == "encrypt" or operation == "decrypt"): + raise ValueError("Please select a folder to encrypt/decrypt") + + self.setup_operation_thread(operation) + + return + + if operation == "encrypt": + log_activity( + operation, + self.encrypt_file_line.text(), + self.encrypt_file_line.text(), + ) + + if self.encrypt_file_line.text().endswith(".enc"): + raise ValueError("File already encrypted") + + file_path = self.encrypt_file_line.text() + password = self.encrypt_password.text() + confirm = self.encrypt_confirm.text() + + output_path = self.encrypt_file_line.text() + ".enc" + + if not file_path: + raise ValueError("Please select a file to encrypt") + # if not output_path: + # raise ValueError("Please select output path") + if password != confirm: + raise ValueError("Passwords do not match") + if not password: + raise ValueError("Password cannot be empty") + + self.worker = CryptoWorker(operation, file_path, output_path, password) + self.worker.set_delete_original( + self.delete_original_checkbox.isChecked() + ) + self.encrypt_log.append(f"Starting encryption of {file_path}...") + + elif operation == "decrypt": + log_activity( + operation, + self.decrypt_file_line.text(), + self.decrypt_file_line.text(), + ) + + if not self.decrypt_file_line.text().endswith(".enc"): + raise ValueError("File already decrypted") + + file_path = self.decrypt_file_line.text() + password = self.decrypt_password.text() + + output_path = self.decrypt_file_line.text()[:-4] + + if not file_path: + raise ValueError("Please select a file to decrypt") + if not output_path: + raise ValueError("Please select output path") + if not password: + raise ValueError("Password cannot be empty") + + self.worker = CryptoWorker(operation, file_path, output_path, password) + self.worker.set_delete_original( + self.delete_original_checkbox_decrypt.isChecked() + ) + self.decrypt_log.append(f"Starting decryption of {file_path}...") + + self.setup_worker_connections(operation) + self.worker.start() + + except Exception as e: + log_activity("error", files=str(e)) + self.show_error(str(e)) + + def setup_operation_thread(self, operation): + """ + Sets up the operation thread for encryption or decryption. + Args: + operation (str): The operation to perform ("encrypt" or "decrypt"). + """ + self.spinner.start() + if operation == "encrypt": + self.operation_thread = DriveCrypto( + self.encrypt_file_line.text(), + operation, + self.encrypt_password.text(), + self.delete_original_checkbox.isChecked(), + ) + self.encrypt_log.append( + self.operation_thread.visualize_directory_structure_as_string() + ) + else: + self.operation_thread = DriveCrypto( + self.decrypt_file_line.text(), + operation, + self.decrypt_password.text(), + self.delete_original_checkbox_decrypt.isChecked(), + ) + self.decrypt_log.append( + self.operation_thread.visualize_directory_structure_as_string() + ) + + self.operation_thread.result_ready.connect(self.on_complete) + self.operation_thread.progress_updated.connect( + lambda progress: ( + self.encrypt_progress.setValue(progress) + if operation == "encrypt" + else self.decrypt_progress.setValue(progress) + ) + ) + self.operation_thread.status_updated.connect( + lambda message: ( + self.encrypt_log.append(message) + if operation == "encrypt" + else self.decrypt_log.append(message) + ) + ) + self.operation_thread.error_occurred.connect( + lambda error: self.show_error(error) + ) + self.operation_thread.operation_completed.connect( + lambda success, message: self.on_operation_complete( + success, + message, + self.encrypt_btn if operation == "encrypt" else self.decrypt_btn, + self.encrypt_log if operation == "encrypt" else self.decrypt_log, + operation, + ) + ) + self.operation_thread.start() + + def setup_worker_connections(self, operation): + """ + Sets up the connections for the worker thread based on the operation type. + Args: + operation (str): The operation to perform ("encrypt" or "decrypt"). + """ + if operation == "encrypt": + log = self.encrypt_log + progress = self.encrypt_progress + btn = self.encrypt_btn + else: + log = self.decrypt_log + progress = self.decrypt_progress + btn = self.decrypt_btn + + btn.setEnabled(False) + progress.setValue(0) + self.worker.progress_updated.connect(progress.setValue) + self.worker.status_updated.connect(log.append) + self.worker.operation_completed.connect( + lambda success, msg: self.on_operation_complete( + success, msg, btn, log, operation + ) + ) + self.worker.error_occurred.connect(lambda err: self.show_error(err, log)) + self.worker.delete_original_requested.connect( + lambda path: log.append(f"Original file deleted: {path}") + ) + + def on_operation_complete(self, success, message, btn, log, type): + """ + Handles the completion of the encryption or decryption operation. + Args: + success (bool): Indicates whether the operation was successful. + message (str): The message to display. + btn (QPushButton): The button to enable/disable. + log (QTextEdit): The log widget to append messages to. + type (str): The type of operation ("encrypt" or "decrypt"). + """ + btn.setEnabled(True) + if success: + log.append("Operation completed successfully") + QMessageBox.information(self, "Success", message) + log_activity( + type, + ( + os.path.dirname(self.encrypt_file_line.text()) + if type == "encrypt" + else os.path.dirname(self.decrypt_file_line.text()) + ), + f"Operation completed successfully: {message}", + ) + else: + log_activity( + "error", + files=f"Operation failed: {message}", + ) + log.append(f"Operation failed: {message}") + QMessageBox.critical(self, "Error", message) + self.spinner.stop() + + def show_error(self, message, log=None): + """ + Displays an error message in a message box and logs it if provided. + Args: + message (str): The error message to display. + log (QTextEdit, optional): The log widget to append the error message to. + """ + if log: + log.append(f"Error: {message}") + QMessageBox.critical(self, "Error", message) + + def closeEvent(self, event): + """ + Handles the close event of the main window. + It stops the worker thread if it's running and waits for it to finish. + Args: + event (QCloseEvent): The close event. + """ + if self.worker and self.worker.isRunning(): + self.worker.stop() + self.worker.wait(2000) # Wait up to 2 seconds for clean exit + event.accept() + + def toggle_recovery_section(self): + """Toggle visibility of the recovery options section.""" + if self.toggle_recovery_btn.isChecked(): + self.recovery_section.setVisible(True) + self.toggle_recovery_btn.setText("β–² Hide Key Recovery Options") + else: + self.recovery_section.setVisible(False) + self.toggle_recovery_btn.setText("β–Ό Set up Key Recovery Options") + + def generate_seed_phrase(self): + """Generate a random seed phrase.""" + + self.seed_phrase_text.setPlainText(generate_seed_phrase(256, "en")) + QMessageBox.information( + self, + "Seed Phrase", + "Write this down and store it securely!\n" + "It cannot be recovered if lost.", + ) + + def register_hardware_token(self): + """Handle hardware token registration.""" + + if not self.hardware_token_radio_btn.isChecked(): + QMessageBox.warning( + self, + "Hardware Token", + "Select the Hardware Token Option", + ) + return + + self.hardware_token = HardwareToken() + + try: + self.hardware_token.connect() + self.encrypt_log.append( + f"Success: Connected to {self.hardware_token.token_name}" + ) + + self.encrypt_log.append( + f"Info: Key has space for {self.hardware_token.get_space()} and {'has space' if self.hardware_token.has_space() else 'is full.'}" + ) + + if not self.hardware_token.has_space(): + QMessageBox.critical( + self, "Hardware Token Full", "Can't use this token as it is full." + ) + raise Exception("Can't use this token as it is full.") + + self.hardware_token_seed_phrase = generate_seed_phrase(256, "en") + self.encrypt_log.append( + f"Info: The seed phrase stored in the Hardware Token is:\n {self.hardware_token_seed_phrase}\nYou can keep it if you want to recover using seed phrase." + ) + self.hardware_token.write_seed_phrase_to_token( + self.hardware_token_seed_phrase + ) + + QMessageBox.information( + self, + "Success", + f"Seed Phrase Successfully stored in your {self.hardware_token.token_name}", + ) + + self.encrypt_log.append( + f"Success: Seed Phrase Successfully stored in your {self.hardware_token.token_name}" + ) + + except Exception as e: + self.encrypt_log.append(f"Error: {e}") + QMessageBox.critical(self, "Error", f"{e}") + finally: + self.hardware_token.disconnect() + + def toggle_decrypt_recovery_section(self): + """Toggle visibility of the decrypt recovery options section.""" + if self.decrypt_toggle_recovery_btn.isChecked(): + self.decrypt_recovery_section.setVisible(True) + self.decrypt_toggle_recovery_btn.setText("β–² Hide Recovery Options") + else: + self.decrypt_recovery_section.setVisible(False) + self.decrypt_toggle_recovery_btn.setText("β–Ό Alternative Recovery Options") + + def verify_hardware_token(self): + """Handle hardware token verification during decryption.""" + + if not self.recovery_hardware_token_radio_btn.isChecked(): + QMessageBox.warning( + self, + "Hardware Token", + "Select the Hardware Token Option", + ) + return + + self.hardware_token = HardwareToken() + + try: + self.hardware_token.connect() + self.encrypt_log.append( + f"Success: Connected to {self.hardware_token.token_name}" + ) + + self.encrypt_log.append( + f"Info: Key has space for {self.hardware_token.get_space()} and {'has space' if self.hardware_token.has_space() else 'is full.'}" + ) + + if not self.hardware_token.has_space(): + QMessageBox.warning(self, "Hardware Token Full", "Be carefull.") + + self.recovery_hardware_token_seed_phrases = ( + self.hardware_token.get_seed_phrase_from_token() + ) + + if len(self.recovery_hardware_token_seed_phrases) > 0: + QMessageBox.information( + self, + "Success", + f"Seed Phrases Successfully retrieved from {self.hardware_token.token_name}", + ) + + self.encrypt_log.append( + f"Success: Seed Phrases Successfully retrieved from {self.hardware_token.token_name}" + ) + return + QMessageBox.warning( + self, + "Warning", + f"No Seed Phrases found on {self.hardware_token.token_name}", + ) + + self.encrypt_log.append( + f"Success: No Seed Phrases found on {self.hardware_token.token_name}" + ) + + except Exception as e: + self.encrypt_log.append(f"Error: {e}") + QMessageBox.critical(self, "Error", f"{e}") + + finally: + self.hardware_token.disconnect() diff --git a/main.py b/main.py new file mode 100644 index 0000000..7ea87a0 --- /dev/null +++ b/main.py @@ -0,0 +1,56 @@ +""" +Main module for the Secure File/Folder/Drive Encryptor/Decryptor application. + +This module initializes the PyQt5 application, applies a dark blue theme, +and launches the main GUI window. + +Features: +- Secure encryption and decryption using AES-GCM with unique nonce per chunk. +- Chunk sequence validation for enhanced security. +- Password strength checking to ensure robust protection. +- Progress reporting during encryption/decryption processes. +- Secure file handling to prevent data leaks. +- Graphical User Interface (GUI) for user-friendly interaction. +- Password recovery options including: + +Classes: +- MainWindow: The main GUI window of the application (imported from `gui` module). + +Functions: +- None + +Attributes: +- app (QApplication): The main application instance. +- window (MainWindow): The main GUI window instance. + +Usage: +Run this module directly to start the application. +Secure File/Folder/Drive Encryptor/Decryptor +- Uses AES-GCM with unique nonce per chunk +- Includes chunk sequence validation +- Password strength checking +- Progress reporting +- Secure file handling +- Includes a GUI +- Has Password strength checking +- Provides Password Recovery Options: + - Seed Phrase + - Security Questions + - Hardware Token (Pico Key) +""" + +import os +import sys +from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import QCoreApplication, Qt +from gui import MainWindow +from qt_material import apply_stylesheet + +if __name__ == "__main__": + QCoreApplication.setAttribute(Qt.AA_DisableWindowContextHelpButton) + app: QApplication = QApplication(sys.argv) + + apply_stylesheet(app, theme="dark_blue.xml") + window: MainWindow = MainWindow() + window.show() + sys.exit(app.exec_()) diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..833f37c --- /dev/null +++ b/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# activate the virtual environment +conda activate BASIC + +# give permission to access the serial port in linux for Pico Key +sudo chmod a+rw /dev/ttyACM0 + +# Check if QT_QPA_PLATFORM is set to xcb +if [ "$QT_QPA_PLATFORM" != "xcb" ]; then + export QT_QPA_PLATFORM=xcb +fi + +clear + +# Run the main.py script +python3 main.py + +# clear \ No newline at end of file diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..7d65e60 Binary files /dev/null and b/screenshot.png differ diff --git a/security.questions b/security.questions new file mode 100644 index 0000000..8832303 --- /dev/null +++ b/security.questions @@ -0,0 +1,40 @@ +What was the name of your first pet? +What is your favorite book? +In which city were you born? +What is your favorite childhood movie? +Who was your childhood best friend? +What is your favorite food? +What was the model of your first car? +What is your mother’s middle name? +Where did you go for your first vacation? +What is your favorite sports team? +What is your favorite color? +What is your favorite historical event? +What is your favorite holiday destination? +What is your favorite song? +Who is your favorite teacher? +What is the first name of your childhood hero? +What is the name of your favorite fictional character? +What is your favorite hobby? +What is the name of your first employer? +What is your favorite quote? +What was the name of your first stuffed animal? +What is the name of the street you grew up on? +What was your favorite game as a child? +What was the first concert you attended? +What is the name of a childhood neighbor? +What is the name of your first teacher? +What was the first thing you learned to cook? +What is the name of a place you always wanted to visit but haven’t yet? +What was your favorite subject in school? +What is your least favorite vegetable? +What is the title of the first movie you watched in a theater? +What was your childhood dream job? +What is the name of your first crush? +What was the make of your first bicycle? +What is your favorite homemade dish? +What is the name of your first friend in college? +What is the name of a park you visited often as a child? +What was the first foreign language you learned? +What is the name of your favorite high school teacher? +What was your favorite TV show growing up? \ No newline at end of file diff --git a/utilities.py b/utilities.py new file mode 100644 index 0000000..0539169 --- /dev/null +++ b/utilities.py @@ -0,0 +1,191 @@ +from PyQt5.QtGui import QColor +import re + +from mnemonic import Mnemonic +from unidecode import unidecode + +LANGUAGES = { + "en": "english", + "zh": "chinese_simplified", + "zh2": "chinese_traditional", + "fr": "french", + "it": "italian", + "ja": "japanese", + "ko": "korean", + "es": "spanish", +} + +STRENGTHS = [128, 160, 192, 224, 256] + +LENGTHS = [12, 15, 18, 21, 24] + +LENGTH_STRENGTH = dict(zip(LENGTHS, STRENGTHS)) + + +class PasswordStrengthMeter: + """ + A utility class to calculate the strength of a password and determine its corresponding color. + """ + + @staticmethod + def calculate_strength(password: str) -> int: + """ + Calculate the strength of a given password based on its length and character diversity. + + Args: + password (str): The password to evaluate. + + Returns: + int: The strength of the password as a percentage (0-100). + """ + if not password: + return 0 + + strength = 0 + + # Length check + length = len(password) + if length >= 8: + strength += 1 + if length >= 12: + strength += 1 + if length >= 16: + strength += 1 + + # Character diversity + if re.search(r"[A-Z]", password): + strength += 1 + if re.search(r"[a-z]", password): + strength += 1 + if re.search(r"[0-9]", password): + strength += 1 + if re.search(r"[^A-Za-z0-9]", password): + strength += 1 + + # Normalize to 0-100 range + max_possible = 7 # 3 for length + 4 for diversity + return int((strength / max_possible) * 100) + + @staticmethod + def get_strength_color(strength: int) -> QColor: + """ + Get the color representation of the password strength. + + Args: + strength (int): The strength of the password as a percentage (0-100). + + Returns: + QColor: The color representing the password strength (red, yellow, or green). + """ + if strength < 30: + return QColor(255, 0, 0) # Red + elif strength < 70: + return QColor(255, 255, 0) # Yellow + else: + return QColor(0, 255, 0) # Green + + +def strength(length: int) -> int: + """ + Get the strength value corresponding to a given mnemonic length. + + Args: + length (int): The length of the mnemonic phrase. + + Returns: + int: The strength value corresponding to the length. + """ + return LENGTH_STRENGTH[length] + + +def generate_seed_phrase(strength: int = 128, lang: str = "en") -> str: + """ + Generate a seed phrase using the Mnemonic library. + + The seed phrase is generated based on the specified strength and language. + + Args: + strength (int): The strength of the seed phrase. It should be one of the following values: + 128, 160, 192, 224, or 256. The default is 128. + lang (str): The language for the seed phrase. It should be one of the following values: + "en", "zh", "zh2", "fr", "it", "ja", "ko", or "es". The default is "en" (English). + + Returns: + str: The generated seed phrase. + """ + mnemo = Mnemonic(LANGUAGES[lang]) + + seed_phrase = mnemo.generate(strength=strength) + seed_list = seed_phrase.split() + final_seed_list = [unidecode(word) for word in seed_list] + final_seed_phrase = " ".join(final_seed_list) + + return final_seed_phrase + + +def log_activity(type: str, path: str = "./", files: str = None) -> None: + """ + Log the activity of the user along with a timestamp. + + Args: + type (str): The type of activity to log (e.g., "encrypt", "decrypt"). + path (str): The path where the activity occurred. Default is "./". + files (str): The files involved in the activity. Default is None. + """ + import os + import csv + from datetime import datetime + + # Get the current timestamp + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + if files is None: + return + + # Create the log message + if type == "encrypt": + log_message = f"Encrypted {files} in {path}" + elif type == "decrypt": + log_message = f"Decrypted {files} in {path}" + elif type == "directory-structure": + log_message = f"{files}" + elif type == "error": + log_message = f"Error occurred: {files}" + elif type == "success": + log_message = f"Success: {files}" + elif type == "recover": + log_message = f"Recovery: {files} at {path}" + else: + type = "unknown" + log_message = f"Unknown activity: {type}" + + # if the path includes a file, get the directory + if os.path.isfile(path): + path = os.path.dirname(path) + + # Write the log message to a .log file + with open(os.path.join(path, "activity.log"), "a") as log_file: + log_file.write(f"{timestamp} - {log_message}\n") + + # Write the log message to a .csv file + csv_file_path = os.path.join(path, "activity.log") + file_exists = os.path.isfile(csv_file_path) + with open(csv_file_path, "a", newline="") as csv_file: + csv_writer = csv.writer(csv_file) + if not file_exists: + # Write the header if the file doesn't exist + csv_writer.writerow(["Timestamp", "Activity", "Path"]) + csv_writer.writerow([timestamp, type, log_message]) + + +def convert_to_multi_line(text: str, sep: str = ";") -> str: + """ + Convert a single line of text into a multi-line string. + + Args: + text (str): The input text to convert. + + Returns: + str: The converted multi-line string. + """ + return "\n".join(text.split(sep))