Skip to content

Podkey 0.0.8: general-signer signing, refreshed docs, popup fit, key icons#23

Merged
jjohare merged 4 commits into
JavaScriptSolidServer:mainfrom
jjohare:fix/general-signer-trusted-origins
Jun 16, 2026
Merged

Podkey 0.0.8: general-signer signing, refreshed docs, popup fit, key icons#23
jjohare merged 4 commits into
JavaScriptSolidServer:mainfrom
jjohare:fix/general-signer-trusted-origins

Conversation

@jjohare

@jjohare jjohare commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Brings Podkey to a coherent 0.0.8: the general-signer fix, documentation that matches the current capability and security model, a popup that fits the browser surface, and real extension icons.

Signing — trusted origins sign without a prompt

handleSignEvent only auto-signed kind 27235 (Solid auth) for a trusted origin; every other kind re-prompted on each request. A normal client publishes kind 0 / 10002 / 22242 on each page load, so users saw three approval popups every time. Signing now honours trust like getPublicKey and nip44.{encrypt,decrypt} already do:

let shouldSign = trusted;

A trusted origin signs any kind with no prompt. An untrusted origin always prompts, and approving it grants revocable per-origin trust. No silent first-use.

Docs

README, USAGE and CHANGELOG rewritten to describe the current behaviour: session-only key storage, per-origin consent, trusted-origin signing, NIP-98 with a fresh nonce and body/URL binding, and the getPublicKey / signEvent / nip44 provider surface. UK English; the removed getRelays stub is no longer documented.

Popup

The trusted-sites list is capped and section spacing tightened so the Export / GitHub footer stays inside the browser popup height instead of scrolling off; width is 400px.

Icons

16 / 48 / 128px key icons (icons/icon.svg master) wired into manifest.icons and action.default_icon, so the toolbar shows the Podkey key.

Verification

npm run build ✓ · npm test → 133 pass / 0 fail ✓ · npm run lint → 0 errors ✓. CI uploads the sideloadable podkey-extension zip on every run.

🤖 Generated by Claude Code

…igner)

handleSignEvent gated auto-signing on `trusted && autoSign && isSolidAuth`,
so a trusted origin only signed kind-27235 (Solid auth) silently and
re-prompted for every other event kind. A normal Nostr client — the forum
publishes kind 0 / 10002 / 22242 on every load — therefore raised three
approval popups on each page load, making Podkey unusable as a single
general-purpose signer.

Align signing with the trust model already used by GET_PUBLIC_KEY and
nip44.{encrypt,decrypt}: a trusted origin is sufficient to skip the prompt
(decrypting DMs is strictly more sensitive than signing, and is already
silent for trusted origins). An untrusted origin still ALWAYS prompts, and
approving it establishes revocable trust — no silent first-use.

    let shouldSign = trusted && autoSign && isSolidAuth;
    ->  let shouldSign = trusted;

Tests updated to the general-signer contract (133 pass, lint clean).

Co-Authored-By: jjohare <github@thedreamlab.uk>
@CLAassistant

CLAassistant commented Jun 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

…ey icons

- README/USAGE/CHANGELOG: rewrite to match the current capability and security
  model (session-only key, per-origin consent, trusted-origin signing, NIP-98
  with fresh nonce + body/URL binding, honest window.nostr surface). UK English.
- popup: cap the trusted-sites list and tighten section spacing so the Export /
  GitHub footer stays within the browser popup height instead of scrolling off;
  widen to 400px.
- icons: add 16/48/128px key icons (icons/icon.svg master) and wire them into
  manifest icons + action.default_icon so the toolbar shows the Podkey key.

Co-Authored-By: jjohare <github@thedreamlab.uk>
@jjohare jjohare changed the title fix(signing): trusted origins sign without a prompt (general NIP-07 signer) Podkey 0.0.8: general-signer signing, refreshed docs, popup fit, key icons Jun 15, 2026
PR JavaScriptSolidServer#22's hardening stored the private key only in chrome.storage.session
(in-memory), so it was wiped on every browser restart — getPublicKey() then
threw the non-intuitive "No keypair found" and the user had to re-import their
nsec each time. That is "no at-rest persistence", not "encryption at rest".

Add a real encrypted-at-rest vault:
- src/vault.js: AES-256-GCM ciphertext in chrome.storage.local, key wrapped by
  scrypt(passphrase) (N=2^16, r=8, p=1; params sealed in the blob for future
  upgrade). Pure encrypt/decrypt split from the storage I/O so the crypto is
  unit-tested off-platform. Wrong passphrase / tamper fail closed via the GCM tag.
- Session stays the hot cache: on unlock the decrypted key is held in
  chrome.storage.session for fast signing; a browser restart clears it and the
  user re-unlocks. The raw key never touches disk.
- background.js: generate/import now take a passphrase and seal the vault;
  new UNLOCK_VAULT / LOCK_VAULT; GET_KEYPAIR_STATUS reports none|locked|unlocked.
  A locked vault on getPublicKey/sign/nip44 opens the unlock popup and throws a
  clear "Podkey is locked — unlock and try again" instead of "No keypair found".
- popup: set-passphrase step on generate, passphrase fields on import, an unlock
  screen (with forget-key/start-over), and a Lock action. Pill reads "Unlocked".

Tests: 8 vault crypto tests (round-trip, wrong passphrase, tamper, validation,
salt/iv freshness). Full suite 141 pass; eslint src/ clean.

Co-Authored-By: jjohare <github@thedreamlab.uk>
@jjohare

jjohare commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

Added commit ab5c63fencrypted-at-rest key persistence (fixes the key being dropped on browser restart).

PR #22's hardening stored the private key only in chrome.storage.session, so it was wiped on every browser restart and getPublicKey() then threw "No keypair found". This adds a real vault:

  • src/vault.js: AES-256-GCM ciphertext in chrome.storage.local, key-wrapped by scrypt(passphrase) (N=2¹⁶, params sealed in the blob). Pure encrypt/decrypt split from storage I/O so the crypto is unit-tested off-platform; wrong passphrase / tamper fail closed on the GCM tag.
  • Session remains the hot cache (decrypted key held in memory for fast signing); browser restart → re-unlock. Raw key never touches disk.
  • background.js: generate/import take a passphrase and seal the vault; new UNLOCK_VAULT/LOCK_VAULT; GET_KEYPAIR_STATUS reports none|locked|unlocked; a locked vault opens the unlock popup with a clear message instead of "No keypair found".
  • popup: set-passphrase on generate, passphrase on import, unlock screen (+ forget-key), Lock action.

8 new vault crypto tests; full suite 141 pass, eslint src/ clean.

- PRIVACY.md: repo-hosted privacy policy (local signer; key encrypted at rest;
  no servers/accounts/telemetry; per-permission rationale incl. <all_urls>).
  Doubles as the Chrome Web Store privacy disclosure basis.
- README/USAGE/CHANGELOG: bring the security model up to date with the encrypted
  vault (was "session-only"), document the passphrase + unlock/lock/forget flow,
  bump version 0.0.7 -> 0.0.8, test count 133 -> 141, add vault.js to the file
  map, and add a "Where Podkey fits" section positioning it against existing
  NIP-07 signers, the did:nostr ecosystem (github.com/topics/did-nostr), and
  Solid (solidproject.org) — extends + consolidates rather than reinvents.
- site/: a polished, self-contained docs landing (index.html) + hosted privacy
  page (privacy.html) for a clean Web Store privacy URL.
- gh-pages.yml: complete the migration off the legacy gh-pages branch — assemble
  the docs site + test page and deploy via actions/deploy-pages (the proper
  GitHub Actions Pages workflow), triggered on site/test-page changes.

Co-Authored-By: jjohare <github@thedreamlab.uk>
@jjohare jjohare merged commit 52a5f62 into JavaScriptSolidServer:main Jun 16, 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