mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 23:38:49 +00:00
Add PIN protection to settings
This commit is contained in:
221
src/main.py
221
src/main.py
@@ -3,6 +3,7 @@ import os
|
||||
import sys
|
||||
import logging
|
||||
import signal
|
||||
import getpass
|
||||
from colorama import init as colorama_init
|
||||
from termcolor import colored
|
||||
import traceback
|
||||
@@ -12,6 +13,7 @@ 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
|
||||
@@ -21,20 +23,22 @@ def configure_logging():
|
||||
logger.removeHandler(handler)
|
||||
|
||||
# Ensure the 'logs' directory exists
|
||||
log_directory = 'logs'
|
||||
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'))
|
||||
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]')
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]"
|
||||
)
|
||||
c_handler.setFormatter(formatter)
|
||||
f_handler.setFormatter(formatter)
|
||||
|
||||
@@ -43,8 +47,9 @@ def configure_logging():
|
||||
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)
|
||||
logging.getLogger("monstr").setLevel(logging.WARNING)
|
||||
logging.getLogger("nostr").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def confirm_action(prompt: str) -> bool:
|
||||
"""
|
||||
@@ -54,13 +59,14 @@ def confirm_action(prompt: str) -> bool:
|
||||
:return: True if user confirms, False otherwise.
|
||||
"""
|
||||
while True:
|
||||
choice = input(colored(prompt, 'yellow')).strip().lower()
|
||||
if choice in ['y', 'yes']:
|
||||
choice = input(colored(prompt, "yellow")).strip().lower()
|
||||
if choice in ["y", "yes"]:
|
||||
return True
|
||||
elif choice in ['n', 'no']:
|
||||
elif choice in ["n", "no"]:
|
||||
return False
|
||||
else:
|
||||
print(colored("Please enter 'Y' or 'N'.", 'red'))
|
||||
print(colored("Please enter 'Y' or 'N'.", "red"))
|
||||
|
||||
|
||||
def handle_switch_fingerprint(password_manager: PasswordManager):
|
||||
"""
|
||||
@@ -71,27 +77,33 @@ def handle_switch_fingerprint(password_manager: 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'))
|
||||
print(
|
||||
colored(
|
||||
"No fingerprints available to switch. Please add a new fingerprint first.",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
print(colored("Available Fingerprints:", 'cyan'))
|
||||
print(colored("Available Fingerprints:", "cyan"))
|
||||
for idx, fp in enumerate(fingerprints, start=1):
|
||||
print(colored(f"{idx}. {fp}", 'cyan'))
|
||||
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'))
|
||||
print(colored("Invalid selection.", "red"))
|
||||
return
|
||||
|
||||
selected_fingerprint = fingerprints[int(choice)-1]
|
||||
selected_fingerprint = fingerprints[int(choice) - 1]
|
||||
if password_manager.select_fingerprint(selected_fingerprint):
|
||||
print(colored(f"Switched to fingerprint {selected_fingerprint}.", 'green'))
|
||||
print(colored(f"Switched to fingerprint {selected_fingerprint}.", "green"))
|
||||
else:
|
||||
print(colored("Failed to switch fingerprint.", 'red'))
|
||||
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'))
|
||||
print(colored(f"Error: Failed to switch fingerprint: {e}", "red"))
|
||||
|
||||
|
||||
def handle_add_new_fingerprint(password_manager: PasswordManager):
|
||||
"""
|
||||
@@ -104,7 +116,8 @@ def handle_add_new_fingerprint(password_manager: PasswordManager):
|
||||
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'))
|
||||
print(colored(f"Error: Failed to add new fingerprint: {e}", "red"))
|
||||
|
||||
|
||||
def handle_remove_fingerprint(password_manager: PasswordManager):
|
||||
"""
|
||||
@@ -115,31 +128,41 @@ def handle_remove_fingerprint(password_manager: PasswordManager):
|
||||
try:
|
||||
fingerprints = password_manager.fingerprint_manager.list_fingerprints()
|
||||
if not fingerprints:
|
||||
print(colored("No fingerprints available to remove.", 'yellow'))
|
||||
print(colored("No fingerprints available to remove.", "yellow"))
|
||||
return
|
||||
|
||||
print(colored("Available Fingerprints:", 'cyan'))
|
||||
print(colored("Available Fingerprints:", "cyan"))
|
||||
for idx, fp in enumerate(fingerprints, start=1):
|
||||
print(colored(f"{idx}. {fp}", 'cyan'))
|
||||
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'))
|
||||
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): ")
|
||||
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'))
|
||||
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'))
|
||||
print(colored("Failed to remove fingerprint.", "red"))
|
||||
else:
|
||||
print(colored("Fingerprint removal cancelled.", 'yellow'))
|
||||
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'))
|
||||
print(colored(f"Error: Failed to remove fingerprint: {e}", "red"))
|
||||
|
||||
|
||||
def handle_list_fingerprints(password_manager: PasswordManager):
|
||||
"""
|
||||
@@ -150,16 +173,17 @@ def handle_list_fingerprints(password_manager: PasswordManager):
|
||||
try:
|
||||
fingerprints = password_manager.fingerprint_manager.list_fingerprints()
|
||||
if not fingerprints:
|
||||
print(colored("No fingerprints available.", 'yellow'))
|
||||
print(colored("No fingerprints available.", "yellow"))
|
||||
return
|
||||
|
||||
print(colored("Available Fingerprints:", 'cyan'))
|
||||
print(colored("Available Fingerprints:", "cyan"))
|
||||
for fp in fingerprints:
|
||||
print(colored(f"- {fp}", 'cyan'))
|
||||
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'))
|
||||
print(colored(f"Error: Failed to list fingerprints: {e}", "red"))
|
||||
|
||||
|
||||
def handle_display_npub(password_manager: PasswordManager):
|
||||
"""
|
||||
@@ -168,15 +192,16 @@ def handle_display_npub(password_manager: PasswordManager):
|
||||
try:
|
||||
npub = password_manager.nostr_client.key_manager.get_npub()
|
||||
if npub:
|
||||
print(colored(f"\nYour Nostr Public Key (npub):\n{npub}\n", 'cyan'))
|
||||
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'))
|
||||
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'))
|
||||
print(colored(f"Error: Failed to display npub: {e}", "red"))
|
||||
|
||||
|
||||
def handle_post_to_nostr(password_manager: PasswordManager):
|
||||
"""
|
||||
@@ -188,15 +213,16 @@ def handle_post_to_nostr(password_manager: PasswordManager):
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
print(colored(f"Error: Failed to post to Nostr: {e}", "red"))
|
||||
|
||||
|
||||
def handle_retrieve_from_nostr(password_manager: PasswordManager):
|
||||
"""
|
||||
@@ -207,16 +233,50 @@ def handle_retrieve_from_nostr(password_manager: PasswordManager):
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
print(colored(f"Error: Failed to retrieve from Nostr: {e}", "red"))
|
||||
|
||||
|
||||
def handle_settings(password_manager: PasswordManager):
|
||||
"""Display settings menu for relay list and PIN changes."""
|
||||
cfg_mgr = password_manager.config_manager
|
||||
if cfg_mgr is None:
|
||||
print(colored("Configuration manager unavailable.", "red"))
|
||||
return
|
||||
try:
|
||||
cfg = cfg_mgr.load_config()
|
||||
except Exception as e:
|
||||
print(colored(f"Error loading settings: {e}", "red"))
|
||||
return
|
||||
|
||||
print("\nSettings:")
|
||||
print("1. Set Nostr relays")
|
||||
print("2. Change settings PIN")
|
||||
print("3. Back")
|
||||
choice = input("Select an option: ").strip()
|
||||
if choice == "1":
|
||||
relays = input("Enter comma-separated relay URLs: ").split(",")
|
||||
relays = [r.strip() for r in relays if r.strip()]
|
||||
cfg_mgr.set_relays(relays)
|
||||
print(colored("Relays updated.", "green"))
|
||||
elif choice == "2":
|
||||
old_pin = getpass.getpass("Current PIN: ")
|
||||
new_pin = getpass.getpass("New PIN: ")
|
||||
if cfg_mgr.change_pin(old_pin, new_pin):
|
||||
print(colored("PIN changed successfully.", "green"))
|
||||
else:
|
||||
print(colored("Incorrect current PIN.", "red"))
|
||||
|
||||
|
||||
def display_menu(password_manager: PasswordManager):
|
||||
"""
|
||||
@@ -236,56 +296,65 @@ def display_menu(password_manager: PasswordManager):
|
||||
10. Add a New Fingerprint
|
||||
11. Remove an Existing Fingerprint
|
||||
12. List All Fingerprints
|
||||
13. Exit
|
||||
13. Settings
|
||||
14. 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()
|
||||
print(colored(menu, "cyan"))
|
||||
choice = input("Enter your choice (1-14): ").strip()
|
||||
if not choice:
|
||||
print(colored("No input detected. Please enter a number between 1 and 13.", 'yellow'))
|
||||
print(
|
||||
colored(
|
||||
"No input detected. Please enter a number between 1 and 14.",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
continue # Re-display the menu without marking as invalid
|
||||
if choice == '1':
|
||||
if choice == "1":
|
||||
password_manager.handle_generate_password()
|
||||
elif choice == '2':
|
||||
elif choice == "2":
|
||||
password_manager.handle_retrieve_password()
|
||||
elif choice == '3':
|
||||
elif choice == "3":
|
||||
password_manager.handle_modify_entry()
|
||||
elif choice == '4':
|
||||
elif choice == "4":
|
||||
password_manager.handle_verify_checksum()
|
||||
elif choice == '5':
|
||||
elif choice == "5":
|
||||
handle_post_to_nostr(password_manager)
|
||||
elif choice == '6':
|
||||
elif choice == "6":
|
||||
handle_retrieve_from_nostr(password_manager)
|
||||
elif choice == '7':
|
||||
elif choice == "7":
|
||||
handle_display_npub(password_manager)
|
||||
elif choice == '8':
|
||||
elif choice == "8":
|
||||
password_manager.handle_backup_reveal_parent_seed()
|
||||
elif choice == '9':
|
||||
elif choice == "9":
|
||||
if not password_manager.handle_switch_fingerprint():
|
||||
print(colored("Failed to switch fingerprint.", 'red'))
|
||||
elif choice == '10':
|
||||
print(colored("Failed to switch fingerprint.", "red"))
|
||||
elif choice == "10":
|
||||
handle_add_new_fingerprint(password_manager)
|
||||
elif choice == '11':
|
||||
elif choice == "11":
|
||||
handle_remove_fingerprint(password_manager)
|
||||
elif choice == '12':
|
||||
elif choice == "12":
|
||||
handle_list_fingerprints(password_manager)
|
||||
elif choice == '13':
|
||||
elif choice == "13":
|
||||
handle_settings(password_manager)
|
||||
elif choice == "14":
|
||||
logging.info("Exiting the program.")
|
||||
print(colored("Exiting the program.", 'green'))
|
||||
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'))
|
||||
print(colored("Invalid choice. Please select a valid option.", "red"))
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
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()
|
||||
@@ -293,49 +362,49 @@ if __name__ == '__main__':
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
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.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'))
|
||||
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'))
|
||||
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'))
|
||||
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)
|
||||
print(colored(f"Error during shutdown: {close_error}", "red"))
|
||||
sys.exit(1)
|
||||
|
@@ -4,7 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
import getpass
|
||||
|
||||
import bcrypt
|
||||
|
||||
@@ -24,8 +26,15 @@ class ConfigManager:
|
||||
self.fingerprint_dir = fingerprint_dir
|
||||
self.config_path = self.fingerprint_dir / self.CONFIG_FILENAME
|
||||
|
||||
def load_config(self) -> dict:
|
||||
"""Load the configuration file, returning defaults if none exists."""
|
||||
def load_config(self, require_pin: bool = True) -> dict:
|
||||
"""Load the configuration file and optionally verify a stored PIN.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
require_pin: bool, default True
|
||||
If True and a PIN is configured, prompt the user to enter it and
|
||||
verify against the stored hash.
|
||||
"""
|
||||
if not self.config_path.exists():
|
||||
logger.info("Config file not found; returning defaults")
|
||||
return {"relays": list(DEFAULT_NOSTR_RELAYS), "pin_hash": ""}
|
||||
@@ -36,6 +45,14 @@ class ConfigManager:
|
||||
# Ensure defaults for missing keys
|
||||
data.setdefault("relays", list(DEFAULT_NOSTR_RELAYS))
|
||||
data.setdefault("pin_hash", "")
|
||||
if require_pin and data.get("pin_hash"):
|
||||
for _ in range(3):
|
||||
pin = getpass.getpass("Enter settings PIN: ").strip()
|
||||
if bcrypt.checkpw(pin.encode(), data["pin_hash"].encode()):
|
||||
break
|
||||
print("Invalid PIN")
|
||||
else:
|
||||
raise ValueError("PIN verification failed")
|
||||
return data
|
||||
except Exception as exc:
|
||||
logger.error(f"Failed to load config: {exc}")
|
||||
@@ -49,23 +66,30 @@ class ConfigManager:
|
||||
logger.error(f"Failed to save config: {exc}")
|
||||
raise
|
||||
|
||||
def set_relays(self, relays: List[str]) -> None:
|
||||
def set_relays(self, relays: List[str], require_pin: bool = True) -> None:
|
||||
"""Update relay list and save."""
|
||||
config = self.load_config()
|
||||
config = self.load_config(require_pin=require_pin)
|
||||
config["relays"] = relays
|
||||
self.save_config(config)
|
||||
|
||||
def set_pin(self, pin: str) -> None:
|
||||
"""Hash and store the provided PIN."""
|
||||
pin_hash = bcrypt.hashpw(pin.encode(), bcrypt.gensalt()).decode()
|
||||
config = self.load_config()
|
||||
config = self.load_config(require_pin=False)
|
||||
config["pin_hash"] = pin_hash
|
||||
self.save_config(config)
|
||||
|
||||
def verify_pin(self, pin: str) -> bool:
|
||||
"""Check a provided PIN against the stored hash."""
|
||||
config = self.load_config()
|
||||
"""Check a provided PIN against the stored hash without prompting."""
|
||||
config = self.load_config(require_pin=False)
|
||||
stored = config.get("pin_hash", "").encode()
|
||||
if not stored:
|
||||
return False
|
||||
return bcrypt.checkpw(pin.encode(), stored)
|
||||
|
||||
def change_pin(self, old_pin: str, new_pin: str) -> bool:
|
||||
"""Update the stored PIN if the old PIN is correct."""
|
||||
if self.verify_pin(old_pin):
|
||||
self.set_pin(new_pin)
|
||||
return True
|
||||
return False
|
||||
|
@@ -17,13 +17,26 @@ def test_config_defaults_and_round_trip():
|
||||
enc_mgr = EncryptionManager(key, Path(tmpdir))
|
||||
cfg_mgr = ConfigManager(enc_mgr, Path(tmpdir))
|
||||
|
||||
cfg = cfg_mgr.load_config()
|
||||
cfg = cfg_mgr.load_config(require_pin=False)
|
||||
assert cfg["relays"] == list(DEFAULT_RELAYS)
|
||||
assert cfg["pin_hash"] == ""
|
||||
|
||||
cfg_mgr.set_pin("1234")
|
||||
cfg_mgr.set_relays(["wss://example.com"])
|
||||
cfg_mgr.set_relays(["wss://example.com"], require_pin=False)
|
||||
|
||||
cfg2 = cfg_mgr.load_config()
|
||||
cfg2 = cfg_mgr.load_config(require_pin=False)
|
||||
assert cfg2["relays"] == ["wss://example.com"]
|
||||
assert bcrypt.checkpw(b"1234", cfg2["pin_hash"].encode())
|
||||
|
||||
|
||||
def test_pin_verification_and_change():
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
key = Fernet.generate_key()
|
||||
enc_mgr = EncryptionManager(key, Path(tmpdir))
|
||||
cfg_mgr = ConfigManager(enc_mgr, Path(tmpdir))
|
||||
|
||||
cfg_mgr.set_pin("1234")
|
||||
assert cfg_mgr.verify_pin("1234")
|
||||
assert not cfg_mgr.verify_pin("0000")
|
||||
assert cfg_mgr.change_pin("1234", "5678")
|
||||
assert cfg_mgr.verify_pin("5678")
|
||||
|
Reference in New Issue
Block a user