mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Record manifest ID and timestamp
This commit is contained in:
@@ -42,7 +42,7 @@ SeedPass now uses the `portalocker` library for cross-platform file locking. No
|
|||||||
- **Deterministic Password Generation:** Utilize BIP-85 for generating deterministic and secure passwords.
|
- **Deterministic Password Generation:** Utilize BIP-85 for generating deterministic and secure passwords.
|
||||||
- **Encrypted Storage:** All seeds, login passwords, and sensitive index data are encrypted locally.
|
- **Encrypted Storage:** All seeds, login passwords, and sensitive index data are encrypted locally.
|
||||||
- **Nostr Integration:** Post and retrieve your encrypted password index to/from the Nostr network.
|
- **Nostr Integration:** Post and retrieve your encrypted password index to/from the Nostr network.
|
||||||
- **Chunked Snapshots:** Encrypted vaults are compressed and split into 50 KB chunks published as `kind 30071` events with a `kind 30070` manifest and `kind 30072` deltas.
|
- **Chunked Snapshots:** Encrypted vaults are compressed and split into 50 KB chunks published as `kind 30071` events with a `kind 30070` manifest and `kind 30072` deltas. The manifest's `delta_since` field records the UNIX timestamp of the most recent delta.
|
||||||
- **Automatic Checksum Generation:** The script generates and verifies a SHA-256 checksum to detect tampering.
|
- **Automatic Checksum Generation:** The script generates and verifies a SHA-256 checksum to detect tampering.
|
||||||
- **Multiple Seed Profiles:** Manage separate seed profiles and switch between them seamlessly.
|
- **Multiple Seed Profiles:** Manage separate seed profiles and switch between them seamlessly.
|
||||||
- **Nested Managed Account Seeds:** SeedPass can derive nested managed account seeds.
|
- **Nested Managed Account Seeds:** SeedPass can derive nested managed account seeds.
|
||||||
|
@@ -40,7 +40,7 @@ SeedPass now uses the `portalocker` library for cross-platform file locking. No
|
|||||||
- **Deterministic Password Generation:** Utilize BIP-85 for generating deterministic and secure passwords.
|
- **Deterministic Password Generation:** Utilize BIP-85 for generating deterministic and secure passwords.
|
||||||
- **Encrypted Storage:** All seeds, login passwords, and sensitive index data are encrypted locally.
|
- **Encrypted Storage:** All seeds, login passwords, and sensitive index data are encrypted locally.
|
||||||
- **Nostr Integration:** Post and retrieve your encrypted password index to/from the Nostr network.
|
- **Nostr Integration:** Post and retrieve your encrypted password index to/from the Nostr network.
|
||||||
- **Chunked Snapshots:** Encrypted vaults are compressed and split into 50 KB chunks published as `kind 30071` events with a `kind 30070` manifest and `kind 30072` deltas.
|
- **Chunked Snapshots:** Encrypted vaults are compressed and split into 50 KB chunks published as `kind 30071` events with a `kind 30070` manifest and `kind 30072` deltas. The manifest's `delta_since` field stores the UNIX timestamp of the most recent delta.
|
||||||
- **Automatic Checksum Generation:** The script generates and verifies a SHA-256 checksum to detect tampering.
|
- **Automatic Checksum Generation:** The script generates and verifies a SHA-256 checksum to detect tampering.
|
||||||
- **Multiple Seed Profiles:** Manage separate seed profiles and switch between them seamlessly.
|
- **Multiple Seed Profiles:** Manage separate seed profiles and switch between them seamlessly.
|
||||||
- **Nested Managed Account Seeds:** SeedPass can derive nested managed account seeds.
|
- **Nested Managed Account Seeds:** SeedPass can derive nested managed account seeds.
|
||||||
|
15
src/main.py
15
src/main.py
@@ -318,15 +318,12 @@ def handle_retrieve_from_nostr(password_manager: PasswordManager):
|
|||||||
manifest, chunks = result
|
manifest, chunks = result
|
||||||
encrypted = gzip.decompress(b"".join(chunks))
|
encrypted = gzip.decompress(b"".join(chunks))
|
||||||
if manifest.delta_since:
|
if manifest.delta_since:
|
||||||
try:
|
version = int(manifest.delta_since)
|
||||||
version = int(manifest.delta_since)
|
deltas = asyncio.run(
|
||||||
deltas = asyncio.run(
|
password_manager.nostr_client.fetch_deltas_since(version)
|
||||||
password_manager.nostr_client.fetch_deltas_since(version)
|
)
|
||||||
)
|
if deltas:
|
||||||
if deltas:
|
encrypted = deltas[-1]
|
||||||
encrypted = deltas[-1]
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
password_manager.encryption_manager.decrypt_and_save_index_from_nostr(
|
password_manager.encryption_manager.decrypt_and_save_index_from_nostr(
|
||||||
encrypted
|
encrypted
|
||||||
)
|
)
|
||||||
|
@@ -23,4 +23,4 @@ class Manifest:
|
|||||||
ver: int
|
ver: int
|
||||||
algo: str
|
algo: str
|
||||||
chunks: List[ChunkMeta]
|
chunks: List[ChunkMeta]
|
||||||
delta_since: Optional[str] = None
|
delta_since: Optional[int] = None
|
||||||
|
@@ -135,6 +135,7 @@ class NostrClient:
|
|||||||
|
|
||||||
self.delta_threshold = 100
|
self.delta_threshold = 100
|
||||||
self.current_manifest: Manifest | None = None
|
self.current_manifest: Manifest | None = None
|
||||||
|
self.current_manifest_id: str | None = None
|
||||||
self._delta_events: list[str] = []
|
self._delta_events: list[str] = []
|
||||||
|
|
||||||
# Configure and initialize the nostr-sdk Client
|
# Configure and initialize the nostr-sdk Client
|
||||||
@@ -388,6 +389,8 @@ class NostrClient:
|
|||||||
result = await self.client.send_event(manifest_event)
|
result = await self.client.send_event(manifest_event)
|
||||||
manifest_id = result.id.to_hex() if hasattr(result, "id") else str(result)
|
manifest_id = result.id.to_hex() if hasattr(result, "id") else str(result)
|
||||||
self.current_manifest = manifest
|
self.current_manifest = manifest
|
||||||
|
self.current_manifest_id = manifest_id
|
||||||
|
self.current_manifest.delta_since = int(time.time())
|
||||||
self._delta_events = []
|
self._delta_events = []
|
||||||
if getattr(self, "verbose_timing", False):
|
if getattr(self, "verbose_timing", False):
|
||||||
duration = time.perf_counter() - start
|
duration = time.perf_counter() - start
|
||||||
@@ -406,13 +409,18 @@ class NostrClient:
|
|||||||
events = (await self.client.fetch_events(f, timeout)).to_vec()
|
events = (await self.client.fetch_events(f, timeout)).to_vec()
|
||||||
if not events:
|
if not events:
|
||||||
return None
|
return None
|
||||||
manifest_raw = events[0].content()
|
manifest_event = events[0]
|
||||||
|
manifest_raw = manifest_event.content()
|
||||||
data = json.loads(manifest_raw)
|
data = json.loads(manifest_raw)
|
||||||
manifest = Manifest(
|
manifest = Manifest(
|
||||||
ver=data["ver"],
|
ver=data["ver"],
|
||||||
algo=data["algo"],
|
algo=data["algo"],
|
||||||
chunks=[ChunkMeta(**c) for c in data["chunks"]],
|
chunks=[ChunkMeta(**c) for c in data["chunks"]],
|
||||||
delta_since=data.get("delta_since"),
|
delta_since=(
|
||||||
|
int(data["delta_since"])
|
||||||
|
if data.get("delta_since") is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
chunks: list[bytes] = []
|
chunks: list[bytes] = []
|
||||||
@@ -433,6 +441,7 @@ class NostrClient:
|
|||||||
chunks.append(chunk_bytes)
|
chunks.append(chunk_bytes)
|
||||||
|
|
||||||
self.current_manifest = manifest
|
self.current_manifest = manifest
|
||||||
|
self.current_manifest_id = getattr(manifest_event, "id", None)
|
||||||
return manifest, chunks
|
return manifest, chunks
|
||||||
|
|
||||||
async def publish_delta(self, delta_bytes: bytes, manifest_id: str) -> str:
|
async def publish_delta(self, delta_bytes: bytes, manifest_id: str) -> str:
|
||||||
@@ -447,8 +456,28 @@ class NostrClient:
|
|||||||
event = builder.build(self.keys.public_key()).sign_with_keys(self.keys)
|
event = builder.build(self.keys.public_key()).sign_with_keys(self.keys)
|
||||||
result = await self.client.send_event(event)
|
result = await self.client.send_event(event)
|
||||||
delta_id = result.id.to_hex() if hasattr(result, "id") else str(result)
|
delta_id = result.id.to_hex() if hasattr(result, "id") else str(result)
|
||||||
|
created_at = getattr(
|
||||||
|
event, "created_at", getattr(event, "timestamp", int(time.time()))
|
||||||
|
)
|
||||||
|
if hasattr(created_at, "secs"):
|
||||||
|
created_at = created_at.secs
|
||||||
if self.current_manifest is not None:
|
if self.current_manifest is not None:
|
||||||
self.current_manifest.delta_since = delta_id
|
self.current_manifest.delta_since = int(created_at)
|
||||||
|
manifest_json = json.dumps(
|
||||||
|
{
|
||||||
|
"ver": self.current_manifest.ver,
|
||||||
|
"algo": self.current_manifest.algo,
|
||||||
|
"chunks": [meta.__dict__ for meta in self.current_manifest.chunks],
|
||||||
|
"delta_since": self.current_manifest.delta_since,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
manifest_event = (
|
||||||
|
EventBuilder(Kind(KIND_MANIFEST), manifest_json)
|
||||||
|
.tags([Tag.identifier(self.current_manifest_id)])
|
||||||
|
.build(self.keys.public_key())
|
||||||
|
.sign_with_keys(self.keys)
|
||||||
|
)
|
||||||
|
await self.client.send_event(manifest_event)
|
||||||
self._delta_events.append(delta_id)
|
self._delta_events.append(delta_id)
|
||||||
return delta_id
|
return delta_id
|
||||||
|
|
||||||
|
@@ -1022,13 +1022,10 @@ class PasswordManager:
|
|||||||
manifest, chunks = result
|
manifest, chunks = result
|
||||||
encrypted = gzip.decompress(b"".join(chunks))
|
encrypted = gzip.decompress(b"".join(chunks))
|
||||||
if manifest.delta_since:
|
if manifest.delta_since:
|
||||||
try:
|
version = int(manifest.delta_since)
|
||||||
version = int(manifest.delta_since)
|
deltas = asyncio.run(self.nostr_client.fetch_deltas_since(version))
|
||||||
deltas = asyncio.run(self.nostr_client.fetch_deltas_since(version))
|
if deltas:
|
||||||
if deltas:
|
encrypted = deltas[-1]
|
||||||
encrypted = deltas[-1]
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
current = self.vault.get_encrypted_index()
|
current = self.vault.get_encrypted_index()
|
||||||
if current != encrypted:
|
if current != encrypted:
|
||||||
self.vault.decrypt_and_save_index_from_nostr(encrypted)
|
self.vault.decrypt_and_save_index_from_nostr(encrypted)
|
||||||
@@ -1108,15 +1105,10 @@ class PasswordManager:
|
|||||||
manifest, chunks = result
|
manifest, chunks = result
|
||||||
encrypted = gzip.decompress(b"".join(chunks))
|
encrypted = gzip.decompress(b"".join(chunks))
|
||||||
if manifest.delta_since:
|
if manifest.delta_since:
|
||||||
try:
|
version = int(manifest.delta_since)
|
||||||
version = int(manifest.delta_since)
|
deltas = asyncio.run(self.nostr_client.fetch_deltas_since(version))
|
||||||
deltas = asyncio.run(
|
if deltas:
|
||||||
self.nostr_client.fetch_deltas_since(version)
|
encrypted = deltas[-1]
|
||||||
)
|
|
||||||
if deltas:
|
|
||||||
encrypted = deltas[-1]
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
try:
|
try:
|
||||||
self.vault.decrypt_and_save_index_from_nostr(encrypted)
|
self.vault.decrypt_and_save_index_from_nostr(encrypted)
|
||||||
logger.info("Initialized local database from Nostr.")
|
logger.info("Initialized local database from Nostr.")
|
||||||
@@ -3841,4 +3833,6 @@ class PasswordManager:
|
|||||||
print(color_text(f"Snapshot chunks: {stats['chunk_count']}", "stats"))
|
print(color_text(f"Snapshot chunks: {stats['chunk_count']}", "stats"))
|
||||||
print(color_text(f"Pending deltas: {stats['pending_deltas']}", "stats"))
|
print(color_text(f"Pending deltas: {stats['pending_deltas']}", "stats"))
|
||||||
if stats.get("delta_since"):
|
if stats.get("delta_since"):
|
||||||
print(color_text(f"Latest delta id: {stats['delta_since']}", "stats"))
|
print(
|
||||||
|
color_text(f"Latest delta timestamp: {stats['delta_since']}", "stats")
|
||||||
|
)
|
||||||
|
Reference in New Issue
Block a user