From 8780ef3fbbac07aab656f690f94b8dfb7298faa3 Mon Sep 17 00:00:00 2001 From: Avi Date: Thu, 10 Sep 2026 12:50:24 -0500 Subject: [PATCH] checkpoint: document undo-delete secret-preserving fix (2026-09-10) --- CHECKPOINT-encryption.md | 94 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md index 352d9bf..a7f2d34 100644 --- a/CHECKPOINT-encryption.md +++ b/CHECKPOINT-encryption.md @@ -1,3 +1,97 @@ +# Checkpoint — Undo-delete restores working profiles (2026-09-10) + +## Where things are +- Project: `/home/avi/Projects/Keynctr` +- Branch: `master` @ **`d101b8e`** ("fix(undo): restore full profile with secret key on undo-delete"). +- Working tree: **clean for tracked files.** Only untracked entries are the pre-existing + hygiene leftovers plus two Rust scratch files (all intentionally untracked — see + "Still untracked"). + +## What was completed (this session) + +**Step 6 (undo history) — the hollow-undo bug is fixed, landed as `d101b8e`.** +Deleting a profile used to keep only a `ProfileSummary` on the undo stack, so +undo re-created the profile with `secret_key = ""` — a dead shell that could never +sign. The undo stack now keeps the full stored record: + +- **`src/profiles.rs`** — new `DeletedProfile { summary, stored }` struct; + `delete_profile_record()` returns the real stored secret (plaintext or encrypted + blob, exactly as on disk) plus the safe UI summary; `delete_profile()` stays as + the summary-only wrapper for callers that keep no undo entry. +- **`src/app.rs`** — `App.undo_history` is now `Vec` (secret material + never leaves the backend); `undo_delete()` restores the real `StoredProfile`, + refuses to create a hollow profile (empty secret → entry handed back + error), + and `state_view()` still exposes only `summary` items so the renderer never sees + a secret. New regression test + `undo_delete_restores_working_profile_without_leaking_secret` locks this in. +- **`src/ipc.rs`** — `DeleteProfile` routes through `delete_profile_record` so the + GUI undo entry carries the secret (previously it pushed a summary-only entry, + so GUI undo was still hollow). +- **`src/main.rs`** — CLI `delete-profile`/`undo-delete` use the same record path + (`delete_profile_direct` → `delete_profile_record`, `cli_undo_delete` → + `app.undo_delete()`); also fixes the old "label looked up after removal" bug by + capturing the label before deletion, and drops the now-unused imports. +- Compile fixes included: the inherited work-in-progress did not build (`DeletedProfile` + vs `ProfileSummary` mismatch in `ipc.rs`, partial moves in `undo_delete`); both + resolved, plus `cargo fmt` applied. + +Security properties: secret material stays backend-only (`AppStateView.undo_history` +is still `Vec`); undo restores the exact stored blob (no re-derivation, +no logging); empty-secret entries fail closed instead of writing hollow profiles. + +## Commits added this session (newest first) +| Hash | Message | +|------|---------| +| `d101b8e` | fix(undo): restore full profile with secret key on undo-delete | + +(Parent chain — `1d5940f` display/icons checkpoint, `d580139` icon alpha fix, +`0814a53` Linux display compat, `715c99c`/`510cb65` connection-secrets vault +integration — is unchanged.) + +## Verification (run this session, on top of `d101b8e`) +- **Rust**: `cargo test` → **197 passed**, 0 failed (196 pre-existing + 1 new + undo regression test); `cargo clippy --all-targets` → clean (exit 0); + `cargo fmt --check` → clean (exit 0); `cargo build --release` → Finished, exit 0. +- **Frontend** (in `frontend/`, Rust-only change so no frontend files touched): + `npm test` → **116/116 passed**; `npm run typecheck` → exit 0; + `npm run lint` → exit 0; `npm run electron:build` → exit 0; `npm run build` → + exit 0. `npm run format:check` → warns on the same 5 pre-existing files + (`ExportSecretKeyModal.tsx`, `SignerModeScreen.tsx`, `AppProvider.tsx`, + `ExportSecretKey.test.tsx`, `fakeBackend.ts`) documented in earlier checkpoints — + not introduced here, left untouched. + +## How to reproduce / exercise +- Backend: `cargo run --release -- serve` (JSON-lines IPC on stdio) or the CLI in + `src/main.rs`. +- GUI: from `frontend/`, `npm run electron:build && electron .` (prod) or `npm run + start:dev` with `NOSTR_GUI_DEV_URL`. +- Exercise undo: Profiles → delete a profile → Undo delete → the restored profile + signs/publishes (previously it came back secret-less). CLI equivalent: + `keynectr delete-profile ` then `keynectr undo-delete`. + +## Still untracked (do NOT lose; do NOT commit the hygiene junk) +- **Source JPEG** `KeynectrAppIconPossibility02.jpeg` — intentionally untracked. +- Pre-existing untracked hygiene leftovers: `COSMIC_THEME.md`, `.opencode/`, + `.impeccable/critique/`, `.directory`, `deferred/SignerConnectionPanel.tsx.wip/`. +- Rust scratch files (unreferenced, harmless — neither is wired into the build): + `src/signer/nip46_external.rs` (dead stub, not declared in `src/signer/mod.rs`), + `src/publish.rs.bak` (backup copy). Left alone this session; delete or wire up + in a later pass. +- Build artifacts `release/` and `dist/` are gitignored and not committed. +- `profiles_vault.json*` and `target/` remain correctly untracked and uncommitted. + +## Deferred / next steps (unchanged, minus the undo item) +- Step 3 sub-step 2 (IPC reroute) remains the next signer milestone; external + (remote) signing in the publish path still returns "not yet supported". +- External-signer permissions (Step 4), deferred security (Step 5: KDF upgrade, + `--allow-env-secret`, gate deprecated `RevealSecretKey`), hygiene (Step 7: + Keynctr rename incl. `package.json` → `homepage`, legacy Python removal, vault + relocation, Prettier pass over the 5 known files). +- Open question carried forward: `migrate_vault_signer_modes` reports a change on + every load (always `changed = true`), so `App::load` re-saves each start. + +--- + # Checkpoint — NIP-46 Connection Secrets in the Vault (2026-09-04) ## Where things are