mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 07:48:57 +00:00
Merge pull request #149 from PR0M3TH3AN/codex/refactor-sync-and-vault-update-logic
Implement vault sync with Nostr snapshots
This commit is contained in:
44
src/main.py
44
src/main.py
@@ -7,6 +7,8 @@ import signal
|
||||
import getpass
|
||||
import time
|
||||
import argparse
|
||||
import asyncio
|
||||
import gzip
|
||||
import tomli
|
||||
from colorama import init as colorama_init
|
||||
from termcolor import colored
|
||||
@@ -225,23 +227,13 @@ def handle_post_to_nostr(
|
||||
Handles the action of posting the encrypted password index to Nostr.
|
||||
"""
|
||||
try:
|
||||
# Get the encrypted data from the index file
|
||||
encrypted_data = password_manager.get_encrypted_data()
|
||||
if encrypted_data:
|
||||
# Post to Nostr
|
||||
success = password_manager.nostr_client.publish_json_to_nostr(
|
||||
encrypted_data,
|
||||
alt_summary=alt_summary,
|
||||
)
|
||||
if success:
|
||||
print(colored("\N{WHITE HEAVY CHECK MARK} Sync complete.", "green"))
|
||||
logging.info("Encrypted index posted to Nostr successfully.")
|
||||
else:
|
||||
print(colored("\N{CROSS MARK} Sync failed…", "red"))
|
||||
logging.error("Failed to post encrypted index to Nostr.")
|
||||
success = password_manager.sync_vault(alt_summary=alt_summary)
|
||||
if success:
|
||||
print(colored("\N{WHITE HEAVY CHECK MARK} Sync complete.", "green"))
|
||||
logging.info("Encrypted index posted to Nostr successfully.")
|
||||
else:
|
||||
print(colored("No data available to post.", "yellow"))
|
||||
logging.warning("No data available to post to Nostr.")
|
||||
print(colored("\N{CROSS MARK} Sync failed…", "red"))
|
||||
logging.error("Failed to post encrypted index to Nostr.")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to post to Nostr: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to post to Nostr: {e}", "red"))
|
||||
@@ -252,12 +244,22 @@ def handle_retrieve_from_nostr(password_manager: PasswordManager):
|
||||
Handles the action of retrieving the encrypted password index from Nostr.
|
||||
"""
|
||||
try:
|
||||
# Use the Nostr client from the password_manager
|
||||
encrypted_data = password_manager.nostr_client.retrieve_json_from_nostr_sync()
|
||||
if encrypted_data:
|
||||
# Decrypt and save the index
|
||||
result = asyncio.run(password_manager.nostr_client.fetch_latest_snapshot())
|
||||
if result:
|
||||
manifest, chunks = result
|
||||
encrypted = gzip.decompress(b"".join(chunks))
|
||||
if manifest.delta_since:
|
||||
try:
|
||||
version = int(manifest.delta_since)
|
||||
deltas = asyncio.run(
|
||||
password_manager.nostr_client.fetch_deltas_since(version)
|
||||
)
|
||||
if deltas:
|
||||
encrypted = deltas[-1]
|
||||
except ValueError:
|
||||
pass
|
||||
password_manager.encryption_manager.decrypt_and_save_index_from_nostr(
|
||||
encrypted_data
|
||||
encrypted
|
||||
)
|
||||
print(colored("Encrypted index retrieved and saved successfully.", "green"))
|
||||
logging.info("Encrypted index retrieved and saved successfully from Nostr.")
|
||||
|
@@ -55,6 +55,8 @@ from constants import (
|
||||
)
|
||||
|
||||
import traceback
|
||||
import asyncio
|
||||
import gzip
|
||||
import bcrypt
|
||||
from pathlib import Path
|
||||
|
||||
@@ -812,8 +814,20 @@ class PasswordManager:
|
||||
if index_file.exists():
|
||||
return
|
||||
try:
|
||||
encrypted = self.nostr_client.retrieve_json_from_nostr_sync()
|
||||
if encrypted:
|
||||
result = asyncio.run(self.nostr_client.fetch_latest_snapshot())
|
||||
if result:
|
||||
manifest, chunks = result
|
||||
encrypted = gzip.decompress(b"".join(chunks))
|
||||
if manifest.delta_since:
|
||||
try:
|
||||
version = int(manifest.delta_since)
|
||||
deltas = asyncio.run(
|
||||
self.nostr_client.fetch_deltas_since(version)
|
||||
)
|
||||
if deltas:
|
||||
encrypted = deltas[-1]
|
||||
except ValueError:
|
||||
pass
|
||||
self.vault.decrypt_and_save_index_from_nostr(encrypted)
|
||||
logger.info("Initialized local database from Nostr.")
|
||||
except Exception as e:
|
||||
@@ -871,12 +885,8 @@ class PasswordManager:
|
||||
# Automatically push the updated encrypted index to Nostr so the
|
||||
# latest changes are backed up remotely.
|
||||
try:
|
||||
encrypted_data = self.get_encrypted_data()
|
||||
if encrypted_data:
|
||||
self.nostr_client.publish_json_to_nostr(encrypted_data)
|
||||
logging.info(
|
||||
"Encrypted index posted to Nostr after entry addition."
|
||||
)
|
||||
self.sync_vault()
|
||||
logging.info("Encrypted index posted to Nostr after entry addition.")
|
||||
except Exception as nostr_error:
|
||||
logging.error(
|
||||
f"Failed to post updated index to Nostr: {nostr_error}",
|
||||
@@ -1040,12 +1050,10 @@ class PasswordManager:
|
||||
|
||||
# Push the updated index to Nostr so changes are backed up.
|
||||
try:
|
||||
encrypted_data = self.get_encrypted_data()
|
||||
if encrypted_data:
|
||||
self.nostr_client.publish_json_to_nostr(encrypted_data)
|
||||
logging.info(
|
||||
"Encrypted index posted to Nostr after entry modification."
|
||||
)
|
||||
self.sync_vault()
|
||||
logging.info(
|
||||
"Encrypted index posted to Nostr after entry modification."
|
||||
)
|
||||
except Exception as nostr_error:
|
||||
logging.error(
|
||||
f"Failed to post updated index to Nostr: {nostr_error}",
|
||||
@@ -1081,12 +1089,8 @@ class PasswordManager:
|
||||
|
||||
# Push updated index to Nostr after deletion
|
||||
try:
|
||||
encrypted_data = self.get_encrypted_data()
|
||||
if encrypted_data:
|
||||
self.nostr_client.publish_json_to_nostr(encrypted_data)
|
||||
logging.info(
|
||||
"Encrypted index posted to Nostr after entry deletion."
|
||||
)
|
||||
self.sync_vault()
|
||||
logging.info("Encrypted index posted to Nostr after entry deletion.")
|
||||
except Exception as nostr_error:
|
||||
logging.error(
|
||||
f"Failed to post updated index to Nostr: {nostr_error}",
|
||||
@@ -1172,6 +1176,27 @@ class PasswordManager:
|
||||
# Re-raise the exception to inform the calling function of the failure
|
||||
raise
|
||||
|
||||
def sync_vault(self, alt_summary: str | None = None) -> bool:
|
||||
"""Publish the current vault contents to Nostr."""
|
||||
try:
|
||||
encrypted = self.get_encrypted_data()
|
||||
if not encrypted:
|
||||
return False
|
||||
pub_snap = getattr(self.nostr_client, "publish_snapshot", None)
|
||||
if callable(pub_snap):
|
||||
if asyncio.iscoroutinefunction(pub_snap):
|
||||
asyncio.run(pub_snap(encrypted))
|
||||
else:
|
||||
pub_snap(encrypted)
|
||||
else:
|
||||
# Fallback for tests using simplified stubs
|
||||
self.nostr_client.publish_json_to_nostr(encrypted)
|
||||
self.is_dirty = False
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to sync vault: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def backup_database(self) -> None:
|
||||
"""
|
||||
Creates a backup of the encrypted JSON index file.
|
||||
@@ -1451,13 +1476,8 @@ class PasswordManager:
|
||||
# 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,
|
||||
)
|
||||
summary = f"password-change-{int(time.time())}"
|
||||
self.sync_vault(alt_summary=summary)
|
||||
except Exception as nostr_error:
|
||||
logging.error(
|
||||
f"Failed to post updated index to Nostr after password change: {nostr_error}"
|
||||
@@ -1501,13 +1521,8 @@ class PasswordManager:
|
||||
print(colored("Encryption mode changed successfully.", "green"))
|
||||
|
||||
try:
|
||||
encrypted_data = self.get_encrypted_data()
|
||||
if encrypted_data:
|
||||
summary = f"mode-change-{int(time.time())}"
|
||||
self.nostr_client.publish_json_to_nostr(
|
||||
encrypted_data,
|
||||
alt_summary=summary,
|
||||
)
|
||||
summary = f"mode-change-{int(time.time())}"
|
||||
self.sync_vault(alt_summary=summary)
|
||||
except Exception as nostr_error:
|
||||
logging.error(
|
||||
f"Failed to post updated index to Nostr after encryption mode change: {nostr_error}"
|
||||
|
@@ -8,6 +8,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
@@ -103,7 +104,7 @@ def export_backup(
|
||||
os.chmod(enc_file, 0o600)
|
||||
try:
|
||||
client = NostrClient(vault.encryption_manager, vault.fingerprint_dir.name)
|
||||
client.publish_json_to_nostr(encrypted)
|
||||
asyncio.run(client.publish_snapshot(encrypted))
|
||||
except Exception:
|
||||
logger.error("Failed to publish backup via Nostr", exc_info=True)
|
||||
|
||||
|
@@ -2,7 +2,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, AsyncMock
|
||||
|
||||
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
||||
|
||||
@@ -44,9 +44,10 @@ def test_change_encryption_mode(monkeypatch):
|
||||
|
||||
with patch("password_manager.manager.NostrClient") as MockClient:
|
||||
mock = MockClient.return_value
|
||||
mock.publish_snapshot = AsyncMock(return_value=None)
|
||||
pm.nostr_client = mock
|
||||
pm.change_encryption_mode(EncryptionMode.SEED_PLUS_PW)
|
||||
mock.publish_json_to_nostr.assert_called_once()
|
||||
mock.publish_snapshot.assert_called_once()
|
||||
|
||||
assert pm.encryption_mode is EncryptionMode.SEED_PLUS_PW
|
||||
assert pm.password_generator.encryption_manager is pm.encryption_manager
|
||||
|
@@ -60,7 +60,7 @@ def test_manager_workflow(monkeypatch):
|
||||
monkeypatch.setattr("builtins.input", lambda *args, **kwargs: next(inputs))
|
||||
|
||||
pm.handle_add_password()
|
||||
assert pm.is_dirty is True
|
||||
assert pm.is_dirty is False
|
||||
backups = list(tmp_path.glob("passwords_db_backup_*.json.enc"))
|
||||
assert len(backups) == 1
|
||||
checksum_file = tmp_path / "seedpass_passwords_db_checksum.txt"
|
||||
@@ -73,7 +73,7 @@ def test_manager_workflow(monkeypatch):
|
||||
assert pm.is_dirty is False
|
||||
|
||||
pm.handle_modify_entry()
|
||||
assert pm.is_dirty is True
|
||||
assert pm.is_dirty is False
|
||||
pm.backup_manager.create_backup()
|
||||
backup_dir = tmp_path / "backups"
|
||||
backups_mod = list(backup_dir.glob("passwords_db_backup_*.json.enc"))
|
||||
|
@@ -2,7 +2,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, AsyncMock
|
||||
|
||||
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
||||
|
||||
@@ -42,6 +42,7 @@ def test_change_password_triggers_nostr_backup(monkeypatch):
|
||||
|
||||
with patch("password_manager.manager.NostrClient") as MockClient:
|
||||
mock_instance = MockClient.return_value
|
||||
mock_instance.publish_snapshot = AsyncMock(return_value=None)
|
||||
pm.nostr_client = mock_instance
|
||||
pm.change_password()
|
||||
mock_instance.publish_json_to_nostr.assert_called_once()
|
||||
mock_instance.publish_snapshot.assert_called_once()
|
||||
|
@@ -9,10 +9,7 @@ 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, alt_summary=None: True
|
||||
),
|
||||
sync_vault=lambda alt_summary=None: True,
|
||||
)
|
||||
main.handle_post_to_nostr(pm)
|
||||
out = capsys.readouterr().out
|
||||
@@ -21,10 +18,7 @@ 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, alt_summary=None: False
|
||||
),
|
||||
sync_vault=lambda alt_summary=None: False,
|
||||
)
|
||||
main.handle_post_to_nostr(pm)
|
||||
out = capsys.readouterr().out
|
||||
|
Reference in New Issue
Block a user