Add parent seed backup command and API

This commit is contained in:
thePR0M3TH3AN
2025-07-09 19:53:10 -04:00
parent e904710cd0
commit a0e2d227a5
7 changed files with 95 additions and 17 deletions

View File

@@ -452,6 +452,7 @@ Mutation testing is disabled in the GitHub workflow due to reliability issues an
- **Backup Your Data:** Regularly back up your encrypted data and checksum files to prevent data loss. - **Backup Your Data:** Regularly back up your encrypted data and checksum files to prevent data loss.
- **Backup the Settings PIN:** Your settings PIN is stored in the encrypted configuration file. Keep a copy of this file or remember the PIN, as losing it will require deleting the file and reconfiguring your relays. - **Backup the Settings PIN:** Your settings PIN is stored in the encrypted configuration file. Keep a copy of this file or remember the PIN, as losing it will require deleting the file and reconfiguring your relays.
- **Protect Your Passwords:** Do not share your master password or seed phrases with anyone and ensure they are strong and unique. - **Protect Your Passwords:** Do not share your master password or seed phrases with anyone and ensure they are strong and unique.
- **Revealing the Parent Seed:** The `vault reveal-parent-seed` command and `/api/v1/parent-seed` endpoint print your seed in plain text. Run them only in a secure environment.
- **No PBKDF2 Salt Needed:** SeedPass deliberately omits an explicit PBKDF2 salt. Every password is derived from a unique 512-bit BIP-85 child seed, which already provides stronger per-password uniqueness than a conventional 128-bit salt. - **No PBKDF2 Salt Needed:** SeedPass deliberately omits an explicit PBKDF2 salt. Every password is derived from a unique 512-bit BIP-85 child seed, which already provides stronger per-password uniqueness than a conventional 128-bit salt.
- **Checksum Verification:** Always verify the script's checksum to ensure its integrity and protect against unauthorized modifications. - **Checksum Verification:** Always verify the script's checksum to ensure its integrity and protect against unauthorized modifications.
- **Potential Bugs and Limitations:** Be aware that the software may contain bugs and lacks certain features. Snapshot chunks are capped at 50KB and the client rotates snapshots after enough delta events accumulate. The security of memory management and logs has not been thoroughly evaluated and may pose risks of leaking sensitive information. - **Potential Bugs and Limitations:** Be aware that the software may contain bugs and lacks certain features. Snapshot chunks are capped at 50KB and the client rotates snapshots after enough delta events accumulate. The security of memory management and logs has not been thoroughly evaluated and may pose risks of leaking sensitive information.

View File

@@ -29,6 +29,7 @@ Keep this token secret. Every request must include it in the `Authorization` hea
- `POST /api/v1/fingerprint/select` Switch the active fingerprint. - `POST /api/v1/fingerprint/select` Switch the active fingerprint.
- `GET /api/v1/totp/export` Export all TOTP entries as JSON. - `GET /api/v1/totp/export` Export all TOTP entries as JSON.
- `GET /api/v1/totp` Return current TOTP codes and remaining time. - `GET /api/v1/totp` Return current TOTP codes and remaining time.
- `GET /api/v1/parent-seed` Reveal the parent seed or save it with `?file=`.
- `GET /api/v1/nostr/pubkey` Fetch the Nostr public key for the active seed. - `GET /api/v1/nostr/pubkey` Fetch the Nostr public key for the active seed.
- `POST /api/v1/checksum/verify` Verify the checksum of the running script. - `POST /api/v1/checksum/verify` Verify the checksum of the running script.
- `POST /api/v1/checksum/update` Update the stored script checksum. - `POST /api/v1/checksum/update` Update the stored script checksum.
@@ -36,6 +37,8 @@ Keep this token secret. Every request must include it in the `Authorization` hea
- `POST /api/v1/vault/import` Import a vault backup from a file or path. - `POST /api/v1/vault/import` Import a vault backup from a file or path.
- `POST /api/v1/shutdown` Stop the server gracefully. - `POST /api/v1/shutdown` Stop the server gracefully.
**Security Warning:** Accessing `/api/v1/parent-seed` exposes your master seed in plain text. Use it only from a trusted environment.
## Example Requests ## Example Requests
Send requests with the token in the header: Send requests with the token in the header:

View File

@@ -3309,9 +3309,15 @@ class PasswordManager:
print(colored(f"Error: Failed to export 2FA codes: {e}", "red")) print(colored(f"Error: Failed to export 2FA codes: {e}", "red"))
return None return None
def handle_backup_reveal_parent_seed(self) -> None: def handle_backup_reveal_parent_seed(self, file: Path | None = None) -> None:
""" """Reveal the parent seed and optionally save an encrypted backup.
Handles the backup and reveal of the parent seed.
Parameters
----------
file:
Optional path where an encrypted backup should be written. When
provided, the confirmation and filename prompts are skipped and the
seed is saved directly to this location.
""" """
try: try:
fp, parent_fp, child_fp = self.header_fingerprint_args fp, parent_fp, child_fp = self.header_fingerprint_args
@@ -3360,24 +3366,26 @@ class PasswordManager:
) )
) )
# Option to save to file with default filename backup_path: Path | None = None
if confirm_action( if file is not None:
"Do you want to save this to an encrypted backup file? (Y/N): " backup_path = file
): save = True
filename = input( else:
f"Enter filename to save (default: {DEFAULT_SEED_BACKUP_FILENAME}): " save = confirm_action(
).strip() "Do you want to save this to an encrypted backup file? (Y/N): "
filename = filename if filename else DEFAULT_SEED_BACKUP_FILENAME )
backup_path = ( if save:
self.fingerprint_dir / filename filename = input(
) # Save in fingerprint directory f"Enter filename to save (default: {DEFAULT_SEED_BACKUP_FILENAME}): "
).strip()
filename = filename if filename else DEFAULT_SEED_BACKUP_FILENAME
backup_path = self.fingerprint_dir / filename
# Validate filename if save and backup_path is not None:
if not self.is_valid_filename(filename): if not self.is_valid_filename(backup_path.name):
print(colored("Invalid filename. Operation aborted.", "red")) print(colored("Invalid filename. Operation aborted.", "red"))
return return
# Encrypt and save the parent seed to the backup path
self.encryption_manager.encrypt_and_save_file( self.encryption_manager.encrypt_and_save_file(
self.parent_seed.encode("utf-8"), backup_path self.parent_seed.encode("utf-8"), backup_path
) )

View File

@@ -333,6 +333,22 @@ def get_totp_codes(authorization: str | None = Header(None)) -> dict:
return {"codes": codes} return {"codes": codes}
@app.get("/api/v1/parent-seed")
def get_parent_seed(
authorization: str | None = Header(None), file: str | None = None
) -> dict:
"""Return the parent seed or save it as an encrypted backup."""
_check_token(authorization)
assert _pm is not None
if file:
path = Path(file)
_pm.encryption_manager.encrypt_and_save_file(
_pm.parent_seed.encode("utf-8"), path
)
return {"status": "saved", "path": str(path)}
return {"seed": _pm.parent_seed}
@app.get("/api/v1/nostr/pubkey") @app.get("/api/v1/nostr/pubkey")
def get_nostr_pubkey(authorization: str | None = Header(None)) -> Any: def get_nostr_pubkey(authorization: str | None = Header(None)) -> Any:
_check_token(authorization) _check_token(authorization)

View File

@@ -360,6 +360,18 @@ def vault_change_password(ctx: typer.Context) -> None:
pm.change_password() pm.change_password()
@vault_app.command("reveal-parent-seed")
def vault_reveal_parent_seed(
ctx: typer.Context,
file: Optional[str] = typer.Option(
None, "--file", help="Save encrypted seed to this path"
),
) -> None:
"""Display the parent seed and optionally write an encrypted backup file."""
pm = _get_pm(ctx)
pm.handle_backup_reveal_parent_seed(Path(file) if file else None)
@nostr_app.command("sync") @nostr_app.command("sync")
def nostr_sync(ctx: typer.Context) -> None: def nostr_sync(ctx: typer.Context) -> None:
"""Sync with configured Nostr relays.""" """Sync with configured Nostr relays."""

View File

@@ -138,6 +138,26 @@ def test_totp_codes_endpoint(client):
} }
def test_parent_seed_endpoint(client, tmp_path):
cl, token = client
api._pm.parent_seed = "seed"
called = {}
api._pm.encryption_manager = SimpleNamespace(
encrypt_and_save_file=lambda data, path: called.setdefault("path", path)
)
headers = {"Authorization": f"Bearer {token}"}
res = cl.get("/api/v1/parent-seed", headers=headers)
assert res.status_code == 200
assert res.json() == {"seed": "seed"}
out = tmp_path / "bk.enc"
res = cl.get("/api/v1/parent-seed", params={"file": str(out)}, headers=headers)
assert res.status_code == 200
assert res.json() == {"status": "saved", "path": str(out)}
assert called["path"] == out
def test_fingerprint_endpoints(client): def test_fingerprint_endpoints(client):
cl, token = client cl, token = client
calls = {} calls = {}

View File

@@ -111,6 +111,24 @@ def test_vault_change_password(monkeypatch):
assert called.get("called") is True assert called.get("called") is True
def test_vault_reveal_parent_seed(monkeypatch, tmp_path):
called = {}
def reveal(path=None):
called["path"] = path
pm = SimpleNamespace(
handle_backup_reveal_parent_seed=reveal, select_fingerprint=lambda fp: None
)
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
out_path = tmp_path / "seed.enc"
result = runner.invoke(
app, ["vault", "reveal-parent-seed", "--file", str(out_path)]
)
assert result.exit_code == 0
assert called["path"] == out_path
def test_nostr_get_pubkey(monkeypatch): def test_nostr_get_pubkey(monkeypatch):
pm = SimpleNamespace( pm = SimpleNamespace(
nostr_client=SimpleNamespace( nostr_client=SimpleNamespace(