diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md index b509a8c..07515fb 100644 --- a/CHECKPOINT-encryption.md +++ b/CHECKPOINT-encryption.md @@ -1,17 +1,42 @@ -# Checkpoint — Signer Modes + Fail-Closed Key Export + Packaging Icon (2026-09-03) +# Checkpoint — Signer Abstraction (SigningBackend/SigningError) (2026-09-03) ## Where things are - Project: `/home/avi/Projects/Keynctr` -- Branch: `master` @ **`114b343`** ("fix(packaging): restore Linux app icon so dist builds ship an icon"). +- Branch: `master` @ **`e6e4922`** ("feat(signer): add SigningBackend + SigningError + VaultRef"). - Working tree: **effectively clean for tracked files.** No tracked file is modified or staged. The only untracked entries are the pre-existing hygiene leftovers and the source JPEG (all intentionally untracked — see "Still untracked"). Build artifacts (`release/`, `dist/`) are gitignored. ## What was completed (this session) + +**Step 3 (signer abstraction) — foundation landed.** The approved API types are now in +the codebase as a standalone, independently-testable commit (`e6e4922`), ahead of the +vault-integration and IPC-rerouting sub-steps. `src/signer/backend.rs` (new) adds: + +- `SigningBackend { Internal, Remote { vault_ref } }` — the per-profile choice of where + user content is signed. `Remote` holds **only** an opaque `VaultRef` + (`profile_npub` + `signer_pubkey`); the NIP-46 connection secret is **never** inlined + (enforced by a test that serializes a `Remote` backend and asserts no secret appears). +- `VaultRef` — an opaque, secret-free pointer into the vault's encrypted + connection-secret store. Resolution to the decrypted secret happens only at the vault + boundary, while the vault is unlocked (that wiring is the next sub-step). +- `SigningError` — the closed set of signing failures (no active profile, internal key + unavailable, remote connection missing, secret resolution, identity mismatch, not + connected, permission denied, expired, revoked, rejected, timeout, invalid signature, + network, storage, internal). Each carries a stable `ErrorKind`, a user-facing message + (mirroring the existing `AppError` copy), and an optional technical detail. + `From for AppError` converts at the IPC boundary; a best-effort + `SigningError::from_app` lifts upstream `AppError`s back into the signing space. + +The existing `Signer` trait, `Signing` enum, `EmbeddedSigner`, `Nip46ClientSigner`, and +permission model are untouched by this commit — they are what the upcoming vault +integration and IPC reroute will drive through `SigningBackend`. + +--- The previously-uncommitted working tree (27 modified + 10 untracked files, ~1852/692) was triaged into **three logical, independently-verifiable commits** in a prior session, -and this session **landed the Step-2 packaging fix** on top. Order is +and the packaging fix landed in `114b343`. Order is `a → c → b → (packaging)`: `ExportSecretKey` (b) reads the per-profile `signer_mode` field and the `external_signer_not_connected` error that the signer-mode commit (c) introduces, so (c) had to land first. The only uncommitted *tests* were the export @@ -58,36 +83,20 @@ each feature's tests travel with it. ## Commits added this session (newest first) | Hash | Message | |------|---------| -| `114b343` | fix(packaging): restore Linux app icon so dist builds ship an icon | +| `e6e4922` | feat(signer): add SigningBackend + SigningError + VaultRef | -(The prior session's three feature commits and their checkpoint `d92371f` remain the -parent chain: `6eff510` → `2c61830` → `caed722` → … → `d92371f` → `114b343`.) +(The prior session's feature commits — `114b343` packaging icon, `d92371f` checkpoint, +`6eff510` export, `2c61830` signer modes, `caed722` audit log — remain the parent chain.) ## Verification (run this session) -Full suite per AGENTS.md, run on top of `114b343` (icon is a binary asset, no code -change, so the suite is expected to hold): -- **Rust**: `cargo test --release` → **186 passed**, 0 failed; `cargo clippy - --all-targets` → clean (exit 0); `cargo fmt --check` → clean (exit 0); `cargo build - --release` → Finished, exit 0. -- **Frontend**: `npm test` → **116 passed** (16 files); `npm run typecheck` → clean - (exit 0); `npm run lint` → clean (exit 0). - - `npm run format:check` → **exit 1** on 5 files (`src/components/ExportSecretKeyModal.tsx`, - `src/screens/SignerModeScreen.tsx`, `src/state/AppProvider.tsx`, - `src/test/ExportSecretKey.test.tsx`, `src/test/fakeBackend.ts`). **Pre-existing** — - those files are committed as-is at `6eff510` (prior session) and are clean in the - working tree; this icon-only change touched none of them. Left for the Step-7 - hygiene/format pass; not silently auto-fixed here. -- **Packaging (the point of this fix)**: `npm run dist` ran end-to-end. Note the script - is `electron-builder --linux dir`, which builds only the unpacked dir; the declared - `linux.target` (AppImage + deb) was also built directly to prove the icon lands: - - `release/Keynctr-0.1.0.AppImage` — 135,749,547 bytes. - - `release/keynectr_0.1.0_amd64.deb` — 105,810,972 bytes. - - **Icon proven inside both artifacts**: the embedded icon is **byte-identical - (md5 `b3e372f7`)** to the generated `build/icon.png` in all four locations checked — - the deb's `usr/share/icons/hicolor/512x512/apps/keynectr.png`, the AppImage's hicolor - png, the AppImage's `.DirIcon`, and `build/icon.png` itself — all 512x512 RGBA with - real transparency (32,626 opaque px, 226,582 transparent, corners alpha 0), ink - pure black. The deb `.desktop` reads `Icon=keynectr`, matching the hicolor name. +Full suite per AGENTS.md, run on top of `e6e4922`: +- **Rust**: `cargo test` → **196 passed**, 0 failed (186 prior + 10 new + `signer::backend` tests); `cargo clippy --all-targets` → clean (exit 0); + `cargo fmt --check` → clean (exit 0); `cargo build --release` → Finished, exit 0. +- **Frontend**: not re-run this session — `e6e4922` is Rust-only (new + `src/signer/backend.rs` + a module line in `src/signer/mod.rs`), so the frontend + suite is unchanged from `114b343` (116 passed / typecheck / lint clean). Re-verified + in full at the end of Step 3 (IPC reroute), where frontend handlers change. ## How to reproduce / exercise - Backend: `cargo run --release -- serve` (JSON-lines IPC on stdio) or the CLI in @@ -121,18 +130,23 @@ change, so the suite is expected to hold): ## Deferred / next steps (unchanged, plus new) - **Step 2 (packaging): DONE.** Icon restored, `npm run dist` + declared targets green, icon proven inside both artifacts, committed as `114b343`. -- **Step 3 (signer abstraction)** — proposed for sign-off, NOT yet implemented. Current - state that motivates the API: `src/signer/mod.rs` already defines a `Signer` trait and - a `Signing` enum (`Local(Keys)` / `External{signer, profile_pubkey}`); `SignerMode` +- **Step 3 (signer abstraction)** — foundation **landed** as `e6e4922` + (`SigningBackend` + `VaultRef` + `SigningError` in `src/signer/backend.rs`, 10 tests, + full Rust suite green). The `Remote { vault_ref }` variant holds **only** a `VaultRef` + — the NIP-46 connection secret is never inlined. **Remaining sub-steps, in order:** + 1. **Vault integration** — encrypted connection-secret store keyed by `VaultRef`, + `VaultRef` → decrypted-secret resolution at the vault boundary (only while + unlocked), and removal of the inline `Nip46Connection.secret` field so secrets live + only in the vault. This is where the `vault_ref` constraint becomes load-bearing. + 2. **IPC reroute** — drive `src/ipc.rs` handlers through `SigningBackend` (selected + per-profile) so no inline `signer_mode`/handle-presence branching remains; convert + handler results to `AppError` via `From`. + Prior state that motivated the API: `src/signer/mod.rs` already had a `Signer` trait + and `Signing` enum (`Local(Keys)` / `External{signer, profile_pubkey}`); `SignerMode` (`Embedded`/`Nip46Bunker`/`Nip46Client`) lives on `StoredProfile` (per-profile) *and* is duplicated on `App` (app-level), and `App` holds three separate signer handles (`embedded_signer`, `nip46_signer`, `nip46_bunker_signer`). The IPC dispatcher - (`src/ipc.rs`) branches on `signer_mode`/handle presence in ~12 sites (see the - grep list pasted to the user this session). The proposal promotes `src/signer` to the - single `Signer` source of truth, introduces a `SigningBackend { Internal{..}, - Remote{..} }` per profile, and routes every IPC handler through the trait so no inline - mode branching remains. **Awaiting the user's sign-off on the API before any - implementation.** + (`src/ipc.rs`) branches on `signer_mode`/handle presence in ~12 sites. - External-signer permissions (Step 4): wire `src/signer/permissions.rs` into the approval modal so grants (kinds, relays, expiry, rate) are persisted AND enforced in the UI, not just parsed.