From e95c60c9d95bc5934355ead22ae83ba70a45b182 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:16:13 -0400 Subject: [PATCH] Document Nostr key QR codes --- README.md | 3 ++ src/password_manager/manager.py | 12 +++++++ src/tests/test_nostr_qr.py | 60 +++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/tests/test_nostr_qr.py diff --git a/README.md b/README.md index 519c841..4026271 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,9 @@ SeedPass supports storing more than just passwords and 2FA secrets. You can also - **Seed Phrase** – generate a BIP-39 mnemonic and keep it encrypted until needed. - **PGP Key** – derive an OpenPGP key pair from your master seed. - **Nostr Key Pair** – store the index used to derive an `npub`/`nsec` pair for Nostr clients. + When you retrieve one of these entries, SeedPass can display QR codes for the + keys. The `npub` is wrapped in the `nostr:` URI scheme so any client can scan + it, while the `nsec` QR is shown only after a security warning. ### Managing Multiple Seeds diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 06b7726..522f366 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -1147,6 +1147,12 @@ class PasswordManager: ) else: print(colored(f"nsec: {nsec}", "cyan")) + if confirm_action("Show QR code for npub? (Y/N): "): + TotpManager.print_qr_code(f"nostr:{npub}") + if confirm_action( + "WARNING: Displaying the nsec QR reveals your private key. Continue? (Y/N): " + ): + TotpManager.print_qr_code(nsec) try: self.sync_vault() except Exception as nostr_error: # pragma: no cover - best effort @@ -1342,6 +1348,12 @@ class PasswordManager: ) else: print(colored(f"nsec: {nsec}", "cyan")) + if confirm_action("Show QR code for npub? (Y/N): "): + TotpManager.print_qr_code(f"nostr:{npub}") + if confirm_action( + "WARNING: Displaying the nsec QR reveals your private key. Continue? (Y/N): " + ): + TotpManager.print_qr_code(nsec) if notes: print(colored(f"Notes: {notes}", "cyan")) except Exception as e: diff --git a/src/tests/test_nostr_qr.py b/src/tests/test_nostr_qr.py new file mode 100644 index 0000000..fe8cc84 --- /dev/null +++ b/src/tests/test_nostr_qr.py @@ -0,0 +1,60 @@ +import sys +from pathlib import Path +from tempfile import TemporaryDirectory + +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, TotpManager +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 test_show_qr_for_nostr_keys(monkeypatch): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + 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 + + idx = entry_mgr.add_nostr_key("main") + npub, _ = entry_mgr.get_nostr_key_pair(idx, TEST_SEED) + + monkeypatch.setattr("builtins.input", lambda *a, **k: str(idx)) + responses = iter([True, False]) + monkeypatch.setattr( + "password_manager.manager.confirm_action", + lambda *_a, **_k: next(responses), + ) + called = [] + monkeypatch.setattr( + "password_manager.manager.TotpManager.print_qr_code", + lambda data: called.append(data), + ) + + pm.handle_retrieve_entry() + assert called == [f"nostr:{npub}"]