diff --git a/README.md b/README.md index 6e742e7..4bdda4f 100644 --- a/README.md +++ b/README.md @@ -237,8 +237,9 @@ The SeedPass roadmap outlines a structured development plan divided into distinc - **Implementation Steps:** - Create a `config.yaml` or `config.json` file in the SeedPass data directory. - Define a structure to store user configurations, starting with a list of Nostr relay URLs. - - Allow users to add, remove, and manage an unlimited number of Nostr relays through the CLI or configuration file. - - Ensure the configuration file is securely stored and encrypted if necessary. + - Allow users to add, remove, and manage an unlimited number of Nostr relays through the CLI or configuration file. + - Ensure the configuration file is securely stored and encrypted if necessary. + - The Nostr client loads its relay list from this encrypted file. New accounts start with the default relays until you update the settings. 2. **Individual JSON File Management** - **Separate Entry Files:** diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index 4c0a53d..cdd90d9 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -418,6 +418,7 @@ seedpass show-pubkey **Description:** Allows users to specify custom Nostr relays for publishing their encrypted backup index, providing flexibility and control over data distribution. +Relay URLs are stored in an encrypted configuration file and loaded each time the Nostr client starts. New accounts use the default relays until changed. **Usage Example:** ```bash diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index ee8d0c0..77d703d 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -25,7 +25,11 @@ 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, prompt_existing_password, confirm_action +from utils.password_prompt import ( + prompt_for_password, + prompt_existing_password, + confirm_action, +) from constants import ( APP_DIR, @@ -34,12 +38,12 @@ from constants import ( MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, DEFAULT_PASSWORD_LENGTH, - DEFAULT_SEED_BACKUP_FILENAME + DEFAULT_SEED_BACKUP_FILENAME, ) -import traceback -import bcrypt -from pathlib import Path +import traceback +import bcrypt +from pathlib import Path from local_bip85.bip85 import BIP85 from bip_utils import Bip39SeedGenerator, Bip39MnemonicGenerator, Bip39Languages @@ -47,11 +51,13 @@ from bip_utils import Bip39SeedGenerator, Bip39MnemonicGenerator, Bip39Languages from utils.fingerprint_manager import FingerprintManager # Import NostrClient -from nostr.client import NostrClient +from nostr.client import NostrClient, DEFAULT_RELAYS +from password_manager.config_manager import ConfigManager # Instantiate the logger logger = logging.getLogger(__name__) + class PasswordManager: """ PasswordManager Class @@ -74,6 +80,7 @@ class PasswordManager: self.parent_seed: Optional[str] = None self.bip85: Optional[BIP85] = None self.nostr_client: Optional[NostrClient] = None + self.config_manager: Optional[ConfigManager] = None # Initialize the fingerprint manager first self.initialize_fingerprint_manager() @@ -94,7 +101,9 @@ class PasswordManager: except Exception as e: logger.error(f"Failed to initialize FingerprintManager: {e}") logger.error(traceback.format_exc()) - print(colored(f"Error: Failed to initialize FingerprintManager: {e}", 'red')) + print( + colored(f"Error: Failed to initialize FingerprintManager: {e}", "red") + ) sys.exit(1) def setup_parent_seed(self) -> None: @@ -114,31 +123,31 @@ class PasswordManager: Prompts the user to select an existing fingerprint or add a new one. """ try: - print(colored("\nAvailable Fingerprints:", 'cyan')) + print(colored("\nAvailable Fingerprints:", "cyan")) fingerprints = self.fingerprint_manager.list_fingerprints() for idx, fp in enumerate(fingerprints, start=1): - print(colored(f"{idx}. {fp}", 'cyan')) + print(colored(f"{idx}. {fp}", "cyan")) - print(colored(f"{len(fingerprints)+1}. Add a new fingerprint", 'cyan')) + print(colored(f"{len(fingerprints)+1}. Add a new fingerprint", "cyan")) choice = input("Select a fingerprint by number: ").strip() - if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)+1): - print(colored("Invalid selection. Exiting.", 'red')) + if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints) + 1): + print(colored("Invalid selection. Exiting.", "red")) sys.exit(1) choice = int(choice) - if choice == len(fingerprints)+1: + if choice == len(fingerprints) + 1: # Add a new fingerprint self.add_new_fingerprint() else: # Select existing fingerprint - selected_fingerprint = fingerprints[choice-1] + selected_fingerprint = fingerprints[choice - 1] self.select_fingerprint(selected_fingerprint) except Exception as e: logger.error(f"Error during fingerprint selection: {e}") logger.error(traceback.format_exc()) - print(colored(f"Error: Failed to select fingerprint: {e}", 'red')) + print(colored(f"Error: Failed to select fingerprint: {e}", "red")) sys.exit(1) def add_new_fingerprint(self): @@ -146,31 +155,45 @@ class PasswordManager: Adds a new fingerprint by generating it from a seed phrase. """ try: - choice = input("Do you want to (1) Enter an existing seed or (2) Generate a new seed? (1/2): ").strip() - if choice == '1': + choice = input( + "Do you want to (1) Enter an existing seed or (2) Generate a new seed? (1/2): " + ).strip() + if choice == "1": fingerprint = self.setup_existing_seed() - elif choice == '2': + elif choice == "2": fingerprint = self.generate_new_seed() else: - print(colored("Invalid choice. Exiting.", 'red')) + print(colored("Invalid choice. Exiting.", "red")) sys.exit(1) # Set current_fingerprint in FingerprintManager only self.fingerprint_manager.current_fingerprint = fingerprint - print(colored(f"New fingerprint '{fingerprint}' added and set as current.", 'green')) + print( + colored( + f"New fingerprint '{fingerprint}' added and set as current.", + "green", + ) + ) except Exception as e: logger.error(f"Error adding new fingerprint: {e}") logger.error(traceback.format_exc()) - print(colored(f"Error: Failed to add new fingerprint: {e}", 'red')) + print(colored(f"Error: Failed to add new fingerprint: {e}", "red")) sys.exit(1) def select_fingerprint(self, fingerprint: str) -> None: if self.fingerprint_manager.select_fingerprint(fingerprint): self.current_fingerprint = fingerprint # Add this line - self.fingerprint_dir = self.fingerprint_manager.get_current_fingerprint_dir() + self.fingerprint_dir = ( + self.fingerprint_manager.get_current_fingerprint_dir() + ) if not self.fingerprint_dir: - print(colored(f"Error: Fingerprint directory for {fingerprint} not found.", 'red')) + print( + colored( + f"Error: Fingerprint directory for {fingerprint} not found.", + "red", + ) + ) sys.exit(1) # Setup the encryption manager and load parent seed self.setup_encryption_manager(self.fingerprint_dir) @@ -178,12 +201,19 @@ class PasswordManager: # Initialize BIP85 and other managers self.initialize_bip85() self.initialize_managers() - print(colored(f"Fingerprint {fingerprint} selected and managers initialized.", 'green')) + print( + colored( + f"Fingerprint {fingerprint} selected and managers initialized.", + "green", + ) + ) else: - print(colored(f"Error: Fingerprint {fingerprint} not found.", 'red')) + print(colored(f"Error: Fingerprint {fingerprint} not found.", "red")) sys.exit(1) - def setup_encryption_manager(self, fingerprint_dir: Path, password: Optional[str] = None): + def setup_encryption_manager( + self, fingerprint_dir: Path, password: Optional[str] = None + ): """ Sets up the EncryptionManager for the selected fingerprint. @@ -198,17 +228,19 @@ class PasswordManager: # Derive key from password key = derive_key_from_password(password) self.encryption_manager = EncryptionManager(key, fingerprint_dir) - logger.debug("EncryptionManager set up successfully for selected fingerprint.") + logger.debug( + "EncryptionManager set up successfully for selected fingerprint." + ) # Verify the password self.fingerprint_dir = fingerprint_dir # Ensure self.fingerprint_dir is set if not self.verify_password(password): - print(colored("Invalid password. Exiting.", 'red')) + print(colored("Invalid password. Exiting.", "red")) sys.exit(1) except Exception as e: logger.error(f"Failed to set up EncryptionManager: {e}") logger.error(traceback.format_exc()) - print(colored(f"Error: Failed to set up encryption: {e}", 'red')) + print(colored(f"Error: Failed to set up encryption: {e}", "red")) sys.exit(1) def load_parent_seed(self, fingerprint_dir: Path): @@ -220,7 +252,9 @@ class PasswordManager: """ try: self.parent_seed = self.encryption_manager.decrypt_parent_seed() - logger.debug(f"Parent seed loaded for fingerprint {self.current_fingerprint}.") + logger.debug( + f"Parent seed loaded for fingerprint {self.current_fingerprint}." + ) # Initialize BIP85 with the parent seed seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate() self.bip85 = BIP85(seed_bytes) @@ -228,7 +262,7 @@ class PasswordManager: except Exception as e: logger.error(f"Failed to load parent seed: {e}") logger.error(traceback.format_exc()) - print(colored(f"Error: Failed to load parent seed: {e}", 'red')) + print(colored(f"Error: Failed to load parent seed: {e}", "red")) sys.exit(1) def handle_switch_fingerprint(self) -> bool: @@ -239,14 +273,14 @@ class PasswordManager: bool: True if switch was successful, False otherwise. """ try: - print(colored("\nAvailable Fingerprints:", 'cyan')) + print(colored("\nAvailable Fingerprints:", "cyan")) fingerprints = self.fingerprint_manager.list_fingerprints() for idx, fp in enumerate(fingerprints, start=1): - print(colored(f"{idx}. {fp}", 'cyan')) + print(colored(f"{idx}. {fp}", "cyan")) choice = input("Select a fingerprint by number to switch: ").strip() if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)): - print(colored("Invalid selection. Returning to main menu.", 'red')) + print(colored("Invalid selection. Returning to main menu.", "red")) return False # Return False to indicate failure selected_fingerprint = fingerprints[int(choice) - 1] @@ -254,9 +288,16 @@ class PasswordManager: self.current_fingerprint = selected_fingerprint # Update fingerprint directory - self.fingerprint_dir = self.fingerprint_manager.get_current_fingerprint_dir() + self.fingerprint_dir = ( + self.fingerprint_manager.get_current_fingerprint_dir() + ) if not self.fingerprint_dir: - print(colored(f"Error: Fingerprint directory for {selected_fingerprint} not found.", 'red')) + print( + colored( + f"Error: Fingerprint directory for {selected_fingerprint} not found.", + "red", + ) + ) return False # Return False to indicate failure # Prompt for master password for the selected fingerprint @@ -271,18 +312,22 @@ class PasswordManager: # Initialize BIP85 and other managers self.initialize_bip85() self.initialize_managers() - print(colored(f"Switched to fingerprint {selected_fingerprint}.", 'green')) + print(colored(f"Switched to fingerprint {selected_fingerprint}.", "green")) # Re-initialize NostrClient with the new fingerprint try: self.nostr_client = NostrClient( encryption_manager=self.encryption_manager, - fingerprint=self.current_fingerprint + fingerprint=self.current_fingerprint, + ) + logging.info( + f"NostrClient re-initialized with fingerprint {self.current_fingerprint}." ) - logging.info(f"NostrClient re-initialized with fingerprint {self.current_fingerprint}.") except Exception as e: logging.error(f"Failed to re-initialize NostrClient: {e}") - print(colored(f"Error: Failed to re-initialize NostrClient: {e}", 'red')) + print( + colored(f"Error: Failed to re-initialize NostrClient: {e}", "red") + ) return False return True # Return True to indicate success @@ -290,7 +335,7 @@ class PasswordManager: except Exception as e: logging.error(f"Error during fingerprint switching: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to switch fingerprints: {e}", 'red')) + print(colored(f"Error: Failed to switch fingerprints: {e}", "red")) return False # Return False to indicate failure def handle_existing_seed(self) -> None: @@ -300,56 +345,65 @@ class PasswordManager: """ try: # Prompt for password - password = getpass.getpass(prompt='Enter your login password: ').strip() - + password = getpass.getpass(prompt="Enter your login password: ").strip() + # Derive encryption key from password key = derive_key_from_password(password) - + # Initialize FingerprintManager if not already initialized if not self.fingerprint_manager: self.initialize_fingerprint_manager() - + # Prompt the user to select an existing fingerprint fingerprints = self.fingerprint_manager.list_fingerprints() if not fingerprints: - print(colored("No fingerprints available. Please add a fingerprint first.", 'red')) + print( + colored( + "No fingerprints available. Please add a fingerprint first.", + "red", + ) + ) sys.exit(1) - - print(colored("Available Fingerprints:", 'cyan')) + + print(colored("Available Fingerprints:", "cyan")) for idx, fp in enumerate(fingerprints, start=1): - print(colored(f"{idx}. {fp}", 'cyan')) - + print(colored(f"{idx}. {fp}", "cyan")) + choice = input("Select a fingerprint by number: ").strip() if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)): - print(colored("Invalid selection. Exiting.", 'red')) + print(colored("Invalid selection. Exiting.", "red")) sys.exit(1) - - selected_fingerprint = fingerprints[int(choice)-1] + + selected_fingerprint = fingerprints[int(choice) - 1] self.current_fingerprint = selected_fingerprint - fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory(selected_fingerprint) + fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory( + selected_fingerprint + ) if not fingerprint_dir: - print(colored("Error: Fingerprint directory not found.", 'red')) + print(colored("Error: Fingerprint directory not found.", "red")) sys.exit(1) - + # Initialize EncryptionManager with key and fingerprint_dir self.encryption_manager = EncryptionManager(key, fingerprint_dir) self.parent_seed = self.encryption_manager.decrypt_parent_seed() - + # Log the type and content of parent_seed - logger.debug(f"Decrypted parent_seed: {self.parent_seed} (type: {type(self.parent_seed)})") - + logger.debug( + f"Decrypted parent_seed: {self.parent_seed} (type: {type(self.parent_seed)})" + ) + # Validate the decrypted seed if not self.validate_bip85_seed(self.parent_seed): logging.error("Decrypted seed is invalid. Exiting.") - print(colored("Error: Decrypted seed is invalid.", 'red')) + print(colored("Error: Decrypted seed is invalid.", "red")) sys.exit(1) - + self.initialize_bip85() logging.debug("Parent seed decrypted and validated successfully.") except Exception as e: logging.error(f"Failed to decrypt parent seed: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to decrypt parent seed: {e}", 'red')) + print(colored(f"Error: Failed to decrypt parent seed: {e}", "red")) sys.exit(1) def handle_new_seed_setup(self) -> None: @@ -357,36 +411,51 @@ class PasswordManager: Handles the setup process when no existing parent seed is found. Asks the user whether to enter an existing BIP-85 seed or generate a new one. """ - print(colored("No existing seed found. Let's set up a new one!", 'yellow')) - choice = input("Do you want to (1) Enter an existing BIP-85 seed or (2) Generate a new BIP-85 seed? (1/2): ").strip() + print(colored("No existing seed found. Let's set up a new one!", "yellow")) + choice = input( + "Do you want to (1) Enter an existing BIP-85 seed or (2) Generate a new BIP-85 seed? (1/2): " + ).strip() - if choice == '1': + if choice == "1": self.setup_existing_seed() - elif choice == '2': + elif choice == "2": self.generate_new_seed() else: - print(colored("Invalid choice. Exiting.", 'red')) + print(colored("Invalid choice. Exiting.", "red")) sys.exit(1) def setup_existing_seed(self) -> Optional[str]: """ Prompts the user to enter an existing BIP-85 seed and validates it. - + Returns: Optional[str]: The fingerprint if setup is successful, None otherwise. """ try: - parent_seed = getpass.getpass(prompt='Enter your 12-word BIP-85 seed: ').strip() + parent_seed = getpass.getpass( + prompt="Enter your 12-word BIP-85 seed: " + ).strip() if self.validate_bip85_seed(parent_seed): # Add a fingerprint using the existing seed fingerprint = self.fingerprint_manager.add_fingerprint(parent_seed) if not fingerprint: - print(colored("Error: Failed to generate fingerprint for the provided seed.", 'red')) + print( + colored( + "Error: Failed to generate fingerprint for the provided seed.", + "red", + ) + ) sys.exit(1) - fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory(fingerprint) + fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory( + fingerprint + ) if not fingerprint_dir: - print(colored("Error: Failed to retrieve fingerprint directory.", 'red')) + print( + colored( + "Error: Failed to retrieve fingerprint directory.", "red" + ) + ) sys.exit(1) # Set the current fingerprint in both PasswordManager and FingerprintManager @@ -409,18 +478,20 @@ class PasswordManager: logging.info("User password hashed and stored successfully.") self.parent_seed = parent_seed # Ensure this is a string - logger.debug(f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})") + logger.debug( + f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})" + ) self.initialize_bip85() self.initialize_managers() return fingerprint # Return the generated or added fingerprint else: logging.error("Invalid BIP-85 seed phrase. Exiting.") - print(colored("Error: Invalid BIP-85 seed phrase.", 'red')) + print(colored("Error: Invalid BIP-85 seed phrase.", "red")) sys.exit(1) except KeyboardInterrupt: logging.info("Operation cancelled by user.") - print(colored("\nOperation cancelled by user.", 'yellow')) + print(colored("\nOperation cancelled by user.", "yellow")) sys.exit(0) def generate_new_seed(self) -> Optional[str]: @@ -431,20 +502,28 @@ class PasswordManager: Optional[str]: The fingerprint if generation is successful, None otherwise. """ new_seed = self.generate_bip85_seed() - print(colored("Your new BIP-85 seed phrase is:", 'green')) - print(colored(new_seed, 'yellow')) - print(colored("Please write this down and keep it in a safe place!", 'red')) + print(colored("Your new BIP-85 seed phrase is:", "green")) + print(colored(new_seed, "yellow")) + print(colored("Please write this down and keep it in a safe place!", "red")) if confirm_action("Do you want to use this generated seed? (Y/N): "): # Add a new fingerprint using the generated seed fingerprint = self.fingerprint_manager.add_fingerprint(new_seed) if not fingerprint: - print(colored("Error: Failed to generate fingerprint for the new seed.", 'red')) + print( + colored( + "Error: Failed to generate fingerprint for the new seed.", "red" + ) + ) sys.exit(1) - fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory(fingerprint) + fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory( + fingerprint + ) if not fingerprint_dir: - print(colored("Error: Failed to retrieve fingerprint directory.", 'red')) + print( + colored("Error: Failed to retrieve fingerprint directory.", "red") + ) sys.exit(1) # Set the current fingerprint in both PasswordManager and FingerprintManager @@ -457,7 +536,7 @@ class PasswordManager: return fingerprint # Return the generated fingerprint else: - print(colored("Seed generation cancelled. Exiting.", 'yellow')) + print(colored("Seed generation cancelled. Exiting.", "yellow")) sys.exit(0) def validate_bip85_seed(self, seed: str) -> bool: @@ -491,12 +570,14 @@ class PasswordManager: master_seed = os.urandom(32) # Generate a random 32-byte seed bip85 = BIP85(master_seed) mnemonic_obj = bip85.derive_mnemonic(index=0, words_num=12) - mnemonic_str = mnemonic_obj.ToStr() # Convert Bip39Mnemonic object to string + mnemonic_str = ( + mnemonic_obj.ToStr() + ) # Convert Bip39Mnemonic object to string return mnemonic_str except Exception as e: logging.error(f"Failed to generate BIP-85 seed: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to generate BIP-85 seed: {e}", 'red')) + print(colored(f"Error: Failed to generate BIP-85 seed: {e}", "red")) sys.exit(1) def save_and_encrypt_seed(self, seed: str, fingerprint_dir: Path) -> None: @@ -527,14 +608,16 @@ class PasswordManager: logging.info("Parent seed encrypted and saved successfully.") self.parent_seed = seed # Ensure this is a string - logger.debug(f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})") + logger.debug( + f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})" + ) self.initialize_bip85() self.initialize_managers() except Exception as e: logging.error(f"Failed to encrypt and save parent seed: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to encrypt and save parent seed: {e}", 'red')) + print(colored(f"Error: Failed to encrypt and save parent seed: {e}", "red")) sys.exit(1) def initialize_bip85(self): @@ -548,7 +631,7 @@ class PasswordManager: except Exception as e: logging.error(f"Failed to initialize BIP-85: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to initialize BIP-85: {e}", 'red')) + print(colored(f"Error: Failed to initialize BIP-85: {e}", "red")) sys.exit(1) def initialize_managers(self) -> None: @@ -564,66 +647,87 @@ class PasswordManager: # Reinitialize the managers with the updated EncryptionManager and current fingerprint context self.entry_manager = EntryManager( encryption_manager=self.encryption_manager, - fingerprint_dir=self.fingerprint_dir + fingerprint_dir=self.fingerprint_dir, ) - + self.password_generator = PasswordGenerator( encryption_manager=self.encryption_manager, parent_seed=self.parent_seed, - bip85=self.bip85 + bip85=self.bip85, ) - + self.backup_manager = BackupManager(fingerprint_dir=self.fingerprint_dir) - # Initialize the NostrClient with the current fingerprint + # Load relay configuration and initialize NostrClient + self.config_manager = ConfigManager( + encryption_manager=self.encryption_manager, + fingerprint_dir=self.fingerprint_dir, + ) + config = self.config_manager.load_config() + relay_list = config.get("relays", list(DEFAULT_RELAYS)) + self.nostr_client = NostrClient( encryption_manager=self.encryption_manager, - fingerprint=self.current_fingerprint # Pass the current fingerprint + fingerprint=self.current_fingerprint, + relays=relay_list, ) logger.debug("Managers re-initialized for the new fingerprint.") - + except Exception as e: logger.error(f"Failed to initialize managers: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to initialize managers: {e}", 'red')) + print(colored(f"Error: Failed to initialize managers: {e}", "red")) sys.exit(1) def handle_generate_password(self) -> None: try: - website_name = input('Enter the website name: ').strip() + website_name = input("Enter the website name: ").strip() if not website_name: - print(colored("Error: Website name cannot be empty.", 'red')) + print(colored("Error: Website name cannot be empty.", "red")) return - username = input('Enter the username (optional): ').strip() - url = input('Enter the URL (optional): ').strip() + username = input("Enter the username (optional): ").strip() + url = input("Enter the URL (optional): ").strip() - length_input = input(f'Enter desired password length (default {DEFAULT_PASSWORD_LENGTH}): ').strip() + length_input = input( + f"Enter desired password length (default {DEFAULT_PASSWORD_LENGTH}): " + ).strip() length = DEFAULT_PASSWORD_LENGTH if length_input: if not length_input.isdigit(): - print(colored("Error: Password length must be a number.", 'red')) + print(colored("Error: Password length must be a number.", "red")) return length = int(length_input) if not (MIN_PASSWORD_LENGTH <= length <= MAX_PASSWORD_LENGTH): - print(colored(f"Error: Password length must be between {MIN_PASSWORD_LENGTH} and {MAX_PASSWORD_LENGTH}.", 'red')) + print( + colored( + f"Error: Password length must be between {MIN_PASSWORD_LENGTH} and {MAX_PASSWORD_LENGTH}.", + "red", + ) + ) return # Add the entry to the index and get the assigned index - index = self.entry_manager.add_entry(website_name, length, username, url, blacklisted=False) + index = self.entry_manager.add_entry( + website_name, length, username, url, blacklisted=False + ) # Generate the password using the assigned index password = self.password_generator.generate_password(length, index) # Provide user feedback - print(colored(f"\n[+] Password generated and indexed with ID {index}.\n", 'green')) - print(colored(f"Password for {website_name}: {password}\n", 'yellow')) + print( + colored( + f"\n[+] Password generated and indexed with ID {index}.\n", "green" + ) + ) + print(colored(f"Password for {website_name}: {password}\n", "yellow")) except Exception as e: logging.error(f"Error during password generation: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to generate password: {e}", 'red')) + print(colored(f"Error: Failed to generate password: {e}", "red")) def handle_retrieve_password(self) -> None: """ @@ -631,9 +735,11 @@ class PasswordManager: and displaying the corresponding password and associated details. """ try: - index_input = input('Enter the index number of the password to retrieve: ').strip() + index_input = input( + "Enter the index number of the password to retrieve: " + ).strip() if not index_input.isdigit(): - print(colored("Error: Index must be a number.", 'red')) + print(colored("Error: Index must be a number.", "red")) return index = int(index_input) @@ -643,36 +749,53 @@ class PasswordManager: return # Display entry details - website_name = entry.get('website') - length = entry.get('length') - username = entry.get('username') - url = entry.get('url') - blacklisted = entry.get('blacklisted') + website_name = entry.get("website") + length = entry.get("length") + username = entry.get("username") + url = entry.get("url") + blacklisted = entry.get("blacklisted") - print(colored(f"Retrieving password for '{website_name}' with length {length}.", 'cyan')) + print( + colored( + f"Retrieving password for '{website_name}' with length {length}.", + "cyan", + ) + ) if username: - print(colored(f"Username: {username}", 'cyan')) + print(colored(f"Username: {username}", "cyan")) if url: - print(colored(f"URL: {url}", 'cyan')) + print(colored(f"URL: {url}", "cyan")) if blacklisted: - print(colored(f"Warning: This password is blacklisted and should not be used.", 'red')) + print( + colored( + f"Warning: This password is blacklisted and should not be used.", + "red", + ) + ) # Generate the password password = self.password_generator.generate_password(length, index) # Display the password and associated details if password: - print(colored(f"\n[+] Retrieved Password for {website_name}:\n", 'green')) - print(colored(f"Password: {password}", 'yellow')) - print(colored(f"Associated Username: {username or 'N/A'}", 'cyan')) - print(colored(f"Associated URL: {url or 'N/A'}", 'cyan')) - print(colored(f"Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}", 'cyan')) + print( + colored(f"\n[+] Retrieved Password for {website_name}:\n", "green") + ) + print(colored(f"Password: {password}", "yellow")) + print(colored(f"Associated Username: {username or 'N/A'}", "cyan")) + print(colored(f"Associated URL: {url or 'N/A'}", "cyan")) + print( + colored( + f"Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}", + "cyan", + ) + ) else: - print(colored("Error: Failed to retrieve the password.", 'red')) + print(colored("Error: Failed to retrieve the password.", "red")) except Exception as e: logging.error(f"Error during password retrieval: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to retrieve password: {e}", 'red')) + print(colored(f"Error: Failed to retrieve password: {e}", "red")) def handle_modify_entry(self) -> None: """ @@ -680,9 +803,11 @@ class PasswordManager: and new details to update. """ try: - index_input = input('Enter the index number of the entry to modify: ').strip() + index_input = input( + "Enter the index number of the entry to modify: " + ).strip() if not index_input.isdigit(): - print(colored("Error: Index must be a number.", 'red')) + print(colored("Error: Index must be a number.", "red")) return index = int(index_input) @@ -691,41 +816,71 @@ class PasswordManager: if not entry: return - website_name = entry.get('website') - length = entry.get('length') - username = entry.get('username') - url = entry.get('url') - blacklisted = entry.get('blacklisted') + website_name = entry.get("website") + length = entry.get("length") + username = entry.get("username") + url = entry.get("url") + blacklisted = entry.get("blacklisted") # Display current values - print(colored(f"Modifying entry for '{website_name}' (Index: {index}):", 'cyan')) - print(colored(f"Current Username: {username or 'N/A'}", 'cyan')) - print(colored(f"Current URL: {url or 'N/A'}", 'cyan')) - print(colored(f"Current Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}", 'cyan')) + print( + colored( + f"Modifying entry for '{website_name}' (Index: {index}):", "cyan" + ) + ) + print(colored(f"Current Username: {username or 'N/A'}", "cyan")) + print(colored(f"Current URL: {url or 'N/A'}", "cyan")) + print( + colored( + f"Current Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}", + "cyan", + ) + ) # Prompt for new values (optional) - new_username = input(f'Enter new username (leave blank to keep "{username or "N/A"}"): ').strip() or username - new_url = input(f'Enter new URL (leave blank to keep "{url or "N/A"}"): ').strip() or url - blacklist_input = input(f'Is this password blacklisted? (Y/N, current: {"Y" if blacklisted else "N"}): ').strip().lower() - if blacklist_input == '': + new_username = ( + input( + f'Enter new username (leave blank to keep "{username or "N/A"}"): ' + ).strip() + or username + ) + new_url = ( + input(f'Enter new URL (leave blank to keep "{url or "N/A"}"): ').strip() + or url + ) + blacklist_input = ( + input( + f'Is this password blacklisted? (Y/N, current: {"Y" if blacklisted else "N"}): ' + ) + .strip() + .lower() + ) + if blacklist_input == "": new_blacklisted = blacklisted - elif blacklist_input == 'y': + elif blacklist_input == "y": new_blacklisted = True - elif blacklist_input == 'n': + elif blacklist_input == "n": new_blacklisted = False else: - print(colored("Invalid input for blacklist status. Keeping the current status.", 'yellow')) + print( + colored( + "Invalid input for blacklist status. Keeping the current status.", + "yellow", + ) + ) new_blacklisted = blacklisted # Update the entry - self.entry_manager.modify_entry(index, new_username, new_url, new_blacklisted) + self.entry_manager.modify_entry( + index, new_username, new_url, new_blacklisted + ) - print(colored(f"Entry updated successfully for index {index}.", 'green')) + print(colored(f"Entry updated successfully for index {index}.", "green")) except Exception as e: logging.error(f"Error during modifying entry: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to modify entry: {e}", 'red')) + print(colored(f"Error: Failed to modify entry: {e}", "red")) def handle_verify_checksum(self) -> None: """ @@ -734,15 +889,20 @@ class PasswordManager: try: current_checksum = calculate_checksum(__file__) if verify_checksum(current_checksum, SCRIPT_CHECKSUM_FILE): - print(colored("Checksum verification passed.", 'green')) + print(colored("Checksum verification passed.", "green")) logging.info("Checksum verification passed.") else: - print(colored("Checksum verification failed. The script may have been modified.", 'red')) + print( + colored( + "Checksum verification failed. The script may have been modified.", + "red", + ) + ) logging.error("Checksum verification failed.") except Exception as e: logging.error(f"Error during checksum verification: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to verify checksum: {e}", 'red')) + print(colored(f"Error: Failed to verify checksum: {e}", "red")) def get_encrypted_data(self) -> Optional[bytes]: """ @@ -757,12 +917,12 @@ class PasswordManager: return encrypted_data else: logging.error("Failed to retrieve encrypted index data.") - print(colored("Error: Failed to retrieve encrypted index data.", 'red')) + print(colored("Error: Failed to retrieve encrypted index data.", "red")) return None except Exception as e: logging.error(f"Error retrieving encrypted data: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to retrieve encrypted data: {e}", 'red')) + print(colored(f"Error: Failed to retrieve encrypted data: {e}", "red")) return None def decrypt_and_save_index_from_nostr(self, encrypted_data: bytes) -> None: @@ -774,18 +934,22 @@ class PasswordManager: try: # Decrypt the data using EncryptionManager's decrypt_data method decrypted_data = self.encryption_manager.decrypt_data(encrypted_data) - + # Save the decrypted data to the index file - index_file_path = self.fingerprint_dir / 'seedpass_passwords_db.json.enc' - with open(index_file_path, 'wb') as f: + index_file_path = self.fingerprint_dir / "seedpass_passwords_db.json.enc" + with open(index_file_path, "wb") as f: f.write(decrypted_data) - + logging.info("Index file updated from Nostr successfully.") - print(colored("Index file updated from Nostr successfully.", 'green')) + print(colored("Index file updated from Nostr successfully.", "green")) except Exception as e: logging.error(f"Failed to decrypt and save data from Nostr: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to decrypt and save data from Nostr: {e}", 'red')) + print( + colored( + f"Error: Failed to decrypt and save data from Nostr: {e}", "red" + ) + ) # Re-raise the exception to inform the calling function of the failure raise @@ -795,11 +959,11 @@ class PasswordManager: """ try: self.backup_manager.create_backup() - print(colored("Backup created successfully.", 'green')) + print(colored("Backup created successfully.", "green")) except Exception as e: logging.error(f"Failed to create backup: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to create backup: {e}", 'red')) + print(colored(f"Error: Failed to create backup: {e}", "red")) def restore_database(self) -> None: """ @@ -807,56 +971,92 @@ class PasswordManager: """ try: self.backup_manager.restore_latest_backup() - print(colored("Database restored from the latest backup successfully.", 'green')) + print( + colored( + "Database restored from the latest backup successfully.", "green" + ) + ) except Exception as e: logging.error(f"Failed to restore backup: {e}") logging.error(traceback.format_exc()) - print(colored(f"Error: Failed to restore backup: {e}", 'red')) + print(colored(f"Error: Failed to restore backup: {e}", "red")) 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')) - + 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: ") + password = prompt_existing_password( + "Enter your master password to continue: " + ) if not self.verify_password(password): - print(colored("Incorrect password. Operation aborted.", 'red')) + 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')) + 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 BIP-85 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')) + print(colored("\n=== Your BIP-85 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() + 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 = self.fingerprint_dir / filename # Save in fingerprint directory + backup_path = ( + self.fingerprint_dir / filename + ) # Save in fingerprint directory # Validate filename if not self.is_valid_filename(filename): - print(colored("Invalid filename. Operation aborted.", 'red')) + print(colored("Invalid filename. Operation aborted.", "red")) return # Encrypt and save the parent seed to the backup path - self.encryption_manager.encrypt_and_save_file(self.parent_seed.encode('utf-8'), backup_path) - print(colored(f"Encrypted seed backup saved to '{backup_path}'. Ensure this file is stored securely.", 'green')) + self.encryption_manager.encrypt_and_save_file( + self.parent_seed.encode("utf-8"), 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')) + print(colored(f"Error: Failed to backup/reveal parent seed: {e}", "red")) def verify_password(self, password: str) -> bool: """ @@ -869,14 +1069,14 @@ class PasswordManager: bool: True if the password is correct, False otherwise. """ try: - hashed_password_file = self.fingerprint_dir / 'hashed_password.enc' + hashed_password_file = self.fingerprint_dir / "hashed_password.enc" if not hashed_password_file.exists(): logging.error("Hashed password file not found.") - print(colored("Error: Hashed password file not found.", 'red')) + print(colored("Error: Hashed password file not found.", "red")) return False - with open(hashed_password_file, 'rb') as f: + with open(hashed_password_file, "rb") as f: stored_hash = f.read() - is_correct = bcrypt.checkpw(password.encode('utf-8'), stored_hash) + is_correct = bcrypt.checkpw(password.encode("utf-8"), stored_hash) if is_correct: logging.debug("Password verification successful.") else: @@ -885,7 +1085,7 @@ class PasswordManager: 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')) + print(colored(f"Error: Failed to verify password: {e}", "red")) return False def is_valid_filename(self, filename: str) -> bool: @@ -899,7 +1099,7 @@ class PasswordManager: bool: True if valid, False otherwise. """ # Basic validation: filename should not contain path separators or be empty - invalid_chars = ['/', '\\', '..'] + invalid_chars = ["/", "\\", ".."] if any(char in filename for char in invalid_chars) or not filename: logging.warning(f"Invalid filename attempted: {filename}") return False @@ -911,29 +1111,34 @@ class PasswordManager: This should be called during the initial setup. """ try: - hashed_password_file = self.fingerprint_dir / 'hashed_password.enc' - hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) - with open(hashed_password_file, 'wb') as f: + hashed_password_file = self.fingerprint_dir / "hashed_password.enc" + hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) + with open(hashed_password_file, "wb") as f: f.write(hashed) os.chmod(hashed_password_file, 0o600) logging.info("User password hashed and stored successfully.") except AttributeError: # If bcrypt.hashpw is not available, try using bcrypt directly salt = bcrypt.gensalt() - hashed = bcrypt.hashpw(password.encode('utf-8'), salt) - with open(hashed_password_file, 'wb') as f: + hashed = bcrypt.hashpw(password.encode("utf-8"), salt) + with open(hashed_password_file, "wb") as f: f.write(hashed) os.chmod(hashed_password_file, 0o600) - logging.info("User password hashed and stored successfully (using alternative method).") + logging.info( + "User password hashed and stored successfully (using alternative method)." + ) 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')) + 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__": - from nostr.client import NostrClient # Ensure this import is correct based on your project structure + from nostr.client import ( + NostrClient, + ) # Ensure this import is correct based on your project structure # Initialize PasswordManager manager = PasswordManager()