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** |  |
-| **β 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
+
+
+
+---
+
+## β 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))