mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
1232 lines
49 KiB
Python
1232 lines
49 KiB
Python
# password_manager/manager.py
|
|
|
|
"""
|
|
Password Manager Module
|
|
|
|
This module implements the PasswordManager class, which orchestrates various functionalities
|
|
of the deterministic password manager, including encryption, entry management, password
|
|
generation, backup, and checksum verification. It serves as the core interface for interacting
|
|
with the password manager functionalities.
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
import logging
|
|
import getpass
|
|
import os
|
|
from typing import Optional
|
|
import shutil
|
|
from termcolor import colored
|
|
|
|
from password_manager.encryption import EncryptionManager
|
|
from password_manager.entry_management import EntryManager
|
|
from password_manager.password_generation import PasswordGenerator
|
|
from password_manager.backup import BackupManager
|
|
from utils.key_derivation import derive_key_from_parent_seed, derive_key_from_password
|
|
from utils.checksum import calculate_checksum, verify_checksum
|
|
from utils.password_prompt import (
|
|
prompt_for_password,
|
|
prompt_existing_password,
|
|
confirm_action,
|
|
)
|
|
|
|
from constants import (
|
|
APP_DIR,
|
|
PARENT_SEED_FILE,
|
|
SCRIPT_CHECKSUM_FILE,
|
|
MIN_PASSWORD_LENGTH,
|
|
MAX_PASSWORD_LENGTH,
|
|
DEFAULT_PASSWORD_LENGTH,
|
|
DEFAULT_SEED_BACKUP_FILENAME,
|
|
)
|
|
|
|
import traceback
|
|
import bcrypt
|
|
from pathlib import Path
|
|
|
|
from local_bip85.bip85 import BIP85
|
|
from bip_utils import Bip39SeedGenerator, Bip39MnemonicGenerator, Bip39Languages
|
|
|
|
from utils.fingerprint_manager import FingerprintManager
|
|
|
|
# Import NostrClient
|
|
from nostr.client import NostrClient, DEFAULT_RELAYS
|
|
from password_manager.config_manager import ConfigManager
|
|
|
|
# Instantiate the logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PasswordManager:
|
|
"""
|
|
PasswordManager Class
|
|
|
|
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.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""
|
|
Initializes the PasswordManager by setting up encryption, loading or setting up the parent seed,
|
|
and initializing other components like EntryManager, PasswordGenerator, BackupManager, and FingerprintManager.
|
|
"""
|
|
self.encryption_manager: Optional[EncryptionManager] = None
|
|
self.entry_manager: Optional[EntryManager] = None
|
|
self.password_generator: Optional[PasswordGenerator] = None
|
|
self.backup_manager: Optional[BackupManager] = None
|
|
self.fingerprint_manager: Optional[FingerprintManager] = None
|
|
self.parent_seed: Optional[str] = None
|
|
self.bip85: Optional[BIP85] = None
|
|
self.nostr_client: Optional[NostrClient] = None
|
|
self.config_manager: Optional[ConfigManager] = None
|
|
|
|
# Initialize the fingerprint manager first
|
|
self.initialize_fingerprint_manager()
|
|
|
|
# Ensure a parent seed is set up before accessing the fingerprint directory
|
|
self.setup_parent_seed()
|
|
|
|
# Set the current fingerprint directory
|
|
self.fingerprint_dir = self.fingerprint_manager.get_current_fingerprint_dir()
|
|
|
|
def initialize_fingerprint_manager(self):
|
|
"""
|
|
Initializes the FingerprintManager.
|
|
"""
|
|
try:
|
|
self.fingerprint_manager = FingerprintManager(APP_DIR)
|
|
logger.debug("FingerprintManager initialized successfully.")
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize FingerprintManager: {e}")
|
|
logger.error(traceback.format_exc())
|
|
print(
|
|
colored(f"Error: Failed to initialize FingerprintManager: {e}", "red")
|
|
)
|
|
sys.exit(1)
|
|
|
|
def setup_parent_seed(self) -> None:
|
|
"""
|
|
Sets up the parent seed by determining if existing fingerprints are present or if a new one needs to be created.
|
|
"""
|
|
fingerprints = self.fingerprint_manager.list_fingerprints()
|
|
if fingerprints:
|
|
# There are existing fingerprints
|
|
self.select_or_add_fingerprint()
|
|
else:
|
|
# No existing fingerprints, proceed to set up new seed
|
|
self.handle_new_seed_setup()
|
|
|
|
def select_or_add_fingerprint(self):
|
|
"""
|
|
Prompts the user to select an existing fingerprint or add a new one.
|
|
"""
|
|
try:
|
|
print(colored("\nAvailable Seed Profiles:", "cyan"))
|
|
fingerprints = self.fingerprint_manager.list_fingerprints()
|
|
for idx, fp in enumerate(fingerprints, start=1):
|
|
print(colored(f"{idx}. {fp}", "cyan"))
|
|
|
|
print(colored(f"{len(fingerprints)+1}. Add a new seed profile", "cyan"))
|
|
|
|
choice = input("Select a seed profile by number: ").strip()
|
|
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints) + 1):
|
|
print(colored("Invalid selection. Exiting.", "red"))
|
|
sys.exit(1)
|
|
|
|
choice = int(choice)
|
|
if choice == len(fingerprints) + 1:
|
|
# Add a new seed profile
|
|
self.add_new_fingerprint()
|
|
else:
|
|
# Select existing seed profile
|
|
selected_fingerprint = fingerprints[choice - 1]
|
|
self.select_fingerprint(selected_fingerprint)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during seed profile selection: {e}")
|
|
logger.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to select seed profile: {e}", "red"))
|
|
sys.exit(1)
|
|
|
|
def add_new_fingerprint(self):
|
|
"""
|
|
Adds a new seed profile by generating it from a seed phrase.
|
|
"""
|
|
try:
|
|
choice = input(
|
|
"Do you want to (1) Enter an existing seed or (2) Generate a new seed? (1/2): "
|
|
).strip()
|
|
if choice == "1":
|
|
fingerprint = self.setup_existing_seed()
|
|
elif choice == "2":
|
|
fingerprint = self.generate_new_seed()
|
|
else:
|
|
print(colored("Invalid choice. Exiting.", "red"))
|
|
sys.exit(1)
|
|
|
|
# Set current_fingerprint in FingerprintManager only
|
|
self.fingerprint_manager.current_fingerprint = fingerprint
|
|
print(
|
|
colored(
|
|
f"New seed profile '{fingerprint}' added and set as current.",
|
|
"green",
|
|
)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding new seed profile: {e}")
|
|
logger.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to add new seed profile: {e}", "red"))
|
|
sys.exit(1)
|
|
|
|
def select_fingerprint(self, fingerprint: str) -> None:
|
|
if self.fingerprint_manager.select_fingerprint(fingerprint):
|
|
self.current_fingerprint = fingerprint # Add this line
|
|
self.fingerprint_dir = (
|
|
self.fingerprint_manager.get_current_fingerprint_dir()
|
|
)
|
|
if not self.fingerprint_dir:
|
|
print(
|
|
colored(
|
|
f"Error: Seed profile directory for {fingerprint} not found.",
|
|
"red",
|
|
)
|
|
)
|
|
sys.exit(1)
|
|
# Setup the encryption manager and load parent seed
|
|
self.setup_encryption_manager(self.fingerprint_dir)
|
|
self.load_parent_seed(self.fingerprint_dir)
|
|
# Initialize BIP85 and other managers
|
|
self.initialize_bip85()
|
|
self.initialize_managers()
|
|
self.sync_index_from_nostr_if_missing()
|
|
print(
|
|
colored(
|
|
f"Seed profile {fingerprint} selected and managers initialized.",
|
|
"green",
|
|
)
|
|
)
|
|
else:
|
|
print(colored(f"Error: Seed profile {fingerprint} not found.", "red"))
|
|
sys.exit(1)
|
|
|
|
def setup_encryption_manager(
|
|
self, fingerprint_dir: Path, password: Optional[str] = None
|
|
):
|
|
"""
|
|
Sets up the EncryptionManager for the selected fingerprint.
|
|
|
|
Parameters:
|
|
fingerprint_dir (Path): The directory corresponding to the fingerprint.
|
|
password (Optional[str]): The user's master password.
|
|
"""
|
|
try:
|
|
# Prompt for password if not provided
|
|
if password is None:
|
|
password = prompt_existing_password("Enter your master password: ")
|
|
# Derive key from password
|
|
key = derive_key_from_password(password)
|
|
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
|
|
logger.debug(
|
|
"EncryptionManager set up successfully for selected fingerprint."
|
|
)
|
|
|
|
# Verify the password
|
|
self.fingerprint_dir = fingerprint_dir # Ensure self.fingerprint_dir is set
|
|
if not self.verify_password(password):
|
|
print(colored("Invalid password. Exiting.", "red"))
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
logger.error(f"Failed to set up EncryptionManager: {e}")
|
|
logger.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to set up encryption: {e}", "red"))
|
|
sys.exit(1)
|
|
|
|
def load_parent_seed(self, fingerprint_dir: Path):
|
|
"""
|
|
Loads and decrypts the parent seed from the fingerprint directory.
|
|
|
|
Parameters:
|
|
fingerprint_dir (Path): The directory corresponding to the fingerprint.
|
|
"""
|
|
try:
|
|
self.parent_seed = self.encryption_manager.decrypt_parent_seed()
|
|
logger.debug(
|
|
f"Parent seed loaded for fingerprint {self.current_fingerprint}."
|
|
)
|
|
# Initialize BIP85 with the parent seed
|
|
seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate()
|
|
self.bip85 = BIP85(seed_bytes)
|
|
logger.debug("BIP-85 initialized successfully.")
|
|
except Exception as e:
|
|
logger.error(f"Failed to load parent seed: {e}")
|
|
logger.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to load parent seed: {e}", "red"))
|
|
sys.exit(1)
|
|
|
|
def handle_switch_fingerprint(self) -> bool:
|
|
"""
|
|
Handles switching to a different seed profile.
|
|
|
|
Returns:
|
|
bool: True if switch was successful, False otherwise.
|
|
"""
|
|
try:
|
|
print(colored("\nAvailable Seed Profiles:", "cyan"))
|
|
fingerprints = self.fingerprint_manager.list_fingerprints()
|
|
for idx, fp in enumerate(fingerprints, start=1):
|
|
print(colored(f"{idx}. {fp}", "cyan"))
|
|
|
|
choice = input("Select a seed profile by number to switch: ").strip()
|
|
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
|
|
print(colored("Invalid selection. Returning to main menu.", "red"))
|
|
return False # Return False to indicate failure
|
|
|
|
selected_fingerprint = fingerprints[int(choice) - 1]
|
|
self.fingerprint_manager.current_fingerprint = selected_fingerprint
|
|
self.current_fingerprint = selected_fingerprint
|
|
|
|
# Update fingerprint directory
|
|
self.fingerprint_dir = (
|
|
self.fingerprint_manager.get_current_fingerprint_dir()
|
|
)
|
|
if not self.fingerprint_dir:
|
|
print(
|
|
colored(
|
|
f"Error: Seed profile directory for {selected_fingerprint} not found.",
|
|
"red",
|
|
)
|
|
)
|
|
return False # Return False to indicate failure
|
|
|
|
# Prompt for master password for the selected seed profile
|
|
password = prompt_existing_password("Enter your master password: ")
|
|
|
|
# Set up the encryption manager with the new password and seed profile directory
|
|
self.setup_encryption_manager(self.fingerprint_dir, password)
|
|
|
|
# Load the parent seed for the selected seed profile
|
|
self.load_parent_seed(self.fingerprint_dir)
|
|
|
|
# Initialize BIP85 and other managers
|
|
self.initialize_bip85()
|
|
self.initialize_managers()
|
|
self.sync_index_from_nostr_if_missing()
|
|
print(colored(f"Switched to seed profile {selected_fingerprint}.", "green"))
|
|
|
|
# Re-initialize NostrClient with the new fingerprint
|
|
try:
|
|
self.nostr_client = NostrClient(
|
|
encryption_manager=self.encryption_manager,
|
|
fingerprint=self.current_fingerprint,
|
|
)
|
|
logging.info(
|
|
f"NostrClient re-initialized with seed profile {self.current_fingerprint}."
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"Failed to re-initialize NostrClient: {e}")
|
|
print(
|
|
colored(f"Error: Failed to re-initialize NostrClient: {e}", "red")
|
|
)
|
|
return False
|
|
|
|
return True # Return True to indicate success
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error during seed profile switching: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to switch seed profiles: {e}", "red"))
|
|
return False # Return False to indicate failure
|
|
|
|
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.
|
|
"""
|
|
try:
|
|
# Prompt for password
|
|
password = getpass.getpass(prompt="Enter your login password: ").strip()
|
|
|
|
# Derive encryption key from password
|
|
key = derive_key_from_password(password)
|
|
|
|
# Initialize FingerprintManager if not already initialized
|
|
if not self.fingerprint_manager:
|
|
self.initialize_fingerprint_manager()
|
|
|
|
# Prompt the user to select an existing seed profile
|
|
fingerprints = self.fingerprint_manager.list_fingerprints()
|
|
if not fingerprints:
|
|
print(
|
|
colored(
|
|
"No seed profiles available. Please add a seed profile first.",
|
|
"red",
|
|
)
|
|
)
|
|
sys.exit(1)
|
|
|
|
print(colored("Available Seed Profiles:", "cyan"))
|
|
for idx, fp in enumerate(fingerprints, start=1):
|
|
print(colored(f"{idx}. {fp}", "cyan"))
|
|
|
|
choice = input("Select a seed profile by number: ").strip()
|
|
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
|
|
print(colored("Invalid selection. Exiting.", "red"))
|
|
sys.exit(1)
|
|
|
|
selected_fingerprint = fingerprints[int(choice) - 1]
|
|
self.current_fingerprint = selected_fingerprint
|
|
fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory(
|
|
selected_fingerprint
|
|
)
|
|
if not fingerprint_dir:
|
|
print(colored("Error: Seed profile directory not found.", "red"))
|
|
sys.exit(1)
|
|
|
|
# Initialize EncryptionManager with key and fingerprint_dir
|
|
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
|
|
self.parent_seed = self.encryption_manager.decrypt_parent_seed()
|
|
|
|
# Log the type and content of parent_seed
|
|
logger.debug(
|
|
f"Decrypted parent_seed: {self.parent_seed} (type: {type(self.parent_seed)})"
|
|
)
|
|
|
|
# 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.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:
|
|
print(colored("Invalid choice. Exiting.", "red"))
|
|
sys.exit(1)
|
|
|
|
def setup_existing_seed(self) -> Optional[str]:
|
|
"""
|
|
Prompts the user to enter an existing BIP-85 seed and validates it.
|
|
|
|
Returns:
|
|
Optional[str]: The fingerprint if setup is successful, None otherwise.
|
|
"""
|
|
try:
|
|
parent_seed = getpass.getpass(
|
|
prompt="Enter your 12-word BIP-85 seed: "
|
|
).strip()
|
|
if self.validate_bip85_seed(parent_seed):
|
|
# Add a fingerprint using the existing seed
|
|
fingerprint = self.fingerprint_manager.add_fingerprint(parent_seed)
|
|
if not fingerprint:
|
|
print(
|
|
colored(
|
|
"Error: Failed to generate seed profile for the provided seed.",
|
|
"red",
|
|
)
|
|
)
|
|
sys.exit(1)
|
|
|
|
fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory(
|
|
fingerprint
|
|
)
|
|
if not fingerprint_dir:
|
|
print(
|
|
colored(
|
|
"Error: Failed to retrieve seed profile directory.", "red"
|
|
)
|
|
)
|
|
sys.exit(1)
|
|
|
|
# Set the current fingerprint in both PasswordManager and FingerprintManager
|
|
self.current_fingerprint = fingerprint
|
|
self.fingerprint_manager.current_fingerprint = fingerprint
|
|
self.fingerprint_dir = fingerprint_dir
|
|
logging.info(f"Current seed profile set to {fingerprint}")
|
|
|
|
# Initialize EncryptionManager with key and fingerprint_dir
|
|
password = prompt_for_password()
|
|
key = derive_key_from_password(password)
|
|
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
|
|
|
|
# Encrypt and save the parent seed
|
|
self.encryption_manager.encrypt_parent_seed(parent_seed)
|
|
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 = parent_seed # Ensure this is a string
|
|
logger.debug(
|
|
f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})"
|
|
)
|
|
|
|
self.initialize_bip85()
|
|
self.initialize_managers()
|
|
self.sync_index_from_nostr_if_missing()
|
|
return fingerprint # Return the generated or added fingerprint
|
|
else:
|
|
logging.error("Invalid BIP-85 seed phrase. Exiting.")
|
|
print(colored("Error: Invalid BIP-85 seed phrase.", "red"))
|
|
sys.exit(1)
|
|
except KeyboardInterrupt:
|
|
logging.info("Operation cancelled by user.")
|
|
print(colored("\nOperation cancelled by user.", "yellow"))
|
|
sys.exit(0)
|
|
|
|
def generate_new_seed(self) -> Optional[str]:
|
|
"""
|
|
Generates a new BIP-85 seed, displays it to the user, and prompts for confirmation before saving.
|
|
|
|
Returns:
|
|
Optional[str]: The fingerprint if generation is successful, None otherwise.
|
|
"""
|
|
new_seed = self.generate_bip85_seed()
|
|
print(colored("Your new BIP-85 seed phrase is:", "green"))
|
|
print(colored(new_seed, "yellow"))
|
|
print(colored("Please write this down and keep it in a safe place!", "red"))
|
|
|
|
if confirm_action("Do you want to use this generated seed? (Y/N): "):
|
|
# Add a new fingerprint using the generated seed
|
|
fingerprint = self.fingerprint_manager.add_fingerprint(new_seed)
|
|
if not fingerprint:
|
|
print(
|
|
colored(
|
|
"Error: Failed to generate seed profile for the new seed.",
|
|
"red",
|
|
)
|
|
)
|
|
sys.exit(1)
|
|
|
|
fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory(
|
|
fingerprint
|
|
)
|
|
if not fingerprint_dir:
|
|
print(
|
|
colored("Error: Failed to retrieve seed profile directory.", "red")
|
|
)
|
|
sys.exit(1)
|
|
|
|
# Set the current fingerprint in both PasswordManager and FingerprintManager
|
|
self.current_fingerprint = fingerprint
|
|
self.fingerprint_manager.current_fingerprint = fingerprint
|
|
logging.info(f"Current seed profile set to {fingerprint}")
|
|
|
|
# Now, save and encrypt the seed with the fingerprint_dir
|
|
self.save_and_encrypt_seed(new_seed, fingerprint_dir)
|
|
|
|
return fingerprint # Return the generated fingerprint
|
|
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(index=0, words_num=12)
|
|
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, fingerprint_dir: Path) -> None:
|
|
"""
|
|
Saves and encrypts the parent seed.
|
|
|
|
Parameters:
|
|
seed (str): The BIP-85 seed phrase to save and encrypt.
|
|
fingerprint_dir (Path): The directory corresponding to the fingerprint.
|
|
"""
|
|
try:
|
|
# Set self.fingerprint_dir
|
|
self.fingerprint_dir = fingerprint_dir
|
|
|
|
# Prompt for password
|
|
password = prompt_for_password()
|
|
# Derive key from password
|
|
key = derive_key_from_password(password)
|
|
# Re-initialize EncryptionManager with the new key and fingerprint_dir
|
|
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
|
|
|
|
# Store the hashed password
|
|
self.store_hashed_password(password)
|
|
logging.info("User password hashed and stored successfully.")
|
|
|
|
# Encrypt and save the parent seed
|
|
self.encryption_manager.encrypt_parent_seed(seed)
|
|
logging.info("Parent seed encrypted and saved successfully.")
|
|
|
|
self.parent_seed = seed # Ensure this is a string
|
|
logger.debug(
|
|
f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})"
|
|
)
|
|
|
|
self.initialize_bip85()
|
|
self.initialize_managers()
|
|
self.sync_index_from_nostr_if_missing()
|
|
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, BackupManager, and NostrClient with the EncryptionManager
|
|
and BIP-85 instance within the context of the selected fingerprint.
|
|
"""
|
|
try:
|
|
# Ensure self.encryption_manager is already initialized
|
|
if not self.encryption_manager:
|
|
raise ValueError("EncryptionManager is not initialized.")
|
|
|
|
# Reinitialize the managers with the updated EncryptionManager and current fingerprint context
|
|
self.entry_manager = EntryManager(
|
|
encryption_manager=self.encryption_manager,
|
|
fingerprint_dir=self.fingerprint_dir,
|
|
)
|
|
|
|
self.password_generator = PasswordGenerator(
|
|
encryption_manager=self.encryption_manager,
|
|
parent_seed=self.parent_seed,
|
|
bip85=self.bip85,
|
|
)
|
|
|
|
self.backup_manager = BackupManager(fingerprint_dir=self.fingerprint_dir)
|
|
|
|
# Load relay configuration and initialize NostrClient
|
|
self.config_manager = ConfigManager(
|
|
encryption_manager=self.encryption_manager,
|
|
fingerprint_dir=self.fingerprint_dir,
|
|
)
|
|
config = self.config_manager.load_config()
|
|
relay_list = config.get("relays", list(DEFAULT_RELAYS))
|
|
|
|
self.nostr_client = NostrClient(
|
|
encryption_manager=self.encryption_manager,
|
|
fingerprint=self.current_fingerprint,
|
|
relays=relay_list,
|
|
)
|
|
|
|
logger.debug("Managers re-initialized for the new fingerprint.")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize managers: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to initialize managers: {e}", "red"))
|
|
sys.exit(1)
|
|
|
|
def sync_index_from_nostr_if_missing(self) -> None:
|
|
"""Retrieve the password database from Nostr if it doesn't exist locally."""
|
|
index_file = self.fingerprint_dir / "seedpass_passwords_db.json.enc"
|
|
if index_file.exists():
|
|
return
|
|
try:
|
|
encrypted = self.nostr_client.retrieve_json_from_nostr_sync()
|
|
if encrypted:
|
|
self.encryption_manager.decrypt_and_save_index_from_nostr(encrypted)
|
|
logger.info("Initialized local database from Nostr.")
|
|
except Exception as e:
|
|
logger.warning(f"Unable to sync index from Nostr: {e}")
|
|
|
|
def handle_add_password(self) -> None:
|
|
try:
|
|
website_name = input("Enter the website name: ").strip()
|
|
if not website_name:
|
|
print(colored("Error: Website name cannot be empty.", "red"))
|
|
return
|
|
|
|
username = input("Enter the username (optional): ").strip()
|
|
url = input("Enter the URL (optional): ").strip()
|
|
|
|
length_input = input(
|
|
f"Enter desired password length (default {DEFAULT_PASSWORD_LENGTH}): "
|
|
).strip()
|
|
length = DEFAULT_PASSWORD_LENGTH
|
|
if length_input:
|
|
if not length_input.isdigit():
|
|
print(colored("Error: Password length must be a number.", "red"))
|
|
return
|
|
length = int(length_input)
|
|
if not (MIN_PASSWORD_LENGTH <= length <= MAX_PASSWORD_LENGTH):
|
|
print(
|
|
colored(
|
|
f"Error: Password length must be between {MIN_PASSWORD_LENGTH} and {MAX_PASSWORD_LENGTH}.",
|
|
"red",
|
|
)
|
|
)
|
|
return
|
|
|
|
# Add the entry to the index and get the assigned index
|
|
index = self.entry_manager.add_entry(
|
|
website_name, length, username, url, blacklisted=False
|
|
)
|
|
|
|
# Generate the password using the assigned index
|
|
password = self.password_generator.generate_password(length, index)
|
|
|
|
# Provide user feedback
|
|
print(
|
|
colored(
|
|
f"\n[+] Password generated and indexed with ID {index}.\n",
|
|
"green",
|
|
)
|
|
)
|
|
print(colored(f"Password for {website_name}: {password}\n", "yellow"))
|
|
|
|
# Automatically push the updated encrypted index to Nostr so the
|
|
# latest changes are backed up remotely.
|
|
try:
|
|
encrypted_data = self.get_encrypted_data()
|
|
if encrypted_data:
|
|
self.nostr_client.publish_json_to_nostr(encrypted_data)
|
|
logging.info(
|
|
"Encrypted index posted to Nostr after entry addition."
|
|
)
|
|
except Exception as nostr_error:
|
|
logging.error(f"Failed to post updated index to Nostr: {nostr_error}")
|
|
logging.error(traceback.format_exc())
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error during password generation: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to generate password: {e}", "red"))
|
|
|
|
def handle_retrieve_entry(self) -> None:
|
|
"""
|
|
Handles retrieving a password from the index by prompting the user for the index number
|
|
and displaying the corresponding password and associated details.
|
|
"""
|
|
try:
|
|
index_input = input(
|
|
"Enter the index number of the password to retrieve: "
|
|
).strip()
|
|
if not index_input.isdigit():
|
|
print(colored("Error: Index must be a number.", "red"))
|
|
return
|
|
index = int(index_input)
|
|
|
|
# Retrieve entry details
|
|
entry = self.entry_manager.retrieve_entry(index)
|
|
if not entry:
|
|
return
|
|
|
|
# Display entry details
|
|
website_name = entry.get("website")
|
|
length = entry.get("length")
|
|
username = entry.get("username")
|
|
url = entry.get("url")
|
|
blacklisted = entry.get("blacklisted")
|
|
|
|
print(
|
|
colored(
|
|
f"Retrieving password for '{website_name}' with length {length}.",
|
|
"cyan",
|
|
)
|
|
)
|
|
if username:
|
|
print(colored(f"Username: {username}", "cyan"))
|
|
if url:
|
|
print(colored(f"URL: {url}", "cyan"))
|
|
if blacklisted:
|
|
print(
|
|
colored(
|
|
f"Warning: This password is blacklisted and should not be used.",
|
|
"red",
|
|
)
|
|
)
|
|
|
|
# Generate the password
|
|
password = self.password_generator.generate_password(length, index)
|
|
|
|
# Display the password and associated details
|
|
if password:
|
|
print(
|
|
colored(f"\n[+] Retrieved Password for {website_name}:\n", "green")
|
|
)
|
|
print(colored(f"Password: {password}", "yellow"))
|
|
print(colored(f"Associated Username: {username or 'N/A'}", "cyan"))
|
|
print(colored(f"Associated URL: {url or 'N/A'}", "cyan"))
|
|
print(
|
|
colored(
|
|
f"Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}",
|
|
"cyan",
|
|
)
|
|
)
|
|
else:
|
|
print(colored("Error: Failed to retrieve the password.", "red"))
|
|
except Exception as e:
|
|
logging.error(f"Error during password retrieval: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to retrieve password: {e}", "red"))
|
|
|
|
def handle_modify_entry(self) -> None:
|
|
"""
|
|
Handles modifying an existing password entry by prompting the user for the index number
|
|
and new details to update.
|
|
"""
|
|
try:
|
|
index_input = input(
|
|
"Enter the index number of the entry to modify: "
|
|
).strip()
|
|
if not index_input.isdigit():
|
|
print(colored("Error: Index must be a number.", "red"))
|
|
return
|
|
index = int(index_input)
|
|
|
|
# Retrieve existing entry
|
|
entry = self.entry_manager.retrieve_entry(index)
|
|
if not entry:
|
|
return
|
|
|
|
website_name = entry.get("website")
|
|
length = entry.get("length")
|
|
username = entry.get("username")
|
|
url = entry.get("url")
|
|
blacklisted = entry.get("blacklisted")
|
|
|
|
# Display current values
|
|
print(
|
|
colored(
|
|
f"Modifying entry for '{website_name}' (Index: {index}):", "cyan"
|
|
)
|
|
)
|
|
print(colored(f"Current Username: {username or 'N/A'}", "cyan"))
|
|
print(colored(f"Current URL: {url or 'N/A'}", "cyan"))
|
|
print(
|
|
colored(
|
|
f"Current Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}",
|
|
"cyan",
|
|
)
|
|
)
|
|
|
|
# Prompt for new values (optional)
|
|
new_username = (
|
|
input(
|
|
f'Enter new username (leave blank to keep "{username or "N/A"}"): '
|
|
).strip()
|
|
or username
|
|
)
|
|
new_url = (
|
|
input(f'Enter new URL (leave blank to keep "{url or "N/A"}"): ').strip()
|
|
or url
|
|
)
|
|
blacklist_input = (
|
|
input(
|
|
f'Is this password blacklisted? (Y/N, current: {"Y" if blacklisted else "N"}): '
|
|
)
|
|
.strip()
|
|
.lower()
|
|
)
|
|
if blacklist_input == "":
|
|
new_blacklisted = blacklisted
|
|
elif blacklist_input == "y":
|
|
new_blacklisted = True
|
|
elif blacklist_input == "n":
|
|
new_blacklisted = False
|
|
else:
|
|
print(
|
|
colored(
|
|
"Invalid input for blacklist status. Keeping the current status.",
|
|
"yellow",
|
|
)
|
|
)
|
|
new_blacklisted = blacklisted
|
|
|
|
# Update the entry
|
|
self.entry_manager.modify_entry(
|
|
index, new_username, new_url, new_blacklisted
|
|
)
|
|
|
|
print(colored(f"Entry updated successfully for index {index}.", "green"))
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error during modifying entry: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to modify entry: {e}", "red"))
|
|
|
|
def handle_verify_checksum(self) -> None:
|
|
"""
|
|
Handles verifying the script's checksum against the stored checksum to ensure integrity.
|
|
"""
|
|
try:
|
|
current_checksum = calculate_checksum(__file__)
|
|
if verify_checksum(current_checksum, SCRIPT_CHECKSUM_FILE):
|
|
print(colored("Checksum verification passed.", "green"))
|
|
logging.info("Checksum verification passed.")
|
|
else:
|
|
print(
|
|
colored(
|
|
"Checksum verification failed. The script may have been modified.",
|
|
"red",
|
|
)
|
|
)
|
|
logging.error("Checksum verification failed.")
|
|
except Exception as e:
|
|
logging.error(f"Error during checksum verification: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to verify checksum: {e}", "red"))
|
|
|
|
def get_encrypted_data(self) -> Optional[bytes]:
|
|
"""
|
|
Retrieves the encrypted password index data.
|
|
|
|
:return: The encrypted data as bytes, or None if retrieval fails.
|
|
"""
|
|
try:
|
|
encrypted_data = self.entry_manager.get_encrypted_index()
|
|
if encrypted_data:
|
|
logging.debug("Encrypted index data retrieved successfully.")
|
|
return encrypted_data
|
|
else:
|
|
logging.error("Failed to retrieve encrypted index data.")
|
|
print(colored("Error: Failed to retrieve encrypted index data.", "red"))
|
|
return None
|
|
except Exception as e:
|
|
logging.error(f"Error retrieving encrypted data: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to retrieve encrypted data: {e}", "red"))
|
|
return None
|
|
|
|
def decrypt_and_save_index_from_nostr(self, encrypted_data: bytes) -> None:
|
|
"""
|
|
Decrypts the encrypted data retrieved from Nostr and updates the local index.
|
|
|
|
:param encrypted_data: The encrypted data retrieved from Nostr.
|
|
"""
|
|
try:
|
|
# Decrypt the data using EncryptionManager's decrypt_data method
|
|
decrypted_data = self.encryption_manager.decrypt_data(encrypted_data)
|
|
|
|
# Save the decrypted data to the index file
|
|
index_file_path = self.fingerprint_dir / "seedpass_passwords_db.json.enc"
|
|
with open(index_file_path, "wb") as f:
|
|
f.write(decrypted_data)
|
|
|
|
logging.info("Index file updated from Nostr successfully.")
|
|
print(colored("Index file updated from Nostr successfully.", "green"))
|
|
except Exception as e:
|
|
logging.error(f"Failed to decrypt and save data from Nostr: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(
|
|
colored(
|
|
f"Error: Failed to decrypt and save data from Nostr: {e}", "red"
|
|
)
|
|
)
|
|
# Re-raise the exception to inform the calling function of the failure
|
|
raise
|
|
|
|
def backup_database(self) -> None:
|
|
"""
|
|
Creates a backup of the encrypted JSON index file.
|
|
"""
|
|
try:
|
|
self.backup_manager.create_backup()
|
|
print(colored("Backup created successfully.", "green"))
|
|
except Exception as e:
|
|
logging.error(f"Failed to create backup: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to create backup: {e}", "red"))
|
|
|
|
def restore_database(self) -> None:
|
|
"""
|
|
Restores the encrypted JSON index file from the latest backup.
|
|
"""
|
|
try:
|
|
self.backup_manager.restore_latest_backup()
|
|
print(
|
|
colored(
|
|
"Database restored from the latest backup successfully.", "green"
|
|
)
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"Failed to restore backup: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to restore backup: {e}", "red"))
|
|
|
|
def handle_backup_reveal_parent_seed(self) -> None:
|
|
"""
|
|
Handles the backup and reveal of the parent seed.
|
|
"""
|
|
try:
|
|
print(colored("\n=== Backup Parent Seed ===", "yellow"))
|
|
print(
|
|
colored(
|
|
"Warning: Revealing your parent seed is a highly sensitive operation.",
|
|
"red",
|
|
)
|
|
)
|
|
print(
|
|
colored(
|
|
"Ensure you're in a secure, private environment and no one is watching your screen.",
|
|
"red",
|
|
)
|
|
)
|
|
|
|
# Verify user's identity with secure password verification
|
|
password = prompt_existing_password(
|
|
"Enter your master password to continue: "
|
|
)
|
|
if not self.verify_password(password):
|
|
print(colored("Incorrect password. Operation aborted.", "red"))
|
|
return
|
|
|
|
# Double confirmation
|
|
if not confirm_action(
|
|
"Are you absolutely sure you want to reveal your parent seed? (Y/N): "
|
|
):
|
|
print(colored("Operation cancelled by user.", "yellow"))
|
|
return
|
|
|
|
# Reveal the parent seed
|
|
print(colored("\n=== Your BIP-85 Parent Seed ===", "green"))
|
|
print(colored(self.parent_seed, "yellow"))
|
|
print(
|
|
colored(
|
|
"\nPlease write this down and store it securely. Do not share it with anyone.",
|
|
"red",
|
|
)
|
|
)
|
|
|
|
# Option to save to file with default filename
|
|
if confirm_action(
|
|
"Do you want to save this to an encrypted backup file? (Y/N): "
|
|
):
|
|
filename = input(
|
|
f"Enter filename to save (default: {DEFAULT_SEED_BACKUP_FILENAME}): "
|
|
).strip()
|
|
filename = filename if filename else DEFAULT_SEED_BACKUP_FILENAME
|
|
backup_path = (
|
|
self.fingerprint_dir / filename
|
|
) # Save in fingerprint directory
|
|
|
|
# Validate filename
|
|
if not self.is_valid_filename(filename):
|
|
print(colored("Invalid filename. Operation aborted.", "red"))
|
|
return
|
|
|
|
# Encrypt and save the parent seed to the backup path
|
|
self.encryption_manager.encrypt_and_save_file(
|
|
self.parent_seed.encode("utf-8"), backup_path
|
|
)
|
|
print(
|
|
colored(
|
|
f"Encrypted seed backup saved to '{backup_path}'. Ensure this file is stored securely.",
|
|
"green",
|
|
)
|
|
)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error during parent seed backup/reveal: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to backup/reveal parent seed: {e}", "red"))
|
|
|
|
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:
|
|
hashed_password_file = self.fingerprint_dir / "hashed_password.enc"
|
|
if not hashed_password_file.exists():
|
|
logging.error("Hashed password file not found.")
|
|
print(colored("Error: Hashed password file not found.", "red"))
|
|
return False
|
|
with open(hashed_password_file, "rb") as f:
|
|
stored_hash = f.read()
|
|
is_correct = bcrypt.checkpw(password.encode("utf-8"), stored_hash)
|
|
if is_correct:
|
|
logging.debug("Password verification successful.")
|
|
else:
|
|
logging.warning("Password verification failed.")
|
|
return is_correct
|
|
except Exception as e:
|
|
logging.error(f"Error verifying password: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to verify password: {e}", "red"))
|
|
return False
|
|
|
|
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 = ["/", "\\", ".."]
|
|
if any(char in filename for char in invalid_chars) or not filename:
|
|
logging.warning(f"Invalid filename attempted: {filename}")
|
|
return False
|
|
return True
|
|
|
|
def store_hashed_password(self, password: str) -> None:
|
|
"""
|
|
Hashes and stores the user's password securely using bcrypt.
|
|
This should be called during the initial setup.
|
|
"""
|
|
try:
|
|
hashed_password_file = self.fingerprint_dir / "hashed_password.enc"
|
|
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
|
with open(hashed_password_file, "wb") as f:
|
|
f.write(hashed)
|
|
os.chmod(hashed_password_file, 0o600)
|
|
logging.info("User password hashed and stored successfully.")
|
|
except AttributeError:
|
|
# If bcrypt.hashpw is not available, try using bcrypt directly
|
|
salt = bcrypt.gensalt()
|
|
hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
|
|
with open(hashed_password_file, "wb") as f:
|
|
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())
|
|
print(colored(f"Error: Failed to store hashed password: {e}", "red"))
|
|
raise
|
|
|
|
def change_password(self) -> None:
|
|
"""Change the master password used for encryption."""
|
|
try:
|
|
current = prompt_existing_password("Enter your current master password: ")
|
|
if not self.verify_password(current):
|
|
print(colored("Incorrect password.", "red"))
|
|
return
|
|
|
|
new_password = prompt_for_password()
|
|
|
|
# Load data with existing encryption manager
|
|
index_data = self.entry_manager.encryption_manager.load_json_data()
|
|
config_data = self.config_manager.load_config(require_pin=False)
|
|
|
|
# Create a new encryption manager with the new password
|
|
new_key = derive_key_from_password(new_password)
|
|
new_enc_mgr = EncryptionManager(new_key, self.fingerprint_dir)
|
|
|
|
# Re-encrypt sensitive files using the new manager
|
|
new_enc_mgr.encrypt_parent_seed(self.parent_seed)
|
|
new_enc_mgr.save_json_data(index_data)
|
|
self.config_manager.encryption_manager = new_enc_mgr
|
|
self.config_manager.save_config(config_data)
|
|
|
|
# Update hashed password and replace managers
|
|
self.encryption_manager = new_enc_mgr
|
|
self.entry_manager.encryption_manager = new_enc_mgr
|
|
self.password_generator.encryption_manager = new_enc_mgr
|
|
self.store_hashed_password(new_password)
|
|
|
|
relay_list = config_data.get("relays", list(DEFAULT_RELAYS))
|
|
self.nostr_client = NostrClient(
|
|
encryption_manager=self.encryption_manager,
|
|
fingerprint=self.current_fingerprint,
|
|
relays=relay_list,
|
|
)
|
|
|
|
print(colored("Master password changed successfully.", "green"))
|
|
except Exception as e:
|
|
logging.error(f"Failed to change password: {e}")
|
|
logging.error(traceback.format_exc())
|
|
print(colored(f"Error: Failed to change password: {e}", "red"))
|
|
|
|
|
|
# Example usage (this part should be removed or commented out when integrating into the larger application)
|
|
if __name__ == "__main__":
|
|
from nostr.client import (
|
|
NostrClient,
|
|
) # Ensure this import is correct based on your project structure
|
|
|
|
# Initialize PasswordManager
|
|
manager = PasswordManager()
|
|
|
|
# Initialize NostrClient with the EncryptionManager from PasswordManager
|
|
manager.nostr_client = NostrClient(encryption_manager=manager.encryption_manager)
|
|
|
|
# Example operations
|
|
# These would typically be triggered by user interactions, e.g., via a CLI menu
|
|
# manager.handle_add_password()
|
|
# manager.handle_retrieve_entry()
|
|
# manager.handle_modify_entry()
|
|
# manager.handle_verify_checksum()
|
|
# manager.nostr_client.publish_and_subscribe("Sample password data")
|
|
# manager.backup_database()
|
|
# manager.restore_database()
|