mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 23:38:49 +00:00
11
README.md
11
README.md
@@ -369,8 +369,9 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as
|
|||||||
|
|
||||||
- **Add a New Seed Profile:**
|
- **Add a New Seed Profile:**
|
||||||
1. From the main menu, select **Settings** then **Profiles** and choose "Add a New Seed Profile".
|
1. From the main menu, select **Settings** then **Profiles** and choose "Add a New Seed Profile".
|
||||||
2. Choose to enter an existing seed or generate a new one.
|
2. Choose to paste in a full seed, enter one word at a time, or generate a new seed.
|
||||||
3. If generating a new seed, you'll be provided with a 12-word BIP-85 seed phrase. **Ensure you write this down and store it securely.**
|
3. If you enter the seed word by word, each word is hidden with `*` and the screen refreshes after every entry for clarity. SeedPass then shows the completed phrase for confirmation so you can fix any mistakes before it is stored.
|
||||||
|
4. If generating a new seed, you'll be provided with a 12-word BIP-85 seed phrase. **Ensure you write this down and store it securely.**
|
||||||
|
|
||||||
- **Switch Between Seed Profiles:**
|
- **Switch Between Seed Profiles:**
|
||||||
1. From the **Profiles** menu, select "Switch Seed Profile".
|
1. From the **Profiles** menu, select "Switch Seed Profile".
|
||||||
@@ -378,8 +379,12 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as
|
|||||||
3. Enter the number corresponding to the seed profile you wish to switch to.
|
3. Enter the number corresponding to the seed profile you wish to switch to.
|
||||||
4. Enter the master password associated with that seed profile.
|
4. Enter the master password associated with that seed profile.
|
||||||
|
|
||||||
- **List All Seed Profiles:**
|
- **List All Seed Profiles:**
|
||||||
In the **Profiles** menu, choose "List All Seed Profiles" to view all existing profiles.
|
In the **Profiles** menu, choose "List All Seed Profiles" to view all existing profiles.
|
||||||
|
- **Set Seed Profile Name:**
|
||||||
|
In the **Profiles** menu, choose "Set Seed Profile Name" to assign an optional
|
||||||
|
label to the currently selected profile. The name is stored locally and shown
|
||||||
|
alongside the fingerprint in menus.
|
||||||
|
|
||||||
**Note:** The term "seed profile" is used to represent different sets of seeds you can manage within SeedPass. This provides an intuitive way to handle multiple identities or sets of passwords.
|
**Note:** The term "seed profile" is used to represent different sets of seeds you can manage within SeedPass. This provides an intuitive way to handle multiple identities or sets of passwords.
|
||||||
|
|
||||||
|
@@ -357,7 +357,8 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as
|
|||||||
|
|
||||||
- **Add a New Seed Profile:**
|
- **Add a New Seed Profile:**
|
||||||
- From the main menu, select **Settings** then **Profiles** and choose "Add a New Seed Profile".
|
- From the main menu, select **Settings** then **Profiles** and choose "Add a New Seed Profile".
|
||||||
- Choose to enter an existing seed or generate a new one.
|
- Choose to paste in a full seed, enter one word at a time, or generate a new seed.
|
||||||
|
- When entering a seed word by word, each word is hidden with `*` and the screen refreshes after every entry for clarity. You'll review the completed phrase after the last word and can correct mistakes before it is saved.
|
||||||
- If generating a new seed, you'll be provided with a 12-word BIP-85 seed phrase. **Ensure you write this down and store it securely.**
|
- If generating a new seed, you'll be provided with a 12-word BIP-85 seed phrase. **Ensure you write this down and store it securely.**
|
||||||
|
|
||||||
- **Switch Between Seed Profiles:**
|
- **Switch Between Seed Profiles:**
|
||||||
@@ -368,6 +369,8 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as
|
|||||||
|
|
||||||
- **List All Seed Profiles:**
|
- **List All Seed Profiles:**
|
||||||
- In the **Profiles** menu, choose "List All Seed Profiles" to view all existing profiles.
|
- In the **Profiles** menu, choose "List All Seed Profiles" to view all existing profiles.
|
||||||
|
- **Set Seed Profile Name:**
|
||||||
|
- In the **Profiles** menu, choose "Set Seed Profile Name" to assign a label to the current profile. The name is stored locally and shown next to the fingerprint.
|
||||||
|
|
||||||
**Note:** The term "seed profile" is used to represent different sets of seeds you can manage within SeedPass. This provides an intuitive way to handle multiple identities or sets of passwords.
|
**Note:** The term "seed profile" is used to represent different sets of seeds you can manage within SeedPass. This provides an intuitive way to handle multiple identities or sets of passwords.
|
||||||
|
|
||||||
|
31
src/main.py
31
src/main.py
@@ -151,7 +151,8 @@ def handle_switch_fingerprint(password_manager: PasswordManager):
|
|||||||
|
|
||||||
print(colored("Available Seed Profiles:", "cyan"))
|
print(colored("Available Seed Profiles:", "cyan"))
|
||||||
for idx, fp in enumerate(fingerprints, start=1):
|
for idx, fp in enumerate(fingerprints, start=1):
|
||||||
print(colored(f"{idx}. {fp}", "cyan"))
|
label = password_manager.fingerprint_manager.display_name(fp)
|
||||||
|
print(colored(f"{idx}. {label}", "cyan"))
|
||||||
|
|
||||||
choice = input("Select a seed profile by number to switch: ").strip()
|
choice = input("Select a seed profile by number to switch: ").strip()
|
||||||
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
|
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
|
||||||
@@ -195,7 +196,8 @@ def handle_remove_fingerprint(password_manager: PasswordManager):
|
|||||||
|
|
||||||
print(colored("Available Seed Profiles:", "cyan"))
|
print(colored("Available Seed Profiles:", "cyan"))
|
||||||
for idx, fp in enumerate(fingerprints, start=1):
|
for idx, fp in enumerate(fingerprints, start=1):
|
||||||
print(colored(f"{idx}. {fp}", "cyan"))
|
label = password_manager.fingerprint_manager.display_name(fp)
|
||||||
|
print(colored(f"{idx}. {label}", "cyan"))
|
||||||
|
|
||||||
choice = input("Select a seed profile by number to remove: ").strip()
|
choice = input("Select a seed profile by number to remove: ").strip()
|
||||||
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
|
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
|
||||||
@@ -239,7 +241,8 @@ def handle_list_fingerprints(password_manager: PasswordManager):
|
|||||||
|
|
||||||
print(colored("Available Seed Profiles:", "cyan"))
|
print(colored("Available Seed Profiles:", "cyan"))
|
||||||
for fp in fingerprints:
|
for fp in fingerprints:
|
||||||
print(colored(f"- {fp}", "cyan"))
|
label = password_manager.fingerprint_manager.display_name(fp)
|
||||||
|
print(colored(f"- {label}", "cyan"))
|
||||||
pause()
|
pause()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error listing seed profiles: {e}", exc_info=True)
|
logging.error(f"Error listing seed profiles: {e}", exc_info=True)
|
||||||
@@ -641,6 +644,25 @@ def handle_set_additional_backup_location(pm: PasswordManager) -> None:
|
|||||||
print(colored(f"Error: {e}", "red"))
|
print(colored(f"Error: {e}", "red"))
|
||||||
|
|
||||||
|
|
||||||
|
def handle_set_profile_name(pm: PasswordManager) -> None:
|
||||||
|
"""Set or clear the custom name for the current seed profile."""
|
||||||
|
fp = getattr(pm.fingerprint_manager, "current_fingerprint", None)
|
||||||
|
if not fp:
|
||||||
|
print(colored("No seed profile selected.", "red"))
|
||||||
|
return
|
||||||
|
current = pm.fingerprint_manager.get_name(fp)
|
||||||
|
if current:
|
||||||
|
print(colored(f"Current name: {current}", "cyan"))
|
||||||
|
else:
|
||||||
|
print(colored("No custom name set.", "cyan"))
|
||||||
|
value = input("Enter new name (leave blank to remove): ").strip()
|
||||||
|
if pm.fingerprint_manager.set_name(fp, value or None):
|
||||||
|
if value:
|
||||||
|
print(colored("Name updated.", "green"))
|
||||||
|
else:
|
||||||
|
print(colored("Name removed.", "green"))
|
||||||
|
|
||||||
|
|
||||||
def handle_toggle_secret_mode(pm: PasswordManager) -> None:
|
def handle_toggle_secret_mode(pm: PasswordManager) -> None:
|
||||||
"""Toggle secret mode and adjust clipboard delay."""
|
"""Toggle secret mode and adjust clipboard delay."""
|
||||||
cfg = pm.config_manager
|
cfg = pm.config_manager
|
||||||
@@ -756,6 +778,7 @@ def handle_profiles_menu(password_manager: PasswordManager) -> None:
|
|||||||
print(color_text("2. Add a New Seed Profile", "menu"))
|
print(color_text("2. Add a New Seed Profile", "menu"))
|
||||||
print(color_text("3. Remove an Existing Seed Profile", "menu"))
|
print(color_text("3. Remove an Existing Seed Profile", "menu"))
|
||||||
print(color_text("4. List All Seed Profiles", "menu"))
|
print(color_text("4. List All Seed Profiles", "menu"))
|
||||||
|
print(color_text("5. Set Seed Profile Name", "menu"))
|
||||||
choice = input("Select an option or press Enter to go back: ").strip()
|
choice = input("Select an option or press Enter to go back: ").strip()
|
||||||
password_manager.update_activity()
|
password_manager.update_activity()
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
@@ -767,6 +790,8 @@ def handle_profiles_menu(password_manager: PasswordManager) -> None:
|
|||||||
handle_remove_fingerprint(password_manager)
|
handle_remove_fingerprint(password_manager)
|
||||||
elif choice == "4":
|
elif choice == "4":
|
||||||
handle_list_fingerprints(password_manager)
|
handle_list_fingerprints(password_manager)
|
||||||
|
elif choice == "5":
|
||||||
|
handle_set_profile_name(password_manager)
|
||||||
elif not choice:
|
elif not choice:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@@ -15,7 +15,7 @@ import logging
|
|||||||
import getpass
|
import getpass
|
||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import Optional
|
from typing import Optional, Literal
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
import builtins
|
import builtins
|
||||||
@@ -54,6 +54,7 @@ from utils.password_prompt import (
|
|||||||
prompt_new_password,
|
prompt_new_password,
|
||||||
confirm_action,
|
confirm_action,
|
||||||
)
|
)
|
||||||
|
from utils import masked_input, prompt_seed_words
|
||||||
from utils.memory_protection import InMemorySecret
|
from utils.memory_protection import InMemorySecret
|
||||||
from utils.clipboard import copy_to_clipboard
|
from utils.clipboard import copy_to_clipboard
|
||||||
from utils.terminal_utils import (
|
from utils.terminal_utils import (
|
||||||
@@ -87,6 +88,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from local_bip85.bip85 import BIP85, Bip85Error
|
from local_bip85.bip85 import BIP85, Bip85Error
|
||||||
from bip_utils import Bip39SeedGenerator, Bip39MnemonicGenerator, Bip39Languages
|
from bip_utils import Bip39SeedGenerator, Bip39MnemonicGenerator, Bip39Languages
|
||||||
|
from mnemonic import Mnemonic
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from utils.fingerprint_manager import FingerprintManager
|
from utils.fingerprint_manager import FingerprintManager
|
||||||
@@ -329,8 +331,13 @@ class PasswordManager:
|
|||||||
|
|
||||||
print(colored("\nAvailable Seed Profiles:", "cyan"))
|
print(colored("\nAvailable Seed Profiles:", "cyan"))
|
||||||
for idx, fp in enumerate(fingerprints, start=1):
|
for idx, fp in enumerate(fingerprints, start=1):
|
||||||
|
label = (
|
||||||
|
self.fingerprint_manager.display_name(fp)
|
||||||
|
if hasattr(self.fingerprint_manager, "display_name")
|
||||||
|
else fp
|
||||||
|
)
|
||||||
marker = " *" if fp == current else ""
|
marker = " *" if fp == current else ""
|
||||||
print(colored(f"{idx}. {fp}{marker}", "cyan"))
|
print(colored(f"{idx}. {label}{marker}", "cyan"))
|
||||||
|
|
||||||
print(colored(f"{len(fingerprints)+1}. Add a new seed profile", "cyan"))
|
print(colored(f"{len(fingerprints)+1}. Add a new seed profile", "cyan"))
|
||||||
|
|
||||||
@@ -360,11 +367,15 @@ class PasswordManager:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
choice = input(
|
choice = input(
|
||||||
"Do you want to (1) Enter an existing seed or (2) Generate a new seed? (1/2): "
|
"Do you want to (1) Paste in an existing seed in full "
|
||||||
|
"(2) Enter an existing seed one word at a time or "
|
||||||
|
"(3) Generate a new seed? (1/2/3): "
|
||||||
).strip()
|
).strip()
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
fingerprint = self.setup_existing_seed()
|
fingerprint = self.setup_existing_seed(method="paste")
|
||||||
elif choice == "2":
|
elif choice == "2":
|
||||||
|
fingerprint = self.setup_existing_seed(method="words")
|
||||||
|
elif choice == "3":
|
||||||
fingerprint = self.generate_new_seed()
|
fingerprint = self.generate_new_seed()
|
||||||
else:
|
else:
|
||||||
print(colored("Invalid choice. Exiting.", "red"))
|
print(colored("Invalid choice. Exiting.", "red"))
|
||||||
@@ -532,7 +543,12 @@ class PasswordManager:
|
|||||||
print(colored("\nAvailable Seed Profiles:", "cyan"))
|
print(colored("\nAvailable Seed Profiles:", "cyan"))
|
||||||
fingerprints = self.fingerprint_manager.list_fingerprints()
|
fingerprints = self.fingerprint_manager.list_fingerprints()
|
||||||
for idx, fp in enumerate(fingerprints, start=1):
|
for idx, fp in enumerate(fingerprints, start=1):
|
||||||
print(colored(f"{idx}. {fp}", "cyan"))
|
display = (
|
||||||
|
self.fingerprint_manager.display_name(fp)
|
||||||
|
if hasattr(self.fingerprint_manager, "display_name")
|
||||||
|
else fp
|
||||||
|
)
|
||||||
|
print(colored(f"{idx}. {display}", "cyan"))
|
||||||
|
|
||||||
choice = input("Select a seed profile by number to switch: ").strip()
|
choice = input("Select a seed profile by number to switch: ").strip()
|
||||||
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
|
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
|
||||||
@@ -680,7 +696,12 @@ class PasswordManager:
|
|||||||
|
|
||||||
print(colored("Available Seed Profiles:", "cyan"))
|
print(colored("Available Seed Profiles:", "cyan"))
|
||||||
for idx, fp in enumerate(fingerprints, start=1):
|
for idx, fp in enumerate(fingerprints, start=1):
|
||||||
print(colored(f"{idx}. {fp}", "cyan"))
|
label = (
|
||||||
|
self.fingerprint_manager.display_name(fp)
|
||||||
|
if hasattr(self.fingerprint_manager, "display_name")
|
||||||
|
else fp
|
||||||
|
)
|
||||||
|
print(colored(f"{idx}. {label}", "cyan"))
|
||||||
|
|
||||||
choice = input("Select a seed profile by number: ").strip()
|
choice = input("Select a seed profile by number: ").strip()
|
||||||
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
|
if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)):
|
||||||
@@ -727,110 +748,127 @@ class PasswordManager:
|
|||||||
self.notify("No existing seed found. Let's set up a new one!", level="WARNING")
|
self.notify("No existing seed found. Let's set up a new one!", level="WARNING")
|
||||||
|
|
||||||
choice = input(
|
choice = input(
|
||||||
"Do you want to (1) Enter an existing BIP-85 seed or (2) Generate a new BIP-85 seed? (1/2): "
|
"Do you want to (1) Paste in an existing seed in full "
|
||||||
|
"(2) Enter an existing seed one word at a time or "
|
||||||
|
"(3) Generate a new seed? (1/2/3): "
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
self.setup_existing_seed()
|
self.setup_existing_seed(method="paste")
|
||||||
elif choice == "2":
|
elif choice == "2":
|
||||||
|
self.setup_existing_seed(method="words")
|
||||||
|
elif choice == "3":
|
||||||
self.generate_new_seed()
|
self.generate_new_seed()
|
||||||
else:
|
else:
|
||||||
print(colored("Invalid choice. Exiting.", "red"))
|
print(colored("Invalid choice. Exiting.", "red"))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def setup_existing_seed(self) -> Optional[str]:
|
def setup_existing_seed(
|
||||||
"""
|
self, method: Literal["paste", "words"] = "paste"
|
||||||
Prompts the user to enter an existing BIP-85 seed and validates it.
|
) -> Optional[str]:
|
||||||
|
"""Prompt for an existing BIP-85 seed and set it up.
|
||||||
|
|
||||||
Returns:
|
Parameters
|
||||||
Optional[str]: The fingerprint if setup is successful, None otherwise.
|
----------
|
||||||
|
method:
|
||||||
|
``"paste"`` to enter the entire phrase at once or ``"words"`` to
|
||||||
|
be prompted one word at a time.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Optional[str]
|
||||||
|
The fingerprint if setup is successful, ``None`` otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
parent_seed = getpass.getpass(
|
if method == "words":
|
||||||
prompt="Enter your 12-word BIP-85 seed: "
|
parent_seed = prompt_seed_words()
|
||||||
).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}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Initialize EncryptionManager with key and fingerprint_dir
|
|
||||||
password = prompt_for_password()
|
|
||||||
index_key = derive_index_key(parent_seed)
|
|
||||||
iterations = (
|
|
||||||
self.config_manager.get_kdf_iterations()
|
|
||||||
if getattr(self, "config_manager", None)
|
|
||||||
else 50_000
|
|
||||||
)
|
|
||||||
seed_key = derive_key_from_password(password, iterations=iterations)
|
|
||||||
|
|
||||||
self.encryption_manager = EncryptionManager(
|
|
||||||
index_key, fingerprint_dir
|
|
||||||
)
|
|
||||||
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
|
||||||
self.vault = Vault(self.encryption_manager, fingerprint_dir)
|
|
||||||
|
|
||||||
# Ensure config manager is set for the new fingerprint
|
|
||||||
self.config_manager = ConfigManager(
|
|
||||||
vault=self.vault,
|
|
||||||
fingerprint_dir=fingerprint_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Encrypt and save the parent seed
|
|
||||||
seed_mgr.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.start_background_sync()
|
|
||||||
return fingerprint # Return the generated or added fingerprint
|
|
||||||
except BaseException:
|
|
||||||
# Clean up partial profile on failure or interruption
|
|
||||||
self.fingerprint_manager.remove_fingerprint(fingerprint)
|
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
|
parent_seed = masked_input("Enter your 12-word BIP-85 seed: ").strip()
|
||||||
|
|
||||||
|
if not self.validate_bip85_seed(parent_seed):
|
||||||
logging.error("Invalid BIP-85 seed phrase. Exiting.")
|
logging.error("Invalid BIP-85 seed phrase. Exiting.")
|
||||||
print(colored("Error: Invalid BIP-85 seed phrase.", "red"))
|
print(colored("Error: Invalid BIP-85 seed phrase.", "red"))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
return self._finalize_existing_seed(parent_seed)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logging.info("Operation cancelled by user.")
|
logging.info("Operation cancelled by user.")
|
||||||
self.notify("Operation cancelled by user.", level="WARNING")
|
self.notify("Operation cancelled by user.", level="WARNING")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
def setup_existing_seed_word_by_word(self) -> Optional[str]:
|
||||||
|
"""Prompt for an existing seed one word at a time and set it up."""
|
||||||
|
return self.setup_existing_seed(method="words")
|
||||||
|
|
||||||
|
def _finalize_existing_seed(self, parent_seed: str) -> Optional[str]:
|
||||||
|
"""Common logic for initializing an existing seed."""
|
||||||
|
if self.validate_bip85_seed(parent_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)
|
||||||
|
|
||||||
|
self.current_fingerprint = fingerprint
|
||||||
|
self.fingerprint_manager.current_fingerprint = fingerprint
|
||||||
|
self.fingerprint_dir = fingerprint_dir
|
||||||
|
logging.info(f"Current seed profile set to {fingerprint}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
password = prompt_for_password()
|
||||||
|
index_key = derive_index_key(parent_seed)
|
||||||
|
iterations = (
|
||||||
|
self.config_manager.get_kdf_iterations()
|
||||||
|
if getattr(self, "config_manager", None)
|
||||||
|
else 50_000
|
||||||
|
)
|
||||||
|
seed_key = derive_key_from_password(password, iterations=iterations)
|
||||||
|
|
||||||
|
self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
|
||||||
|
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
||||||
|
self.vault = Vault(self.encryption_manager, fingerprint_dir)
|
||||||
|
|
||||||
|
self.config_manager = ConfigManager(
|
||||||
|
vault=self.vault,
|
||||||
|
fingerprint_dir=fingerprint_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
seed_mgr.encrypt_parent_seed(parent_seed)
|
||||||
|
logging.info("Parent seed encrypted and saved successfully.")
|
||||||
|
|
||||||
|
self.store_hashed_password(password)
|
||||||
|
logging.info("User password hashed and stored successfully.")
|
||||||
|
|
||||||
|
self.parent_seed = parent_seed
|
||||||
|
logger.debug(
|
||||||
|
f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.initialize_bip85()
|
||||||
|
self.initialize_managers()
|
||||||
|
self.start_background_sync()
|
||||||
|
return fingerprint
|
||||||
|
except BaseException:
|
||||||
|
self.fingerprint_manager.remove_fingerprint(fingerprint)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
logging.error("Invalid BIP-85 seed phrase. Exiting.")
|
||||||
|
print(colored("Error: Invalid BIP-85 seed phrase.", "red"))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def generate_new_seed(self) -> Optional[str]:
|
def generate_new_seed(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Generates a new BIP-85 seed, displays it to the user, and prompts for confirmation before saving.
|
Generates a new BIP-85 seed, displays it to the user, and prompts for confirmation before saving.
|
||||||
@@ -894,11 +932,11 @@ class PasswordManager:
|
|||||||
bool: True if valid, False otherwise.
|
bool: True if valid, False otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
words = seed.split()
|
checker = Mnemonic("english")
|
||||||
if len(words) != 12:
|
if checker.check(seed):
|
||||||
return False
|
return True
|
||||||
# Additional validation can be added here if needed (e.g., word list checks)
|
logging.error("Invalid BIP-85 seed provided")
|
||||||
return True
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error validating BIP-85 seed: {e}")
|
logging.error(f"Error validating BIP-85 seed: {e}")
|
||||||
return False
|
return False
|
||||||
|
65
src/tests/test_manager_seed_setup.py
Normal file
65
src/tests/test_manager_seed_setup.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import builtins
|
||||||
|
from mnemonic import Mnemonic
|
||||||
|
from password_manager.manager import PasswordManager
|
||||||
|
from utils import seed_prompt
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_bip85_seed_invalid_word():
|
||||||
|
pm = PasswordManager.__new__(PasswordManager)
|
||||||
|
bad_phrase = "abandon " * 11 + "zzzz"
|
||||||
|
assert not pm.validate_bip85_seed(bad_phrase)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_bip85_seed_checksum_failure():
|
||||||
|
pm = PasswordManager.__new__(PasswordManager)
|
||||||
|
m = Mnemonic("english")
|
||||||
|
phrase = m.generate(strength=128)
|
||||||
|
words = phrase.split()
|
||||||
|
words[-1] = "abandon" if words[-1] != "abandon" else "about"
|
||||||
|
bad_phrase = " ".join(words)
|
||||||
|
assert not pm.validate_bip85_seed(bad_phrase)
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_existing_seed_words(monkeypatch):
|
||||||
|
m = Mnemonic("english")
|
||||||
|
phrase = m.generate(strength=128)
|
||||||
|
words = phrase.split()
|
||||||
|
word_iter = iter(words)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"password_manager.manager.masked_input",
|
||||||
|
lambda *_: next(word_iter),
|
||||||
|
)
|
||||||
|
# Ensure prompt_seed_words uses the patched function
|
||||||
|
monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(word_iter))
|
||||||
|
monkeypatch.setattr(builtins, "input", lambda *_: "y")
|
||||||
|
|
||||||
|
pm = PasswordManager.__new__(PasswordManager)
|
||||||
|
monkeypatch.setattr(pm, "_finalize_existing_seed", lambda seed: seed)
|
||||||
|
|
||||||
|
result = pm.setup_existing_seed(method="words")
|
||||||
|
assert result == phrase
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_existing_seed_paste(monkeypatch):
|
||||||
|
m = Mnemonic("english")
|
||||||
|
phrase = m.generate(strength=128)
|
||||||
|
|
||||||
|
called = {}
|
||||||
|
|
||||||
|
def fake_masked_input(prompt: str) -> str:
|
||||||
|
called["prompt"] = prompt
|
||||||
|
return phrase
|
||||||
|
|
||||||
|
monkeypatch.setattr("password_manager.manager.masked_input", fake_masked_input)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
builtins,
|
||||||
|
"input",
|
||||||
|
lambda *_: (_ for _ in ()).throw(RuntimeError("input called")),
|
||||||
|
)
|
||||||
|
|
||||||
|
pm = PasswordManager.__new__(PasswordManager)
|
||||||
|
monkeypatch.setattr(pm, "_finalize_existing_seed", lambda seed: seed)
|
||||||
|
|
||||||
|
result = pm.setup_existing_seed(method="paste")
|
||||||
|
assert result == phrase
|
||||||
|
assert called["prompt"].startswith("Enter your 12-word BIP-85 seed")
|
@@ -41,7 +41,7 @@ def test_add_and_delete_entry(monkeypatch):
|
|||||||
manager_module.PasswordManager, "generate_bip85_seed", lambda self: seed
|
manager_module.PasswordManager, "generate_bip85_seed", lambda self: seed
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(manager_module, "confirm_action", lambda *_a, **_k: True)
|
monkeypatch.setattr(manager_module, "confirm_action", lambda *_a, **_k: True)
|
||||||
monkeypatch.setattr("builtins.input", lambda *_a, **_k: "2")
|
monkeypatch.setattr("builtins.input", lambda *_a, **_k: "3")
|
||||||
|
|
||||||
pm.add_new_fingerprint()
|
pm.add_new_fingerprint()
|
||||||
|
|
||||||
|
60
src/tests/test_seed_prompt.py
Normal file
60
src/tests/test_seed_prompt.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import types
|
||||||
|
from utils import seed_prompt
|
||||||
|
|
||||||
|
|
||||||
|
def test_masked_input_posix_backspace(monkeypatch, capsys):
|
||||||
|
seq = iter(["a", "b", "\x7f", "c", "\n"])
|
||||||
|
monkeypatch.setattr(seed_prompt.sys.stdin, "read", lambda n=1: next(seq))
|
||||||
|
monkeypatch.setattr(seed_prompt.sys.stdin, "fileno", lambda: 0)
|
||||||
|
monkeypatch.setattr(seed_prompt.termios, "tcgetattr", lambda fd: None)
|
||||||
|
monkeypatch.setattr(seed_prompt.termios, "tcsetattr", lambda fd, *_: None)
|
||||||
|
monkeypatch.setattr(seed_prompt.tty, "setraw", lambda fd: None)
|
||||||
|
|
||||||
|
result = seed_prompt.masked_input("Enter: ")
|
||||||
|
assert result == "ac"
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert out.startswith("Enter: ")
|
||||||
|
assert out.count("*") == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_masked_input_windows_space(monkeypatch, capsys):
|
||||||
|
seq = iter(["x", "y", " ", "z", "\r"])
|
||||||
|
fake_msvcrt = types.SimpleNamespace(getwch=lambda: next(seq))
|
||||||
|
monkeypatch.setattr(seed_prompt, "msvcrt", fake_msvcrt)
|
||||||
|
monkeypatch.setattr(seed_prompt.sys, "platform", "win32", raising=False)
|
||||||
|
|
||||||
|
result = seed_prompt.masked_input("Password: ")
|
||||||
|
assert result == "xy z"
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert out.startswith("Password: ")
|
||||||
|
assert out.count("*") == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompt_seed_words_valid(monkeypatch):
|
||||||
|
from mnemonic import Mnemonic
|
||||||
|
|
||||||
|
m = Mnemonic("english")
|
||||||
|
phrase = m.generate(strength=128)
|
||||||
|
words = phrase.split()
|
||||||
|
|
||||||
|
word_iter = iter(words)
|
||||||
|
monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(word_iter))
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *_: "y")
|
||||||
|
|
||||||
|
result = seed_prompt.prompt_seed_words(len(words))
|
||||||
|
assert result == phrase
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompt_seed_words_invalid_word(monkeypatch):
|
||||||
|
from mnemonic import Mnemonic
|
||||||
|
|
||||||
|
m = Mnemonic("english")
|
||||||
|
phrase = m.generate(strength=128)
|
||||||
|
words = phrase.split()
|
||||||
|
# Insert an invalid word for the first entry then the correct one
|
||||||
|
inputs = iter(["invalid"] + words)
|
||||||
|
monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(inputs))
|
||||||
|
monkeypatch.setattr("builtins.input", lambda *_: "y")
|
||||||
|
|
||||||
|
result = seed_prompt.prompt_seed_words(len(words))
|
||||||
|
assert result == phrase
|
@@ -25,6 +25,7 @@ try:
|
|||||||
update_checksum_file,
|
update_checksum_file,
|
||||||
)
|
)
|
||||||
from .password_prompt import prompt_for_password
|
from .password_prompt import prompt_for_password
|
||||||
|
from .seed_prompt import masked_input, prompt_seed_words
|
||||||
from .input_utils import timed_input
|
from .input_utils import timed_input
|
||||||
from .memory_protection import InMemorySecret
|
from .memory_protection import InMemorySecret
|
||||||
from .clipboard import copy_to_clipboard
|
from .clipboard import copy_to_clipboard
|
||||||
@@ -58,6 +59,8 @@ __all__ = [
|
|||||||
"exclusive_lock",
|
"exclusive_lock",
|
||||||
"shared_lock",
|
"shared_lock",
|
||||||
"prompt_for_password",
|
"prompt_for_password",
|
||||||
|
"masked_input",
|
||||||
|
"prompt_seed_words",
|
||||||
"timed_input",
|
"timed_input",
|
||||||
"InMemorySecret",
|
"InMemorySecret",
|
||||||
"copy_to_clipboard",
|
"copy_to_clipboard",
|
||||||
|
@@ -34,7 +34,11 @@ class FingerprintManager:
|
|||||||
self.app_dir = app_dir
|
self.app_dir = app_dir
|
||||||
self.fingerprints_file = self.app_dir / "fingerprints.json"
|
self.fingerprints_file = self.app_dir / "fingerprints.json"
|
||||||
self._ensure_app_directory()
|
self._ensure_app_directory()
|
||||||
self.fingerprints, self.current_fingerprint = self._load_fingerprints()
|
(
|
||||||
|
self.fingerprints,
|
||||||
|
self.current_fingerprint,
|
||||||
|
self.names,
|
||||||
|
) = self._load_fingerprints()
|
||||||
|
|
||||||
def get_current_fingerprint_dir(self) -> Optional[Path]:
|
def get_current_fingerprint_dir(self) -> Optional[Path]:
|
||||||
"""
|
"""
|
||||||
@@ -62,25 +66,26 @@ class FingerprintManager:
|
|||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _load_fingerprints(self) -> tuple[list[str], Optional[str]]:
|
def _load_fingerprints(self) -> tuple[list[str], Optional[str], dict[str, str]]:
|
||||||
"""Return stored fingerprints and the last used fingerprint."""
|
"""Return stored fingerprints, the last used fingerprint, and name mapping."""
|
||||||
try:
|
try:
|
||||||
if self.fingerprints_file.exists():
|
if self.fingerprints_file.exists():
|
||||||
with open(self.fingerprints_file, "r") as f:
|
with open(self.fingerprints_file, "r") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
fingerprints = data.get("fingerprints", [])
|
fingerprints = data.get("fingerprints", [])
|
||||||
current = data.get("last_used")
|
current = data.get("last_used")
|
||||||
|
names = data.get("names", {})
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Loaded fingerprints: {fingerprints} (last used: {current})"
|
f"Loaded fingerprints: {fingerprints} (last used: {current})"
|
||||||
)
|
)
|
||||||
return fingerprints, current
|
return fingerprints, current, names
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"fingerprints.json not found. Initializing empty fingerprint list."
|
"fingerprints.json not found. Initializing empty fingerprint list."
|
||||||
)
|
)
|
||||||
return [], None
|
return [], None, {}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load fingerprints: {e}", exc_info=True)
|
logger.error(f"Failed to load fingerprints: {e}", exc_info=True)
|
||||||
return [], None
|
return [], None, {}
|
||||||
|
|
||||||
def _save_fingerprints(self):
|
def _save_fingerprints(self):
|
||||||
"""
|
"""
|
||||||
@@ -92,6 +97,7 @@ class FingerprintManager:
|
|||||||
{
|
{
|
||||||
"fingerprints": self.fingerprints,
|
"fingerprints": self.fingerprints,
|
||||||
"last_used": self.current_fingerprint,
|
"last_used": self.current_fingerprint,
|
||||||
|
"names": self.names,
|
||||||
},
|
},
|
||||||
f,
|
f,
|
||||||
indent=4,
|
indent=4,
|
||||||
@@ -116,6 +122,7 @@ class FingerprintManager:
|
|||||||
fingerprint = generate_fingerprint(seed_phrase)
|
fingerprint = generate_fingerprint(seed_phrase)
|
||||||
if fingerprint and fingerprint not in self.fingerprints:
|
if fingerprint and fingerprint not in self.fingerprints:
|
||||||
self.fingerprints.append(fingerprint)
|
self.fingerprints.append(fingerprint)
|
||||||
|
self.names.setdefault(fingerprint, "")
|
||||||
self.current_fingerprint = fingerprint
|
self.current_fingerprint = fingerprint
|
||||||
self._save_fingerprints()
|
self._save_fingerprints()
|
||||||
logger.info(f"Fingerprint {fingerprint} added successfully.")
|
logger.info(f"Fingerprint {fingerprint} added successfully.")
|
||||||
@@ -144,6 +151,7 @@ class FingerprintManager:
|
|||||||
if fingerprint in self.fingerprints:
|
if fingerprint in self.fingerprints:
|
||||||
try:
|
try:
|
||||||
self.fingerprints.remove(fingerprint)
|
self.fingerprints.remove(fingerprint)
|
||||||
|
self.names.pop(fingerprint, None)
|
||||||
if self.current_fingerprint == fingerprint:
|
if self.current_fingerprint == fingerprint:
|
||||||
self.current_fingerprint = (
|
self.current_fingerprint = (
|
||||||
self.fingerprints[0] if self.fingerprints else None
|
self.fingerprints[0] if self.fingerprints else None
|
||||||
@@ -198,6 +206,26 @@ class FingerprintManager:
|
|||||||
logger.error(f"Fingerprint {fingerprint} not found.")
|
logger.error(f"Fingerprint {fingerprint} not found.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def set_name(self, fingerprint: str, name: str | None) -> bool:
|
||||||
|
"""Set a custom name for a fingerprint."""
|
||||||
|
if fingerprint not in self.fingerprints:
|
||||||
|
return False
|
||||||
|
if name:
|
||||||
|
self.names[fingerprint] = name
|
||||||
|
else:
|
||||||
|
self.names.pop(fingerprint, None)
|
||||||
|
self._save_fingerprints()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_name(self, fingerprint: str) -> Optional[str]:
|
||||||
|
"""Return the custom name for ``fingerprint`` if set."""
|
||||||
|
return self.names.get(fingerprint) or None
|
||||||
|
|
||||||
|
def display_name(self, fingerprint: str) -> str:
|
||||||
|
"""Return name and fingerprint for display."""
|
||||||
|
name = self.get_name(fingerprint)
|
||||||
|
return f"{name} ({fingerprint})" if name else fingerprint
|
||||||
|
|
||||||
def get_fingerprint_directory(self, fingerprint: str) -> Optional[Path]:
|
def get_fingerprint_directory(self, fingerprint: str) -> Optional[Path]:
|
||||||
"""
|
"""
|
||||||
Retrieves the directory path for a given fingerprint.
|
Retrieves the directory path for a given fingerprint.
|
||||||
|
152
src/utils/seed_prompt.py
Normal file
152
src/utils/seed_prompt.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import msvcrt # type: ignore
|
||||||
|
except ImportError: # pragma: no cover - Windows only
|
||||||
|
msvcrt = None # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
import termios
|
||||||
|
import tty
|
||||||
|
except ImportError: # pragma: no cover - POSIX only
|
||||||
|
termios = None # type: ignore
|
||||||
|
tty = None # type: ignore
|
||||||
|
|
||||||
|
from utils.terminal_utils import clear_screen
|
||||||
|
|
||||||
|
|
||||||
|
def _masked_input_windows(prompt: str) -> str:
|
||||||
|
"""Windows implementation using ``msvcrt``."""
|
||||||
|
if msvcrt is None: # pragma: no cover - should not happen
|
||||||
|
return input(prompt)
|
||||||
|
|
||||||
|
sys.stdout.write(prompt)
|
||||||
|
sys.stdout.flush()
|
||||||
|
buffer: list[str] = []
|
||||||
|
while True:
|
||||||
|
ch = msvcrt.getwch()
|
||||||
|
if ch in ("\r", "\n"):
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
return "".join(buffer)
|
||||||
|
if ch in ("\b", "\x7f"):
|
||||||
|
if buffer:
|
||||||
|
buffer.pop()
|
||||||
|
sys.stdout.write("\b \b")
|
||||||
|
else:
|
||||||
|
buffer.append(ch)
|
||||||
|
sys.stdout.write("*")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def _masked_input_posix(prompt: str) -> str:
|
||||||
|
"""POSIX implementation using ``termios`` and ``tty``."""
|
||||||
|
if termios is None or tty is None: # pragma: no cover - should not happen
|
||||||
|
return input(prompt)
|
||||||
|
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
sys.stdout.write(prompt)
|
||||||
|
sys.stdout.flush()
|
||||||
|
buffer: list[str] = []
|
||||||
|
try:
|
||||||
|
tty.setraw(fd)
|
||||||
|
while True:
|
||||||
|
ch = sys.stdin.read(1)
|
||||||
|
if ch in ("\r", "\n"):
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
return "".join(buffer)
|
||||||
|
if ch in ("\x7f", "\b"):
|
||||||
|
if buffer:
|
||||||
|
buffer.pop()
|
||||||
|
sys.stdout.write("\b \b")
|
||||||
|
else:
|
||||||
|
buffer.append(ch)
|
||||||
|
sys.stdout.write("*")
|
||||||
|
sys.stdout.flush()
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
|
||||||
|
|
||||||
|
def masked_input(prompt: str) -> str:
|
||||||
|
"""Return input from the user while masking typed characters."""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
return _masked_input_windows(prompt)
|
||||||
|
return _masked_input_posix(prompt)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_seed_words(count: int = 12) -> str:
|
||||||
|
"""Prompt the user for a BIP-39 seed phrase.
|
||||||
|
|
||||||
|
The user is asked for each word one at a time. A numbered list is
|
||||||
|
displayed showing ``*`` for entered words and ``_`` for words yet to be
|
||||||
|
provided. After all words are entered the user is asked to confirm each
|
||||||
|
word individually. If the user answers ``no`` to a confirmation prompt the
|
||||||
|
word can be re-entered.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
count:
|
||||||
|
Number of words to prompt for. Defaults to ``12``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The complete seed phrase.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the resulting phrase fails ``Mnemonic.check`` validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from mnemonic import Mnemonic
|
||||||
|
|
||||||
|
m = Mnemonic("english")
|
||||||
|
words: list[str] = [""] * count
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
while idx < count:
|
||||||
|
clear_screen()
|
||||||
|
progress = [f"{i+1}: {'*' if w else '_'}" for i, w in enumerate(words)]
|
||||||
|
print("\n".join(progress))
|
||||||
|
entered = masked_input(f"Enter word number {idx+1}: ").strip().lower()
|
||||||
|
if entered not in m.wordlist:
|
||||||
|
print("Invalid word, try again.")
|
||||||
|
continue
|
||||||
|
words[idx] = entered
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
for i in range(count):
|
||||||
|
while True:
|
||||||
|
clear_screen()
|
||||||
|
progress = [f"{j+1}: {'*' if j < i else '_'}" for j in range(count)]
|
||||||
|
print("\n".join(progress))
|
||||||
|
response = (
|
||||||
|
input(f"Is this the correct word for number {i+1}? {words[i]} (Y/N): ")
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
if response in ("y", "yes"):
|
||||||
|
break
|
||||||
|
if response in ("n", "no"):
|
||||||
|
while True:
|
||||||
|
clear_screen()
|
||||||
|
progress = [f"{j+1}: {'*' if j < i else '_'}" for j in range(count)]
|
||||||
|
print("\n".join(progress))
|
||||||
|
new_word = (
|
||||||
|
masked_input(f"Re-enter word number {i+1}: ").strip().lower()
|
||||||
|
)
|
||||||
|
if new_word in m.wordlist:
|
||||||
|
words[i] = new_word
|
||||||
|
break
|
||||||
|
print("Invalid word, try again.")
|
||||||
|
# Ask for confirmation again with the new word
|
||||||
|
else:
|
||||||
|
print("Please respond with 'Y' or 'N'.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
phrase = " ".join(words)
|
||||||
|
if not m.check(phrase):
|
||||||
|
raise ValueError("Invalid BIP-39 seed phrase")
|
||||||
|
return phrase
|
@@ -8,6 +8,20 @@ from termcolor import colored
|
|||||||
from utils.color_scheme import color_text
|
from utils.color_scheme import color_text
|
||||||
|
|
||||||
|
|
||||||
|
def format_profile(fingerprint: str | None, pm=None) -> str | None:
|
||||||
|
"""Return display string for a fingerprint with optional custom name."""
|
||||||
|
if not fingerprint:
|
||||||
|
return None
|
||||||
|
if pm and getattr(pm, "fingerprint_manager", None):
|
||||||
|
try:
|
||||||
|
name = pm.fingerprint_manager.get_name(fingerprint)
|
||||||
|
if name:
|
||||||
|
return f"{name} ({fingerprint})"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return fingerprint
|
||||||
|
|
||||||
|
|
||||||
def clear_screen() -> None:
|
def clear_screen() -> None:
|
||||||
"""Clear the terminal screen using an ANSI escape code."""
|
"""Clear the terminal screen using an ANSI escape code."""
|
||||||
print("\033c", end="")
|
print("\033c", end="")
|
||||||
@@ -18,16 +32,17 @@ def clear_and_print_fingerprint(
|
|||||||
breadcrumb: str | None = None,
|
breadcrumb: str | None = None,
|
||||||
parent_fingerprint: str | None = None,
|
parent_fingerprint: str | None = None,
|
||||||
child_fingerprint: str | None = None,
|
child_fingerprint: str | None = None,
|
||||||
|
pm=None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Clear the screen and optionally display the current fingerprint and path."""
|
"""Clear the screen and optionally display the current fingerprint and path."""
|
||||||
clear_screen()
|
clear_screen()
|
||||||
header_fp = None
|
header_fp = None
|
||||||
if parent_fingerprint and child_fingerprint:
|
if parent_fingerprint and child_fingerprint:
|
||||||
header_fp = f"{parent_fingerprint} > Managed Account > {child_fingerprint}"
|
header_fp = f"{format_profile(parent_fingerprint, pm)} > Managed Account > {format_profile(child_fingerprint, pm)}"
|
||||||
elif fingerprint:
|
elif fingerprint:
|
||||||
header_fp = fingerprint
|
header_fp = format_profile(fingerprint, pm)
|
||||||
elif parent_fingerprint or child_fingerprint:
|
elif parent_fingerprint or child_fingerprint:
|
||||||
header_fp = parent_fingerprint or child_fingerprint
|
header_fp = format_profile(parent_fingerprint or child_fingerprint, pm)
|
||||||
if header_fp:
|
if header_fp:
|
||||||
header = f"Seed Profile: {header_fp}"
|
header = f"Seed Profile: {header_fp}"
|
||||||
if breadcrumb:
|
if breadcrumb:
|
||||||
@@ -36,15 +51,15 @@ def clear_and_print_fingerprint(
|
|||||||
|
|
||||||
|
|
||||||
def clear_and_print_profile_chain(
|
def clear_and_print_profile_chain(
|
||||||
fingerprints: list[str] | None, breadcrumb: str | None = None
|
fingerprints: list[str] | None, breadcrumb: str | None = None, pm=None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Clear the screen and display a chain of fingerprints."""
|
"""Clear the screen and display a chain of fingerprints."""
|
||||||
clear_screen()
|
clear_screen()
|
||||||
if not fingerprints:
|
if not fingerprints:
|
||||||
return
|
return
|
||||||
chain = fingerprints[0]
|
chain = format_profile(fingerprints[0], pm)
|
||||||
for fp in fingerprints[1:]:
|
for fp in fingerprints[1:]:
|
||||||
chain += f" > Managed Account > {fp}"
|
chain += f" > Managed Account > {format_profile(fp, pm)}"
|
||||||
header = f"Seed Profile: {chain}"
|
header = f"Seed Profile: {chain}"
|
||||||
if breadcrumb:
|
if breadcrumb:
|
||||||
header += f" > {breadcrumb}"
|
header += f" > {breadcrumb}"
|
||||||
@@ -63,11 +78,11 @@ def clear_header_with_notification(
|
|||||||
clear_screen()
|
clear_screen()
|
||||||
header_fp = None
|
header_fp = None
|
||||||
if parent_fingerprint and child_fingerprint:
|
if parent_fingerprint and child_fingerprint:
|
||||||
header_fp = f"{parent_fingerprint} > Managed Account > {child_fingerprint}"
|
header_fp = f"{format_profile(parent_fingerprint, pm)} > Managed Account > {format_profile(child_fingerprint, pm)}"
|
||||||
elif fingerprint:
|
elif fingerprint:
|
||||||
header_fp = fingerprint
|
header_fp = format_profile(fingerprint, pm)
|
||||||
elif parent_fingerprint or child_fingerprint:
|
elif parent_fingerprint or child_fingerprint:
|
||||||
header_fp = parent_fingerprint or child_fingerprint
|
header_fp = format_profile(parent_fingerprint or child_fingerprint, pm)
|
||||||
if header_fp:
|
if header_fp:
|
||||||
header = f"Seed Profile: {header_fp}"
|
header = f"Seed Profile: {header_fp}"
|
||||||
if breadcrumb:
|
if breadcrumb:
|
||||||
|
Reference in New Issue
Block a user