diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 8a3fa8f..dd818d5 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -5,8 +5,10 @@ on: branches: - main paths: + - 'site/**' - 'test-page/**' - '.github/workflows/gh-pages.yml' + workflow_dispatch: permissions: contents: read @@ -30,10 +32,18 @@ jobs: - name: Setup Pages uses: actions/configure-pages@v4 + - name: Assemble site (docs landing + privacy + test page) + run: | + mkdir -p _site + cp -r site/. _site/ + mkdir -p _site/test-page + cp -r test-page/. _site/test-page/ + echo "Assembled _site:" && ls -R _site | head -40 + - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: './test-page' + path: './_site' - name: Deploy to GitHub Pages id: deployment diff --git a/CHANGELOG.md b/CHANGELOG.md index 42d26f9..38b05d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,46 @@ 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. +- **Encrypted-at-rest key vault.** The private key is persisted only as an + AES-256-GCM ciphertext in `chrome.storage.local`, wrapped by a scrypt-derived + key from the user's passphrase (`src/vault.js`). On unlock the decrypted key + is cached in `chrome.storage.session` for fast signing and cleared when the + browser closes; the raw key never touches disk. New `UNLOCK_VAULT`/`LOCK_VAULT` + messages, a popup unlock screen, and a clear "Podkey is locked" prompt replace + the previous "No keypair found" error after a restart. +- **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 141-case suite covers signing, NIP-44 vectors, NIP-98 + token shape, the content-script message whitelist, the consent flow, and the + vault crypto (round-trip, wrong passphrase, tamper, salt/iv freshness). + +### 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/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..dcba05b --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,75 @@ +# Podkey Privacy Policy + +_Last updated: 2026-06-16_ + +Podkey is a browser extension that holds a Nostr/[did:nostr](https://nostrcg.github.io/did-nostr/) +key on your device and uses it to sign Nostr events ([NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md)) +and to authenticate to [Solid](https://solidproject.org) pods over +[NIP-98](https://github.com/nostr-protocol/nips/blob/master/98.md). It is a +**local signer**: your key never leaves your device, and Podkey has no servers, +no accounts, and no analytics. + +## What Podkey stores, and where + +All data is stored **locally** in the browser's extension storage. Nothing is +sent to the Podkey developers or to any third party. + +| Data | Where | Notes | +|------|-------|-------| +| **Private key** | `chrome.storage.local`, **encrypted** | Stored only as an AES-256-GCM ciphertext, wrapped by a key derived from your passphrase with scrypt. The plaintext key is never written to disk. | +| **Private key (unlocked)** | `chrome.storage.session`, in-memory | After you unlock with your passphrase, the decrypted key is held in memory for the browser session so signing is fast. It is cleared when the browser closes. | +| **Public key** | `chrome.storage.local` | Your `did:nostr` identity. Not secret. | +| **Trusted origins** | `chrome.storage.local` | The sites you have approved to sign without re-prompting, and an optional per-site auto-sign preference. You can revoke any of them from the popup. | + +There is **no telemetry, no tracking, no advertising identifiers, and no +remote logging.** Podkey collects none of your browsing history or page content. + +## How your key is used + +- **Signing** (NIP-07 `signEvent`, `nip44.encrypt/decrypt`) and **Solid + authentication** (NIP-98) happen entirely inside the extension's background + service worker. The raw private key is never exposed to web pages — only the + resulting signature, ciphertext/plaintext, or `Authorization` header crosses + to the page. +- A site you have not approved triggers a consent prompt on its first request. + Closing the prompt, or a 60-second timeout, denies it. +- Podkey makes **no network requests of its own.** A NIP-98 `Authorization` + header is attached only to requests **you** initiate to a Solid/Nostr server + you have trusted; the data goes to that server, under your control, not to + Podkey. + +## Permissions, and why they are needed + +- **`storage`** — to keep the encrypted key, your public key, and your trusted + sites locally, as described above. +- **Host access (``)** — Podkey is a NIP-07 signer, so it injects a + `window.nostr` provider into pages and, for sites you have trusted, attaches + NIP-98 auth to your own requests. This requires script access to the pages + where you use a Nostr/Solid app. Podkey **does not read, collect, or transmit + page content or browsing history**; host access is used solely to expose the + signer and to intercept auth for origins you have explicitly approved. + +## Data sharing and retention + +- Podkey **does not sell or share** any data. There is no data broker, no + third-party processor, and no off-device transfer of your key or activity. +- All data remains on your device until you delete it. **Removing the + extension, or using "Forget key" in the popup, erases the stored vault, public + key, and trusted sites.** Back up your private key before doing so — without a + backup it cannot be recovered. + +## Your control + +- Unlock / lock your key on demand from the popup. +- Review and revoke trusted sites at any time. +- Export your private key (after unlocking) to back it up. +- Forget the key entirely to start over. + +## Contact + +Podkey is open source under AGPL-3.0. Questions, issues, or security reports: +. + +This policy may be updated as the extension evolves; the "Last updated" date +above reflects the current version. Material changes will be noted in the +project [CHANGELOG](CHANGELOG.md). diff --git a/README.md b/README.md index eb19d56..f8240bd 100644 --- a/README.md +++ b/README.md @@ -2,229 +2,152 @@ > Browser extension for **did:nostr** and **Solid** authentication -[![Version](https://img.shields.io/badge/version-0.0.7-blue.svg)](https://github.com/JavaScriptSolidServer/podkey/releases) +[![Version](https://img.shields.io/badge/version-0.0.8-blue.svg)](https://github.com/JavaScriptSolidServer/podkey/releases) [![License](https://img.shields.io/badge/license-AGPL--3.0-green.svg)](LICENSE) [![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 + +- **Encrypted at rest.** The private key is persisted only as an AES-256-GCM + ciphertext in `chrome.storage.local`, wrapped by a key derived from your + passphrase with scrypt. The raw key is never written to disk. +- **Unlocked in memory.** When you unlock with your passphrase, the decrypted + key is cached in `chrome.storage.session` for the browser session so signing + is fast; it is cleared when the browser closes, so you re-unlock next time. It + is 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, choosing an **encryption passphrase**. The key is sealed under that +passphrase (see [Security model](#security-model)); you unlock it once per +browser session. 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: - -- Verify extension installation -- Test key generation and import -- Test event signing with various event types -- View real-time diagnostics and logs - -## 🏗️ Architecture +### API -### Components +#### `window.nostr.getPublicKey()` -``` -┌─────────────────────────────────────────┐ -│ 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 │ │ -│ └───────────────────────────────────┘ │ -└─────────────────────────────────────────┘ -``` +Returns your public key as 64-character hex. Prompts once for a new origin. -### Security Model +#### `window.nostr.signEvent(event)` -- 🔒 **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 +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. -## 🛠️ Development +#### `window.nostr.nip44.encrypt(pubkey, plaintext)` / `window.nostr.nip44.decrypt(pubkey, ciphertext)` -### Project Structure +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. +```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) ``` -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 -``` - -### 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 +## Architecture -# Lint code -npm run lint - -# Run tests -npm test +``` +┌─────────────────────────────────────────┐ +│ Podkey (MV3 extension) │ +│ │ +│ Popup UI (popup/) │ +│ key generation / import, trusted-site │ +│ management, identity display, consent │ +│ │ +│ Background worker (src/) │ +│ encrypted key vault (vault.js) + session│ +│ cache (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 +155,113 @@ const did = `did:nostr:${pubkey}` // did:nostr:3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d ``` -This enables: - -- ✅ **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 +That identifier authenticates you to Solid pods and travels across any +NIP-07-aware app. -## 📖 API Reference +## Where Podkey fits -### `window.nostr.getPublicKey()` +Podkey sits at the join of two mature, independently-built ecosystems and +consolidates them behind one key: -Returns the user's public key (64-char hex). - -```javascript -const pubkey = await window.nostr.getPublicKey() -``` +- **Nostr signing already exists.** NIP-07 browser signers (nos2x, Alby, and + others) are well-established. Podkey is fully NIP-07 compatible, so every + existing Nostr client works with it unchanged — it does not reinvent that + surface. +- **[did:nostr](https://github.com/topics/did-nostr)** is an emerging ecosystem + of decentralized-identity tooling built on Nostr keys. Podkey treats your + public key as a first-class `did:nostr` identifier rather than just a signing + key. +- **[Solid](https://solidproject.org)** (the W3C-aligned personal-data-pod + standard) is highly mature but has historically required OIDC/WebID identity + providers. Podkey authenticates to Solid pods over NIP-98 keyed to your + did:nostr — no OAuth redirect, no IdP account. -### `window.nostr.signEvent(event)` +The novel part is the **consolidation**: one locally-held, encrypted key that +is simultaneously your Nostr signer, your `did:nostr` identity, and your Solid +login. Podkey extends what existing Nostr signers do (NIP-07) with did:nostr +identity and Solid/NIP-98 authentication, and adds an encrypted-at-rest vault on +top. -Signs a Nostr event. Shows permission prompt if origin is not trusted. +## Development -```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, 141 cases (incl. vault crypto) +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 +│ ├── vault.js # AES-GCM encrypted-at-rest key vault (scrypt) +│ ├── storage.js # session key cache + 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: test coverage, NIP-04, i18n, `nsec`/`npub` Bech32 +display, 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. If the popup shows +**Unlock**, the vault is locked (e.g. after a browser restart) — enter your +passphrase to unlock for the session. Also 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/ +- **Privacy policy**: [PRIVACY.md](PRIVACY.md) +- **Test page**: https://javascriptsolidserver.github.io/podkey/test-page/ +- **did:nostr**: https://nostrcg.github.io/did-nostr/ · [ecosystem](https://github.com/topics/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..756cd02 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 @@ -46,17 +46,27 @@ This will: 1. Click the **Podkey icon** (🔑) in your browser toolbar 2. Click **"✨ Generate New Key"** -3. Your new Nostr identity is ready! 🎉 +3. Choose an **encryption passphrase** (at least 8 characters) and confirm it +4. Your new Nostr identity is ready, sealed under your passphrase! 🎉 ### Import an Existing Key 1. Click the **Podkey icon** (🔑) in your browser toolbar 2. Click **"📥 Import Existing Key"** 3. Paste your 64-character hexadecimal private key -4. Click **"Import"** +4. Choose an **encryption passphrase** (at least 8 characters) and confirm it +5. Click **"Import"** ⚠️ **Warning**: Never share your private key with anyone! +### Unlocking after a browser restart + +Your key is encrypted at rest, so when you restart the browser the popup shows +an **Unlock** screen. Enter your passphrase to unlock it for the session — until +you do, signing requests show a clear "Podkey is locked" prompt rather than +failing silently. Your passphrase is never stored and cannot be recovered, so +keep a backup of your private key (and the passphrase). + --- ## Using Podkey with Nostr Apps @@ -116,24 +126,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. --- @@ -179,11 +190,14 @@ This enables: ⚠️ **DANGER**: Only do this if you need to backup your key! -1. Click the Podkey icon +1. Click the Podkey icon (unlock it first if it shows the Unlock screen) 2. Click **"Export Key"** in the footer 3. Confirm the warning 4. Your private key will be shown (keep it safe!) +You can also **Lock** the key on demand from the footer, or **Forget key** from +the Unlock screen to wipe the encrypted vault and start over. + --- ## Development Workflow @@ -258,7 +272,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 +284,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 0000000..029ec0f Binary files /dev/null and b/icons/icon128.png differ diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000..71cd8df Binary files /dev/null and b/icons/icon16.png differ diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000..52e877c Binary files /dev/null and b/icons/icon48.png differ 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..a3daf78 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); } @@ -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); @@ -503,7 +511,7 @@ input:focus-visible + .slider { /* ---- Trusted sites ---- */ .trusted-list { - max-height: 168px; + max-height: 120px; overflow-y: auto; margin: -2px; padding: 2px; @@ -599,7 +607,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); } 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

+ + + + + +