Files
seedPass/src/password_manager/manager.py
Keep Creating Online 412296d545 update
2024-10-19 20:46:23 -04:00

468 lines
20 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
from colorama import Fore
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
from constants import (
APP_DIR,
INDEX_FILE,
PARENT_SEED_FILE,
DATA_CHECKSUM_FILE,
SCRIPT_CHECKSUM_FILE,
MIN_PASSWORD_LENGTH,
MAX_PASSWORD_LENGTH,
DEFAULT_PASSWORD_LENGTH
)
import traceback # Added for exception traceback logging
# Configure logging at the start of the module
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.
"""
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Set to DEBUG for detailed output
# Prevent adding multiple handlers if configure_logging is called multiple times
if not logger.handlers:
# Create the 'logs' folder if it doesn't exist
if not os.path.exists('logs'):
os.makedirs('logs')
# Create handlers
c_handler = logging.StreamHandler()
f_handler = logging.FileHandler(os.path.join('logs', 'password_manager.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 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)
# Call the logging configuration function
configure_logging()
class PasswordManager:
"""
PasswordManager Class
Manages the generation, encryption, and retrieval of deterministic passwords using a BIP-39 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, and BackupManager.
"""
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.parent_seed: Optional[str] = None # Added parent_seed attribute
self.setup_parent_seed()
self.initialize_managers()
def setup_parent_seed(self) -> None:
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)
# **Add validation for 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)
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()
# 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.")
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]:
"""
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.
"""
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
def validate_seed_phrase(self, seed_phrase: str) -> Optional[str]:
"""
Validates the seed phrase using the EncryptionManager if available,
otherwise performs basic validation.
Parameters:
seed_phrase (str): The seed phrase to validate.
Returns:
Optional[str]: The validated seed phrase or None if invalid.
"""
try:
if self.encryption_manager:
# Use EncryptionManager to validate seed
if self.encryption_manager.validate_seed(seed_phrase):
logging.debug("Seed phrase validated successfully using EncryptionManager.")
return seed_phrase
else:
logging.error("Invalid seed phrase.")
print(colored("Error: Invalid seed phrase.", 'red'))
return None
else:
# Perform basic validation
return self.basic_validate_seed_phrase(seed_phrase)
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 None
def initialize_managers(self) -> None:
"""
Initializes the EntryManager, PasswordGenerator, and BackupManager with the EncryptionManager
and parent seed.
"""
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.")
except Exception as e:
logging.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 handle_generate_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'))
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_password(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.encryption_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:
self.encryption_manager.decrypt_and_save_index_from_nostr(encrypted_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'))
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'))
# Additional methods can be added here as needed
# 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 parent seed from PasswordManager
nostr_client = NostrClient(parent_seed=manager.parent_seed)
# Example operations
# These would typically be triggered by user interactions, e.g., via a CLI menu
# manager.handle_generate_password()
# manager.handle_retrieve_password()
# manager.handle_modify_entry()
# manager.handle_verify_checksum()
# manager.post_to_nostr(nostr_client)
# manager.retrieve_from_nostr(nostr_client)
# manager.backup_database()
# manager.restore_database()