From d679d52b6614a46d1ab979c5a46c1641c27b1107 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 18 Jul 2025 08:25:07 -0400 Subject: [PATCH] Refactor manager to accept provided credentials --- src/seedpass/cli.py | 8 +++- src/seedpass/core/api.py | 8 +++- src/seedpass/core/manager.py | 59 +++++++++++++++--------- src/tests/test_background_sync_always.py | 5 +- src/tests/test_cli_doc_examples.py | 4 +- src/tests/test_manager_seed_setup.py | 28 ++++++++++- src/tests/test_parent_seed_backup.py | 10 +--- src/tests/test_profiles.py | 6 +-- src/tests/test_typer_cli.py | 10 ++-- 9 files changed, 88 insertions(+), 50 deletions(-) diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index 309b3ac..81b1628 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -462,8 +462,9 @@ def vault_reveal_parent_seed( ) -> None: """Display the parent seed and optionally write an encrypted backup file.""" vault_service, _profile, _sync = _get_services(ctx) + password = typer.prompt("Master password", hide_input=True) vault_service.backup_parent_seed( - BackupParentSeedRequest(path=Path(file) if file else None) + BackupParentSeedRequest(path=Path(file) if file else None, password=password) ) @@ -630,7 +631,10 @@ def fingerprint_remove(ctx: typer.Context, fingerprint: str) -> None: def fingerprint_switch(ctx: typer.Context, fingerprint: str) -> None: """Switch to another seed profile.""" _vault, profile_service, _sync = _get_services(ctx) - profile_service.switch_profile(ProfileSwitchRequest(fingerprint=fingerprint)) + password = typer.prompt("Master password", hide_input=True) + profile_service.switch_profile( + ProfileSwitchRequest(fingerprint=fingerprint, password=password) + ) @util_app.command("generate-password") diff --git a/src/seedpass/core/api.py b/src/seedpass/core/api.py index 9f6671c..eeb76af 100644 --- a/src/seedpass/core/api.py +++ b/src/seedpass/core/api.py @@ -57,12 +57,14 @@ class BackupParentSeedRequest(BaseModel): """Optional path to write the encrypted seed backup.""" path: Optional[Path] = None + password: Optional[str] = None class ProfileSwitchRequest(BaseModel): """Select a different seed profile.""" fingerprint: str + password: Optional[str] = None class ProfileRemoveRequest(BaseModel): @@ -123,7 +125,9 @@ class VaultService: """Backup and reveal the parent seed.""" with self._lock: - self._manager.handle_backup_reveal_parent_seed(req.path) + self._manager.handle_backup_reveal_parent_seed( + req.path, password=req.password + ) def stats(self) -> Dict: """Return statistics about the current profile.""" @@ -164,7 +168,7 @@ class ProfileService: """Switch to ``req.fingerprint``.""" with self._lock: - self._manager.select_fingerprint(req.fingerprint) + self._manager.select_fingerprint(req.fingerprint, password=req.password) class SyncService: diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index e42c8f7..06ffa07 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -546,7 +546,7 @@ class PasswordManager: print(colored(f"Error: Failed to load parent seed: {e}", "red")) sys.exit(1) - def handle_switch_fingerprint(self) -> bool: + def handle_switch_fingerprint(self, *, password: Optional[str] = None) -> bool: """ Handles switching to a different seed profile. @@ -587,9 +587,10 @@ class PasswordManager: return False # Return False to indicate failure # Prompt for master password for the selected seed profile - password = prompt_existing_password( - "Enter the master password for the selected seed profile: " - ) + if password is None: + password = prompt_existing_password( + "Enter the master password for the selected seed profile: " + ) # Set up the encryption manager with the new password and seed profile directory if not self.setup_encryption_manager( @@ -676,14 +677,14 @@ class PasswordManager: self.update_activity() self.start_background_sync() - def handle_existing_seed(self) -> None: + def handle_existing_seed(self, *, password: Optional[str] = None) -> None: """ Handles the scenario where an existing parent seed file is found. Prompts the user for the master password to decrypt the seed. """ try: - # Prompt for password using masked input - password = prompt_existing_password("Enter your login password: ") + if password is None: + password = prompt_existing_password("Enter your login password: ") # Derive encryption key from password iterations = ( @@ -778,7 +779,11 @@ class PasswordManager: sys.exit(1) def setup_existing_seed( - self, method: Literal["paste", "words"] = "paste" + self, + method: Literal["paste", "words"] = "paste", + *, + seed: Optional[str] = None, + password: Optional[str] = None, ) -> Optional[str]: """Prompt for an existing BIP-85 seed and set it up. @@ -794,7 +799,9 @@ class PasswordManager: The fingerprint if setup is successful, ``None`` otherwise. """ try: - if method == "words": + if seed is not None: + parent_seed = seed + elif method == "words": parent_seed = prompt_seed_words() else: parent_seed = masked_input("Enter your 12-word BIP-85 seed: ").strip() @@ -804,17 +811,21 @@ class PasswordManager: print(colored("Error: Invalid BIP-85 seed phrase.", "red")) sys.exit(1) - return self._finalize_existing_seed(parent_seed) + return self._finalize_existing_seed(parent_seed, password=password) 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]: + def setup_existing_seed_word_by_word( + self, *, seed: Optional[str] = None, password: Optional[str] = None + ) -> Optional[str]: """Prompt for an existing seed one word at a time and set it up.""" - return self.setup_existing_seed(method="words") + return self.setup_existing_seed(method="words", seed=seed, password=password) - def _finalize_existing_seed(self, parent_seed: str) -> Optional[str]: + def _finalize_existing_seed( + self, parent_seed: str, *, password: Optional[str] = None + ) -> Optional[str]: """Common logic for initializing an existing seed.""" if self.validate_bip85_seed(parent_seed): fingerprint = self.fingerprint_manager.add_fingerprint(parent_seed) @@ -842,7 +853,8 @@ class PasswordManager: logging.info(f"Current seed profile set to {fingerprint}") try: - password = prompt_for_password() + if password is None: + password = prompt_for_password() index_key = derive_index_key(parent_seed) iterations = ( self.config_manager.get_kdf_iterations() @@ -976,7 +988,9 @@ class PasswordManager: print(colored(f"Error: Failed to generate BIP-85 seed: {e}", "red")) sys.exit(1) - def save_and_encrypt_seed(self, seed: str, fingerprint_dir: Path) -> None: + def save_and_encrypt_seed( + self, seed: str, fingerprint_dir: Path, *, password: Optional[str] = None + ) -> None: """ Saves and encrypts the parent seed. @@ -988,8 +1002,8 @@ class PasswordManager: # Set self.fingerprint_dir self.fingerprint_dir = fingerprint_dir - # Prompt for password - password = prompt_for_password() + if password is None: + password = prompt_for_password() index_key = derive_index_key(seed) iterations = ( @@ -3732,7 +3746,9 @@ class PasswordManager: print(colored(f"Error: Failed to export 2FA codes: {e}", "red")) return None - def handle_backup_reveal_parent_seed(self, file: Path | None = None) -> None: + def handle_backup_reveal_parent_seed( + self, file: Path | None = None, *, password: Optional[str] = None + ) -> None: """Reveal the parent seed and optionally save an encrypted backup. Parameters @@ -3762,9 +3778,10 @@ class PasswordManager: ) # Verify user's identity with secure password verification - password = prompt_existing_password( - "Enter your master password to continue: " - ) + if password is None: + password = prompt_existing_password( + "Enter your master password to continue: " + ) if not self.verify_password(password): print(colored("Incorrect password. Operation aborted.", "red")) return diff --git a/src/tests/test_background_sync_always.py b/src/tests/test_background_sync_always.py index e94a899..f266489 100644 --- a/src/tests/test_background_sync_always.py +++ b/src/tests/test_background_sync_always.py @@ -22,9 +22,6 @@ def test_switch_fingerprint_triggers_bg_sync(monkeypatch, tmp_path): pm.config_manager = SimpleNamespace(get_quick_unlock=lambda: False) monkeypatch.setattr("builtins.input", lambda *_a, **_k: "1") - monkeypatch.setattr( - "seedpass.core.manager.prompt_existing_password", lambda *_a, **_k: "pw" - ) monkeypatch.setattr( PasswordManager, "setup_encryption_manager", lambda *a, **k: True ) @@ -39,7 +36,7 @@ def test_switch_fingerprint_triggers_bg_sync(monkeypatch, tmp_path): monkeypatch.setattr(PasswordManager, "start_background_sync", fake_bg) - assert pm.handle_switch_fingerprint() + assert pm.handle_switch_fingerprint(password="pw") assert calls["count"] == 1 diff --git a/src/tests/test_cli_doc_examples.py b/src/tests/test_cli_doc_examples.py index f20a2f6..03f162e 100644 --- a/src/tests/test_cli_doc_examples.py +++ b/src/tests/test_cli_doc_examples.py @@ -43,7 +43,7 @@ class DummyPM: self.change_password = lambda *a, **kw: None self.lock_vault = lambda: None self.get_profile_stats = lambda: {"n": 1} - self.handle_backup_reveal_parent_seed = lambda path=None: None + self.handle_backup_reveal_parent_seed = lambda path=None, **_: None self.handle_verify_checksum = lambda: None self.handle_update_script_checksum = lambda: None self.add_new_fingerprint = lambda: None @@ -76,7 +76,7 @@ class DummyPM: ) self.secret_mode_enabled = True self.clipboard_clear_delay = 30 - self.select_fingerprint = lambda fp: None + self.select_fingerprint = lambda fp, **_: None def load_doc_commands() -> list[str]: diff --git a/src/tests/test_manager_seed_setup.py b/src/tests/test_manager_seed_setup.py index 3c1a245..4759373 100644 --- a/src/tests/test_manager_seed_setup.py +++ b/src/tests/test_manager_seed_setup.py @@ -36,7 +36,7 @@ def test_setup_existing_seed_words(monkeypatch): monkeypatch.setattr(builtins, "input", lambda *_: "y") pm = PasswordManager.__new__(PasswordManager) - monkeypatch.setattr(pm, "_finalize_existing_seed", lambda seed: seed) + monkeypatch.setattr(pm, "_finalize_existing_seed", lambda seed, **_: seed) result = pm.setup_existing_seed(method="words") assert result == phrase @@ -60,8 +60,32 @@ def test_setup_existing_seed_paste(monkeypatch): ) pm = PasswordManager.__new__(PasswordManager) - monkeypatch.setattr(pm, "_finalize_existing_seed", lambda seed: seed) + 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") + + +def test_setup_existing_seed_with_args(monkeypatch): + m = Mnemonic("english") + phrase = m.generate(strength=128) + + called = {} + + monkeypatch.setattr( + "seedpass.core.manager.masked_input", + lambda *_: (_ for _ in ()).throw(RuntimeError("prompt")), + ) + + def finalize(seed, *, password=None): + called["seed"] = seed + called["pw"] = password + return "fp" + + pm = PasswordManager.__new__(PasswordManager) + monkeypatch.setattr(pm, "_finalize_existing_seed", finalize) + result = pm.setup_existing_seed(method="paste", seed=phrase, password="pw") + assert result == "fp" + assert called["seed"] == phrase + assert called["pw"] == "pw" diff --git a/src/tests/test_parent_seed_backup.py b/src/tests/test_parent_seed_backup.py index 1f70920..a25fd4a 100644 --- a/src/tests/test_parent_seed_backup.py +++ b/src/tests/test_parent_seed_backup.py @@ -24,9 +24,6 @@ def _make_pm(tmp_path: Path) -> PasswordManager: def test_handle_backup_reveal_parent_seed_confirm(monkeypatch, tmp_path, capsys): pm = _make_pm(tmp_path) - monkeypatch.setattr( - "seedpass.core.manager.prompt_existing_password", lambda *_: "pw" - ) confirms = iter([True, True]) monkeypatch.setattr( "seedpass.core.manager.confirm_action", lambda *_a, **_k: next(confirms) @@ -39,7 +36,7 @@ def test_handle_backup_reveal_parent_seed_confirm(monkeypatch, tmp_path, capsys) pm.encryption_manager = SimpleNamespace(encrypt_and_save_file=fake_save) monkeypatch.setattr(builtins, "input", lambda *_: "mybackup.enc") - pm.handle_backup_reveal_parent_seed() + pm.handle_backup_reveal_parent_seed(password="pw") out = capsys.readouterr().out assert "seed phrase" in out @@ -50,16 +47,13 @@ def test_handle_backup_reveal_parent_seed_confirm(monkeypatch, tmp_path, capsys) def test_handle_backup_reveal_parent_seed_cancel(monkeypatch, tmp_path, capsys): pm = _make_pm(tmp_path) - monkeypatch.setattr( - "seedpass.core.manager.prompt_existing_password", lambda *_: "pw" - ) monkeypatch.setattr("seedpass.core.manager.confirm_action", lambda *_a, **_k: False) saved = [] pm.encryption_manager = SimpleNamespace( encrypt_and_save_file=lambda data, path: saved.append((data, path)) ) - pm.handle_backup_reveal_parent_seed() + pm.handle_backup_reveal_parent_seed(password="pw") out = capsys.readouterr().out assert "seed phrase" not in out diff --git a/src/tests/test_profiles.py b/src/tests/test_profiles.py index fbbf097..35e05db 100644 --- a/src/tests/test_profiles.py +++ b/src/tests/test_profiles.py @@ -31,10 +31,6 @@ def test_add_and_switch_fingerprint(monkeypatch): pm.current_fingerprint = None monkeypatch.setattr("builtins.input", lambda *_args, **_kwargs: "1") - monkeypatch.setattr( - "seedpass.core.manager.prompt_existing_password", - lambda *_a, **_k: "pass", - ) monkeypatch.setattr( PasswordManager, "setup_encryption_manager", @@ -50,7 +46,7 @@ def test_add_and_switch_fingerprint(monkeypatch): "seedpass.core.manager.NostrClient", lambda *a, **kw: object() ) - assert pm.handle_switch_fingerprint() + assert pm.handle_switch_fingerprint(password="pass") assert pm.current_fingerprint == fingerprint assert fm.current_fingerprint == fingerprint assert pm.fingerprint_dir == expected_dir diff --git a/src/tests/test_typer_cli.py b/src/tests/test_typer_cli.py index cce6e6e..b0b2479 100644 --- a/src/tests/test_typer_cli.py +++ b/src/tests/test_typer_cli.py @@ -156,7 +156,7 @@ def test_vault_lock(monkeypatch): def test_vault_reveal_parent_seed(monkeypatch, tmp_path): called = {} - def reveal(path=None): + def reveal(path=None, **_): called["path"] = path pm = SimpleNamespace( @@ -165,7 +165,9 @@ def test_vault_reveal_parent_seed(monkeypatch, tmp_path): monkeypatch.setattr(cli, "PasswordManager", lambda: pm) out_path = tmp_path / "seed.enc" result = runner.invoke( - app, ["vault", "reveal-parent-seed", "--file", str(out_path)] + app, + ["vault", "reveal-parent-seed", "--file", str(out_path)], + input="pw\n", ) assert result.exit_code == 0 assert called["path"] == out_path @@ -231,14 +233,14 @@ def test_fingerprint_remove(monkeypatch): def test_fingerprint_switch(monkeypatch): called = {} - def switch(fp): + def switch(fp, **_): called["fp"] = fp pm = SimpleNamespace( select_fingerprint=switch, fingerprint_manager=SimpleNamespace() ) monkeypatch.setattr(cli, "PasswordManager", lambda: pm) - result = runner.invoke(app, ["fingerprint", "switch", "def"]) + result = runner.invoke(app, ["fingerprint", "switch", "def"], input="pw\n") assert result.exit_code == 0 assert called.get("fp") == "def"