diff --git a/README.md b/README.md index 2658af2..f000dc2 100644 --- a/README.md +++ b/README.md @@ -221,12 +221,6 @@ The SeedPass roadmap outlines a structured development plan divided into distinc - Implement synchronization logic to fetch and update entries from Nostr as needed. 4. **Security Enhancements** - - **Parent Seed Recovery** - - **Description:** Develop a secure method for users to recover their parent seed if lost. - - **Features:** - - **Recovery Phrase:** Allow users to generate and store a recovery phrase or backup file. - - **Multi-Factor Authentication (MFA):** Integrate MFA to enhance the security of the recovery process. - - **Encrypted Storage:** Ensure that recovery data is encrypted and stored securely. - **"Secret" Mode (Clipboard-Only Password Retrieval)** - **Description:** Introduce a "secret" mode where passwords are copied directly to the clipboard rather than displayed on the screen upon retrieval. - **Features:** diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index 7f0d6bd..f2d6118 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -65,15 +65,15 @@ The following table provides a quick reference to all available advanced CLI com | Post encrypted index to Nostr | `post` | `-P` | `--post` | `seedpass post` | | Retrieve from Nostr | `get-nostr` | `-GN` | `--get-nostr` | `seedpass get-nostr` | | Display Nostr public key | `show-pubkey` | `-K` | `--show-pubkey` | `seedpass show-pubkey` | -| **Set Custom Nostr Relays** | `set-relays` | `-SR` | `--set-relays` | `seedpass set-relays --add "wss://relay1.example.com" --add "wss://relay2.example.com"` | -| **Enable "Secret" Mode** | `set-secret` | `-SS` | `--set-secret` | `seedpass set-secret --enable` or `seedpass set-secret --disable` | -| **Batch Post Index Items to Nostr** | `batch-post` | `-BP` | `--batch-post` | `seedpass batch-post --start 0 --end 9` or `seedpass batch-post --range 10-19` | -| **Show All Passwords** | `show-all` | `-SA` | `--show-all` | `seedpass show-all` | -| **Add Notes to an Entry** | `add-notes` | `-AN` | `--add-notes` | `seedpass add-notes --index 3 --notes "This is a secured account"` | -| **Add Tags to an Entry** | `add-tags` | `-AT` | `--add-tags` | `seedpass add-tags --index 3 --tags "personal,finance"` | -| **Search by Tag or Title** | `search-by` | `-SB` | `--search-by` | `seedpass search-by --tag "work"` or `seedpass search-by --title "GitHub"` | -| **Automatically Post Index to Nostr After Edit** | `auto-post` | `-AP` | `--auto-post` | `seedpass auto-post --enable` or `seedpass auto-post --disable` | -| **Initial Setup Prompt for Seed Generation/Import** | `setup` | `-ST` | `--setup` | `seedpass setup` | +| Set Custom Nostr Relays | `set-relays` | `-SR` | `--set-relays` | `seedpass set-relays --add "wss://relay1.example.com" --add "wss://relay2.example.com"` | +| Enable "Secret" Mode | `set-secret` | `-SS` | `--set-secret` | `seedpass set-secret --enable` or `seedpass set-secret --disable` | +| Batch Post Index Items to Nostr | `batch-post` | `-BP` | `--batch-post` | `seedpass batch-post --start 0 --end 9` or `seedpass batch-post --range 10-19` | +| Show All Passwords | `show-all` | `-SA` | `--show-all` | `seedpass show-all` | +| Add Notes to an Entry | `add-notes` | `-AN` | `--add-notes` | `seedpass add-notes --index 3 --notes "This is a secured account"` | +| Add Tags to an Entry | `add-tags` | `-AT` | `--add-tags` | `seedpass add-tags --index 3 --tags "personal,finance"` | +| Search by Tag or Title | `search-by` | `-SB` | `--search-by` | `seedpass search-by --tag "work"` or `seedpass search-by --title "GitHub"` | +| Automatically Post Index to Nostr After Edit | `auto-post` | `-AP` | `--auto-post` | `seedpass auto-post --enable` or `seedpass auto-post --disable` | +| Initial Setup Prompt for Seed Generation/Import | `setup` | `-ST` | `--setup` | `seedpass setup` | --- @@ -577,7 +577,7 @@ seedpass auto-post --disable **Long Flag:** `--setup` **Description:** -Guides users through the initial setup process, allowing them to choose between generating a new seed or importing an existing one. This command also handles the encryption of the seed and the creation of a Nostr profile. +Guides users through the initial setup process, allowing them to choose between generating a new seed or importing an existing one. This command also handles the encryption of the seed and the creation of a profile. **Usage Example:** ```bash @@ -587,7 +587,7 @@ seedpass setup **Features to Implement:** - **Seed Choice Prompt:** Asks users whether they want to generate a new seed or import an existing one. - **Encryption of Seed:** Uses the user-selected password to encrypt the seed, whether generated or imported. -- **Nostr Profile Creation:** Upon first login, automatically generates a Nostr profile and checks for existing index data notes that can be pulled and decrypted. +- **Profile Creation:** Upon first login, automatically generates a profile and checks for existing index data notes that can be pulled and decrypted. --- @@ -604,7 +604,7 @@ seedpass setup - **Features to Implement:** - **Seed Choice Prompt:** Ask users whether they want to generate a new seed or import an existing one. - **Encryption of Seed:** Use the user-selected password to encrypt the seed, whether generated or imported. - - **Nostr Profile Creation:** Upon first login, automatically generate a Nostr profile and check for existing index data notes that can be pulled and decrypted. + - **Profile Creation:** Upon first login, automatically generate a profile and check for existing index data notes that can be pulled and decrypted. - **Usage Example:** `seedpass setup` 3. **Advanced CLI Enhancements:** @@ -618,8 +618,8 @@ seedpass setup - **Description:** When running `seedpass setup`, prompts users to either enter an existing seed or generate a new one, followed by password creation for encryption. - **Usage Example:** `seedpass setup` - - **Automatic Nostr Profile Generation and Index Retrieval:** - - **Description:** During the initial setup or first login, generates a Nostr profile and attempts to retrieve and decrypt any existing index data from Nostr. + - **Automatic Profile Generation and Index Retrieval:** + - **Description:** During the initial setup or first login, generates a profile and attempts to retrieve and decrypt any existing index data from Nostr. - **Usage Example:** `seedpass setup` (handles internally) --- @@ -632,13 +632,13 @@ seedpass setup ``` - **Consistent Flag Usage:** Use either the short flag or the long flag as per your preference, but maintain consistency for readability. - + - **Security Considerations:** - Always use strong, unique master passwords. - Regularly back up your encrypted index. - Enable auto-lock to enhance security. - Be cautious when using the `export` and `import` commands to handle sensitive data securely. - + - **Nostr Integration:** - Ensure that your Nostr relays are reliable and secure. - Regularly verify your Nostr public key and manage relays through the `set-relays` command. diff --git a/docs/json_entries.md b/docs/json_entries.md index fdf90d5..d6e5872 100644 --- a/docs/json_entries.md +++ b/docs/json_entries.md @@ -1,3 +1,7 @@ +Certainly! Below is the updated **SeedPass JSON Entry Management and Extensibility** documentation tailored to align with the new **Fingerprint-Based Backup and Local Storage** approach. This revision ensures that fingerprints are treated as distinct entities within the JSON schema and directory structures, without referencing any SeedSigner devices. + +--- + # SeedPass JSON Entry Management and Extensibility ## Table of Contents @@ -29,7 +33,7 @@ ## Introduction -**SeedPass** is a secure password generator and manager leveraging **Bitcoin's BIP-85 standard** and integrating with the **Nostr network** for decentralized synchronization. To enhance modularity, scalability, and security, SeedPass now manages each password or data entry as a separate JSON file. This document outlines the new entry management system, ensuring that new `kind` types can be added seamlessly without disrupting existing functionalities. +**SeedPass** is a secure password generator and manager leveraging **Bitcoin's BIP-85 standard** and integrating with the **Nostr network** for decentralized synchronization. To enhance modularity, scalability, and security, SeedPass now manages each password or data entry as a separate JSON file within a **Fingerprint-Based Backup and Local Storage** system. This document outlines the new entry management system, ensuring that new `kind` types can be added seamlessly without disrupting existing functionalities. --- @@ -43,6 +47,7 @@ Each SeedPass entry is stored as an individual JSON file, promoting isolated man { "entry_num": 0, "index_num": 0, + "fingerprint": "a1b2c3d4", "kind": "generated_password", "data": { // Fields specific to the kind @@ -64,6 +69,8 @@ Each SeedPass entry is stored as an individual JSON file, promoting isolated man - For `generated_password` kind: Starts from 0 and increments sequentially. - For other kinds: A secure random hexadecimal string (e.g., a hash of the content) used as the BIP-85 index. +- **fingerprint** (`string`): A unique identifier generated from the seed associated with the entry. This fingerprint ensures that each seed's data is isolated and securely managed. + - **kind** (`string`): Specifies the type of entry. Initial kinds include: - `generated_password` - `stored_password` @@ -89,6 +96,7 @@ Each SeedPass entry is stored as an individual JSON file, promoting isolated man { "entry_num": 0, "index_num": 0, + "fingerprint": "a1b2c3d4", "kind": "generated_password", "data": { "title": "Example Website", @@ -111,7 +119,8 @@ Each SeedPass entry is stored as an individual JSON file, promoting isolated man ```json { "entry_num": 1, - "index_num": 1, + "index_num": "q1wec4d426fs", + "fingerprint": "a1b2c3d4", "kind": "stored_password", "data": { "title": "Another Service", @@ -133,6 +142,7 @@ Each SeedPass entry is stored as an individual JSON file, promoting isolated man { "entry_num": 2, "index_num": "a1b2c3d4e5f6", + "fingerprint": "a1b2c3d4", "kind": "managed_user", "data": { "users_password": "" @@ -152,6 +162,7 @@ Each SeedPass entry is stored as an individual JSON file, promoting isolated man { "entry_num": 3, "index_num": "f7g8h9i0j1k2", + "fingerprint": "a1b2c3d4", "kind": "12_word_seed", "data": { "seed_phrase": "" @@ -171,6 +182,7 @@ Each SeedPass entry is stored as an individual JSON file, promoting isolated man { "entry_num": 4, "index_num": "l3m4n5o6p7q8", + "fingerprint": "a1b2c3d4", "kind": "nostr_keys", "data": { "public_key": "", @@ -191,6 +203,7 @@ Each SeedPass entry is stored as an individual JSON file, promoting isolated man { "entry_num": 5, "index_num": "r9s0t1u2v3w4", + "fingerprint": "a1b2c3d4", "kind": "note", "data": { "content": "This is a secure note.", @@ -221,6 +234,7 @@ Each entry is encapsulated in its own JSON file with a standardized structure: { "entry_num": 0, "index_num": 0, + "fingerprint": "a1b2c3d4", "kind": "generated_password", "data": { // Fields specific to the kind @@ -272,11 +286,12 @@ To maintain compatibility as new `kind` types are introduced, implement the foll def process_entry(entry): kind = entry.get("kind") data = entry.get("data") + fingerprint = entry.get("fingerprint") if kind == "generated_password": - handle_generated_password(data) + handle_generated_password(data, fingerprint) elif kind == "stored_password": - handle_stored_password(data) + handle_stored_password(data, fingerprint) # ... other known kinds ... else: log_warning(f"Unknown kind: {kind}. Skipping entry.") @@ -314,13 +329,13 @@ To ensure seamless integration of new `kind` types in the future, consider the f ```python # handlers.py -def handle_generated_password(data): +def handle_generated_password(data, fingerprint): # Implementation -def handle_stored_password(data): +def handle_stored_password(data, fingerprint): # Implementation -def handle_cryptocurrency_wallet(data): +def handle_cryptocurrency_wallet(data, fingerprint): # Implementation ``` @@ -331,7 +346,7 @@ def handle_cryptocurrency_wallet(data): **Example:** ```python -def handle_cryptocurrency_wallet(data): +def handle_cryptocurrency_wallet(data, fingerprint): required_fields = ["wallet_name", "address", "private_key"] for field in required_fields: if field not in data: @@ -360,6 +375,7 @@ Create a JSON file following the standardized structure with the new `kind` valu { "entry_num": 6, "index_num": "x1y2z3a4b5c6", + "fingerprint": "a1b2c3d4", "kind": "cryptocurrency_wallet", "data": { "wallet_name": "My Bitcoin Wallet", @@ -379,7 +395,7 @@ Create a JSON file following the standardized structure with the new `kind` valu **Implement Handler Function:** ```python -def handle_cryptocurrency_wallet(data): +def handle_cryptocurrency_wallet(data, fingerprint): wallet_name = data.get("wallet_name") address = data.get("address") private_key = decrypt(data.get("private_key")) @@ -392,13 +408,14 @@ def handle_cryptocurrency_wallet(data): def process_entry(entry): kind = entry.get("kind") data = entry.get("data") + fingerprint = entry.get("fingerprint") if kind == "generated_password": - handle_generated_password(data) + handle_generated_password(data, fingerprint) elif kind == "stored_password": - handle_stored_password(data) + handle_stored_password(data, fingerprint) elif kind == "cryptocurrency_wallet": - handle_cryptocurrency_wallet(data) + handle_cryptocurrency_wallet(data, fingerprint) # ... other known kinds ... else: log_warning(f"Unknown kind: {kind}. Skipping entry.") @@ -412,32 +429,50 @@ Existing kinds such as `generated_password`, `stored_password`, etc., continue t ## Backup and Rollback Mechanism -To ensure data integrity and provide recovery options, SeedPass implements a robust backup and rollback system. +To ensure data integrity and provide recovery options, SeedPass implements a robust backup and rollback system within the **Fingerprint-Based Backup and Local Storage** framework. ### Backup Directory Structure -- **Primary Entries Directory:** Stores individual JSON files for each entry. -- **Backups Directory:** Stores previous versions of each entry. +All backups are organized based on fingerprints, ensuring that each seed's data remains isolated and secure. -**Example Structure:** ``` -SeedPass/ -├── entries/ -│ ├── entry_0.json -│ ├── entry_1.json -│ └── ... -├── backups/ -│ ├── entry_0_v1.json -│ ├── entry_0_v2.json -│ ├── entry_1_v1.json -│ └── ... +~/.seedpass/ +├── a1b2c3d4/ +│ ├── entries/ +│ │ ├── entry_0.json +│ │ ├── entry_1.json +│ │ └── ... +│ ├── backups/ +│ │ ├── entry_0_v1.json +│ │ ├── entry_0_v2.json +│ │ ├── entry_1_v1.json +│ │ └── ... +│ ├── parent_seed.enc +│ ├── seedpass_passwords_checksum.txt +│ ├── seedpass_passwords_db_checksum.txt +│ └── seedpass_passwords_db.json +├── b5c6d7e8/ +│ ├── entries/ +│ │ ├── entry_0.json +│ │ ├── entry_1.json +│ │ └── ... +│ ├── backups/ +│ │ ├── entry_0_v1.json +│ │ ├── entry_0_v2.json +│ │ ├── entry_1_v1.json +│ │ └── ... +│ ├── parent_seed.enc +│ ├── seedpass_passwords_checksum.txt +│ ├── seedpass_passwords_db_checksum.txt +│ └── seedpass_passwords_db.json +└── ... ``` ### Backup Process 1. **Upon Modifying an Entry:** - - The current version of the entry is copied to the `backups/` directory with a version suffix (e.g., `entry_0_v1.json`). - - The modified entry is saved in the `entries/` directory. + - The current version of the entry is copied to the `backups/` directory within the corresponding fingerprint folder with a version suffix (e.g., `entry_0_v1.json`). + - The modified entry is saved in the `entries/` directory within the same fingerprint folder. 2. **Versioning:** - Each backup file includes a version number to track changes over time. @@ -445,10 +480,32 @@ SeedPass/ ### Rollback Functionality - **Restoring an Entry:** - - Users can select a backup version from the `backups/` directory. + - Users can select a backup version from the `backups/` directory within the specific fingerprint folder. - The selected backup file is copied back to the `entries/` directory, replacing the current version. **Example Command:** ```bash -python main.py rollback entry_0_v1.json -``` \ No newline at end of file +seedpass rollback --fingerprint a1b2c3d4 --file entry_0_v1.json +``` + +**Example Directory Structure After Rollback:** +``` +~/.seedpass/ +├── a1b2c3d4/ +│ ├── entries/ +│ │ ├── entry_0.json # Restored from entry_0_v1.json +│ │ ├── entry_1.json +│ │ └── ... +│ ├── backups/ +│ │ ├── entry_0_v1.json +│ │ ├── entry_0_v2.json +│ │ ├── entry_1_v1.json +│ │ └── ... +│ ├── parent_seed.enc +│ ├── seedpass_passwords_checksum.txt +│ ├── seedpass_passwords_db_checksum.txt +│ └── seedpass_passwords_db.json +├── ... +``` + +*Note: Restoring a backup overwrites the current entry. Ensure that you intend to revert to the selected backup before proceeding.* \ No newline at end of file diff --git a/src/constants.py b/src/constants.py index 0219674..b8655f1 100644 --- a/src/constants.py +++ b/src/constants.py @@ -91,3 +91,5 @@ MAX_PASSWORD_LENGTH = 128 # Maximum allowed password length # Additional Constants (if any) # ----------------------------------- # Add any other constants here as your project expands +HASHED_PASSWORD_FILE = APP_DIR / 'hashed_password.enc' +DEFAULT_SEED_BACKUP_FILENAME = 'parent_seed_backup.enc' diff --git a/src/main.py b/src/main.py index 01e9890..c1f4136 100644 --- a/src/main.py +++ b/src/main.py @@ -57,11 +57,12 @@ def display_menu(password_manager: PasswordManager, nostr_client: NostrClient): 5. Post Encrypted Index to Nostr 6. Retrieve Encrypted Index from Nostr 7. Display Nostr Public Key (npub) - 8. Exit + 8. Backup/Reveal Parent Seed + 9. Exit """ while True: print(colored(menu, 'cyan')) - choice = input('Enter your choice (1-8): ').strip() + choice = input('Enter your choice (1-9): ').strip() # Updated to include option 9 if choice == '1': password_manager.handle_generate_password() elif choice == '2': @@ -77,6 +78,8 @@ def display_menu(password_manager: PasswordManager, nostr_client: NostrClient): elif choice == '7': handle_display_npub(nostr_client) elif choice == '8': + password_manager.handle_backup_reveal_parent_seed() # Corrected variable name + elif choice == '9': logging.info("Exiting the program.") print(colored("Exiting the program.", 'green')) nostr_client.close_client_pool() # Gracefully close the ClientPool diff --git a/src/password_manager/encryption.py b/src/password_manager/encryption.py index 92063a8..eb0212a 100644 --- a/src/password_manager/encryption.py +++ b/src/password_manager/encryption.py @@ -16,6 +16,7 @@ This means it should generate passwords the exact same way every single time. S import os import json +import stat import hashlib import logging import traceback @@ -90,16 +91,18 @@ class EncryptionManager: def encrypt_parent_seed(self, parent_seed: str, file_path: Path) -> None: """ - Encrypts and saves the parent seed to the specified file. + Encrypts and securely saves the parent seed to the specified file. :param parent_seed: The BIP39 parent seed phrase. :param file_path: The path to the file where the encrypted parent seed will be saved. """ try: - # **Do not encrypt the data here** + # Encode the parent seed to bytes data = parent_seed.encode('utf-8') - # Pass the raw data to encrypt_file, which handles encryption + # Encrypt and write to file using encrypt_file self.encrypt_file(file_path, data) + # Set file permissions to read/write for the user only + os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR) logger.info(f"Parent seed encrypted and saved to '{file_path}'.") print(colored(f"Parent seed encrypted and saved to '{file_path}'.", 'green')) except Exception as e: @@ -141,7 +144,7 @@ class EncryptionManager: return encrypted_data except Exception as e: logger.error(f"Error encrypting data: {e}") - logger.error(traceback.format_exc()) # Log full traceback + logger.error(traceback.format_exc()) print(colored(f"Error: Failed to encrypt data: {e}", 'red')) raise diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 65bbb54..fe65de1 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -25,7 +25,7 @@ from password_manager.password_generation import PasswordGenerator from password_manager.backup import BackupManager from utils.key_derivation import derive_key_from_parent_seed, derive_key_from_password from utils.checksum import calculate_checksum, verify_checksum -from utils.password_prompt import prompt_for_password +from utils.password_prompt import prompt_for_password, prompt_existing_password, confirm_action from constants import ( APP_DIR, @@ -35,10 +35,14 @@ from constants import ( SCRIPT_CHECKSUM_FILE, MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, - DEFAULT_PASSWORD_LENGTH + DEFAULT_PASSWORD_LENGTH, + HASHED_PASSWORD_FILE, # Ensure this constant is defined in constants.py + DEFAULT_SEED_BACKUP_FILENAME ) import traceback # Added for exception traceback logging +import bcrypt # Ensure bcrypt is installed in your environment +from pathlib import Path # Required for handling file paths # Configure logging at the start of the module def configure_logging(): @@ -112,7 +116,7 @@ class PasswordManager: self.encryption_manager = EncryptionManager(key) self.parent_seed = self.encryption_manager.decrypt_parent_seed(PARENT_SEED_FILE) - # **Add validation for the decrypted seed** + # Validate the decrypted seed if not self.validate_seed_phrase(self.parent_seed): logging.error("Decrypted seed is invalid. Exiting.") print(colored("Error: Decrypted seed is invalid.", 'red')) @@ -149,6 +153,9 @@ class PasswordManager: try: self.encryption_manager.encrypt_parent_seed(parent_seed, PARENT_SEED_FILE) logging.info("Parent seed encrypted and saved successfully.") + # Store the hashed password + self.store_hashed_password(password) + logging.info("User password hashed and stored successfully.") except Exception as e: logging.error(f"Failed to encrypt and save parent seed: {e}") logging.error(traceback.format_exc()) @@ -182,7 +189,7 @@ class PasswordManager: print(colored(f"Error: {e}", 'red')) return None - def validate_seed_phrase(self, seed_phrase: str) -> Optional[str]: + def validate_seed_phrase(self, seed_phrase: str) -> bool: """ Validates the seed phrase using the EncryptionManager if available, otherwise performs basic validation. @@ -191,26 +198,26 @@ class PasswordManager: seed_phrase (str): The seed phrase to validate. Returns: - Optional[str]: The validated seed phrase or None if invalid. + bool: True if valid, False otherwise. """ try: if self.encryption_manager: # Use EncryptionManager to validate seed - if self.encryption_manager.validate_seed(seed_phrase): + is_valid = self.encryption_manager.validate_seed(seed_phrase) + if is_valid: logging.debug("Seed phrase validated successfully using EncryptionManager.") - return seed_phrase else: logging.error("Invalid seed phrase.") print(colored("Error: Invalid seed phrase.", 'red')) - return None + return is_valid else: # Perform basic validation - return self.basic_validate_seed_phrase(seed_phrase) + return self.basic_validate_seed_phrase(seed_phrase) is not None except Exception as e: logging.error(f"Error validating seed phrase: {e}") logging.error(traceback.format_exc()) print(colored(f"Error: Failed to validate seed phrase: {e}", 'red')) - return None + return False def initialize_managers(self) -> None: """ @@ -443,7 +450,101 @@ class PasswordManager: logging.error(traceback.format_exc()) print(colored(f"Error: Failed to restore backup: {e}", 'red')) - # Additional methods can be added here as needed + def handle_backup_reveal_parent_seed(self) -> None: + """ + Handles the backup and reveal of the parent seed. + """ + try: + print(colored("\n=== Backup/Reveal Parent Seed ===", 'yellow')) + print(colored("Warning: Revealing your parent seed is a highly sensitive operation.", 'red')) + print(colored("Ensure you're in a secure, private environment and no one is watching your screen.", 'red')) + + # Verify user's identity with secure password verification + password = prompt_existing_password("Enter your master password to continue: ") + if not self.verify_password(password): + print(colored("Incorrect password. Operation aborted.", 'red')) + return + + # Double confirmation + if not confirm_action("Are you absolutely sure you want to reveal your parent seed? (Y/N): "): + print(colored("Operation cancelled by user.", 'yellow')) + return + + # Reveal the parent seed + print(colored("\n=== Your Parent Seed ===", 'green')) + print(colored(self.parent_seed, 'yellow')) + print(colored("\nPlease write this down and store it securely. Do not share it with anyone.", 'red')) + + # Option to save to file with default filename + if confirm_action("Do you want to save this to an encrypted backup file? (Y/N): "): + filename = input(f"Enter filename to save (default: {DEFAULT_SEED_BACKUP_FILENAME}): ").strip() + filename = filename if filename else DEFAULT_SEED_BACKUP_FILENAME + backup_path = Path(APP_DIR) / filename + + # Validate filename + if not self.is_valid_filename(filename): + print(colored("Invalid filename. Operation aborted.", 'red')) + return + + self.encryption_manager.encrypt_parent_seed(self.parent_seed, backup_path) + print(colored(f"Encrypted seed backup saved to '{backup_path}'. Ensure this file is stored securely.", 'green')) + + except Exception as e: + logging.error(f"Error during parent seed backup/reveal: {e}") + logging.error(traceback.format_exc()) + print(colored(f"Error: Failed to backup/reveal parent seed: {e}", 'red')) + + def verify_password(self, password: str) -> bool: + """ + Verifies the provided password against the stored hashed password. + """ + try: + if not os.path.exists(HASHED_PASSWORD_FILE): + logging.error("Hashed password file not found.") + print(colored("Error: Hashed password file not found.", 'red')) + return False + with open(HASHED_PASSWORD_FILE, 'rb') as f: + stored_hash = f.read() + is_correct = bcrypt.checkpw(password.encode('utf-8'), stored_hash) + if is_correct: + logging.debug("Password verification successful.") + else: + logging.warning("Password verification failed.") + return is_correct + except Exception as e: + logging.error(f"Error verifying password: {e}") + logging.error(traceback.format_exc()) + print(colored(f"Error: Failed to verify password: {e}", 'red')) + return False + + def is_valid_filename(self, filename: str) -> bool: + """ + Validates the provided filename to prevent directory traversal and invalid characters. + """ + # Basic validation: filename should not contain path separators or be empty + invalid_chars = ['/', '\\', '..'] + if any(char in filename for char in invalid_chars) or not filename: + logging.warning(f"Invalid filename attempted: {filename}") + return False + return True + + def store_hashed_password(self, password: str) -> None: + """ + Hashes and stores the user's password securely using bcrypt. + This should be called during the initial setup. + """ + try: + hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + with open(HASHED_PASSWORD_FILE, 'wb') as f: + f.write(hashed) + # Set file permissions to read/write for the user only + os.chmod(HASHED_PASSWORD_FILE, 0o600) + logging.info("User password hashed and stored successfully.") + except Exception as e: + logging.error(f"Failed to store hashed password: {e}") + logging.error(traceback.format_exc()) + print(colored(f"Error: Failed to store hashed password: {e}", 'red')) + raise # Example usage (this part should be removed or commented out when integrating into the larger application) if __name__ == "__main__": diff --git a/src/requirements.txt b/src/requirements.txt index a782068..1910df9 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -5,4 +5,5 @@ bip-utils>=2.5.0 bech32==1.2.0 monstr @ git+https://github.com/monty888/monstr.git@master#egg=monstr mnemonic -aiohttp \ No newline at end of file +aiohttp +bcrypt \ No newline at end of file