This commit is contained in:
thePR0M3TH3AN
2024-10-23 23:00:22 -04:00
parent d8aff057b7
commit c60ae6b442
11 changed files with 416 additions and 255 deletions

View File

@@ -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']

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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())

View File

@@ -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

View File

@@ -6,4 +6,5 @@ bech32==1.2.0
monstr @ git+https://github.com/monty888/monstr.git@master#egg=monstr
mnemonic
aiohttp
bcrypt
bcrypt
bip85

9
src/tests/test_import.py Normal file
View File

@@ -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}")

View File

@@ -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