mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
update
This commit is contained in:
@@ -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']
|
||||
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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())
|
||||
|
@@ -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
|
||||
|
@@ -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
9
src/tests/test_import.py
Normal 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}")
|
@@ -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
|
Reference in New Issue
Block a user