Backup to Nostr after password change

This commit is contained in:
thePR0M3TH3AN
2025-07-01 10:17:14 -04:00
parent b4eca8e958
commit 92cbaace1f
6 changed files with 50 additions and 17 deletions

View File

@@ -206,7 +206,9 @@ def handle_display_npub(password_manager: PasswordManager):
print(colored(f"Error: Failed to display npub: {e}", "red")) 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. 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: if encrypted_data:
# Post to Nostr # Post to Nostr
success = password_manager.nostr_client.publish_json_to_nostr( success = password_manager.nostr_client.publish_json_to_nostr(
encrypted_data encrypted_data,
alt_summary=alt_summary,
) )
if success: if success:
print(colored("\N{WHITE HEAVY CHECK MARK} Sync complete.", "green")) print(colored("\N{WHITE HEAVY CHECK MARK} Sync complete.", "green"))

View File

@@ -16,6 +16,7 @@ from nostr_sdk import (
Filter, Filter,
Kind, Kind,
KindStandard, KindStandard,
Tag,
) )
from datetime import timedelta from datetime import timedelta
@@ -86,9 +87,23 @@ class NostrClient:
logger.info(f"NostrClient connected to relays: {self.relays}") logger.info(f"NostrClient connected to relays: {self.relays}")
def publish_json_to_nostr( 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: ) -> 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: try:
content = base64.b64encode(encrypted_json).decode("utf-8") content = base64.b64encode(encrypted_json).decode("utf-8")
@@ -98,11 +113,10 @@ class NostrClient:
self.relays, receiver, content self.relays, receiver, content
) )
else: else:
event = ( builder = EventBuilder.text_note(content)
EventBuilder.text_note(content) if alt_summary:
.build(self.keys.public_key()) builder = builder.tags([Tag.alt(alt_summary)])
.sign_with_keys(self.keys) event = builder.build(self.keys.public_key()).sign_with_keys(self.keys)
)
event_output = self.publish_event(event) event_output = self.publish_event(event)
event_id_hex = ( event_id_hex = (

View File

@@ -1318,9 +1318,21 @@ class PasswordManager:
print(colored("Master password changed successfully.", "green")) print(colored("Master password changed successfully.", "green"))
# All data has been re-encrypted with the new password. Since no # Push a fresh backup to Nostr so the newly encrypted index is
# entries changed, avoid pushing the database to Nostr here. # stored remotely. Include a tag to mark the password change.
# Subsequent entry modifications will trigger a push when needed. 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: except Exception as e:
logging.error(f"Failed to change password: {e}") logging.error(f"Failed to change password: {e}")
logging.error(traceback.format_exc()) logging.error(traceback.format_exc())

View File

@@ -15,7 +15,7 @@ from password_manager.vault import Vault
from password_manager.manager import PasswordManager 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: with TemporaryDirectory() as tmpdir:
fp = Path(tmpdir) fp = Path(tmpdir)
enc_mgr = EncryptionManager(Fernet.generate_key(), fp) 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 mock_instance = MockClient.return_value
pm.nostr_client = mock_instance pm.nostr_client = mock_instance
pm.change_password() pm.change_password()
mock_instance.publish_json_to_nostr.assert_not_called() mock_instance.publish_json_to_nostr.assert_called_once()

View File

@@ -10,7 +10,9 @@ import main
def test_handle_post_success(capsys): def test_handle_post_success(capsys):
pm = SimpleNamespace( pm = SimpleNamespace(
get_encrypted_data=lambda: b"data", 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) main.handle_post_to_nostr(pm)
out = capsys.readouterr().out out = capsys.readouterr().out
@@ -20,7 +22,9 @@ def test_handle_post_success(capsys):
def test_handle_post_failure(capsys): def test_handle_post_failure(capsys):
pm = SimpleNamespace( pm = SimpleNamespace(
get_encrypted_data=lambda: b"data", 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) main.handle_post_to_nostr(pm)
out = capsys.readouterr().out out = capsys.readouterr().out

View File

@@ -35,7 +35,7 @@ def setup_pm(tmp_path, monkeypatch):
relays=list(DEFAULT_RELAYS), relays=list(DEFAULT_RELAYS),
close_client_pool=lambda: None, close_client_pool=lambda: None,
initialize_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"), key_manager=SimpleNamespace(get_npub=lambda: "npub"),
) )