Merge pull request #82 from PR0M3TH3AN/codex/backup-to-nostr-on-master-password-change

Auto backup after password change
This commit is contained in:
thePR0M3TH3AN
2025-07-01 10:18:18 -04:00
committed by GitHub
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"))
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"))

View File

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

View File

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

View File

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

View File

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

View File

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