mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 23:38:49 +00:00
Add parent seed backup command and API
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
@@ -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)
|
||||
|
@@ -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."""
|
||||
|
@@ -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 = {}
|
||||
|
@@ -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(
|
||||
|
Reference in New Issue
Block a user