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 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.
- **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.
- **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.

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.
- `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/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.
- `POST /api/v1/checksum/verify` Verify the checksum of the running script.
- `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/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
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"))
return None
def handle_backup_reveal_parent_seed(self) -> None:
"""
Handles the backup and reveal of the parent seed.
def handle_backup_reveal_parent_seed(self, file: Path | None = None) -> None:
"""Reveal the parent seed and optionally save an encrypted backup.
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:
fp, parent_fp, child_fp = self.header_fingerprint_args
@@ -3360,24 +3366,26 @@ class PasswordManager:
)
)
# Option to save to file with default filename
if confirm_action(
backup_path: Path | None = None
if file is not None:
backup_path = file
save = True
else:
save = confirm_action(
"Do you want to save this to an encrypted backup file? (Y/N): "
):
)
if save:
filename = input(
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
) # Save in fingerprint directory
backup_path = self.fingerprint_dir / filename
# Validate filename
if not self.is_valid_filename(filename):
if save and backup_path is not None:
if not self.is_valid_filename(backup_path.name):
print(colored("Invalid filename. Operation aborted.", "red"))
return
# Encrypt and save the parent seed to the backup path
self.encryption_manager.encrypt_and_save_file(
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}
@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")
def get_nostr_pubkey(authorization: str | None = Header(None)) -> Any:
_check_token(authorization)

View File

@@ -360,6 +360,18 @@ def vault_change_password(ctx: typer.Context) -> None:
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")
def nostr_sync(ctx: typer.Context) -> None:
"""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):
cl, token = client
calls = {}

View File

@@ -111,6 +111,24 @@ def test_vault_change_password(monkeypatch):
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):
pm = SimpleNamespace(
nostr_client=SimpleNamespace(