diff --git a/README.md b/README.md index e06649a..09b2610 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,6 @@ SeedPass now uses the `portalocker` library for cross-platform file locking. No - **SeedPass 2FA:** Generate TOTP codes with a real-time countdown progress bar. - **2FA Secret Issuance & Import:** Derive new TOTP secrets from your seed or import existing `otpauth://` URIs. - **Export 2FA Codes:** Save all stored TOTP entries to an encrypted JSON file for use with other apps. -- **SSH Key & Seed Derivation:** Generate deterministic SSH keys and new BIP-39 seed phrases from your master seed. - **Optional External Backup Location:** Configure a second directory where backups are automatically copied. - **Auto‑Lock on Inactivity:** Vault locks after a configurable timeout for additional security. - **Secret Mode:** Copy retrieved passwords directly to your clipboard and automatically clear it after a delay. @@ -206,8 +205,7 @@ python src/main.py Enter your choice (1-7): ``` - When choosing **Add Entry**, you can now select **Password**, **2FA (TOTP)**, - **SSH Key**, or **BIP-39 Seed**. + When choosing **Add Entry**, you can now select **Password** or **2FA (TOTP)**. ### Adding a 2FA Entry diff --git a/src/main.py b/src/main.py index 74a4433..f8bf76c 100644 --- a/src/main.py +++ b/src/main.py @@ -729,9 +729,7 @@ def display_menu( print("\nAdd Entry:") print("1. Password") print("2. 2FA (TOTP)") - print("3. SSH Key") - print("4. BIP-39 Seed") - print("5. Back") + print("3. Back") sub_choice = input("Select entry type: ").strip() password_manager.update_activity() if sub_choice == "1": @@ -741,12 +739,6 @@ def display_menu( password_manager.handle_add_totp() break elif sub_choice == "3": - password_manager.handle_add_ssh_key() - break - elif sub_choice == "4": - password_manager.handle_add_seed() - break - elif sub_choice == "5": break else: print(colored("Invalid choice.", "red")) diff --git a/src/password_manager/entry_management.py b/src/password_manager/entry_management.py index 5cbcfef..098ede5 100644 --- a/src/password_manager/entry_management.py +++ b/src/password_manager/entry_management.py @@ -209,68 +209,27 @@ class EntryManager: logger.error(f"Failed to generate otpauth URI: {e}") raise - def get_next_ssh_index(self) -> int: - """Return the next available derivation index for SSH keys.""" - data = self.vault.load_index() - entries = data.get("entries", {}) - indices = [ - int(v.get("index", 0)) - for v in entries.values() - if v.get("type") == EntryType.SSH.value - ] - return (max(indices) + 1) if indices else 0 - - def add_ssh_key(self, *, index: int | None = None, notes: str = "") -> int: - """Add an SSH key entry and return the entry id.""" - entry_id = self.get_next_index() + def add_ssh_key(self, notes: str = "") -> int: + """Placeholder for adding an SSH key entry.""" + index = self.get_next_index() data = self.vault.load_index() data.setdefault("entries", {}) - if index is None: - index = self.get_next_ssh_index() - data["entries"][str(entry_id)] = { - "type": EntryType.SSH.value, - "index": index, - "notes": notes, - } + data["entries"][str(index)] = {"type": EntryType.SSH.value, "notes": notes} self._save_index(data) self.update_checksum() self.backup_manager.create_backup() - return entry_id + raise NotImplementedError("SSH key entry support not implemented yet") - def get_next_seed_index(self) -> int: - """Return the next available derivation index for seed phrases.""" - data = self.vault.load_index() - entries = data.get("entries", {}) - indices = [ - int(v.get("index", 0)) - for v in entries.values() - if v.get("type") == EntryType.SEED.value - ] - return (max(indices) + 1) if indices else 0 - - def add_seed( - self, - *, - index: int | None = None, - words: int = 24, - notes: str = "", - ) -> int: - """Add a BIP-39 seed phrase entry and return the entry id.""" - entry_id = self.get_next_index() + def add_seed(self, notes: str = "") -> int: + """Placeholder for adding a seed entry.""" + index = self.get_next_index() data = self.vault.load_index() data.setdefault("entries", {}) - if index is None: - index = self.get_next_seed_index() - data["entries"][str(entry_id)] = { - "type": EntryType.SEED.value, - "index": index, - "words": words, - "notes": notes, - } + data["entries"][str(index)] = {"type": EntryType.SEED.value, "notes": notes} self._save_index(data) self.update_checksum() self.backup_manager.create_backup() - return entry_id + raise NotImplementedError("Seed entry support not implemented yet") def get_totp_code( self, index: int, parent_seed: str | None = None, timestamp: int | None = None diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 0e3c1bf..c20e0aa 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -22,11 +22,7 @@ from termcolor import colored from password_manager.encryption import EncryptionManager from password_manager.entry_management import EntryManager -from password_manager.password_generation import ( - PasswordGenerator, - derive_ssh_key, - derive_seed_phrase, -) +from password_manager.password_generation import PasswordGenerator from password_manager.backup import BackupManager from password_manager.vault import Vault from password_manager.portable_backup import export_backup, import_backup @@ -1025,83 +1021,6 @@ class PasswordManager: logging.error(f"Error during TOTP setup: {e}", exc_info=True) print(colored(f"Error: Failed to add TOTP: {e}", "red")) - def handle_add_ssh_key(self) -> None: - """Add a new SSH key entry derived from the seed.""" - try: - notes = input("Enter notes (optional): ").strip() - ssh_index = self.entry_manager.get_next_ssh_index() - entry_id = self.entry_manager.add_ssh_key(index=ssh_index, notes=notes) - self.is_dirty = True - self.last_update = time.time() - key_hex = derive_ssh_key(self.password_generator.bip85, ssh_index).hex() - print(colored(f"\n[+] SSH key entry added with ID {entry_id}.", "green")) - print(colored(f"Derivation index: {ssh_index}", "cyan")) - if self.secret_mode_enabled: - copy_to_clipboard(key_hex, self.clipboard_clear_delay) - print( - colored( - f"SSH key copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.", - "green", - ) - ) - else: - print(colored(f"SSH key (hex): {key_hex}", "yellow")) - try: - self.sync_vault() - except Exception as nostr_error: - logging.error( - f"Failed to post updated index to Nostr: {nostr_error}", - exc_info=True, - ) - except Exception as e: - logging.error(f"Error during SSH key generation: {e}", exc_info=True) - print(colored(f"Error: Failed to add SSH key: {e}", "red")) - - def handle_add_seed(self) -> None: - """Add a new BIP-39 seed phrase entry derived from the seed.""" - try: - words_input = input("Number of words (12 or 24, default 24): ").strip() - notes = input("Enter notes (optional): ").strip() - words = 24 - if words_input: - if words_input not in {"12", "24"}: - print(colored("Invalid word count. Use 12 or 24.", "red")) - return - words = int(words_input) - seed_index = self.entry_manager.get_next_seed_index() - entry_id = self.entry_manager.add_seed( - index=seed_index, words=words, notes=notes - ) - self.is_dirty = True - self.last_update = time.time() - phrase = derive_seed_phrase( - self.password_generator.bip85, seed_index, words - ) - print( - colored(f"\n[+] Seed phrase entry added with ID {entry_id}.", "green") - ) - print(colored(f"Derivation index: {seed_index}", "cyan")) - if self.secret_mode_enabled: - copy_to_clipboard(phrase, self.clipboard_clear_delay) - print( - colored( - f"Seed phrase copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.", - "green", - ) - ) - else: - print(colored(f"Seed phrase: {phrase}", "yellow")) - try: - self.sync_vault() - except Exception as nostr_error: - logging.error( - f"Failed to post updated index to Nostr: {nostr_error}", - exc_info=True, - ) - except Exception as e: - logging.error(f"Error during seed phrase generation: {e}", exc_info=True) - print(colored(f"Error: Failed to add seed phrase: {e}", "red")) - def handle_retrieve_entry(self) -> None: """ Handles retrieving a password from the index by prompting the user for the index number @@ -1175,57 +1094,6 @@ class PasswordManager: print(colored(f"Error: Failed to generate TOTP code: {e}", "red")) return - elif entry_type == EntryType.SSH.value: - ssh_index = int(entry.get("index", 0)) - notes = entry.get("notes", "") - ssh_key = derive_ssh_key(self.password_generator.bip85, ssh_index).hex() - print( - colored( - f"Retrieving SSH key derived from index {ssh_index}.", "cyan" - ) - ) - if self.secret_mode_enabled: - copy_to_clipboard(ssh_key, self.clipboard_clear_delay) - print( - colored( - f"[+] SSH key copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.", - "green", - ) - ) - else: - print(colored("\n[+] Retrieved SSH Key:\n", "green")) - print(colored(ssh_key, "yellow")) - if notes: - print(colored(f"Notes: {notes}", "cyan")) - return - elif entry_type == EntryType.SEED.value: - seed_index = int(entry.get("index", 0)) - words = int(entry.get("words", 24)) - notes = entry.get("notes", "") - phrase = derive_seed_phrase( - self.password_generator.bip85, seed_index, words - ) - print( - colored( - f"Retrieving {words}-word seed derived from index {seed_index}.", - "cyan", - ) - ) - if self.secret_mode_enabled: - copy_to_clipboard(phrase, self.clipboard_clear_delay) - print( - colored( - f"[+] Seed phrase copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.", - "green", - ) - ) - else: - print(colored("\n[+] Retrieved Seed Phrase:\n", "green")) - print(colored(phrase, "yellow")) - if notes: - print(colored(f"Notes: {notes}", "cyan")) - return - website_name = entry.get("website") length = entry.get("length") username = entry.get("username") diff --git a/src/tests/test_cli_invalid_input.py b/src/tests/test_cli_invalid_input.py index ec225fc..df64494 100644 --- a/src/tests/test_cli_invalid_input.py +++ b/src/tests/test_cli_invalid_input.py @@ -77,7 +77,7 @@ def test_out_of_range_menu(monkeypatch, capsys): def test_invalid_add_entry_submenu(monkeypatch, capsys): called = {"add": False, "retrieve": False, "modify": False} pm, _ = _make_pm(called) - inputs = iter(["1", "6", "5", "7"]) + inputs = iter(["1", "4", "3", "7"]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) with pytest.raises(SystemExit): diff --git a/src/tests/test_entry_add.py b/src/tests/test_entry_add.py index 72e91e2..b1f625d 100644 --- a/src/tests/test_entry_add.py +++ b/src/tests/test_entry_add.py @@ -60,10 +60,10 @@ def test_round_trip_entry_types(method, expected_type): elif method == "add_totp": entry_mgr.add_totp("example", TEST_SEED) index = 0 - elif method == "add_ssh_key": - index = entry_mgr.add_ssh_key() - elif method == "add_seed": - index = entry_mgr.add_seed() + else: + with pytest.raises(NotImplementedError): + getattr(entry_mgr, method)() + index = 0 entry = entry_mgr.retrieve_entry(index) assert entry["type"] == expected_type diff --git a/src/tests/test_manager_retrieve_ssh_seed.py b/src/tests/test_manager_retrieve_ssh_seed.py deleted file mode 100644 index a968b73..0000000 --- a/src/tests/test_manager_retrieve_ssh_seed.py +++ /dev/null @@ -1,75 +0,0 @@ -import sys -from pathlib import Path -from tempfile import TemporaryDirectory -from types import SimpleNamespace - -from helpers import create_vault, TEST_SEED, TEST_PASSWORD - -sys.path.append(str(Path(__file__).resolve().parents[1])) - -from password_manager.entry_management import EntryManager -from password_manager.backup import BackupManager -from password_manager.manager import PasswordManager, EncryptionMode -from password_manager.config_manager import ConfigManager - - -class FakeNostrClient: - def __init__(self, *args, **kwargs): - self.published = [] - - def publish_snapshot(self, data: bytes): - self.published.append(data) - return None, "abcd" - - -def _setup(tmp_path: Path) -> PasswordManager: - vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) - cfg_mgr = ConfigManager(vault, tmp_path) - backup_mgr = BackupManager(tmp_path, cfg_mgr) - entry_mgr = EntryManager(vault, backup_mgr) - - pm = PasswordManager.__new__(PasswordManager) - pm.encryption_mode = EncryptionMode.SEED_ONLY - pm.encryption_manager = enc_mgr - pm.vault = vault - pm.entry_manager = entry_mgr - pm.backup_manager = backup_mgr - pm.parent_seed = TEST_SEED - pm.nostr_client = FakeNostrClient() - pm.fingerprint_dir = tmp_path - pm.is_dirty = False - pm.secret_mode_enabled = False - pm.password_generator = SimpleNamespace(bip85=object()) - return pm - - -def test_retrieve_ssh(monkeypatch, capsys): - with TemporaryDirectory() as tmpdir: - tmp_path = Path(tmpdir) - pm = _setup(tmp_path) - idx = pm.entry_manager.add_ssh_key() - monkeypatch.setattr( - "password_manager.manager.derive_ssh_key", - lambda b, i: bytes.fromhex("11" * 32), - ) - monkeypatch.setattr("builtins.input", lambda *a, **k: str(idx)) - pm.handle_retrieve_entry() - out = capsys.readouterr().out - assert "SSH key" in out - assert "11" * 32 in out - - -def test_retrieve_seed(monkeypatch, capsys): - with TemporaryDirectory() as tmpdir: - tmp_path = Path(tmpdir) - pm = _setup(tmp_path) - idx = pm.entry_manager.add_seed() - monkeypatch.setattr( - "password_manager.manager.derive_seed_phrase", - lambda b, i, w: "word " * w, - ) - monkeypatch.setattr("builtins.input", lambda *a, **k: str(idx)) - pm.handle_retrieve_entry() - out = capsys.readouterr().out - assert "Seed Phrase" in out or "seed" in out.lower() - assert "word" in out