From ecc1086d47ec9ac136856516cf4369c091638680 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 17:21:35 -0400 Subject: [PATCH] Add totp codes command and endpoint --- README.md | 1 + docs/README.md | 6 ++++++ docs/advanced_cli.md | 2 ++ docs/api_reference.md | 1 + src/seedpass/api.py | 19 +++++++++++++++++++ src/seedpass/cli.py | 7 +++++++ src/tests/test_api_new_endpoints.py | 16 ++++++++++++++++ src/tests/test_typer_cli.py | 13 +++++++++++++ 8 files changed, 65 insertions(+) diff --git a/README.md b/README.md index 60e8580..deb69b8 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ SeedPass now uses the `portalocker` library for cross-platform file locking. No - **SeedPass 2FA:** Generate TOTP codes with a real-time countdown progress bar. - **2FA Secret Issuance & Import:** Derive new TOTP secrets from your seed or import existing `otpauth://` URIs. - **Export 2FA Codes:** Save all stored TOTP entries to an encrypted JSON file for use with other apps. +- **Display TOTP Codes:** Show all active 2FA codes with a countdown timer. - **Optional External Backup Location:** Configure a second directory where backups are automatically copied. - **Auto‑Lock on Inactivity:** Vault locks after a configurable timeout for additional security. - **Secret Mode:** Copy retrieved passwords directly to your clipboard and automatically clear it after a delay. diff --git a/docs/README.md b/docs/README.md index bf93cc0..bece44b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,12 @@ $ seedpass entry get "email" Code: 123456 ``` +To show all stored TOTP codes with their countdown timers, run: + +```bash +$ seedpass entry totp-codes +``` + ## CLI and API Reference See [advanced_cli.md](advanced_cli.md) for a list of command examples. Detailed information about the REST API is available in [api_reference.md](api_reference.md). When starting the API, set `SEEDPASS_CORS_ORIGINS` if you need to allow requests from specific web origins. diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index f48c708..25d21df 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -61,6 +61,7 @@ Manage individual entries within a vault. | Archive an entry | `entry archive` | `seedpass entry archive 1` | | Unarchive an entry | `entry unarchive` | `seedpass entry unarchive 1` | | Export all TOTP secrets | `entry export-totp` | `seedpass entry export-totp --file totp.json` | +| Show all TOTP codes | `entry totp-codes` | `seedpass entry totp-codes` | ### Vault Commands @@ -141,6 +142,7 @@ Run or stop the local HTTP API. - **`seedpass entry archive `** – Mark an entry as archived so it is hidden from normal lists. - **`seedpass entry unarchive `** – Restore an archived entry. - **`seedpass entry export-totp --file `** – Export all stored TOTP secrets to a JSON file. +- **`seedpass entry totp-codes`** – Display all current TOTP codes with remaining time. Example retrieving a TOTP code: diff --git a/docs/api_reference.md b/docs/api_reference.md index 658f25a..6de61d5 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -28,6 +28,7 @@ Keep this token secret. Every request must include it in the `Authorization` hea - `DELETE /api/v1/fingerprint/{fp}` – Remove a 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` – Return current TOTP codes and remaining time. - `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. diff --git a/src/seedpass/api.py b/src/seedpass/api.py index d156438..a5356ba 100644 --- a/src/seedpass/api.py +++ b/src/seedpass/api.py @@ -14,6 +14,7 @@ import sys from fastapi.middleware.cors import CORSMiddleware from password_manager.manager import PasswordManager +from password_manager.entry_types import EntryType app = FastAPI() @@ -314,6 +315,24 @@ def export_totp(authorization: str | None = Header(None)) -> dict: return _pm.entry_manager.export_totp_entries(_pm.parent_seed) +@app.get("/api/v1/totp") +def get_totp_codes(authorization: str | None = Header(None)) -> dict: + """Return active TOTP codes with remaining seconds.""" + _check_token(authorization) + assert _pm is not None + entries = _pm.entry_manager.list_entries( + filter_kind=EntryType.TOTP.value, include_archived=False + ) + codes = [] + for idx, label, _u, _url, _arch in entries: + code = _pm.entry_manager.get_totp_code(idx, _pm.parent_seed) + rem = _pm.entry_manager.get_totp_time_remaining(idx) + codes.append( + {"id": idx, "label": label, "code": code, "seconds_remaining": rem} + ) + return {"codes": codes} + + @app.get("/api/v1/nostr/pubkey") def get_nostr_pubkey(authorization: str | None = Header(None)) -> Any: _check_token(authorization) diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index cfa4a2c..541b4ec 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -315,6 +315,13 @@ def entry_unarchive(ctx: typer.Context, entry_id: int) -> None: typer.echo(str(entry_id)) +@entry_app.command("totp-codes") +def entry_totp_codes(ctx: typer.Context) -> None: + """Display all current TOTP codes.""" + pm = _get_pm(ctx) + pm.handle_display_totp_codes() + + @entry_app.command("export-totp") def entry_export_totp( ctx: typer.Context, file: str = typer.Option(..., help="Output file") diff --git a/src/tests/test_api_new_endpoints.py b/src/tests/test_api_new_endpoints.py index 9e7fff0..564df02 100644 --- a/src/tests/test_api_new_endpoints.py +++ b/src/tests/test_api_new_endpoints.py @@ -122,6 +122,22 @@ def test_totp_export_endpoint(client): assert res.json() == {"entries": ["x"]} +def test_totp_codes_endpoint(client): + cl, token = client + api._pm.entry_manager.list_entries = lambda **kw: [(0, "Email", None, None, False)] + api._pm.entry_manager.get_totp_code = lambda i, s: "123456" + api._pm.entry_manager.get_totp_time_remaining = lambda i: 30 + api._pm.parent_seed = "seed" + headers = {"Authorization": f"Bearer {token}"} + res = cl.get("/api/v1/totp", headers=headers) + assert res.status_code == 200 + assert res.json() == { + "codes": [ + {"id": 0, "label": "Email", "code": "123456", "seconds_remaining": 30} + ] + } + + def test_fingerprint_endpoints(client): cl, token = client calls = {} diff --git a/src/tests/test_typer_cli.py b/src/tests/test_typer_cli.py index 9fb0f84..1843a8c 100644 --- a/src/tests/test_typer_cli.py +++ b/src/tests/test_typer_cli.py @@ -373,6 +373,19 @@ def test_entry_export_totp(monkeypatch, tmp_path): assert called.get("called") is True +def test_entry_totp_codes(monkeypatch): + called = {} + + pm = SimpleNamespace( + handle_display_totp_codes=lambda: called.setdefault("called", True), + select_fingerprint=lambda fp: None, + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["entry", "totp-codes"]) + assert result.exit_code == 0 + assert called.get("called") is True + + def test_verify_checksum_command(monkeypatch): called = {}