From c60ae6b442aa0efc2e26ab922834ee2ad9ad3529 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:00:22 -0400 Subject: [PATCH] update --- src/nostr/__init__.py | 15 +- src/nostr/client.py | 3 +- src/nostr/encryption_manager.py | 113 ++++++-- src/nostr/key_manager.py | 98 +++---- src/nostr/logging_config.py | 37 +-- src/password_manager/encryption.py | 42 +-- src/password_manager/manager.py | 273 +++++++++++++------- src/password_manager/password_generation.py | 1 + src/requirements.txt | 3 +- src/tests/test_import.py | 9 + src/utils/key_derivation.py | 77 ++---- 11 files changed, 416 insertions(+), 255 deletions(-) create mode 100644 src/tests/test_import.py diff --git a/src/nostr/__init__.py b/src/nostr/__init__.py index e98d3a3..3972ae9 100644 --- a/src/nostr/__init__.py +++ b/src/nostr/__init__.py @@ -3,12 +3,19 @@ import logging import traceback +from .logging_config import configure_logging + +# Configure logging at the start of the module +configure_logging() + +# Initialize the logger for this module +logger = logging.getLogger(__name__) # Correct logger initialization + try: from .client import NostrClient - logging.info("NostrClient module imported successfully.") + logger.info("NostrClient module imported successfully.") except Exception as e: - logging.error(f"Failed to import NostrClient module: {e}") - logging.error(traceback.format_exc()) # Log full traceback + logger.error(f"Failed to import NostrClient module: {e}") + logger.error(traceback.format_exc()) # Log full traceback __all__ = ['NostrClient'] - diff --git a/src/nostr/client.py b/src/nostr/client.py index 0adb720..3367f45 100644 --- a/src/nostr/client.py +++ b/src/nostr/client.py @@ -27,7 +27,8 @@ from .event_handler import EventHandler from constants import APP_DIR, INDEX_FILE, DATA_CHECKSUM_FILE from utils.file_lock import lock_file -logger = configure_logging() +configure_logging() +logger = logging.getLogger(__name__) DEFAULT_RELAYS = [ "wss://relay.snort.social", diff --git a/src/nostr/encryption_manager.py b/src/nostr/encryption_manager.py index 325c40d..6e313dc 100644 --- a/src/nostr/encryption_manager.py +++ b/src/nostr/encryption_manager.py @@ -1,58 +1,123 @@ # nostr/encryption_manager.py import base64 -import json +import logging import traceback - from cryptography.fernet import Fernet, InvalidToken from .logging_config import configure_logging from .key_manager import KeyManager -from monstr.encrypt import NIP4Encrypt # Add if used -logger = configure_logging() +# Configure logging at the start of the module +configure_logging() + +# Initialize the logger for this module +logger = logging.getLogger(__name__) class EncryptionManager: """ - Handles encryption and decryption of data using Fernet symmetric encryption. + Manages encryption and decryption using Fernet symmetric encryption. """ - + def __init__(self, key_manager: KeyManager): - self.key_manager = key_manager - self.fernet = Fernet(self.key_manager.derive_encryption_key()) + """ + Initializes the EncryptionManager with a Fernet instance. + :param key_manager: An instance of KeyManager to derive the encryption key. + """ + try: + # Derive the raw encryption key (32 bytes) + raw_key = key_manager.derive_encryption_key() + logger.debug(f"Derived raw encryption key length: {len(raw_key)} bytes") + + # Ensure the raw key is exactly 32 bytes + if len(raw_key) != 32: + raise ValueError(f"Derived key length is {len(raw_key)} bytes; expected 32 bytes.") + + # Base64-encode the raw key to make it URL-safe + b64_key = base64.urlsafe_b64encode(raw_key) + logger.debug(f"Base64-encoded encryption key length: {len(b64_key)} bytes") + + # Initialize Fernet with the base64-encoded key + self.fernet = Fernet(b64_key) + logger.info("Fernet encryption manager initialized successfully.") + + except Exception as e: + logger.error(f"EncryptionManager initialization failed: {e}") + logger.error(traceback.format_exc()) + raise + + def encrypt_parent_seed(self, seed: str, file_path: str) -> None: + """ + Encrypts the parent seed and saves it to the specified file. + + :param seed: The BIP-39 seed phrase as a string. + :param file_path: The file path to save the encrypted seed. + """ + try: + encrypted_seed = self.fernet.encrypt(seed.encode('utf-8')) + with open(file_path, 'wb') as f: + f.write(encrypted_seed) + logger.debug(f"Parent seed encrypted and saved to '{file_path}'.") + except Exception as e: + logger.error(f"Failed to encrypt and save parent seed: {e}") + logger.error(traceback.format_exc()) + raise + + def decrypt_parent_seed(self, file_path: str) -> str: + """ + Decrypts the parent seed from the specified file. + + :param file_path: The file path to read the encrypted seed. + :return: The decrypted parent seed as a string. + """ + try: + with open(file_path, 'rb') as f: + encrypted_seed = f.read() + decrypted_seed = self.fernet.decrypt(encrypted_seed).decode('utf-8') + logger.debug(f"Parent seed decrypted successfully from '{file_path}'.") + return decrypted_seed + except InvalidToken: + logger.error("Decryption failed: Invalid token. Possibly incorrect password or corrupted file.") + raise ValueError("Decryption failed: Invalid token. Possibly incorrect password or corrupted file.") + except Exception as e: + logger.error(f"Failed to decrypt parent seed: {e}") + logger.error(traceback.format_exc()) + raise + def encrypt_data(self, data: dict) -> bytes: """ - Encrypts a dictionary and returns encrypted bytes. + Encrypts a dictionary by serializing it to JSON and then encrypting it. - :param data: The data to encrypt. + :param data: The dictionary to encrypt. :return: Encrypted data as bytes. """ try: - json_data = json.dumps(data, indent=4).encode('utf-8') - encrypted_data = self.fernet.encrypt(json_data) + json_data = json.dumps(data).encode('utf-8') + encrypted = self.fernet.encrypt(json_data) logger.debug("Data encrypted successfully.") - return encrypted_data + return encrypted except Exception as e: - logger.error(f"Failed to encrypt data: {e}") + logger.error(f"Data encryption failed: {e}") logger.error(traceback.format_exc()) raise - + def decrypt_data(self, encrypted_data: bytes) -> bytes: """ - Decrypts encrypted bytes and returns the original data. + Decrypts encrypted data. - :param encrypted_data: The encrypted data to decrypt. + :param encrypted_data: The encrypted data as bytes. :return: Decrypted data as bytes. """ try: - decrypted_data = self.fernet.decrypt(encrypted_data) + decrypted = self.fernet.decrypt(encrypted_data) logger.debug("Data decrypted successfully.") - return decrypted_data - except InvalidToken: - logger.error("Invalid encryption key or corrupted data.") - raise - except Exception as e: - logger.error(f"Error decrypting data: {e}") + return decrypted + except InvalidToken as e: + logger.error(f"Decryption failed: Invalid token. {e}") + logger.error(traceback.format_exc()) + raise + except Exception as e: + logger.error(f"Data decryption failed: {e}") logger.error(traceback.format_exc()) raise diff --git a/src/nostr/key_manager.py b/src/nostr/key_manager.py index 5dc39a1..956fbd2 100644 --- a/src/nostr/key_manager.py +++ b/src/nostr/key_manager.py @@ -1,30 +1,23 @@ # nostr/key_manager.py -import base64 +import logging import traceback -from typing import Optional - from bip_utils import Bip39SeedGenerator -from bip85.bip85 import BIP85 from cryptography.fernet import Fernet, InvalidToken from bech32 import bech32_encode, convertbits from .logging_config import configure_logging from utils.key_derivation import derive_key_from_parent_seed -# Add the missing import for Keys and NIP4Encrypt from monstr.encrypt import Keys, NIP4Encrypt # Ensure monstr.encrypt is installed and accessible -logger = configure_logging() +# Configure logging at the start of the module +configure_logging() + +# Initialize the logger for this module +logger = logging.getLogger(__name__) def encode_bech32(prefix: str, key_hex: str) -> str: - """ - Encodes a hex key into Bech32 format with the given prefix. - - :param prefix: The Bech32 prefix (e.g., 'nsec', 'npub'). - :param key_hex: The key in hexadecimal format. - :return: The Bech32-encoded string. - """ try: key_bytes = bytes.fromhex(key_hex) data = convertbits(key_bytes, 8, 5, pad=True) @@ -40,31 +33,30 @@ class KeyManager: """ def __init__(self, parent_seed: str): - self.parent_seed = parent_seed - self.keys = None - self.nsec = None - self.npub = None - self.initialize_keys() - - def initialize_keys(self): """ - Derives Nostr keys using BIP85 and initializes Keys. + Initializes the KeyManager with the provided parent_seed. + + Parameters: + parent_seed (str): The parent seed used for key derivation. """ try: - logger.debug("Starting key initialization") - seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate() - bip85 = BIP85(seed_bytes) - entropy = bip85.derive_entropy(app_no=1237, language_code=0, words_num=24, index=0) - - if len(entropy) != 32: - logger.error(f"Derived entropy length is {len(entropy)} bytes; expected 32 bytes.") - raise ValueError("Invalid entropy length.") - - privkey_hex = entropy.hex() - self.keys = Keys(priv_k=privkey_hex) # Now Keys is defined via the import + if not isinstance(parent_seed, str): + raise TypeError(f"Parent seed must be a string, got {type(parent_seed)}") + + self.parent_seed = parent_seed + logger.debug(f"KeyManager initialized with parent_seed: {self.parent_seed} (type: {type(self.parent_seed)})") + + # Derive the encryption key from parent_seed + derived_key = self.derive_encryption_key() + derived_key_hex = derived_key.hex() + logger.debug(f"Derived encryption key (hex): {derived_key_hex}") + + # Initialize Keys with the derived hexadecimal key + self.keys = Keys(priv_k=derived_key_hex) # Pass hex string logger.debug("Nostr Keys initialized successfully.") - self.nsec = encode_bech32('nsec', privkey_hex) + # Generate bech32-encoded keys + self.nsec = encode_bech32('nsec', self.keys.private_key_hex()) logger.debug(f"Nostr Private Key (nsec): {self.nsec}") public_key_hex = self.keys.public_key_hex() @@ -76,11 +68,34 @@ class KeyManager: logger.error(traceback.format_exc()) raise + def derive_encryption_key(self) -> bytes: + """ + Derives the encryption key using the parent seed. + + Returns: + bytes: The derived encryption key. + + Raises: + Exception: If key derivation fails. + """ + try: + key = derive_key_from_parent_seed(self.parent_seed) + logger.debug("Encryption key derived successfully.") + return key # Now returns raw bytes + except Exception as e: + logger.error(f"Failed to derive encryption key: {e}") + logger.error(traceback.format_exc()) + raise + def get_npub(self) -> str: """ Returns the Nostr public key (npub). - :return: The npub as a string. + Returns: + str: The npub as a string. + + Raises: + ValueError: If npub is not available. """ if self.npub: logger.debug(f"Returning npub: {self.npub}") @@ -88,18 +103,3 @@ class KeyManager: else: logger.error("Nostr public key (npub) is not available.") raise ValueError("Nostr public key (npub) is not available.") - - def derive_encryption_key(self) -> bytes: - """ - Derives the encryption key using the parent seed. - - :return: The derived encryption key. - """ - try: - key = derive_key_from_parent_seed(self.parent_seed) - logger.debug("Encryption key derived successfully.") - return key - except Exception as e: - logger.error(f"Failed to derive encryption key: {e}") - logger.error(traceback.format_exc()) - raise diff --git a/src/nostr/logging_config.py b/src/nostr/logging_config.py index 4b77afe..0501c1d 100644 --- a/src/nostr/logging_config.py +++ b/src/nostr/logging_config.py @@ -1,39 +1,40 @@ # nostr/logging_config.py -import os -import sys import logging +import os -def configure_logging(log_file='nostr.log'): +def configure_logging(): """ Configures logging with both file and console handlers. + Logs include the timestamp, log level, message, filename, and line number. Only ERROR and higher-level messages are shown in the terminal, while all messages are logged in the log file. """ - # Create the 'logs' folder if it doesn't exist - if not os.path.exists('logs'): - os.makedirs('logs') - - # Create the custom logger - logger = logging.getLogger('nostr') - logger.setLevel(logging.DEBUG) # Set to DEBUG for detailed output + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) # Set root logger to DEBUG + # Prevent adding multiple handlers if configure_logging is called multiple times if not logger.handlers: - # Create handlers - c_handler = logging.StreamHandler(sys.stdout) - f_handler = logging.FileHandler(os.path.join('logs', log_file)) + # Create the 'logs' folder if it doesn't exist + log_directory = 'logs' + if not os.path.exists(log_directory): + os.makedirs(log_directory) - # Set levels + # Create handlers + c_handler = logging.StreamHandler() + f_handler = logging.FileHandler(os.path.join(log_directory, 'app.log')) + + # Set levels: only errors and critical messages will be shown in the console c_handler.setLevel(logging.ERROR) f_handler.setLevel(logging.DEBUG) - # Create formatters - formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]') + # Create formatters and add them to handlers, include file and line number in log messages + formatter = logging.Formatter( + '%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]' + ) c_handler.setFormatter(formatter) f_handler.setFormatter(formatter) # Add handlers to the logger logger.addHandler(c_handler) logger.addHandler(f_handler) - - return logger diff --git a/src/password_manager/encryption.py b/src/password_manager/encryption.py index eb0212a..f89e54e 100644 --- a/src/password_manager/encryption.py +++ b/src/password_manager/encryption.py @@ -89,25 +89,30 @@ class EncryptionManager: print(colored(f"Error: Failed to initialize encryption manager: {e}", 'red')) raise - def encrypt_parent_seed(self, parent_seed: str, file_path: Path) -> None: + def encrypt_parent_seed(self, parent_seed, file_path: Path) -> None: """ - Encrypts and securely saves the parent seed to the specified file. + Encrypts and saves the parent seed to the specified file. - :param parent_seed: The BIP39 parent seed phrase. + :param parent_seed: The BIP39 parent seed phrase or Bip39Mnemonic object. :param file_path: The path to the file where the encrypted parent seed will be saved. """ try: - # Encode the parent seed to bytes + # Convert Bip39Mnemonic to string if necessary + if hasattr(parent_seed, 'ToStr'): + parent_seed = parent_seed.ToStr() + + # Now encode the string data = parent_seed.encode('utf-8') - # 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}'.") + + # Encrypt and save the data + encrypted_data = self.encrypt_data(data) + with open(file_path, 'wb') as f: + f.write(encrypted_data) + logging.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: - logger.error(f"Failed to encrypt and save parent seed: {e}") - logger.error(traceback.format_exc()) + 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')) raise @@ -322,11 +327,11 @@ class EncryptionManager: try: decrypted_data = self.decrypt_file(file_path) parent_seed = decrypted_data.decode('utf-8').strip() - logger.debug("Parent seed decrypted successfully.") + logger.debug(f"Decrypted parent_seed: {parent_seed} (Type: {type(parent_seed)})") return parent_seed except Exception as e: logger.error(f"Failed to decrypt parent seed from '{file_path}': {e}") - logger.error(traceback.format_exc()) # Log full traceback + logger.error(traceback.format_exc()) print(colored(f"Error: Failed to decrypt parent seed from '{file_path}': {e}", 'red')) raise @@ -361,12 +366,19 @@ class EncryptionManager: :return: The derived seed as bytes. """ try: + if not isinstance(mnemonic, str): + if isinstance(mnemonic, list): + mnemonic = " ".join(mnemonic) + else: + mnemonic = str(mnemonic) + if not isinstance(mnemonic, str): + raise TypeError("Mnemonic must be a string after conversion") mnemo = Mnemonic("english") seed = mnemo.to_seed(mnemonic, passphrase) logger.debug("Seed derived successfully from mnemonic.") return seed except Exception as e: logger.error(f"Failed to derive seed from mnemonic: {e}") - logger.error(traceback.format_exc()) # Log full traceback - print(colored(f"Error: Failed to derive seed from mnemonic: {e}", 'red')) + logger.error(traceback.format_exc()) + print(f"Error: Failed to derive seed from mnemonic: {e}") raise diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index fe65de1..91e47ff 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -44,6 +44,12 @@ 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 +from bip85.bip85 import BIP85 +from bip_utils import Bip39SeedGenerator, Bip39MnemonicGenerator, Bip39Languages + +# Import NostrClient from the nostr package +from nostr import NostrClient # <-- Added import statement + # Configure logging at the start of the module def configure_logging(): """ @@ -83,11 +89,14 @@ def configure_logging(): # Call the logging configuration function configure_logging() +# Initialize the logger for this module +logger = logging.getLogger(__name__) + class PasswordManager: """ PasswordManager Class - Manages the generation, encryption, and retrieval of deterministic passwords using a BIP-39 seed. + Manages the generation, encryption, and retrieval of deterministic passwords using a BIP-85 seed. It handles file encryption/decryption, password generation, entry management, backups, and checksum verification, ensuring the integrity and confidentiality of the stored password database. """ @@ -101,134 +110,192 @@ class PasswordManager: self.entry_manager: Optional[EntryManager] = None self.password_generator: Optional[PasswordGenerator] = None self.backup_manager: Optional[BackupManager] = None - self.parent_seed: Optional[str] = None # Added parent_seed attribute + self.parent_seed: Optional[str] = None # Ensured to be a string + self.bip85: Optional[BIP85] = None # Added bip85 attribute self.setup_parent_seed() self.initialize_managers() def setup_parent_seed(self) -> None: + """ + Sets up the parent seed by determining if an existing seed is present or if a new one needs to be created. + """ if os.path.exists(PARENT_SEED_FILE): - # Parent seed file exists, prompt for password to decrypt - password = getpass.getpass(prompt='Enter your login password: ').strip() - try: - # Derive encryption key from password - key = derive_key_from_password(password) - self.encryption_manager = EncryptionManager(key) - self.parent_seed = self.encryption_manager.decrypt_parent_seed(PARENT_SEED_FILE) - - # 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')) - sys.exit(1) - - 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')) - sys.exit(1) + self.handle_existing_seed() else: - # First-time setup: prompt for parent seed and password - try: - parent_seed = getpass.getpass(prompt='Enter your 12-word parent seed: ').strip() - # Validate parent seed (basic validation) - parent_seed = self.basic_validate_seed_phrase(parent_seed) - if not parent_seed: - logging.error("Invalid seed phrase. Exiting.") - sys.exit(1) - except KeyboardInterrupt: - logging.info("Operation cancelled by user.") - print(colored("\nOperation cancelled by user.", 'yellow')) - sys.exit(0) - - # Prompt for password - password = prompt_for_password() - + self.handle_new_seed_setup() + + def handle_existing_seed(self) -> None: + """ + Handles the scenario where an existing parent seed file is found. + Prompts the user for the master password to decrypt the seed. + """ + password = getpass.getpass(prompt='Enter your login password: ').strip() + try: # Derive encryption key from password key = derive_key_from_password(password) self.encryption_manager = EncryptionManager(key) - - # Encrypt and save the parent seed - 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()) - print(colored(f"Error: Failed to encrypt and save parent seed: {e}", 'red')) + self.parent_seed = self.encryption_manager.decrypt_parent_seed(PARENT_SEED_FILE) + + # Log the type and content of 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')) sys.exit(1) - - self.parent_seed = parent_seed - def basic_validate_seed_phrase(self, seed_phrase: str) -> Optional[str]: + 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')) + sys.exit(1) + + def handle_new_seed_setup(self) -> None: """ - Performs basic validation on the seed phrase without relying on EncryptionManager. + 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() - Parameters: - seed_phrase (str): The seed phrase to validate. + if choice == '1': + self.setup_existing_seed() + elif choice == '2': + self.generate_new_seed() + else: + print(colored("Invalid choice. Exiting.", 'red')) + sys.exit(1) - Returns: - Optional[str]: The validated seed phrase or None if invalid. + def setup_existing_seed(self) -> None: + """ + Prompts the user to enter an existing BIP-85 seed and validates it. """ try: - words = seed_phrase.split() - if len(words) != 12: - logging.error("Seed phrase must contain exactly 12 words.") - print(colored("Error: Seed phrase must contain exactly 12 words.", 'red')) - return None - # Additional basic validations can be added here (e.g., word list checks) - logging.debug("Seed phrase validated successfully.") - return seed_phrase - except Exception as e: - logging.error(f"Error during basic seed validation: {e}") - logging.error(traceback.format_exc()) - print(colored(f"Error: {e}", 'red')) - return None + parent_seed = getpass.getpass(prompt='Enter your 12-word BIP-85 seed: ').strip() + if self.validate_bip85_seed(parent_seed): + self.save_and_encrypt_seed(parent_seed) + else: + logging.error("Invalid BIP-85 seed phrase. Exiting.") + sys.exit(1) + except KeyboardInterrupt: + logging.info("Operation cancelled by user.") + print(colored("\nOperation cancelled by user.", 'yellow')) + sys.exit(0) - def validate_seed_phrase(self, seed_phrase: str) -> bool: + def generate_new_seed(self) -> None: """ - Validates the seed phrase using the EncryptionManager if available, - otherwise performs basic validation. + Generates a new BIP-85 seed, displays it to the user, and prompts for confirmation before saving. + """ + 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')) + + if confirm_action("Do you want to use this generated seed? (Y/N): "): + self.save_and_encrypt_seed(new_seed) + else: + print(colored("Seed generation cancelled. Exiting.", 'yellow')) + sys.exit(0) + + def validate_bip85_seed(self, seed: str) -> bool: + """ + Validates the provided BIP-85 seed phrase. Parameters: - seed_phrase (str): The seed phrase to validate. + seed (str): The seed phrase to validate. Returns: bool: True if valid, False otherwise. """ try: - if self.encryption_manager: - # Use EncryptionManager to validate seed - is_valid = self.encryption_manager.validate_seed(seed_phrase) - if is_valid: - logging.debug("Seed phrase validated successfully using EncryptionManager.") - else: - logging.error("Invalid seed phrase.") - print(colored("Error: Invalid seed phrase.", 'red')) - return is_valid - else: - # Perform basic validation - return self.basic_validate_seed_phrase(seed_phrase) is not None + words = seed.split() + if len(words) != 12: + return False + # Additional validation can be added here if needed (e.g., word list checks) + return True 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')) + logging.error(f"Error validating BIP-85 seed: {e}") return False + def generate_bip85_seed(self) -> str: + """ + Generates a new BIP-85 seed phrase. + + Returns: + str: The generated 12-word mnemonic seed phrase. + """ + try: + master_seed = os.urandom(32) # Generate a random 32-byte seed + bip85 = BIP85(master_seed) + mnemonic_obj = bip85.derive_mnemonic(app_no=39, language_code=0, words_num=12, index=0) + 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')) + sys.exit(1) + + def save_and_encrypt_seed(self, seed: str) -> None: + """ + Saves and encrypts the parent seed. + + Parameters: + seed (str): The BIP-85 seed phrase to save and encrypt. + """ + password = prompt_for_password() + key = derive_key_from_password(password) + self.encryption_manager = EncryptionManager(key) + + try: + self.encryption_manager.encrypt_parent_seed(seed, PARENT_SEED_FILE) + logging.info("Parent seed encrypted and saved successfully.") + + self.store_hashed_password(password) + logging.info("User password hashed and stored 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)})") + + self.initialize_bip85() + 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')) + sys.exit(1) + + def initialize_bip85(self): + """ + Initializes the BIP-85 generator using the parent seed. + """ + try: + seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate() + self.bip85 = BIP85(seed_bytes) + logging.debug("BIP-85 initialized successfully.") + 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')) + sys.exit(1) + def initialize_managers(self) -> None: """ Initializes the EntryManager, PasswordGenerator, and BackupManager with the EncryptionManager - and parent seed. + and BIP-85 instance. """ try: self.entry_manager = EntryManager(self.encryption_manager) self.password_generator = PasswordGenerator(self.encryption_manager, self.parent_seed) self.backup_manager = BackupManager() - logging.debug("EntryManager, PasswordGenerator, and BackupManager initialized.") + + # Directly pass the parent_seed string to NostrClient + self.nostr_client = NostrClient(parent_seed=self.parent_seed) # <-- NostrClient is now imported + + logging.debug("EntryManager, PasswordGenerator, BackupManager, and NostrClient initialized.") except Exception as e: logging.error(f"Failed to initialize managers: {e}") logging.error(traceback.format_exc()) @@ -471,7 +538,7 @@ class PasswordManager: return # Reveal the parent seed - print(colored("\n=== Your Parent Seed ===", 'green')) + 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')) @@ -497,6 +564,12 @@ class PasswordManager: def verify_password(self, password: str) -> bool: """ Verifies the provided password against the stored hashed password. + + Parameters: + password (str): The password to verify. + + Returns: + bool: True if the password is correct, False otherwise. """ try: if not os.path.exists(HASHED_PASSWORD_FILE): @@ -520,6 +593,12 @@ class PasswordManager: def is_valid_filename(self, filename: str) -> bool: """ Validates the provided filename to prevent directory traversal and invalid characters. + + Parameters: + filename (str): The filename to validate. + + Returns: + bool: True if valid, False otherwise. """ # Basic validation: filename should not contain path separators or be empty invalid_chars = ['/', '\\', '..'] @@ -540,6 +619,14 @@ class PasswordManager: # 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 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: + f.write(hashed) + os.chmod(HASHED_PASSWORD_FILE, 0o600) + 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()) diff --git a/src/password_manager/password_generation.py b/src/password_manager/password_generation.py index 6baf9dc..8165822 100644 --- a/src/password_manager/password_generation.py +++ b/src/password_manager/password_generation.py @@ -20,6 +20,7 @@ import base64 import string import traceback from typing import Optional +from termcolor import colored from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives import hashes diff --git a/src/requirements.txt b/src/requirements.txt index 1910df9..13bbf9c 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -6,4 +6,5 @@ bech32==1.2.0 monstr @ git+https://github.com/monty888/monstr.git@master#egg=monstr mnemonic aiohttp -bcrypt \ No newline at end of file +bcrypt +bip85 \ No newline at end of file diff --git a/src/tests/test_import.py b/src/tests/test_import.py new file mode 100644 index 0000000..8c6a743 --- /dev/null +++ b/src/tests/test_import.py @@ -0,0 +1,9 @@ +# test_import.py + +try: + from bip_utils import Bip39SeedGenerator + print("Bip39SeedGenerator imported successfully.") +except ImportError as e: + print(f"ImportError: {e}") +except Exception as e: + print(f"Unexpected error: {e}") diff --git a/src/utils/key_derivation.py b/src/utils/key_derivation.py index 523691e..f342947 100644 --- a/src/utils/key_derivation.py +++ b/src/utils/key_derivation.py @@ -21,9 +21,11 @@ import unicodedata import logging import traceback from typing import Union +from bip_utils import Bip39SeedGenerator -import os -import logging +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend # Configure logging at the start of the module def configure_logging(): @@ -117,57 +119,32 @@ def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes: logger.error(traceback.format_exc()) # Log full traceback raise - -def derive_key_from_parent_seed(parent_seed: str, iterations: int = 100_000) -> bytes: +def derive_key_from_parent_seed(parent_seed: str) -> bytes: """ - Derives a Fernet-compatible encryption key from a BIP-39 parent seed using PBKDF2-HMAC-SHA256. + Derives a 32-byte cryptographic key from a BIP-39 parent seed using HKDF. - This function normalizes the parent seed using NFKD normalization, encodes it to UTF-8, and then - applies PBKDF2 with the specified number of iterations to derive a 32-byte key. The derived key - is then URL-safe base64-encoded to ensure compatibility with Fernet. - - Parameters: - parent_seed (str): The 12-word BIP-39 parent seed phrase. - iterations (int, optional): Number of iterations for the PBKDF2 algorithm. Defaults to 100,000. - - Returns: - bytes: A URL-safe base64-encoded encryption key suitable for Fernet. - - Raises: - ValueError: If the parent seed is empty or does not meet the word count requirements. + :param parent_seed: The 12-word BIP-39 seed phrase. + :return: A 32-byte derived key. """ - if not parent_seed: - logger.error("Parent seed cannot be empty.") - raise ValueError("Parent seed cannot be empty.") - - word_count = len(parent_seed.strip().split()) - if word_count != 12: - logger.error(f"Parent seed must be exactly 12 words, but {word_count} were provided.") - raise ValueError(f"Parent seed must be exactly 12 words, but {word_count} were provided.") - - # Normalize the parent seed to NFKD form and encode to UTF-8 - normalized_seed = unicodedata.normalize('NFKD', parent_seed).strip() - seed_bytes = normalized_seed.encode('utf-8') - try: - # Derive the key using PBKDF2-HMAC-SHA256 - logger.debug("Starting key derivation from parent seed.") - key = hashlib.pbkdf2_hmac( - hash_name='sha256', - password=seed_bytes, - salt=b'', # No salt for deterministic key derivation - iterations=iterations, - dklen=32 # 256-bit key for Fernet + # Generate seed bytes from mnemonic + seed = Bip39SeedGenerator(parent_seed).Generate() + + # Derive key using HKDF + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=None, # No salt for deterministic derivation + info=b'password-manager', + backend=default_backend() ) - logger.debug(f"Derived key from parent seed (hex): {key.hex()}") - - # Encode the key in URL-safe base64 - key_b64 = base64.urlsafe_b64encode(key) - logger.debug(f"Base64-encoded key from parent seed: {key_b64.decode()}") - - return key_b64 - + derived_key = hkdf.derive(seed) + + if len(derived_key) != 32: + raise ValueError(f"Derived key length is {len(derived_key)} bytes; expected 32 bytes.") + + return derived_key except Exception as e: - logger.error(f"Error deriving key from parent seed: {e}") - logger.error(traceback.format_exc()) # Log full traceback - raise + logger.error(f"Failed to derive key using HKDF: {e}") + logger.error(traceback.format_exc()) + raise \ No newline at end of file