Skip to content

B3Note v2: native vetKD, burn-after-reading links, on-chain AI, ic-reactor 4 - #10

Merged
b3hr4d merged 4 commits into
mainfrom
claude/b3note-v2
Oct 8, 2026
Merged

b3hr4d merged 4 commits into
mainfrom
claude/b3note-v2

Conversation

@b3hr4d

@b3hr4d b3hr4d commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Summary

A rewrite of B3Note. v1 had security and correctness problems serious enough that patching wasn't practical (see "What v2 fixes" in the README):

  • notes were encrypted with keys from DFINITY's unsafe example vetKD canister;
  • anyone could overwrite any note;
  • Internet Identity users couldn't read their own notes;
  • writes to stable-map entries were silently dropped;
  • timers stopped after upgrades.

Backend (backend/, Rust)

  • ic-cdk 0.20, ic-stable-structures 0.7 and the IC's native vetKD:
    • per-user note keys (b3note/user-key/v1, input = caller);
    • an IBE share key (b3note/share/v1).
  • Notes: paginated list_notes, optimistic concurrency (expected_version → Conflict), self-destruct timers, quotas.
  • Burn-after-reading share links:
    • an Ed25519 key travels in the URL #fragment, and the reader signs (domain, share id, transport key);
    • opening spends one view, and the view is given back if key derivation fails;
    • replaying the same transport key is refused.
  • AI assistant on the IC LLM canister (ai_assist): summarize, title, tags, improve, grammar, shorten, action items, translate, continue, and ask across notes. It has per-user and global hourly budgets and a controller kill switch.
  • Safety: rate limits, an inspect_message allow-list, cleanup timers that are re-registered after upgrades, and validated install args.
  • backend.did is checked against the code by a unit test (UPDATE_CANDID=1 regenerates it).

Frontend (frontend/)

  • Vite 8, React 19, Tailwind v4, TanStack Router and Query, @icp-sdk/* and @icp-sdk/vetkeys.
  • ic-reactor 4.0.0-beta.3, pinned exactly:
    • bindings are generated from backend.did into src/canisters/backend.ts (candid-core);
    • one client per tab, borrowed by ReactorProvider;
    • client.queryOptions / mutationOptions with TanStack's own hooks.
  • Sign-in (SessionAuth): one AuthLike over Internet Identity and a guest key. The guest key comes with a recovery key that can be exported and imported.
  • Encryption:
    • notes are encrypted in the browser with a non-extractable AES-GCM key (cached in IndexedDB), with the note id as associated data;
    • on mainnet, vetKD public keys are pinned against the IC master key.
  • App features: Markdown with clickable checklists, tags, pins, search, autosave, conflict resolution, export/import, and account deletion.

Tooling

  • icp.yaml (icp-cli 1.6): Rust and asset-canister recipes, a local network with Internet Identity, and a production environment.
  • scripts/build-canisters.sh, scripts/test-backend.sh, and frontend/scripts/pocketic-dev.ts for UI work without icp-cli.
  • CI (.github/workflows/ci.yml):
    • backend: fmt, clippy, unit and PocketIC tests;
    • frontend: format, typecheck, tests, build, a check that the generated bindings match backend.did, and PicJS against the real canister.
  • MIT LICENSE (it was empty).

Testing

  • ./scripts/test-backend.sh: 28 Rust unit tests and 30 PocketIC integration tests with real vetKD. The integration tests check keys with the ic-vetkeys crate and use a mock LLM canister.
  • pnpm test (frontend): 39 tests. The real crypto runs against a fake vetKD with a known master key; the data layer and sign-in run on ic-reactor's test client.
  • pnpm test:pic: 4 tests of the browser code and ic-reactor clients against the real canister in PocketIC.
  • Manual browser runs (Playwright), all with no console errors:
    • PocketIC dev setup: notes, checklists, AI actions, Ask, share links and burning, settings, mobile/dark;
    • also on PocketIC: sign-out and restore, recovery-key import on a second browser, edit conflict, saving a shared note, Markdown import, note and account deletion;
    • an icp deploy on a local icp-cli network: guest sign-in, a note, reload, and opening a share link from a fresh browser.
  • Not verified: a complete Internet Identity passkey sign-in. The local II window opens at id.ai.localhost:8000/authorize and cancelling is handled, but a headless browser couldn't create the passkey, so this needs a manual check.

Notes for reviewers

  • v2 has a new stable-memory layout. Install it as a new canister or reinstall. v1 notes cannot be migrated, because they were encrypted with keys from the insecure demo vetKD canister.
  • ic-reactor 4 is a beta. The @ic-reactor/* and @candid-core/* versions are pinned exactly.

🤖 Generated with Claude Code

https://claude.ai/code/session_017mjAwRTxCWYEMCuMXgkyjS


Generated by Claude Code

claude added 4 commits October 7, 2026 21:50
The old canister relied on DFINITY's unsafe demo vetKD canister (its master
secret key is in the source), encrypted every note to the anonymous
principal and handed that decryption key to any caller, and let anyone
overwrite any note. This replaces it:

- ic-cdk 0.20, ic-stable-structures 0.7 and the management canister's
  vetkd_derive_key / vetkd_public_key
- per-user vetKeys (input: the caller's principal) for AES-GCM note
  encryption done in the browser; owner-checked CRUD with optimistic
  concurrency, quotas and self-destruct timers
- burn-after-reading share links: IBE ciphertext for the share id plus an
  Ed25519 key carried in the link; the canister releases the share's
  vetKey once per view and deletes the ciphertext after the last one
- ai_assist through the on-chain LLM canister (ic-llm types), with
  per-user and global hourly budgets
- cleanup timers re-registered after upgrades, config via install args
- 27 unit tests and 29 PocketIC tests that do the client-side crypto with
  ic-vetkeys against real vetKD, plus a mock LLM canister

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mjAwRTxCWYEMCuMXgkyjS
Replaces the Next.js 13 / rematch / @dfinity/agent 0.19 app.

- Vite + React 19 + Tailwind v4, TanStack Router with lazy routes
- @ic-reactor/vite-plugin codegen with factories; list_notes as an
  infinite query, mutations with invalidation and reactorUpdateRetry
- Internet Identity through ic-reactor's AuthenticationManager, plus
  guest keys (a local Ed25519 identity with a recovery key)
- the vault: the user's vetKey, fetched with a one-time transport key,
  verified, turned into a non-extractable AES-GCM key and cached in
  IndexedDB; on mainnet the canister's public keys are checked against
  the IC master key
- markdown notes with tasks, tags, pinning, search, self-destruct
  timers, autosave with conflict handling, export and import
- burn-after-reading links (IBE + Ed25519 key in the URL fragment)
- AI assistant (summaries, titles, tags, rewrites, translation, to-dos)
  and "Ask your notes" with client-side retrieval, behind a consent step
- icp.yaml for icp-cli (local network with II, production environment)
- tests: crypto with a fake vetKD, session store, Markdown, the data
  layer against ic-reactor's fake replica, and a PicJS suite that runs
  the frontend code against the real canister in PocketIC

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mjAwRTxCWYEMCuMXgkyjS
…message

- GitHub Actions: rustfmt, clippy -D warnings, unit + PocketIC tests,
  frontend format/typecheck/tests/build and the PicJS suite against the
  built canister
- README rewritten: features, encryption design, local development with
  icp-cli or PocketIC, mainnet deployment, testing, what v2 fixes
- MIT license text (the LICENSE file was empty), root package.json
- inspect_message refused public reads and update_config for anonymous
  callers, which is icp-cli's default local identity (and the local
  controller); update_config still checks for a controller itself

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mjAwRTxCWYEMCuMXgkyjS
- Bindings: @ic-reactor/vite-plugin 4 generates src/canisters/backend.ts
  (candid-core) from backend.did; CI checks the committed file matches.
- One client per tab (createClient, network "env") borrowed by
  ReactorProvider; components use TanStack's useQuery/useMutation with
  client.queryOptions/mutationOptions, other code calls the backend
  directly. Results are unwrapped and an Err is a typed canister_err.
- SessionAuth: one AuthLike over Internet Identity and the guest key, so
  useAuth(), query keys and signing follow whichever is active. Signing in
  as a guest signs a lingering Internet Identity session out.
- list_notes pages through useInfiniteQuery over client-built reads under
  the caller-scoped key; decrypted notes never carry over to another caller.
- Share links open with an anonymous reader client, which keeps the
  reader's principal off the share and uses the backend's anonymous budget.
- Tests: the data layer and sign-in on ic-reactor's test client (fake
  replica, mocked canister); the PicJS suite and the PocketIC dev script use
  v4 clients and the generated InitArgs schema. The dev script is now
  TypeScript (Node 22.18+).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mjAwRTxCWYEMCuMXgkyjS
@b3hr4d
b3hr4d merged commit 92427da into main Oct 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants