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 not isinstance(parent_seed, str):
raise TypeError(f"Parent seed must be a string, got {type(parent_seed)}")
if len(entropy) != 32:
logger.error(f"Derived entropy length is {len(entropy)} bytes; expected 32 bytes.")
raise ValueError("Invalid entropy length.")
self.parent_seed = parent_seed
logger.debug(f"KeyManager initialized with parent_seed: {self.parent_seed} (type: {type(self.parent_seed)})")
privkey_hex = entropy.hex()
self.keys = Keys(priv_k=privkey_hex) # Now Keys is defined via the import
# 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,14 +110,26 @@ 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
self.handle_existing_seed()
else:
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
@@ -116,119 +137,165 @@ class PasswordManager:
self.encryption_manager = EncryptionManager(key)
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_seed_phrase(self.parent_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.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:
"""
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()
if choice == '1':
self.setup_existing_seed()
elif choice == '2':
self.generate_new_seed()
else:
# First-time setup: prompt for parent seed and password
print(colored("Invalid choice. Exiting.", 'red'))
sys.exit(1)
def setup_existing_seed(self) -> None:
"""
Prompts the user to enter an existing BIP-85 seed and validates it.
"""
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.")
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)
# Prompt for password
password = prompt_for_password()
def generate_new_seed(self) -> None:
"""
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'))
# Derive encryption key from password
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 (str): The seed phrase to validate.
Returns:
bool: True if valid, False otherwise.
"""
try:
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 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)
# Encrypt and save the parent seed
try:
self.encryption_manager.encrypt_parent_seed(parent_seed, PARENT_SEED_FILE)
self.encryption_manager.encrypt_parent_seed(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.")
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)
self.parent_seed = parent_seed
def basic_validate_seed_phrase(self, seed_phrase: str) -> Optional[str]:
def initialize_bip85(self):
"""
Performs basic validation on the seed phrase without relying on EncryptionManager.
Parameters:
seed_phrase (str): The seed phrase to validate.
Returns:
Optional[str]: The validated seed phrase or None if invalid.
Initializes the BIP-85 generator using the parent seed.
"""
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
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"Error during basic seed validation: {e}")
logging.error(f"Failed to initialize BIP-85: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: {e}", 'red'))
return None
def validate_seed_phrase(self, seed_phrase: str) -> bool:
"""
Validates the seed phrase using the EncryptionManager if available,
otherwise performs basic validation.
Parameters:
seed_phrase (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
except Exception as e:
logging.error(f"Error validating seed phrase: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to validate seed phrase: {e}", 'red'))
return False
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

@@ -7,3 +7,4 @@ monstr @ git+https://github.com/monty888/monstr.git@master#egg=monstr
mnemonic
aiohttp
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()}")
derived_key = hkdf.derive(seed)
# 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
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
logger.error(f"Failed to derive key using HKDF: {e}")
logger.error(traceback.format_exc())
raise