From 12a431b50730e3de55152620829ee227ab386a35 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 15 Jun 2026 13:40:23 +0000 Subject: [PATCH 1/4] fix(signing): trusted origins sign without a prompt (general NIP-07 signer) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/background.js | 12 ++++++--- test/consent-approval.test.js | 49 ++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/background.js b/src/background.js index 4cb7216..62f7c34 100644 --- a/src/background.js +++ b/src/background.js @@ -160,12 +160,18 @@ async function handleSignEvent (event, origin, _sender) { const keypair = await getKeypair(); - // Check if this is a Solid auth event (kind 27235) - auto-sign if trusted + // A trusted origin signs without a prompt — the same trust model already used + // by GET_PUBLIC_KEY and nip44.{encrypt,decrypt} (decrypting DMs is strictly + // more sensitive than signing, and is silent for trusted origins). An + // untrusted origin always prompts, and approving it establishes revocable + // trust. The previous `&& autoSign && isSolidAuth` gate special-cased Solid + // kind-27235 and made Podkey unusable as a general NIP-07 signer — a normal + // client (kind 0/1/10002/22242…) re-prompted on every page load. isSolidAuth + // now only tunes the wording of the first-contact prompt. const isSolidAuth = event.kind === 27235; const trusted = await isTrustedOrigin(origin); - const autoSign = await getAutoSign(); - let shouldSign = trusted && autoSign && isSolidAuth; + let shouldSign = trusted; if (!shouldSign) { // Show signing prompt with event preview diff --git a/test/consent-approval.test.js b/test/consent-approval.test.js index 05775e0..a931f02 100644 --- a/test/consent-approval.test.js +++ b/test/consent-approval.test.js @@ -11,8 +11,9 @@ * - deny rejects the request ("User denied ...") * - the 60s timeout auto-denies * - APPROVE_SIGNING with approved:false (popup beforeunload = deny) rejects - * - trusted-origin + autoSign auto-path: kind 27235 Solid auth signs with NO - * prompt; any other kind still prompts even when trusted+autoSign + * - trusted-origin auto-path: a trusted origin signs ANY event kind with no + * prompt (general NIP-07 signer); an untrusted origin always prompts, and + * approving it establishes revocable trust */ import { describe, it, beforeEach, afterEach, mock } from 'node:test'; @@ -186,7 +187,7 @@ describe('consent gate / approval flow', () => { }); }); -describe('trusted-origin auto-path (autoSign)', () => { +describe('trusted-origin auto-path (general signer)', () => { let keypair; beforeEach(async () => { @@ -197,49 +198,49 @@ describe('trusted-origin auto-path (autoSign)', () => { await storeKeypair(keypair.privateKey, keypair.publicKey); }); - it('signs a kind-27235 Solid event with NO prompt when trusted + autoSign', async () => { + it('signs a kind-27235 Solid event with NO prompt when the origin is trusted', async () => { await addTrustedOrigin('https://pod.test'); - await setAutoSign(true); const result = await send({ type: 'SIGN_EVENT', event: SOLID_EVENT, origin: 'https://pod.test' }); - assert.equal(windowsCreated.length, 0, 'auto-sign path must not open a popup'); + assert.equal(windowsCreated.length, 0, 'trusted origin must not open a popup'); assert.equal(result.kind, 27235); assert.equal(result.pubkey, keypair.publicKey); assert.equal(result.sig.length, 128); }); - it('still PROMPTS for a non-Solid kind even when trusted + autoSign', async () => { + it('signs a non-Solid kind with NO prompt when the origin is trusted (general signer)', async () => { + // The forum publishes kind 0/10002/22242 on every load; a trusted origin + // must sign these without a per-event prompt or Podkey is unusable as a + // general NIP-07 signer. autoSign is irrelevant — trust is the grant. await addTrustedOrigin('https://pod.test'); - await setAutoSign(true); - const pending = send({ type: 'SIGN_EVENT', event: NOTE_EVENT, origin: 'https://pod.test' }); - await flush(); - assert.equal(windowsCreated.length, 1, 'non-Solid kind must still require approval'); - respond(lastRequestId(), true); - const result = await pending; + const result = await send({ type: 'SIGN_EVENT', event: NOTE_EVENT, origin: 'https://pod.test' }); + assert.equal(windowsCreated.length, 0, 'trusted origin signs any kind without a popup'); assert.equal(result.kind, 1); + assert.equal(result.sig.length, 128); }); - it('PROMPTS for a Solid event when autoSign is OFF (even if trusted)', async () => { - await addTrustedOrigin('https://pod.test'); - await setAutoSign(false); - const pending = send({ type: 'SIGN_EVENT', event: SOLID_EVENT, origin: 'https://pod.test' }); + it('ALWAYS prompts an untrusted origin, regardless of autoSign', async () => { + // No silent first-use: an origin the user has never approved must prompt, + // even for a Solid event and even with the autoSign convenience enabled. + await setAutoSign(true); + const pending = send({ type: 'SIGN_EVENT', event: SOLID_EVENT, origin: 'https://untrusted.test' }); await flush(); - assert.equal(windowsCreated.length, 1, 'autoSign off must require explicit approval'); + assert.equal(windowsCreated.length, 1, 'untrusted origin must require explicit approval'); respond(lastRequestId(), true); const result = await pending; assert.equal(result.kind, 27235); }); - it('approving an untrusted signing request trusts the origin (no re-prompt)', async () => { - await setAutoSign(true); - // First request: untrusted -> prompt -> approve. + it('approving an untrusted signing request trusts the origin (no re-prompt, any kind)', async () => { + // First request: untrusted -> prompt -> approve (establishes trust). const first = send({ type: 'SIGN_EVENT', event: SOLID_EVENT, origin: 'https://new.test' }); await flush(); assert.equal(windowsCreated.length, 1); respond(lastRequestId(), true); await first; - // Second Solid request from the now-trusted origin: auto-signs, no popup. - const second = await send({ type: 'SIGN_EVENT', event: SOLID_EVENT, origin: 'https://new.test' }); + // Second request from the now-trusted origin — a *different* (non-Solid) + // kind — auto-signs with no further popup. + const second = await send({ type: 'SIGN_EVENT', event: NOTE_EVENT, origin: 'https://new.test' }); assert.equal(windowsCreated.length, 1, 'origin became trusted; no second popup'); - assert.equal(second.kind, 27235); + assert.equal(second.kind, 1); }); }); From 40fb946ba8f96582d2d1a1ea99277bf8f4889864 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 15 Jun 2026 15:46:36 +0000 Subject: [PATCH 2/4] docs+ui: refactor README/USAGE/CHANGELOG, fit popup to surface, add key 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 --- CHANGELOG.md | 42 ++++- README.md | 452 +++++++++++++++++----------------------------- USAGE.md | 40 ++-- icons/icon.svg | 17 ++ icons/icon128.png | Bin 0 -> 6237 bytes icons/icon16.png | Bin 0 -> 1594 bytes icons/icon48.png | Bin 0 -> 5623 bytes manifest.json | 12 +- popup/popup.css | 14 +- 9 files changed, 253 insertions(+), 324 deletions(-) create mode 100644 icons/icon.svg create mode 100644 icons/icon128.png create mode 100644 icons/icon16.png create mode 100644 icons/icon48.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 42d26f9..42955da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **NIP-44 (v2) encryption** — `window.nostr.nip44.encrypt(pubkey, plaintext)` - and `window.nostr.nip44.decrypt(pubkey, ciphertext)`, enabling NIP-17 / NIP-59 - gift-wrapped direct messages. Crypto runs in the background service worker - (the private key never reaches the page), reusing the existing message-passing - path. Implemented with `@noble/ciphers` (chacha20) + `@noble/hashes` - (hkdf/hmac/sha256) + `@noble/secp256k1` (ECDH). Verified against the official - NIP-44 spec test vectors. +- **NIP-44 (v2) encryption.** `window.nostr.nip44.encrypt(pubkey, plaintext)` + and `nip44.decrypt(pubkey, ciphertext)` for NIP-17 / NIP-59 gift-wrapped + direct messages. The crypto runs in the background worker (`@noble/ciphers` + chacha20, `@noble/hashes` hkdf/hmac, ECDH via `@noble/secp256k1`) and is + checked against the official NIP-44 test vectors. The private key stays in + the worker. +- **Per-origin signing consent.** The first request from a site opens an + approval popup; closing it or a 60-second timeout denies. Approving grants + per-origin trust that you can revoke from the popup. +- **Session-only key storage.** The private key is held in + `chrome.storage.session`: in memory, cleared when the browser closes, never + copied to the page. +- **NIP-98 Solid authentication.** Opt-in HTTP auth to Solid pods. Each token + carries a fresh 16-byte nonce and binds the request body hash and the final + redirect-aware URL, so one token authorises one request. Trusted Solid hosts + are matched exactly. +- **Schnorr self-verify.** Every signature is checked against the public key + before it is returned. +- **Extension icons** at 16, 48 and 128px, and a `script-src 'self'` + content-security policy across the popup and test page. +- **Continuous integration.** `.github/workflows/ci.yml` runs build, test and + lint on every pull request and push to `main`, and uploads a sideloadable + extension zip. The 133-case suite covers signing, NIP-44 vectors, NIP-98 + token shape, the content-script message whitelist, and the consent flow. + +### Changed + +- A trusted origin signs any event kind without a prompt, so Podkey works as a + general NIP-07 signer. A normal client publishes kind 0, 10002 and 22242 on + every load; those no longer raise a prompt once you trust the site. Untrusted + origins still prompt, and approving one grants trust. +- The `window.nostr` provider exposes `getPublicKey`, `signEvent` and + `nip44.{encrypt,decrypt}`. +- The popup fits within the browser popup height, keeping the Export and GitHub + links in view. ## [0.0.7] - 2024-12-XX diff --git a/README.md b/README.md index eb19d56..58da894 100644 --- a/README.md +++ b/README.md @@ -7,224 +7,139 @@ [![NIP-07](https://img.shields.io/badge/NIP--07-compatible-purple.svg)](https://github.com/nostr-protocol/nips/blob/master/07.md) [![Test Page](https://img.shields.io/badge/test--page-live-brightgreen)](https://javascriptsolidserver.github.io/podkey/test-page/) -**Podkey** is a beautiful, secure browser extension for **did:nostr** and **Solid** authentication. It provides a NIP-07-compatible Nostr wallet that enables seamless authentication to Solid pods using [did:nostr](https://nostrcg.github.io/did-nostr/) identities, while remaining fully compatible with the broader Nostr ecosystem. +Podkey is a NIP-07 signer for Nostr and an HTTP-auth signer for Solid pods. It +puts a `window.nostr` provider on every page, signs events with your key, and +authenticates to Solid servers over NIP-98 using your +[did:nostr](https://nostrcg.github.io/did-nostr/) identity. The private key +stays inside the extension and never reaches the page. + +## What it does + +- **NIP-07 provider**: `getPublicKey`, `signEvent`, and `nip44.{encrypt,decrypt}` + on `window.nostr`, so any NIP-07 Nostr client works without extra wiring. +- **Solid authentication**: NIP-98 HTTP auth to Solid pods, keyed to your + did:nostr identifier. No OAuth redirect, no identity-provider account. +- **NIP-44 (v2) encryption** for NIP-17 / NIP-59 gift-wrapped direct messages. + The key never leaves the background worker; only the ciphertext or plaintext + crosses to the page. +- **Per-origin trust**: approve a site once and it signs without asking again. + Revoke any site from the popup. +- **did:nostr identity**: every public key is a 64-character hex string, usable + directly as `did:nostr:`. + +## Security model + +- The private key lives in `chrome.storage.session`: held in memory, cleared + when the browser closes, never copied to the page. Signing, NIP-44 and NIP-98 + all run in the background service worker. +- A site you have not approved raises a consent popup on its first request. + Closing the popup, or a 60-second timeout, denies it. Approving grants + per-origin trust that you can revoke at any time from the popup. +- Signatures use `@noble/secp256k1` v3 Schnorr and are verified against the + public key before they are returned. +- Each NIP-98 token carries a fresh 16-byte nonce and binds the request body + hash and the final (redirect-aware) URL, so one token authorises one request. +- NIP-98 auto-authentication for Solid is opt-in and off by default. When it is + on, it matches trusted Solid hosts exactly, so a lookalike such as + `inrupt.net.evil.com` is rejected. +- The popup and test page run under a `script-src 'self'` content-security + policy with no inline scripts. + +## Install + +### From a packaged release + +1. Download the latest `podkey-extension` build from the + [releases page](https://github.com/JavaScriptSolidServer/podkey/releases) and + unzip it. +2. Open `chrome://extensions` (or `edge://extensions`). +3. Enable **Developer mode** (top-right). +4. Click **Load unpacked** and select the unzipped folder containing + `manifest.json`. + +### From source -## ✨ What Makes Podkey Different - -### Better than nos2x - -- 🎨 **Beautiful, modern UI** with soft gradients and smooth animations -- 🔐 **Enhanced security** with proper key validation and storage -- ⚡ **Built-in Solid authentication** (NIP-98) for seamless pod access -- 📊 **Trust management** with per-origin permissions -- 🌈 **Delightful user experience** with intuitive design - -### did:nostr & Solid Superpowers - -- **did:nostr identity** - Full support for [did:nostr](https://nostrcg.github.io/did-nostr/) decentralized identifiers -- **Solid authentication** - Zero-redirect authentication to Solid servers using did:nostr -- **Automatic signing** - Automatic signing for trusted Solid pods -- **WebID linking** - Link did:nostr identities to Solid WebIDs (coming soon) - -## 🚀 Quick Start - -### Installation - -1. **Clone the repository:** - - ```bash - git clone https://github.com/JavaScriptSolidServer/podkey.git - cd podkey - ``` - -2. **Install dependencies:** - - ```bash - npm install - ``` - -3. **Build the extension:** - - ```bash - npm run build - ``` - -4. **Load in Chrome:** - - - Open `chrome://extensions/` - - Enable "Developer mode" (top-right toggle) - - Click "Load unpacked" - - Select the `podkey` directory - -5. **Test it:** - - Visit the [test page](https://javascriptsolidserver.github.io/podkey/test-page/) to verify everything works - - Click the Podkey icon to generate or import a key - -## 🎯 Features - -### Core Functionality - -- ✅ **NIP-07 Provider** - Full `window.nostr` API implementation -- ✅ **Key Generation** - Secure cryptographic key generation using `@noble/secp256k1` -- ✅ **Key Import** - Import existing 64-char hex private keys -- ✅ **Event Signing** - Sign Nostr events with Schnorr signatures -- ✅ **NIP-44 Encryption** - v2 `encrypt`/`decrypt` for NIP-17/NIP-59 gift-wrapped DMs (key never leaves the background) -- ✅ **Trust Management** - Per-origin permissions with auto-approval -- ✅ **Beautiful UI** - Soft gradients, smooth animations -- ✅ **64-char Hex Keys** - Proper did:nostr format compatibility - -### Roadmap +```bash +git clone https://github.com/JavaScriptSolidServer/podkey.git +cd podkey +npm install +npm run build # bundles @noble deps into src/background.bundle.js +``` -- 🔜 NIP-04 encryption/decryption -- 🔜 Multiple identity support -- 🔜 Relay management -- 🔜 Activity history -- 🔜 nsec/npub Bech32 encoding -- 🔜 WebID linking -- 🔜 Backup & recovery -- 🔜 Automatic NIP-98 authentication for Solid servers +Then load the `podkey` directory as an unpacked extension (steps 2–4 above). -## 📚 Usage +Pin the toolbar icon (🔑), open it, and generate or import a 64-character hex +key. The [test page](https://javascriptsolidserver.github.io/podkey/test-page/) +detects the extension and runs live signing checks. -### Basic API +## Usage ```javascript -// Check if Podkey is available if (window.nostr) { - // Get your public key const pubkey = await window.nostr.getPublicKey() - console.log('Your public key:', pubkey) - // Sign an event - const event = { + const signed = await window.nostr.signEvent({ kind: 1, created_at: Math.floor(Date.now() / 1000), tags: [], content: 'Hello from Podkey! 🔑' - } - - const signed = await window.nostr.signEvent(event) - console.log('Signed event:', signed) + }) } ``` -### Testing - -Visit the [interactive test page](https://javascriptsolidserver.github.io/podkey/test-page/) to: +### API -- Verify extension installation -- Test key generation and import -- Test event signing with various event types -- View real-time diagnostics and logs +#### `window.nostr.getPublicKey()` -## 🏗️ Architecture +Returns your public key as 64-character hex. Prompts once for a new origin. -### Components +#### `window.nostr.signEvent(event)` -``` -┌─────────────────────────────────────────┐ -│ Browser Extension (Podkey) │ -│ │ -│ ┌───────────────────────────────────┐ │ -│ │ Popup UI (popup/) │ │ -│ │ - Key generation/import │ │ -│ │ - Trust management │ │ -│ │ - Settings & identity display │ │ -│ └───────────────────────────────────┘ │ -│ │ -│ ┌───────────────────────────────────┐ │ -│ │ Background Worker (src/) │ │ -│ │ - Key storage (storage.js) │ │ -│ │ - Event signing (crypto.js) │ │ -│ │ - Permission management │ │ -│ └───────────────────────────────────┘ │ -│ │ -│ ┌───────────────────────────────────┐ │ -│ │ Content Script (src/injected.js)│ │ -│ │ - Injects window.nostr │ │ -│ │ - Bridges page ↔ extension │ │ -│ └───────────────────────────────────┘ │ -└─────────────────────────────────────────┘ -``` +Signs a Nostr event and returns it with `id`, `pubkey` and `sig` populated. A +trusted origin signs with no prompt; a new origin prompts once, and approving it +grants trust. -### Security Model +#### `window.nostr.nip44.encrypt(pubkey, plaintext)` / `window.nostr.nip44.decrypt(pubkey, ciphertext)` -- 🔒 **Private keys never leave the extension** - Stored in Chrome's secure local storage -- 🔒 **User permission required** - Every signing operation requires approval (auto-approved for now) -- 🔒 **Per-origin trust** - Granular permissions for each website -- 🔒 **64-char hex validation** - All keys validated for proper did:nostr format -- 🔒 **Secure event signing** - Uses `@noble/secp256k1` v3.0.0 with Schnorr signatures +NIP-44 (v2) encryption for NIP-17 / NIP-59 direct messages. The private key +stays in the background worker; only the base64 payload or decrypted plaintext +crosses to the page. -## 🛠️ Development - -### Project Structure - -``` -podkey/ -├── manifest.json # Extension manifest (MV3) -├── package.json # npm package configuration -├── src/ -│ ├── background.js # Service worker (message handling) -│ ├── crypto.js # Key generation & signing -│ ├── storage.js # Secure key storage -│ ├── injected.js # Content script (page bridge) -│ └── nostr-provider.js # window.nostr implementation -├── popup/ -│ ├── popup.html # Popup UI structure -│ ├── popup.css # Beautiful styling -│ └── popup.js # Popup logic -├── test-page/ -│ └── index.html # Interactive test page -├── scripts/ -│ └── bundle.js # Build script for dependencies -└── icons/ # Extension icons +```javascript +const peer = '<64-char hex pubkey>' +const payload = await window.nostr.nip44.encrypt(peer, 'hello') +const plaintext = await window.nostr.nip44.decrypt(peer, payload) ``` -### Tech Stack - -- **Cryptography**: [@noble/secp256k1](https://github.com/paulmillr/noble-secp256k1) v3.0.0 -- **Hashing**: [@noble/hashes](https://github.com/paulmillr/noble-hashes) v1.8.0 -- **Bundling**: [esbuild](https://esbuild.github.io/) -- **Storage**: Chrome Storage API -- **UI**: Vanilla JavaScript + CSS Gradients -- **Manifest**: V3 (latest) - -### Building - -```bash -# Install dependencies -npm install - -# Build the extension (bundles dependencies) -npm run build - -# Lint code -npm run lint +## Architecture -# Run tests -npm test +``` +┌─────────────────────────────────────────┐ +│ Podkey (MV3 extension) │ +│ │ +│ Popup UI (popup/) │ +│ key generation / import, trusted-site │ +│ management, identity display, consent │ +│ │ +│ Background worker (src/) │ +│ key storage (storage.js), signing & │ +│ NIP-44 (crypto.js, nip44.js), NIP-98 │ +│ auth, per-origin permission gate │ +│ │ +│ Page bridge (src/injected.js) │ +│ injects window.nostr, relays requests │ +│ to the worker, whitelists message types│ +└─────────────────────────────────────────┘ ``` -### Testing - -1. **Build the extension:** - - ```bash - npm run build - ``` - -2. **Load in Chrome** (see Installation above) - -3. **Visit the test page:** - - - Local: Open `test-page/index.html` in your browser - - Online: https://javascriptsolidserver.github.io/podkey/test-page/ - -4. **Check the console** for any errors +The private key is read only inside the background worker. The page sees a +public key, a signed event, a NIP-44 payload, or a NIP-98 header, never the key +itself. -## 🔐 did:nostr Identity +## did:nostr identity -Podkey is built for **did:nostr** and **Solid** authentication. All public keys are proper 64-character hexadecimal strings, making them fully compatible with the [did:nostr specification](https://nostrcg.github.io/did-nostr/): +A Podkey public key is a 64-character hex string, so it is also a +[did:nostr](https://nostrcg.github.io/did-nostr/) identifier: ```javascript const pubkey = await window.nostr.getPublicKey() @@ -232,130 +147,85 @@ const did = `did:nostr:${pubkey}` // did:nostr:3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d ``` -This enables: +That identifier authenticates you to Solid pods and travels across any +NIP-07-aware app. -- ✅ **did:nostr** decentralized identifiers per [W3C specification](https://nostrcg.github.io/did-nostr/) -- ✅ **Solid pod authentication** using did:nostr identities -- ✅ Cross-platform identity portability -- ✅ Verifiable credentials and authentication +## Development -## 📖 API Reference - -### `window.nostr.getPublicKey()` - -Returns the user's public key (64-char hex). - -```javascript -const pubkey = await window.nostr.getPublicKey() -``` - -### `window.nostr.signEvent(event)` - -Signs a Nostr event. Shows permission prompt if origin is not trusted. - -```javascript -const signed = await window.nostr.signEvent({ - kind: 1, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: 'Hello!' -}) +```bash +npm install +npm run build # bundle dependencies into the service worker +npm test # node --test, 133 cases +npm run lint # eslint, no-unused-vars as error ``` -### `window.nostr.nip44.encrypt(pubkey, plaintext)` / `window.nostr.nip44.decrypt(pubkey, ciphertext)` - -NIP-44 (v2) encryption, used by NIP-17 / NIP-59 gift-wrapped direct messages. -The private key never leaves the background service worker — encryption is -performed there and only the resulting base64 payload (or decrypted plaintext) -crosses to the page, mirroring the existing signing message path. - -```javascript -const peer = '<64-char hex pubkey>' -const payload = await window.nostr.nip44.encrypt(peer, 'hello') -const plaintext = await window.nostr.nip44.decrypt(peer, payload) ``` - -### `window.nostr.getRelays()` - -Returns relay configuration (coming soon). - -```javascript -const relays = await window.nostr.getRelays() +podkey/ +├── manifest.json # MV3 manifest (CSP script-src 'self') +├── src/ +│ ├── background.js # service worker: message handling, consent gate +│ ├── crypto.js # key generation & Schnorr signing +│ ├── nip44.js # NIP-44 v2 encrypt/decrypt +│ ├── nip98-interceptor.js # page-context NIP-98 fetch/XHR auth +│ ├── storage.js # session-only key + trusted-origin storage +│ ├── injected.js # content-script page bridge +│ └── nostr-provider.js # window.nostr implementation +├── popup/ # popup + approval UI +├── test-page/ # install + live-signing test page +└── scripts/bundle.js # esbuild bundler ``` -## 🤝 Contributing - -We love contributions! Here's how to get started: - -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Make your changes -4. Add tests if applicable -5. Ensure tests pass (`npm test`) -6. Commit (`git commit -m 'Add amazing feature'`) -7. Push (`git push origin feature/amazing-feature`) -8. Open a Pull Request +Tests cover the consent flow, NIP-44 against the official spec vectors, NIP-98 +token shape, the content-script message whitelist, and signature self-verify. +CI runs build, test and lint on every pull request and push to `main`, and +uploads a sideloadable extension zip. -### Contribution Ideas +## Roadmap -- 🎨 Icon design (16px, 48px, 128px) -- 📝 Documentation improvements -- 🧪 Additional test coverage -- 🌐 i18n/localization -- ✨ NIP-04 encryption -- 🔧 Bug fixes -- 🚀 Performance improvements +- NIP-04 encryption / decryption +- Multiple identities +- Relay management and `getRelays` +- `nsec` / `npub` Bech32 display +- WebID linking for did:nostr ↔ Solid +- Key backup and recovery -## 🐛 Troubleshooting +## Contributing -### Extension doesn't show up +1. Fork and branch (`git checkout -b feature/your-change`). +2. Make the change and add or update tests. +3. Run `npm test` and `npm run lint` until both pass. +4. Open a pull request. -- Make sure Developer Mode is enabled in `chrome://extensions/` -- Check that you selected the correct directory -- Look for errors in the Chrome console +Good first contributions: extension icons (16/48/128px), test coverage, NIP-04, +i18n, and documentation. -### window.nostr is undefined +## Troubleshooting -- Reload the page after installing Podkey -- Check that the extension is enabled -- Look for conflicts with other Nostr extensions -- Check the browser console for errors +**`window.nostr` is undefined.** Reload the page after installing, confirm the +extension is enabled, and check for another Nostr extension claiming +`window.nostr`. -### Events not signing +**Events will not sign.** Generate or import a key first, and check the service +worker console (the "service worker" link on `chrome://extensions`) for a +blocked consent prompt. -- Make sure you've generated or imported a key -- Check the extension console (click "service worker" in chrome://extensions) -- Look for permission prompts that may be blocked +**Build errors.** Reinstall dependencies (`npm install`) and confirm Node.js +18 or newer. -### Build errors +## License -- Make sure all dependencies are installed: `npm install` -- Check Node.js version (needs >= 18.0.0) -- Try deleting `node_modules` and `package-lock.json`, then `npm install` again +AGPL-3.0. See [LICENSE](LICENSE). -## 📄 License +## Links -AGPL-3.0 License - see [LICENSE](LICENSE) for details - -## 🙏 Acknowledgments - -- Built with 💜 for the Nostr and Solid communities -- Inspired by [nos2x](https://github.com/fiatjaf/nos2x) and the NIP-07 specification -- Part of the [JavaScriptSolidServer](https://github.com/JavaScriptSolidServer) ecosystem -- Cryptography powered by [@noble](https://github.com/paulmillr/noble-secp256k1) - -## 🔗 Links - -- **GitHub**: https://github.com/JavaScriptSolidServer/podkey +- **Repository**: https://github.com/JavaScriptSolidServer/podkey - **Issues**: https://github.com/JavaScriptSolidServer/podkey/issues -- **Test Page**: https://javascriptsolidserver.github.io/podkey/test-page/ -- **did:nostr Specification**: https://nostrcg.github.io/did-nostr/ -- **NIP-07 Spec**: https://github.com/nostr-protocol/nips/blob/master/07.md -- **NIP-98 Spec**: https://github.com/nostr-protocol/nips/blob/master/98.md -- **Solid Project**: https://solidproject.org/ +- **Test page**: https://javascriptsolidserver.github.io/podkey/test-page/ +- **did:nostr**: https://nostrcg.github.io/did-nostr/ +- **NIP-07**: https://github.com/nostr-protocol/nips/blob/master/07.md +- **NIP-98**: https://github.com/nostr-protocol/nips/blob/master/98.md +- **Solid**: https://solidproject.org/ --- -**Made with 🔑 by the JavaScriptSolidServer team** - -_Podkey v0.0.7 - Your keys, your identity, your data_ +_Podkey — your keys, your identity, your data._ diff --git a/USAGE.md b/USAGE.md index 05b1151..bc76d56 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,6 +1,6 @@ # 🚀 How to Use Podkey -**Podkey** is a browser extension for **did:nostr** and **Solid** authentication. It provides NIP-07-compatible Nostr wallet functionality with seamless Solid pod authentication using [did:nostr](https://nostrcg.github.io/did-nostr/) identities. +**Podkey** is a browser extension for **did:nostr** and **Solid** authentication. It puts a NIP-07 `window.nostr` provider on every page and authenticates to Solid pods over NIP-98, both keyed to your [did:nostr](https://nostrcg.github.io/did-nostr/) identity. ## Quick Start Guide @@ -116,24 +116,25 @@ if (window.nostr) { ## Using Podkey with Solid Servers -Podkey automatically handles NIP-98 authentication for Solid servers! +Podkey authenticates to Solid servers over NIP-98, using your did:nostr key. -### How It Works +### How it works -1. When you access a protected resource on a Solid server -2. Podkey detects the 401 authentication requirement -3. You'll be prompted to trust the origin (first time only) -4. Podkey signs an NIP-98 HTTP authentication event -5. The request is retried with the signed header -6. You get access! ✨ +1. You request a protected resource on a Solid server. +2. The server replies with a 401. +3. Podkey asks you to trust the origin (first time only). +4. Podkey signs a NIP-98 HTTP authentication event. +5. The request is retried with the signed header, and you get access. -**No OAuth redirects. No IdP accounts. Just seamless authentication.** +No OAuth redirect, no identity-provider account. -### Enable Auto-Sign +### Enable auto-sign -1. Click the Podkey icon -2. Toggle **"Auto-sign for Solid"** to ON -3. Trusted Solid servers will now authenticate automatically +Auto-sign is off by default. To turn it on: + +1. Click the Podkey icon. +2. Toggle **Auto-sign for Solid** to on. +3. Trusted Solid servers then authenticate without a prompt. --- @@ -258,7 +259,8 @@ const pubkey = await window.nostr.getPublicKey() ### window.nostr.signEvent(event) -Signs a Nostr event. Shows permission prompt if origin is not trusted. +Signs a Nostr event. A trusted origin signs with no prompt; a new origin +prompts once, and approving it grants trust. ```javascript const signed = await window.nostr.signEvent({ @@ -269,12 +271,14 @@ const signed = await window.nostr.signEvent({ }) ``` -### window.nostr.getRelays() +### window.nostr.nip44.encrypt(pubkey, plaintext) / .decrypt(pubkey, ciphertext) -Returns relay configuration (coming soon). +NIP-44 (v2) encryption for NIP-17 / NIP-59 direct messages. The private key +stays in the background worker. ```javascript -const relays = await window.nostr.getRelays() +const payload = await window.nostr.nip44.encrypt(peerPubkey, 'hello') +const plaintext = await window.nostr.nip44.decrypt(peerPubkey, payload) ``` --- diff --git a/icons/icon.svg b/icons/icon.svg new file mode 100644 index 0000000..1ff1b35 --- /dev/null +++ b/icons/icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..029ec0fcc3d3164de9d1b6878ad43e97ffba79d3 GIT binary patch literal 6237 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 ropgp&1H*NV3TO`*lfEy9U zWf15bh5`Tkva3Zb52g-R)>2koe5;IV%7>Fr6Y*CrT$&nw zJG_$c)oAd^6Irj=Hpa2BH=$r5Z@lcohXrwc9in8+Cb*}SVO3kROWcO7+GWvC+D_jD z(@#LK4I=;$O+M?Bv|f1)vZA^^Lz-G7G(%0qOCQQ=N$FE-Hgx*sQF6-(hV{`lAOrhR z@Au*PM%8O9cUz~(!=L}`d@$MMpnE9WBdIuNIZRy=Q}2Sr7X(=#LlCg=Ay+;7{Z_sv zsYTI|$&+yrU%*GjRn;i>L~=(;8n+Poi`q=Ofq~ZL$E#P=9L!(=9bQpR>e&@R&^NN3 zS2QunhjFVifS8W`$%p|aoWtA6tcWnmXr z+A`dtN;SC>dp-3*l(uN;{&q2~eXs?UGwU-+8nv;!lf!|{IXX0p^z@0ZyNa;q&gl@B zR)b7eti!TxBS zvbzRbnni~a{UA1um+d|q6(&JB8O#9nfLGp*?cg!civ&np*}3vF)0&Uy!7tTbkmjuG z_<^eV3$;rv0V>C)-Fm(;n{!wajcOnxX>I?Gc2 z<}^@sMi+B~wze9B#m6R_=>YwQ^!)47wCC}2`ttD$;BH}W1ZraJ zmg;yD+&_=UcSI^1_#CI6=mcpNYQfhwb#6VJ`xp~X=bUX)ni!wwSfpG;tOWYGJm{gU z)j=CN9G#-}?uBRdPg@pQ(`4nk*NzGv%-qXT3vJ&JC<8~QEf58iz$ncNycOs1drvtq zp;mi=-S(99wr^*)3@7gsXEeRJ1aT!1m|-FZbYC=_!#}$D3&iEbO_Mg}UO{5%QteJC zNt9Oals!=CC5m07Qh8lUmc-IWn!aW=u`f=X9<^os#KgY!8nS=Z0G@aldQVQ2N}tc0 zyOk31BZeBPt2zetgZ{S=q!>0{r&2?c9M54-0k*7a%d)#S1w3;oCM>L`)dUuRM}8?e8Eay0)i zx~suqns<-r38?+OFY~N7FCxEpH`KT-*he}PYe~JDiStTc@av1F@7M9Bq(LL4THE37 z{t~b@4U*?+5mbsuIlqx}Q@blF&v=WxA)r$irKQD5eI+VJ`l9RPtMpy^XQqQg`qYm( zePT&a5<_pjSMg`RfX5TkJoRjOQWA>H5SLsk#tK7Q*@%oLZk1>P2RZ+f>*|=|8=r8v zuhDKLbYHSE9uFT_zD-w>1PsmH!x~4ZXMEgC8$rt0*-;GHZt%6=)9D%`$=|@UR8)GQ T^~4Uv69q_aUar@iL-~IKifXC} literal 0 HcmV?d00001 diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..52e877c4d95e6c00f6064860549e890ca0917097 GIT binary patch literal 5623 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 literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json index 2aed040..8ef0cde 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,11 @@ "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, "permissions": [ "storage" ], @@ -29,7 +34,12 @@ } ], "action": { - "default_popup": "popup/popup.html" + "default_popup": "popup/popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } }, "web_accessible_resources": [ { diff --git a/popup/popup.css b/popup/popup.css index 3408f7d..149419d 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -102,8 +102,8 @@ } body { - width: 380px; - min-height: 480px; + width: 400px; + min-height: 460px; font-family: var(--sans); background: var(--bg); color: var(--text); @@ -113,7 +113,7 @@ body { } .screen { - padding: 18px 18px 14px; + padding: 16px 18px 12px; } /* ---- Header ---- */ @@ -121,7 +121,7 @@ body { display: flex; align-items: center; gap: 10px; - margin-bottom: 18px; + margin-bottom: 14px; } .header.center { @@ -191,7 +191,7 @@ h2 { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; - margin-bottom: 12px; + margin-bottom: 10px; box-shadow: var(--shadow-sm); } @@ -503,7 +503,7 @@ input:focus-visible + .slider { /* ---- Trusted sites ---- */ .trusted-list { - max-height: 168px; + max-height: 120px; overflow-y: auto; margin: -2px; padding: 2px; @@ -599,7 +599,7 @@ input:focus-visible + .slider { /* ---- Footer ---- */ .footer { text-align: center; - padding: 12px 0 2px; + padding: 10px 0 2px; font-size: 12px; color: var(--text-muted); } From ab5c63f017118262f342051c88e1fac9e0cf9be6 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 16 Jun 2026 09:23:49 +0000 Subject: [PATCH 3/4] feat(vault): encrypted-at-rest key persistence with passphrase unlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #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 --- popup/popup.css | 14 +++- popup/popup.html | 77 ++++++++++++++++++++- popup/popup.js | 162 ++++++++++++++++++++++++++++++++++++++++----- src/background.js | 148 +++++++++++++++++++++++++++++++---------- src/storage.js | 21 +++++- src/vault.js | 142 +++++++++++++++++++++++++++++++++++++++ test/vault.test.js | 68 +++++++++++++++++++ 7 files changed, 571 insertions(+), 61 deletions(-) create mode 100644 src/vault.js create mode 100644 test/vault.test.js diff --git a/popup/popup.css b/popup/popup.css index 149419d..a3daf78 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -371,7 +371,8 @@ label { margin-bottom: 7px; } -textarea { +textarea, +.input { width: 100%; padding: 11px; border: 1px solid var(--border-strong); @@ -385,11 +386,18 @@ textarea { transition: border-color 0.15s ease, box-shadow 0.15s ease; } -textarea::placeholder { +.input { + resize: none; + margin-bottom: 8px; +} + +textarea::placeholder, +.input::placeholder { color: var(--text-faint); } -textarea:focus { +textarea:focus, +.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); diff --git a/popup/popup.html b/popup/popup.html index ecfc185..e609051 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -54,6 +54,66 @@

Welcome to Podkey

+ + + + + +