From 113fd1181aabdc6b295638554bb178d8fbf11069 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 15 Jul 2025 21:56:31 -0400 Subject: [PATCH 01/14] Add custom seed profile names --- README.md | 6 ++++- docs/docs/content/index.md | 2 ++ src/main.py | 31 ++++++++++++++++++++++--- src/password_manager/manager.py | 21 ++++++++++++++--- src/utils/fingerprint_manager.py | 40 +++++++++++++++++++++++++++----- src/utils/terminal_utils.py | 33 +++++++++++++++++++------- 6 files changed, 111 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index a4d9e50..6488169 100644 --- a/README.md +++ b/README.md @@ -378,8 +378,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. 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. +- **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. diff --git a/docs/docs/content/index.md b/docs/docs/content/index.md index 09b38fd..7489fdf 100644 --- a/docs/docs/content/index.md +++ b/docs/docs/content/index.md @@ -368,6 +368,8 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as - **List All Seed 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. diff --git a/src/main.py b/src/main.py index 1751c3f..565ade0 100644 --- a/src/main.py +++ b/src/main.py @@ -151,7 +151,8 @@ def handle_switch_fingerprint(password_manager: PasswordManager): print(colored("Available Seed Profiles:", "cyan")) 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() 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")) 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() 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")) for fp in fingerprints: - print(colored(f"- {fp}", "cyan")) + label = password_manager.fingerprint_manager.display_name(fp) + print(colored(f"- {label}", "cyan")) pause() except Exception as e: 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")) +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: """Toggle secret mode and adjust clipboard delay.""" 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("3. Remove an Existing Seed Profile", "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() password_manager.update_activity() if choice == "1": @@ -767,6 +790,8 @@ def handle_profiles_menu(password_manager: PasswordManager) -> None: handle_remove_fingerprint(password_manager) elif choice == "4": handle_list_fingerprints(password_manager) + elif choice == "5": + handle_set_profile_name(password_manager) elif not choice: break else: diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index b2a2452..8f94817 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -329,8 +329,13 @@ class PasswordManager: print(colored("\nAvailable Seed Profiles:", "cyan")) 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 "" - 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")) @@ -532,7 +537,12 @@ class PasswordManager: print(colored("\nAvailable Seed Profiles:", "cyan")) fingerprints = self.fingerprint_manager.list_fingerprints() for idx, fp in enumerate(fingerprints, start=1): - print(colored(f"{idx}. {fp}", "cyan")) + 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() if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)): @@ -680,7 +690,12 @@ class PasswordManager: print(colored("Available Seed Profiles:", "cyan")) 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() if not choice.isdigit() or not (1 <= int(choice) <= len(fingerprints)): diff --git a/src/utils/fingerprint_manager.py b/src/utils/fingerprint_manager.py index 34ee4a0..470b063 100644 --- a/src/utils/fingerprint_manager.py +++ b/src/utils/fingerprint_manager.py @@ -34,7 +34,11 @@ class FingerprintManager: self.app_dir = app_dir self.fingerprints_file = self.app_dir / "fingerprints.json" 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]: """ @@ -62,25 +66,26 @@ class FingerprintManager: ) raise - def _load_fingerprints(self) -> tuple[list[str], Optional[str]]: - """Return stored fingerprints and the last used fingerprint.""" + def _load_fingerprints(self) -> tuple[list[str], Optional[str], dict[str, str]]: + """Return stored fingerprints, the last used fingerprint, and name mapping.""" try: if self.fingerprints_file.exists(): with open(self.fingerprints_file, "r") as f: data = json.load(f) fingerprints = data.get("fingerprints", []) current = data.get("last_used") + names = data.get("names", {}) logger.debug( f"Loaded fingerprints: {fingerprints} (last used: {current})" ) - return fingerprints, current + return fingerprints, current, names logger.debug( "fingerprints.json not found. Initializing empty fingerprint list." ) - return [], None + return [], None, {} except Exception as e: logger.error(f"Failed to load fingerprints: {e}", exc_info=True) - return [], None + return [], None, {} def _save_fingerprints(self): """ @@ -92,6 +97,7 @@ class FingerprintManager: { "fingerprints": self.fingerprints, "last_used": self.current_fingerprint, + "names": self.names, }, f, indent=4, @@ -116,6 +122,7 @@ class FingerprintManager: fingerprint = generate_fingerprint(seed_phrase) if fingerprint and fingerprint not in self.fingerprints: self.fingerprints.append(fingerprint) + self.names.setdefault(fingerprint, "") self.current_fingerprint = fingerprint self._save_fingerprints() logger.info(f"Fingerprint {fingerprint} added successfully.") @@ -144,6 +151,7 @@ class FingerprintManager: if fingerprint in self.fingerprints: try: self.fingerprints.remove(fingerprint) + self.names.pop(fingerprint, None) if self.current_fingerprint == fingerprint: self.current_fingerprint = ( self.fingerprints[0] if self.fingerprints else None @@ -198,6 +206,26 @@ class FingerprintManager: logger.error(f"Fingerprint {fingerprint} not found.") 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]: """ Retrieves the directory path for a given fingerprint. diff --git a/src/utils/terminal_utils.py b/src/utils/terminal_utils.py index f1e05e7..3b1e853 100644 --- a/src/utils/terminal_utils.py +++ b/src/utils/terminal_utils.py @@ -8,6 +8,20 @@ from termcolor import colored 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: """Clear the terminal screen using an ANSI escape code.""" print("\033c", end="") @@ -18,16 +32,17 @@ def clear_and_print_fingerprint( breadcrumb: str | None = None, parent_fingerprint: str | None = None, child_fingerprint: str | None = None, + pm=None, ) -> None: """Clear the screen and optionally display the current fingerprint and path.""" clear_screen() header_fp = None 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: - header_fp = fingerprint + header_fp = format_profile(fingerprint, pm) 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: header = f"Seed Profile: {header_fp}" if breadcrumb: @@ -36,15 +51,15 @@ def clear_and_print_fingerprint( 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: """Clear the screen and display a chain of fingerprints.""" clear_screen() if not fingerprints: return - chain = fingerprints[0] + chain = format_profile(fingerprints[0], pm) for fp in fingerprints[1:]: - chain += f" > Managed Account > {fp}" + chain += f" > Managed Account > {format_profile(fp, pm)}" header = f"Seed Profile: {chain}" if breadcrumb: header += f" > {breadcrumb}" @@ -63,11 +78,11 @@ def clear_header_with_notification( clear_screen() header_fp = None 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: - header_fp = fingerprint + header_fp = format_profile(fingerprint, pm) 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: header = f"Seed Profile: {header_fp}" if breadcrumb: From f46de144a9d577017aab368897f92813a08909f0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 03:22:47 -0400 Subject: [PATCH 02/14] Validate BIP-85 seeds using Mnemonic --- src/password_manager/manager.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 8f94817..89e5d22 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -87,6 +87,7 @@ from pathlib import Path from local_bip85.bip85 import BIP85, Bip85Error from bip_utils import Bip39SeedGenerator, Bip39MnemonicGenerator, Bip39Languages +from mnemonic import Mnemonic from datetime import datetime from utils.fingerprint_manager import FingerprintManager @@ -909,11 +910,11 @@ class PasswordManager: bool: True if valid, False otherwise. """ try: - words = seed.split() - if len(words) != 12: - return False - # Additional validation can be added here if needed (e.g., word list checks) - return True + checker = Mnemonic("english") + if checker.check(seed): + return True + logging.error("Invalid BIP-85 seed provided") + return False except Exception as e: logging.error(f"Error validating BIP-85 seed: {e}") return False From d7547810fee40cd31abc0c163d165ceaaae404db Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 03:32:06 -0400 Subject: [PATCH 03/14] Add cross-platform masked input utility with tests --- src/tests/test_seed_prompt.py | 30 ++++++++++++++ src/utils/__init__.py | 2 + src/utils/seed_prompt.py | 73 +++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 src/tests/test_seed_prompt.py create mode 100644 src/utils/seed_prompt.py diff --git a/src/tests/test_seed_prompt.py b/src/tests/test_seed_prompt.py new file mode 100644 index 0000000..68876d3 --- /dev/null +++ b/src/tests/test_seed_prompt.py @@ -0,0 +1,30 @@ +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 diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 01e058c..1a94070 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -25,6 +25,7 @@ try: update_checksum_file, ) from .password_prompt import prompt_for_password + from .seed_prompt import masked_input from .input_utils import timed_input from .memory_protection import InMemorySecret from .clipboard import copy_to_clipboard @@ -58,6 +59,7 @@ __all__ = [ "exclusive_lock", "shared_lock", "prompt_for_password", + "masked_input", "timed_input", "InMemorySecret", "copy_to_clipboard", diff --git a/src/utils/seed_prompt.py b/src/utils/seed_prompt.py new file mode 100644 index 0000000..80ff91f --- /dev/null +++ b/src/utils/seed_prompt.py @@ -0,0 +1,73 @@ +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 + + +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) From 04dc4e05daa33bf940e8562c883c2d97ab80285e Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 03:40:24 -0400 Subject: [PATCH 04/14] Add interactive seed word prompt --- src/tests/test_seed_prompt.py | 28 +++++++++++++++ src/utils/__init__.py | 3 +- src/utils/seed_prompt.py | 68 +++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/tests/test_seed_prompt.py b/src/tests/test_seed_prompt.py index 68876d3..44b82e2 100644 --- a/src/tests/test_seed_prompt.py +++ b/src/tests/test_seed_prompt.py @@ -28,3 +28,31 @@ def test_masked_input_windows_space(monkeypatch, capsys): 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() + + inputs = iter(words + ["y"] * len(words)) + monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) + + 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[0]] + words[1:] + ["y"] * len(words)) + monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) + + result = seed_prompt.prompt_seed_words(len(words)) + assert result == phrase diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 1a94070..6e83033 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -25,7 +25,7 @@ try: update_checksum_file, ) from .password_prompt import prompt_for_password - from .seed_prompt import masked_input + from .seed_prompt import masked_input, prompt_seed_words from .input_utils import timed_input from .memory_protection import InMemorySecret from .clipboard import copy_to_clipboard @@ -60,6 +60,7 @@ __all__ = [ "shared_lock", "prompt_for_password", "masked_input", + "prompt_seed_words", "timed_input", "InMemorySecret", "copy_to_clipboard", diff --git a/src/utils/seed_prompt.py b/src/utils/seed_prompt.py index 80ff91f..dd18e0a 100644 --- a/src/utils/seed_prompt.py +++ b/src/utils/seed_prompt.py @@ -71,3 +71,71 @@ def masked_input(prompt: str) -> str: 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: + progress = [f"{i+1}: {'*' if w else '_'}" for i, w in enumerate(words)] + print("\n".join(progress)) + entered = 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: + 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: + new_word = 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 From 6cca270bd6de427b86cfd2e90297557574a7f222 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 03:51:32 -0400 Subject: [PATCH 05/14] Update seed setup prompt with word-by-word option --- src/password_manager/manager.py | 168 +++++++++++++++++--------------- 1 file changed, 87 insertions(+), 81 deletions(-) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 89e5d22..2572770 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -54,6 +54,7 @@ from utils.password_prompt import ( prompt_new_password, confirm_action, ) +from utils import masked_input, prompt_seed_words from utils.memory_protection import InMemorySecret from utils.clipboard import copy_to_clipboard from utils.terminal_utils import ( @@ -743,12 +744,16 @@ class PasswordManager: self.notify("No existing seed found. Let's set up a new one!", level="WARNING") 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() if choice == "1": self.setup_existing_seed() elif choice == "2": + self.setup_existing_seed_word_by_word() + elif choice == "3": self.generate_new_seed() else: print(colored("Invalid choice. Exiting.", "red")) @@ -762,91 +767,92 @@ class PasswordManager: Optional[str]: The fingerprint if setup is successful, None otherwise. """ try: - parent_seed = getpass.getpass( - prompt="Enter your 12-word BIP-85 seed: " - ).strip() - if self.validate_bip85_seed(parent_seed): - # Add a fingerprint using the existing seed - fingerprint = self.fingerprint_manager.add_fingerprint(parent_seed) - if not fingerprint: - print( - colored( - "Error: Failed to generate seed profile for the provided seed.", - "red", - ) - ) - sys.exit(1) - - fingerprint_dir = self.fingerprint_manager.get_fingerprint_directory( - fingerprint - ) - if not fingerprint_dir: - print( - colored( - "Error: Failed to retrieve seed profile directory.", "red" - ) - ) - sys.exit(1) - - # Set the current fingerprint in both PasswordManager and FingerprintManager - self.current_fingerprint = fingerprint - self.fingerprint_manager.current_fingerprint = fingerprint - self.fingerprint_dir = fingerprint_dir - logging.info(f"Current seed profile set to {fingerprint}") - - 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: - logging.error("Invalid BIP-85 seed phrase. Exiting.") - print(colored("Error: Invalid BIP-85 seed phrase.", "red")) - sys.exit(1) + parent_seed = masked_input("Enter your 12-word BIP-85 seed: ").strip() + return self._finalize_existing_seed(parent_seed) except KeyboardInterrupt: logging.info("Operation cancelled by user.") self.notify("Operation cancelled by user.", level="WARNING") 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.""" + try: + parent_seed = prompt_seed_words() + return self._finalize_existing_seed(parent_seed) + except KeyboardInterrupt: + logging.info("Operation cancelled by user.") + self.notify("Operation cancelled by user.", level="WARNING") + sys.exit(0) + + 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]: """ Generates a new BIP-85 seed, displays it to the user, and prompts for confirmation before saving. From f701124fb1089fd61e9a97e1c95fb79ff4c1a232 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 04:04:13 -0400 Subject: [PATCH 06/14] Add method parameter to seed setup --- src/password_manager/manager.py | 44 +++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 2572770..eca2087 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -15,7 +15,7 @@ import logging import getpass import os import hashlib -from typing import Optional +from typing import Optional, Literal import shutil import time import builtins @@ -750,24 +750,42 @@ class PasswordManager: ).strip() if choice == "1": - self.setup_existing_seed() + self.setup_existing_seed(method="paste") elif choice == "2": - self.setup_existing_seed_word_by_word() + self.setup_existing_seed(method="words") elif choice == "3": self.generate_new_seed() else: print(colored("Invalid choice. Exiting.", "red")) sys.exit(1) - def setup_existing_seed(self) -> Optional[str]: - """ - Prompts the user to enter an existing BIP-85 seed and validates it. + def setup_existing_seed( + self, method: Literal["paste", "words"] = "paste" + ) -> Optional[str]: + """Prompt for an existing BIP-85 seed and set it up. - Returns: - Optional[str]: The fingerprint if setup is successful, None otherwise. + Parameters + ---------- + 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: - parent_seed = masked_input("Enter your 12-word BIP-85 seed: ").strip() + if method == "words": + parent_seed = prompt_seed_words() + 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.") + print(colored("Error: Invalid BIP-85 seed phrase.", "red")) + sys.exit(1) + return self._finalize_existing_seed(parent_seed) except KeyboardInterrupt: logging.info("Operation cancelled by user.") @@ -776,13 +794,7 @@ class PasswordManager: def setup_existing_seed_word_by_word(self) -> Optional[str]: """Prompt for an existing seed one word at a time and set it up.""" - try: - parent_seed = prompt_seed_words() - return self._finalize_existing_seed(parent_seed) - except KeyboardInterrupt: - logging.info("Operation cancelled by user.") - self.notify("Operation cancelled by user.", level="WARNING") - sys.exit(0) + return self.setup_existing_seed(method="words") def _finalize_existing_seed(self, parent_seed: str) -> Optional[str]: """Common logic for initializing an existing seed.""" From 0a011f108b344c2d0ac5bcf5a93a1a4a8b2bfa60 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 04:10:57 -0400 Subject: [PATCH 07/14] docs: clarify seed profile setup --- README.md | 5 +++-- docs/docs/content/index.md | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6488169..a484994 100644 --- a/README.md +++ b/README.md @@ -369,8 +369,9 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as - **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. - 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.** + 2. Choose to paste in a full seed, enter one word at a time, or generate a new seed. + 3. If you enter the seed word by word, SeedPass 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:** 1. From the **Profiles** menu, select "Switch Seed Profile". diff --git a/docs/docs/content/index.md b/docs/docs/content/index.md index 7489fdf..822a646 100644 --- a/docs/docs/content/index.md +++ b/docs/docs/content/index.md @@ -357,7 +357,8 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as - **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, 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.** - **Switch Between Seed Profiles:** From cc68f05130a5f446a8a85557a92323f544b426b7 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 04:16:36 -0400 Subject: [PATCH 08/14] test: seed setup interactions --- src/tests/test_manager_seed_setup.py | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/tests/test_manager_seed_setup.py diff --git a/src/tests/test_manager_seed_setup.py b/src/tests/test_manager_seed_setup.py new file mode 100644 index 0000000..8adfb95 --- /dev/null +++ b/src/tests/test_manager_seed_setup.py @@ -0,0 +1,58 @@ +import builtins +from mnemonic import Mnemonic +from password_manager.manager import PasswordManager + + +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() + inputs = iter(words + ["y"] * len(words)) + monkeypatch.setattr(builtins, "input", lambda *_: next(inputs)) + + 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") From a610272552c368c0a006e0422cb0cffd27c2e930 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:25:56 -0400 Subject: [PATCH 09/14] Update seed profile creation prompt --- src/password_manager/manager.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index eca2087..8b8ca8a 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -367,11 +367,15 @@ class PasswordManager: """ try: 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() if choice == "1": - fingerprint = self.setup_existing_seed() + fingerprint = self.setup_existing_seed(method="paste") elif choice == "2": + fingerprint = self.setup_existing_seed(method="words") + elif choice == "3": fingerprint = self.generate_new_seed() else: print(colored("Invalid choice. Exiting.", "red")) From 6f21c5cb9d3bba09cd71385a6fb131ab4766bdec Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:58:19 -0400 Subject: [PATCH 10/14] Update profile management test for new seed prompt --- src/tests/test_profile_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/test_profile_management.py b/src/tests/test_profile_management.py index bbb52de..7665b87 100644 --- a/src/tests/test_profile_management.py +++ b/src/tests/test_profile_management.py @@ -41,7 +41,7 @@ def test_add_and_delete_entry(monkeypatch): manager_module.PasswordManager, "generate_bip85_seed", lambda self: seed ) 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() From 04548c44f566ba263e0e0f410214fe68824a6faa Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:37:58 -0400 Subject: [PATCH 11/14] Enhance seed entry prompts with masking and clear screen --- src/tests/test_manager_seed_setup.py | 2 ++ src/tests/test_seed_prompt.py | 2 ++ src/utils/seed_prompt.py | 15 +++++++++++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/tests/test_manager_seed_setup.py b/src/tests/test_manager_seed_setup.py index 8adfb95..73b91d9 100644 --- a/src/tests/test_manager_seed_setup.py +++ b/src/tests/test_manager_seed_setup.py @@ -1,6 +1,7 @@ import builtins from mnemonic import Mnemonic from password_manager.manager import PasswordManager +from utils import seed_prompt def test_validate_bip85_seed_invalid_word(): @@ -24,6 +25,7 @@ def test_setup_existing_seed_words(monkeypatch): phrase = m.generate(strength=128) words = phrase.split() inputs = iter(words + ["y"] * len(words)) + monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(inputs)) monkeypatch.setattr(builtins, "input", lambda *_: next(inputs)) pm = PasswordManager.__new__(PasswordManager) diff --git a/src/tests/test_seed_prompt.py b/src/tests/test_seed_prompt.py index 44b82e2..c030b69 100644 --- a/src/tests/test_seed_prompt.py +++ b/src/tests/test_seed_prompt.py @@ -38,6 +38,7 @@ def test_prompt_seed_words_valid(monkeypatch): words = phrase.split() inputs = iter(words + ["y"] * len(words)) + monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(inputs)) monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) result = seed_prompt.prompt_seed_words(len(words)) @@ -52,6 +53,7 @@ def test_prompt_seed_words_invalid_word(monkeypatch): words = phrase.split() # Insert an invalid word for the first entry then the correct one inputs = iter(["invalid"] + [words[0]] + words[1:] + ["y"] * len(words)) + monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(inputs)) monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) result = seed_prompt.prompt_seed_words(len(words)) diff --git a/src/utils/seed_prompt.py b/src/utils/seed_prompt.py index dd18e0a..7c83977 100644 --- a/src/utils/seed_prompt.py +++ b/src/utils/seed_prompt.py @@ -13,6 +13,8 @@ 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``.""" @@ -105,9 +107,10 @@ def prompt_seed_words(count: int = 12) -> str: 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 = input(f"Enter word number {idx+1}: ").strip().lower() + entered = masked_input(f"Enter word number {idx+1}: ").strip().lower() if entered not in m.wordlist: print("Invalid word, try again.") continue @@ -116,6 +119,9 @@ def prompt_seed_words(count: int = 12) -> str: 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() @@ -125,7 +131,12 @@ def prompt_seed_words(count: int = 12) -> str: break if response in ("n", "no"): while True: - new_word = input(f"Re-enter word number {i+1}: ").strip().lower() + 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 From 6b7815f28e30fd486ca69158319a256156210bc9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:48:56 -0400 Subject: [PATCH 12/14] Update seed prompt tests --- src/tests/test_seed_prompt.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tests/test_seed_prompt.py b/src/tests/test_seed_prompt.py index c030b69..700c03f 100644 --- a/src/tests/test_seed_prompt.py +++ b/src/tests/test_seed_prompt.py @@ -37,9 +37,9 @@ def test_prompt_seed_words_valid(monkeypatch): phrase = m.generate(strength=128) words = phrase.split() - inputs = iter(words + ["y"] * len(words)) - monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(inputs)) - monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) + 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 @@ -52,9 +52,9 @@ def test_prompt_seed_words_invalid_word(monkeypatch): phrase = m.generate(strength=128) words = phrase.split() # Insert an invalid word for the first entry then the correct one - inputs = iter(["invalid"] + [words[0]] + words[1:] + ["y"] * len(words)) + inputs = iter(["invalid"] + words) monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(inputs)) - monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) + monkeypatch.setattr("builtins.input", lambda *_: "y") result = seed_prompt.prompt_seed_words(len(words)) assert result == phrase From 78368c0e2f3e18a6527fd541c31f4a86dabb95bd Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:57:19 -0400 Subject: [PATCH 13/14] Fix seed setup test to patch masked input --- src/tests/test_manager_seed_setup.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/tests/test_manager_seed_setup.py b/src/tests/test_manager_seed_setup.py index 73b91d9..e8277c9 100644 --- a/src/tests/test_manager_seed_setup.py +++ b/src/tests/test_manager_seed_setup.py @@ -24,9 +24,14 @@ def test_setup_existing_seed_words(monkeypatch): m = Mnemonic("english") phrase = m.generate(strength=128) words = phrase.split() - inputs = iter(words + ["y"] * len(words)) - monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(inputs)) - monkeypatch.setattr(builtins, "input", lambda *_: next(inputs)) + 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) From ea9665383eb4ca1bbe56a97b04386288d9808043 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:35:15 -0400 Subject: [PATCH 14/14] docs: clarify hidden entry behaviour --- README.md | 2 +- docs/docs/content/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a484994..c86c184 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,7 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as - **Add a New Seed Profile:** 1. From the main menu, select **Settings** then **Profiles** and choose "Add a New Seed Profile". 2. Choose to paste in a full seed, enter one word at a time, or generate a new seed. - 3. If you enter the seed word by word, SeedPass shows the completed phrase for confirmation so you can fix any mistakes before it is stored. + 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:** diff --git a/docs/docs/content/index.md b/docs/docs/content/index.md index 822a646..d417824 100644 --- a/docs/docs/content/index.md +++ b/docs/docs/content/index.md @@ -358,7 +358,7 @@ SeedPass allows you to manage multiple seed profiles (previously referred to as - **Add a New Seed Profile:** - From the main menu, select **Settings** then **Profiles** and choose "Add a New Seed Profile". - 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, you'll review the completed phrase after the last word and can correct mistakes before it is saved. + - 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.** - **Switch Between Seed Profiles:**