Files
seedPass/src/main.py
thePR0M3TH3AN 7d4eef2110 update
2024-10-26 22:56:57 -04:00

341 lines
14 KiB
Python

# main.py
import os
import sys
import logging
import signal
from colorama import init as colorama_init
from termcolor import colored
import traceback
from password_manager.manager import PasswordManager
from nostr.client import NostrClient
colorama_init()
def configure_logging():
logger = logging.getLogger()
logger.setLevel(logging.DEBUG) # Keep this as DEBUG to capture all logs
# Remove all handlers associated with the root logger object
for handler in logger.handlers[:]:
logger.removeHandler(handler)
# Ensure the 'logs' directory exists
log_directory = 'logs'
if not os.path.exists(log_directory):
os.makedirs(log_directory)
# Create handlers
c_handler = logging.StreamHandler(sys.stdout)
f_handler = logging.FileHandler(os.path.join(log_directory, 'main.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
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)
# Set logging level for third-party libraries to WARNING to suppress their debug logs
logging.getLogger('monstr').setLevel(logging.WARNING)
logging.getLogger('nostr').setLevel(logging.WARNING)
def confirm_action(prompt: str) -> bool:
"""
Prompts the user for confirmation.
:param prompt: The confirmation message to display.
:return: True if user confirms, False otherwise.
"""
while True:
choice = input(colored(prompt, 'yellow')).strip().lower()
if choice in ['y', 'yes']:
return True
elif choice in ['n', 'no']:
return False
else:
print(colored("Please enter 'Y' or 'N'.", 'red'))
def handle_switch_fingerprint(password_manager: PasswordManager):
"""
Handles switching the active fingerprint.
:param password_manager: An instance of PasswordManager.
"""
try:
fingerprints = password_manager.fingerprint_manager.list_fingerprints()
if not fingerprints:
print(colored("No fingerprints available to switch. Please add a new fingerprint first.", 'yellow'))
return
print(colored("Available Fingerprints:", 'cyan'))
for idx, fp in enumerate(fingerprints, start=1):
print(colored(f"{idx}. {fp}", 'cyan'))
choice = input("Select a fingerprint by number to switch: ").strip()
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
print(colored("Invalid selection.", 'red'))
return
selected_fingerprint = fingerprints[int(choice)-1]
if password_manager.select_fingerprint(selected_fingerprint):
print(colored(f"Switched to fingerprint {selected_fingerprint}.", 'green'))
else:
print(colored("Failed to switch fingerprint.", 'red'))
except Exception as e:
logging.error(f"Error during fingerprint switch: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to switch fingerprint: {e}", 'red'))
def handle_add_new_fingerprint(password_manager: PasswordManager):
"""
Handles adding a new fingerprint.
:param password_manager: An instance of PasswordManager.
"""
try:
password_manager.add_new_fingerprint()
except Exception as e:
logging.error(f"Error adding new fingerprint: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to add new fingerprint: {e}", 'red'))
def handle_remove_fingerprint(password_manager: PasswordManager):
"""
Handles removing an existing fingerprint.
:param password_manager: An instance of PasswordManager.
"""
try:
fingerprints = password_manager.fingerprint_manager.list_fingerprints()
if not fingerprints:
print(colored("No fingerprints available to remove.", 'yellow'))
return
print(colored("Available Fingerprints:", 'cyan'))
for idx, fp in enumerate(fingerprints, start=1):
print(colored(f"{idx}. {fp}", 'cyan'))
choice = input("Select a fingerprint by number to remove: ").strip()
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
print(colored("Invalid selection.", 'red'))
return
selected_fingerprint = fingerprints[int(choice)-1]
confirm = confirm_action(f"Are you sure you want to remove fingerprint {selected_fingerprint}? This will delete all associated data. (Y/N): ")
if confirm:
if password_manager.fingerprint_manager.remove_fingerprint(selected_fingerprint):
print(colored(f"Fingerprint {selected_fingerprint} removed successfully.", 'green'))
else:
print(colored("Failed to remove fingerprint.", 'red'))
else:
print(colored("Fingerprint removal cancelled.", 'yellow'))
except Exception as e:
logging.error(f"Error removing fingerprint: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to remove fingerprint: {e}", 'red'))
def handle_list_fingerprints(password_manager: PasswordManager):
"""
Handles listing all available fingerprints.
:param password_manager: An instance of PasswordManager.
"""
try:
fingerprints = password_manager.fingerprint_manager.list_fingerprints()
if not fingerprints:
print(colored("No fingerprints available.", 'yellow'))
return
print(colored("Available Fingerprints:", 'cyan'))
for fp in fingerprints:
print(colored(f"- {fp}", 'cyan'))
except Exception as e:
logging.error(f"Error listing fingerprints: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to list fingerprints: {e}", 'red'))
def handle_display_npub(password_manager: PasswordManager):
"""
Handles displaying the Nostr public key (npub) to the user.
"""
try:
npub = password_manager.nostr_client.key_manager.get_npub()
if npub:
print(colored(f"\nYour Nostr Public Key (npub):\n{npub}\n", 'cyan'))
logging.info("Displayed npub to the user.")
else:
print(colored("Nostr public key not available.", 'red'))
logging.error("Nostr public key not available.")
except Exception as e:
logging.error(f"Failed to display npub: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to display npub: {e}", 'red'))
def handle_post_to_nostr(password_manager: PasswordManager):
"""
Handles the action of posting the encrypted password index to Nostr.
"""
try:
# Get the encrypted data from the index file
encrypted_data = password_manager.get_encrypted_data()
if encrypted_data:
# Post to Nostr
password_manager.nostr_client.publish_json_to_nostr(encrypted_data)
print(colored("Encrypted index posted to Nostr successfully.", 'green'))
logging.info("Encrypted index posted to Nostr successfully.")
else:
print(colored("No data available to post.", 'yellow'))
logging.warning("No data available to post to Nostr.")
except Exception as e:
logging.error(f"Failed to post to Nostr: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to post to Nostr: {e}", 'red'))
def handle_retrieve_from_nostr(password_manager: PasswordManager):
"""
Handles the action of retrieving the encrypted password index from Nostr.
"""
try:
# Use the Nostr client from the password_manager
encrypted_data = password_manager.nostr_client.retrieve_json_from_nostr_sync()
if encrypted_data:
# Decrypt and save the index
password_manager.encryption_manager.decrypt_and_save_index_from_nostr(encrypted_data)
print(colored("Encrypted index retrieved and saved successfully.", 'green'))
logging.info("Encrypted index retrieved and saved successfully from Nostr.")
else:
print(colored("Failed to retrieve data from Nostr.", 'red'))
logging.error("Failed to retrieve data from Nostr.")
except Exception as e:
logging.error(f"Failed to retrieve from Nostr: {e}")
logging.error(traceback.format_exc())
print(colored(f"Error: Failed to retrieve from Nostr: {e}", 'red'))
def display_menu(password_manager: PasswordManager):
"""
Displays the interactive menu and handles user input to perform various actions.
"""
menu = """
Select an option:
1. Generate a New Password and Add to Index
2. Retrieve a Password from Index
3. Modify an Existing Entry
4. Verify Script Checksum
5. Post Encrypted Index to Nostr
6. Retrieve Encrypted Index from Nostr
7. Display Nostr Public Key (npub)
8. Backup/Reveal Parent Seed
9. Switch Fingerprint
10. Add a New Fingerprint
11. Remove an Existing Fingerprint
12. List All Fingerprints
13. Exit
"""
while True:
# Flush logging handlers
for handler in logging.getLogger().handlers:
handler.flush()
print(colored(menu, 'cyan'))
choice = input('Enter your choice (1-13): ').strip()
if not choice:
print(colored("No input detected. Please enter a number between 1 and 13.", 'yellow'))
continue # Re-display the menu without marking as invalid
if choice == '1':
password_manager.handle_generate_password()
elif choice == '2':
password_manager.handle_retrieve_password()
elif choice == '3':
password_manager.handle_modify_entry()
elif choice == '4':
password_manager.handle_verify_checksum()
elif choice == '5':
handle_post_to_nostr(password_manager)
elif choice == '6':
handle_retrieve_from_nostr(password_manager)
elif choice == '7':
handle_display_npub(password_manager)
elif choice == '8':
password_manager.handle_backup_reveal_parent_seed()
elif choice == '9':
if not password_manager.handle_switch_fingerprint():
print(colored("Failed to switch fingerprint.", 'red'))
elif choice == '10':
handle_add_new_fingerprint(password_manager)
elif choice == '11':
handle_remove_fingerprint(password_manager)
elif choice == '12':
handle_list_fingerprints(password_manager)
elif choice == '13':
logging.info("Exiting the program.")
print(colored("Exiting the program.", 'green'))
password_manager.nostr_client.close_client_pool()
sys.exit(0)
else:
print(colored("Invalid choice. Please select a valid option.", 'red'))
if __name__ == '__main__':
# Configure logging with both file and console handlers
configure_logging()
logger = logging.getLogger(__name__)
logger.info("Starting SeedPass Password Manager")
# Initialize PasswordManager and proceed with application logic
try:
password_manager = PasswordManager()
logger.info("PasswordManager initialized successfully.")
except Exception as e:
logger.error(f"Failed to initialize PasswordManager: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: Failed to initialize PasswordManager: {e}", 'red'))
sys.exit(1)
# Register signal handlers for graceful shutdown
def signal_handler(sig, frame):
"""
Handles termination signals to gracefully shutdown the NostrClient.
"""
print(colored("\nReceived shutdown signal. Exiting gracefully...", 'yellow'))
logging.info(f"Received shutdown signal: {sig}. Initiating graceful shutdown.")
try:
password_manager.nostr_client.close_client_pool() # Gracefully close the ClientPool
logging.info("NostrClient closed successfully.")
except Exception as e:
logging.error(f"Error during shutdown: {e}")
print(colored(f"Error during shutdown: {e}", 'red'))
sys.exit(0)
# Register the signal handlers
signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C
signal.signal(signal.SIGTERM, signal_handler) # Handle termination signals
# Display the interactive menu to the user
try:
display_menu(password_manager)
except KeyboardInterrupt:
logger.info("Program terminated by user via KeyboardInterrupt.")
print(colored("\nProgram terminated by user.", 'yellow'))
try:
password_manager.nostr_client.close_client_pool() # Gracefully close the ClientPool
logging.info("NostrClient closed successfully.")
except Exception as e:
logging.error(f"Error during shutdown: {e}")
print(colored(f"Error during shutdown: {e}", 'red'))
sys.exit(0)
except Exception as e:
logger.error(f"An unexpected error occurred: {e}")
logger.error(traceback.format_exc()) # Log full traceback
print(colored(f"Error: An unexpected error occurred: {e}", 'red'))
try:
password_manager.nostr_client.close_client_pool() # Attempt to close the ClientPool
logging.info("NostrClient closed successfully.")
except Exception as close_error:
logging.error(f"Error during shutdown: {close_error}")
print(colored(f"Error during shutdown: {close_error}", 'red'))
sys.exit(1)