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

@@ -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(
"Do you want to save this to an encrypted backup file? (Y/N): "
):
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: 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
# 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(