Refresh checkpoint with IPC allowlist hardening

This commit is contained in:
Avi 2026-08-21 13:31:34 -05:00
commit dafed3330b

View file

@ -1,4 +1,4 @@
# Checkpoint — Contact-aware feed (2026-08-06)
# Checkpoint — Renderer IPC method allowlist (2026-08-21)
A stopping point you can return to if this session is closed. Everything below was
verified green at the moment this file was written.
@ -6,46 +6,35 @@ verified green at the moment this file was written.
## Where things are
- Project: `/home/avi/Projects/0_Nostr`
- Git repo: `master` @ `a1445d1` ("Add contact-aware feed scope").
- Working tree is **clean** — this session's contact-aware feed changes and its checkpoint
are committed.
- On top of that sits the NIP-46 signer work and the original feed aggregation, all committed.
- Git repo: `master` @ `4bde395` ("Restrict renderer IPC to an explicit method allowlist").
- Working tree is **clean** apart from this checkpoint update, which is committed right after.
- Underneath sit the contact-aware feed (`a1445d1`), feed aggregation (`37a9d94`) and the
NIP-46 signer work, all committed.
## What was completed: contact-aware feed
## What was completed: security hardening item #1 (IPC allowlist)
The Feed screen (and CLI) can now be scoped to **My contacts**: instead of showing the global
stream, it fetches the active profile's kind 3 contact list from the enabled relays and returns
only recent notes authored by those contacts. This builds on the existing aggregation engine.
First fix from the 2026-08-21 security audit. The Electron main process now enforces an
explicit allowlist of renderer-callable methods, so a compromised web page inside the app
can no longer invent arbitrary backend calls (e.g. unknown/future methods or typos that
might alias onto sensitive ones).
- **Backend** (`src/feed.rs`):
- `contact_feed(settings, limit, owner_hex)` fetches the owner's contact pubkeys
(`contact_pubkeys`) then aggregates only notes from those authors.
- `contact_pubkeys` subscribes to kind 3 (contact list) for the owner, collecting the hex
pubkeys of everyone they follow from `TagStandard::PublicKey` tags, de-duplicated.
- `aggregate_feed` still serves the global feed and now delegates to a private
`aggregate_for(settings, limit, authors)` used by both scopes. When `authors` is set the
NIP-01 filter restricts to those authors **and** `FeedBuilder` drops any note from someone
else (defence in depth for relays that ignore the `authors` filter).
- `FeedBuilder::new(limit, authors)` takes an optional author whitelist.
- `owner_pubkey(npub)` decodes a stored `npub1...` (or hex) to a `PublicKey`; shared by the
IPC server and the CLI.
- **IPC** (`src/ipc.rs`): `FeedGet` now accepts `contacts_only: Option<bool>`. With
`contacts_only: true`, it resolves the active profile's npub, decodes it, and calls
`contact_feed`. Without an active profile it returns a `no_active_profile` error.
- **CLI** (`src/main.rs`): `feed [--contacts] [limit]``--contacts` filters to the active
profile's contacts; the no-results message is adjusted accordingly.
- **Electron/React frontend**: the Feed screen (`FeedScreen.tsx`) gains a segmented **Everyone /
My contacts** scope toggle. "My contacts" is disabled when no profile is active (and silently
forced back to Everyone). Scope-aware empty states ("No notes from your contacts"), and
`feedGet(limit, contactsOnly)` threads the flag from `api.ts` through `AppProvider`.
- **Styles** (`styles.css`): a `.segmented` pill control for the scope toggle.
- **Electron** (`frontend/electron/main.ts`):
- New `RENDERER_METHODS` set: the three native methods (`pick_image`, `link_preview`,
`upload_image`) plus every Rust-backend method actually used by `frontend/src/lib/api.ts`
(init/get_state, profiles, publish, feed, relays, settings, vault password/unlock,
reveal_secret_key, signer control/approve).
- `isAllowedMethod()` gate at the top of the `backend:request` handler; anything not in
the list returns `{status:'error', code:'unknown_method'}` and is logged to stderr —
it never reaches the backend process.
- No behaviour change for the real UI: `window.backend.request` is only ever called from
`api.ts`, whose full surface is covered by the allowlist (confirmed by grep + tests).
- Remaining audit items (not yet done): token-based upload path validation (#2), CSP
`'unsafe-inline'` removal + window-open deny (#3), legacy-vault permissions (#4),
permission race windows (#5), key zeroization (#6).
## Commits
- `a1445d1` "Add contact-aware feed scope" — the full feature: `src/feed.rs`
(`contact_feed`/`contact_pubkeys`/`aggregate_for` + author whitelist), IPC `FeedGet`
`contacts_only`, CLI `feed --contacts`, the frontend scope toggle + styles + tests, README.
(Parent: `1fd057d` "Refresh checkpoint with feed aggregation".)
- `4bde395` "Restrict renderer IPC to an explicit method allowlist" — `main.ts` only (+49 lines).
## How it was verified (all green)
@ -53,33 +42,36 @@ only recent notes authored by those contacts. This builds on the existing aggreg
cargo test # 84 passed
cargo clippy --all-targets # clean
cargo fmt --check # clean
cargo build --release # builds (rebuilt so the GUI runs the new backend)
cargo build --release # ok
npm run typecheck # clean (frontend/)
npm run lint # clean (pre-existing module warning only)
npm run lint # clean (pre-existing module-type warning only)
npm run format:check # clean
npm test # 78 passed (14 files)
npm run electron:build # compiles the Electron main process
npm run electron:build # compiles the Electron main process with the new gate
npm run build # rebuilds the React bundle (dist/)
```
## How to resume
1. Open the repo: `cd /home/avi/Projects/0_Nostr`
2. State is committed: `git status` should be clean; `git log --oneline -5` shows `a1445d1`
at the top.
3. To try the contact feed:
- GUI: open the **Feed** screen and switch **Everyone → My contacts**. You need an active
profile that follows people; the segmented control filters the feed to their notes.
- CLI: `./target/release/nostr-manager-backend feed --contacts 10` prints up to 10 recent
notes from the active profile's contacts.
4. Re-run verification with the commands above.
2. State is committed: `git status` should be clean; `git log --oneline -3` shows `4bde395`
on top.
3. Launch as usual: `cd frontend && npm start` (backend already built in `target/release/`).
4. To see the gate working: from the DevTools console run
`window.backend.request('not_a_method')` → resolves to a `unknown_method` error envelope;
normal screens keep working unchanged.
5. Re-run verification with the commands above.
## Outstanding / next steps (if you continue)
- The contact feed depends on the kind 3 contact list being present on the enabled relays; a
profile with no contacts (or a newly created one that has never published a contact list)
produces an empty feed, which is expected but could be surfaced more clearly in the GUI.
- A live end-to-end check of the feed (global and contacts) against public relays has not been
run in this session; the deterministic parts are covered by unit + frontend tests.
- The checkpoint/session discipline in AGENTS.md says to commit the feature first, then the
checkpoint update. This was done: `a1445d1` (feature) then this checkpoint commit.
Security-audit follow-ups in priority order:
1. **Upload path tokens**`upload_image` currently reads any renderer-supplied file path;
issue random tokens per picked file and accept only those (`main.ts`).
2. **CSP** — drop `'unsafe-inline'` from `script-src` in `frontend/index.html`; add
`setWindowOpenHandler` deny + `shell.openExternal` for external links in `main.ts`.
3. **Legacy vault perms** — repo-root `profiles_vault.json` is group-readable (0664);
chmod 600 / delete after confirming migration; also auto-tighten during migration in
`src/vault.rs`.
4. **Write-race fixes** — create files with mode 0600 at creation time in `vault.rs`.
5. **Zeroize** — wipe decrypted key material via the `zeroize` crate.