From f664a6c40faa5f62d3ec04bc44772308ead83ead Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 3 Aug 2025 11:41:06 -0400 Subject: [PATCH] fix: migrate legacy nostr payloads --- src/seedpass/core/manager.py | 12 ++++++++ src/seedpass/core/vault.py | 7 ++++- src/tests/test_legacy_migration.py | 46 ++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index 659b9cb..7861e82 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -1255,12 +1255,14 @@ class PasswordManager: encrypted = gzip.decompress(b"".join(chunks)) current = self.vault.get_encrypted_index() updated = False + migrated = False if current != encrypted: if self.vault.decrypt_and_save_index_from_nostr( encrypted, strict=False, merge=False ): updated = True current = encrypted + migrated = migrated or self.vault.migrated_from_legacy if manifest.delta_since: version = int(manifest.delta_since) deltas = await self.nostr_client.fetch_deltas_since(version) @@ -1271,6 +1273,9 @@ class PasswordManager: ): updated = True current = delta + migrated = migrated or self.vault.migrated_from_legacy + if migrated and not getattr(self, "offline_mode", False): + self.start_background_vault_sync() if updated: logger.info("Local database synchronized from Nostr.") except Exception as e: @@ -1403,11 +1408,13 @@ class PasswordManager: if result: manifest, chunks = result encrypted = gzip.decompress(b"".join(chunks)) + migrated = False success = self.vault.decrypt_and_save_index_from_nostr( encrypted, strict=False, merge=False ) if success: have_data = True + migrated = migrated or self.vault.migrated_from_legacy current = encrypted if manifest.delta_since: version = int(manifest.delta_since) @@ -1418,6 +1425,11 @@ class PasswordManager: delta, strict=False, merge=True ): current = delta + migrated = ( + migrated or self.vault.migrated_from_legacy + ) + if migrated and not getattr(self, "offline_mode", False): + self.start_background_vault_sync() logger.info("Initialized local database from Nostr.") except Exception as e: # pragma: no cover - network errors logger.warning(f"Unable to sync index from Nostr: {e}") diff --git a/src/seedpass/core/vault.py b/src/seedpass/core/vault.py index 5825995..17eee51 100644 --- a/src/seedpass/core/vault.py +++ b/src/seedpass/core/vault.py @@ -25,6 +25,7 @@ class Vault: self.fingerprint_dir = Path(fingerprint_dir) self.index_file = self.fingerprint_dir / self.INDEX_FILENAME self.config_file = self.fingerprint_dir / self.CONFIG_FILENAME + self.migrated_from_legacy = False def set_encryption_manager(self, manager: EncryptionManager) -> None: """Replace the internal encryption manager.""" @@ -97,9 +98,13 @@ class Vault: self, encrypted_data: bytes, *, strict: bool = True, merge: bool = False ) -> bool: """Decrypt Nostr payload and update the local index.""" - return self.encryption_manager.decrypt_and_save_index_from_nostr( + self.migrated_from_legacy = not encrypted_data.startswith(b"V2:") + result = self.encryption_manager.decrypt_and_save_index_from_nostr( encrypted_data, strict=strict, merge=merge ) + if not result: + self.migrated_from_legacy = False + return result # ----- Config helpers ----- def load_config(self) -> dict: diff --git a/src/tests/test_legacy_migration.py b/src/tests/test_legacy_migration.py index 3c97368..a81d446 100644 --- a/src/tests/test_legacy_migration.py +++ b/src/tests/test_legacy_migration.py @@ -6,6 +6,8 @@ from helpers import create_vault, TEST_SEED, TEST_PASSWORD from utils.key_derivation import derive_index_key from cryptography.fernet import Fernet from types import SimpleNamespace +import asyncio +import gzip from seedpass.core.manager import PasswordManager, EncryptionMode from seedpass.core.vault import Vault @@ -81,3 +83,47 @@ def test_migration_triggers_sync(monkeypatch, tmp_path: Path): pm.initialize_managers() assert calls["sync"] == 1 + + +def test_legacy_nostr_payload_triggers_sync(monkeypatch, tmp_path: Path): + vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + + key = derive_index_key(TEST_SEED) + data = {"schema_version": 4, "entries": {}} + legacy_enc = Fernet(key).encrypt(json.dumps(data).encode()) + compressed = gzip.compress(legacy_enc) + + class DummyClient: + def __init__(self): + self.relays = [] + self.last_error = None + self.fingerprint = None + + async def fetch_latest_snapshot(self): + from nostr.backup_models import Manifest + + return Manifest(ver=1, algo="gzip", chunks=[], delta_since=None), [ + compressed + ] + + async def fetch_deltas_since(self, version): + return [] + + pm = PasswordManager.__new__(PasswordManager) + pm.encryption_mode = EncryptionMode.SEED_ONLY + pm.encryption_manager = enc_mgr + pm.vault = Vault(enc_mgr, tmp_path) + pm.parent_seed = TEST_SEED + pm.fingerprint_dir = tmp_path + pm.current_fingerprint = tmp_path.name + pm.nostr_client = DummyClient() + pm.offline_mode = False + + calls = {"sync": 0} + pm.start_background_vault_sync = lambda *a, **k: calls.__setitem__( + "sync", calls["sync"] + 1 + ) + + asyncio.run(pm.sync_index_from_nostr_async()) + assert calls["sync"] == 1 + assert pm.vault.load_index() == data