diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index 25d21df..f8e3b75 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -72,6 +72,7 @@ Manage the entire vault for a profile. | Export the vault | `vault export` | `seedpass vault export --file backup.json` | | Import a vault | `vault import` | `seedpass vault import --file backup.json` | | Change the master password | `vault change-password` | `seedpass vault change-password` | +| Lock the vault | `vault lock` | `seedpass vault lock` | ### Nostr Commands @@ -157,6 +158,7 @@ Code: 123456 - **`seedpass vault export`** – Export the entire vault to an encrypted JSON file. - **`seedpass vault import`** – Import a vault from an encrypted JSON file. - **`seedpass vault change-password`** – Change the master password used for encryption. +- **`seedpass vault lock`** – Clear sensitive data from memory and require reauthentication. ### `nostr` Commands diff --git a/docs/api_reference.md b/docs/api_reference.md index 14eba27..1179fea 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -35,6 +35,7 @@ Keep this token secret. Every request must include it in the `Authorization` hea - `POST /api/v1/checksum/update` – Update the stored script checksum. - `POST /api/v1/change-password` – Change the master password for the active profile. - `POST /api/v1/vault/import` – Import a vault backup from a file or path. +- `POST /api/v1/vault/lock` – Lock the vault and clear sensitive data from memory. - `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. diff --git a/src/seedpass/api.py b/src/seedpass/api.py index 5963343..65333b6 100644 --- a/src/seedpass/api.py +++ b/src/seedpass/api.py @@ -414,6 +414,15 @@ def change_password(authorization: str | None = Header(None)) -> dict[str, str]: return {"status": "ok"} +@app.post("/api/v1/vault/lock") +def lock_vault(authorization: str | None = Header(None)) -> dict[str, str]: + """Lock the vault and clear sensitive data from memory.""" + _check_token(authorization) + assert _pm is not None + _pm.lock_vault() + return {"status": "locked"} + + @app.post("/api/v1/shutdown") async def shutdown_server(authorization: str | None = Header(None)) -> dict[str, str]: _check_token(authorization) diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index c0a9110..4941dc5 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -360,6 +360,14 @@ def vault_change_password(ctx: typer.Context) -> None: pm.change_password() +@vault_app.command("lock") +def vault_lock(ctx: typer.Context) -> None: + """Lock the vault and clear sensitive data from memory.""" + pm = _get_pm(ctx) + pm.lock_vault() + typer.echo("locked") + + @vault_app.command("reveal-parent-seed") def vault_reveal_parent_seed( ctx: typer.Context, diff --git a/src/tests/test_api.py b/src/tests/test_api.py index a16adbf..2e1f7e2 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -220,6 +220,7 @@ def test_shutdown(client, monkeypatch): ("post", "/api/v1/entry/1/archive"), ("post", "/api/v1/entry/1/unarchive"), ("post", "/api/v1/change-password"), + ("post", "/api/v1/vault/lock"), ], ) def test_invalid_token_other_endpoints(client, method, path): diff --git a/src/tests/test_api_new_endpoints.py b/src/tests/test_api_new_endpoints.py index 1e62ec8..fbe09b4 100644 --- a/src/tests/test_api_new_endpoints.py +++ b/src/tests/test_api_new_endpoints.py @@ -253,3 +253,25 @@ def test_vault_import_via_upload(client, tmp_path): assert res.status_code == 200 assert res.json() == {"status": "ok"} assert isinstance(called.get("path"), Path) + + +def test_vault_lock_endpoint(client): + cl, token = client + called = {} + + def lock(): + called["locked"] = True + api._pm.locked = True + + api._pm.lock_vault = lock + api._pm.locked = False + + headers = {"Authorization": f"Bearer {token}"} + res = cl.post("/api/v1/vault/lock", headers=headers) + assert res.status_code == 200 + assert res.json() == {"status": "locked"} + assert called.get("locked") is True + assert api._pm.locked is True + api._pm.unlock_vault = lambda: setattr(api._pm, "locked", False) + api._pm.unlock_vault() + assert api._pm.locked is False diff --git a/src/tests/test_typer_cli.py b/src/tests/test_typer_cli.py index 7b15867..21feb6c 100644 --- a/src/tests/test_typer_cli.py +++ b/src/tests/test_typer_cli.py @@ -111,6 +111,23 @@ def test_vault_change_password(monkeypatch): assert called.get("called") is True +def test_vault_lock(monkeypatch): + called = {} + + def lock(): + called["locked"] = True + pm.locked = True + + pm = SimpleNamespace( + lock_vault=lock, locked=False, select_fingerprint=lambda fp: None + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["vault", "lock"]) + assert result.exit_code == 0 + assert called.get("locked") is True + assert pm.locked is True + + def test_vault_reveal_parent_seed(monkeypatch, tmp_path): called = {}