From 92cbaace1f0ca75c1f84cbc82164994b30f5ecda Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:17:14 -0400 Subject: [PATCH] Backup to Nostr after password change --- src/main.py | 7 +++++-- src/nostr/client.py | 28 +++++++++++++++++++++------- src/password_manager/manager.py | 18 +++++++++++++++--- src/tests/test_password_change.py | 4 ++-- src/tests/test_post_sync_messages.py | 8 ++++++-- src/tests/test_settings_menu.py | 2 +- 6 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/main.py b/src/main.py index a60adee..964d402 100644 --- a/src/main.py +++ b/src/main.py @@ -206,7 +206,9 @@ def handle_display_npub(password_manager: PasswordManager): print(colored(f"Error: Failed to display npub: {e}", "red")) -def handle_post_to_nostr(password_manager: PasswordManager): +def handle_post_to_nostr( + password_manager: PasswordManager, alt_summary: str | None = None +): """ Handles the action of posting the encrypted password index to Nostr. """ @@ -216,7 +218,8 @@ def handle_post_to_nostr(password_manager: PasswordManager): if encrypted_data: # Post to Nostr success = password_manager.nostr_client.publish_json_to_nostr( - encrypted_data + encrypted_data, + alt_summary=alt_summary, ) if success: print(colored("\N{WHITE HEAVY CHECK MARK} Sync complete.", "green")) diff --git a/src/nostr/client.py b/src/nostr/client.py index d139aef..d4872b9 100644 --- a/src/nostr/client.py +++ b/src/nostr/client.py @@ -16,6 +16,7 @@ from nostr_sdk import ( Filter, Kind, KindStandard, + Tag, ) from datetime import timedelta @@ -86,9 +87,23 @@ class NostrClient: logger.info(f"NostrClient connected to relays: {self.relays}") def publish_json_to_nostr( - self, encrypted_json: bytes, to_pubkey: str | None = None + self, + encrypted_json: bytes, + to_pubkey: str | None = None, + alt_summary: str | None = None, ) -> bool: - """Builds and publishes a Kind 1 text note or direct message.""" + """Builds and publishes a Kind 1 text note or direct message. + + Parameters + ---------- + encrypted_json : bytes + The encrypted index data to publish. + to_pubkey : str | None, optional + If provided, send as a direct message to this public key. + alt_summary : str | None, optional + If provided, include an ``alt`` tag so uploads can be + associated with a specific event like a password change. + """ try: content = base64.b64encode(encrypted_json).decode("utf-8") @@ -98,11 +113,10 @@ class NostrClient: self.relays, receiver, content ) else: - event = ( - EventBuilder.text_note(content) - .build(self.keys.public_key()) - .sign_with_keys(self.keys) - ) + builder = EventBuilder.text_note(content) + if alt_summary: + builder = builder.tags([Tag.alt(alt_summary)]) + event = builder.build(self.keys.public_key()).sign_with_keys(self.keys) event_output = self.publish_event(event) event_id_hex = ( diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 98a657c..8fea00a 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -1318,9 +1318,21 @@ class PasswordManager: print(colored("Master password changed successfully.", "green")) - # All data has been re-encrypted with the new password. Since no - # entries changed, avoid pushing the database to Nostr here. - # Subsequent entry modifications will trigger a push when needed. + # Push a fresh backup to Nostr so the newly encrypted index is + # stored remotely. Include a tag to mark the password change. + try: + encrypted_data = self.get_encrypted_data() + if encrypted_data: + summary = f"password-change-{int(time.time())}" + self.nostr_client.publish_json_to_nostr( + encrypted_data, + alt_summary=summary, + ) + except Exception as nostr_error: + logging.error( + f"Failed to post updated index to Nostr after password change: {nostr_error}" + ) + logging.error(traceback.format_exc()) except Exception as e: logging.error(f"Failed to change password: {e}") logging.error(traceback.format_exc()) diff --git a/src/tests/test_password_change.py b/src/tests/test_password_change.py index a06f55b..d986b37 100644 --- a/src/tests/test_password_change.py +++ b/src/tests/test_password_change.py @@ -15,7 +15,7 @@ from password_manager.vault import Vault from password_manager.manager import PasswordManager -def test_change_password_does_not_trigger_nostr_backup(monkeypatch): +def test_change_password_triggers_nostr_backup(monkeypatch): with TemporaryDirectory() as tmpdir: fp = Path(tmpdir) enc_mgr = EncryptionManager(Fernet.generate_key(), fp) @@ -46,4 +46,4 @@ def test_change_password_does_not_trigger_nostr_backup(monkeypatch): mock_instance = MockClient.return_value pm.nostr_client = mock_instance pm.change_password() - mock_instance.publish_json_to_nostr.assert_not_called() + mock_instance.publish_json_to_nostr.assert_called_once() diff --git a/src/tests/test_post_sync_messages.py b/src/tests/test_post_sync_messages.py index e13fe8f..05b2194 100644 --- a/src/tests/test_post_sync_messages.py +++ b/src/tests/test_post_sync_messages.py @@ -10,7 +10,9 @@ import main def test_handle_post_success(capsys): pm = SimpleNamespace( get_encrypted_data=lambda: b"data", - nostr_client=SimpleNamespace(publish_json_to_nostr=lambda data: True), + nostr_client=SimpleNamespace( + publish_json_to_nostr=lambda data, alt_summary=None: True + ), ) main.handle_post_to_nostr(pm) out = capsys.readouterr().out @@ -20,7 +22,9 @@ def test_handle_post_success(capsys): def test_handle_post_failure(capsys): pm = SimpleNamespace( get_encrypted_data=lambda: b"data", - nostr_client=SimpleNamespace(publish_json_to_nostr=lambda data: False), + nostr_client=SimpleNamespace( + publish_json_to_nostr=lambda data, alt_summary=None: False + ), ) main.handle_post_to_nostr(pm) out = capsys.readouterr().out diff --git a/src/tests/test_settings_menu.py b/src/tests/test_settings_menu.py index 57cca6a..c02f22b 100644 --- a/src/tests/test_settings_menu.py +++ b/src/tests/test_settings_menu.py @@ -35,7 +35,7 @@ def setup_pm(tmp_path, monkeypatch): relays=list(DEFAULT_RELAYS), close_client_pool=lambda: None, initialize_client_pool=lambda: None, - publish_json_to_nostr=lambda data: None, + publish_json_to_nostr=lambda data, alt_summary=None: None, key_manager=SimpleNamespace(get_npub=lambda: "npub"), )