Merge pull request #239 from PR0M3TH3AN/revert-238-codex/add-ui-elements-for-ssh-and-bip-39-seed

Revert "Add SSH and seed entry support"
This commit is contained in:
thePR0M3TH3AN
2025-07-04 17:05:32 -04:00
committed by GitHub
7 changed files with 18 additions and 276 deletions

View File

@@ -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.
- **AutoLock 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

View File

@@ -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"))

View File

@@ -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

View File

@@ -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")

View File

@@ -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):

View File

@@ -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

View File

@@ -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