Skip to content

Commit 1595bdc

Browse files
committed
docs: privacy policy, vault-current docs, and Actions-based Pages site
- 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>
1 parent ab5c63f commit 1595bdc

8 files changed

Lines changed: 439 additions & 25 deletions

File tree

.github/workflows/gh-pages.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ on:
55
branches:
66
- main
77
paths:
8+
- 'site/**'
89
- 'test-page/**'
910
- '.github/workflows/gh-pages.yml'
11+
workflow_dispatch:
1012

1113
permissions:
1214
contents: read
@@ -30,10 +32,18 @@ jobs:
3032
- name: Setup Pages
3133
uses: actions/configure-pages@v4
3234

35+
- name: Assemble site (docs landing + privacy + test page)
36+
run: |
37+
mkdir -p _site
38+
cp -r site/. _site/
39+
mkdir -p _site/test-page
40+
cp -r test-page/. _site/test-page/
41+
echo "Assembled _site:" && ls -R _site | head -40
42+
3343
- name: Upload artifact
3444
uses: actions/upload-pages-artifact@v3
3545
with:
36-
path: './test-page'
46+
path: './_site'
3747

3848
- name: Deploy to GitHub Pages
3949
id: deployment

CHANGELOG.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- **Per-origin signing consent.** The first request from a site opens an
1919
approval popup; closing it or a 60-second timeout denies. Approving grants
2020
per-origin trust that you can revoke from the popup.
21-
- **Session-only key storage.** The private key is held in
22-
`chrome.storage.session`: in memory, cleared when the browser closes, never
23-
copied to the page.
21+
- **Encrypted-at-rest key vault.** The private key is persisted only as an
22+
AES-256-GCM ciphertext in `chrome.storage.local`, wrapped by a scrypt-derived
23+
key from the user's passphrase (`src/vault.js`). On unlock the decrypted key
24+
is cached in `chrome.storage.session` for fast signing and cleared when the
25+
browser closes; the raw key never touches disk. New `UNLOCK_VAULT`/`LOCK_VAULT`
26+
messages, a popup unlock screen, and a clear "Podkey is locked" prompt replace
27+
the previous "No keypair found" error after a restart.
2428
- **NIP-98 Solid authentication.** Opt-in HTTP auth to Solid pods. Each token
2529
carries a fresh 16-byte nonce and binds the request body hash and the final
2630
redirect-aware URL, so one token authorises one request. Trusted Solid hosts
@@ -31,8 +35,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3135
content-security policy across the popup and test page.
3236
- **Continuous integration.** `.github/workflows/ci.yml` runs build, test and
3337
lint on every pull request and push to `main`, and uploads a sideloadable
34-
extension zip. The 133-case suite covers signing, NIP-44 vectors, NIP-98
35-
token shape, the content-script message whitelist, and the consent flow.
38+
extension zip. The 141-case suite covers signing, NIP-44 vectors, NIP-98
39+
token shape, the content-script message whitelist, the consent flow, and the
40+
vault crypto (round-trip, wrong passphrase, tamper, salt/iv freshness).
3641

3742
### Changed
3843

PRIVACY.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Podkey Privacy Policy
2+
3+
_Last updated: 2026-06-16_
4+
5+
Podkey is a browser extension that holds a Nostr/[did:nostr](https://nostrcg.github.io/did-nostr/)
6+
key on your device and uses it to sign Nostr events ([NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md))
7+
and to authenticate to [Solid](https://solidproject.org) pods over
8+
[NIP-98](https://github.com/nostr-protocol/nips/blob/master/98.md). It is a
9+
**local signer**: your key never leaves your device, and Podkey has no servers,
10+
no accounts, and no analytics.
11+
12+
## What Podkey stores, and where
13+
14+
All data is stored **locally** in the browser's extension storage. Nothing is
15+
sent to the Podkey developers or to any third party.
16+
17+
| Data | Where | Notes |
18+
|------|-------|-------|
19+
| **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. |
20+
| **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. |
21+
| **Public key** | `chrome.storage.local` | Your `did:nostr` identity. Not secret. |
22+
| **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. |
23+
24+
There is **no telemetry, no tracking, no advertising identifiers, and no
25+
remote logging.** Podkey collects none of your browsing history or page content.
26+
27+
## How your key is used
28+
29+
- **Signing** (NIP-07 `signEvent`, `nip44.encrypt/decrypt`) and **Solid
30+
authentication** (NIP-98) happen entirely inside the extension's background
31+
service worker. The raw private key is never exposed to web pages — only the
32+
resulting signature, ciphertext/plaintext, or `Authorization` header crosses
33+
to the page.
34+
- A site you have not approved triggers a consent prompt on its first request.
35+
Closing the prompt, or a 60-second timeout, denies it.
36+
- Podkey makes **no network requests of its own.** A NIP-98 `Authorization`
37+
header is attached only to requests **you** initiate to a Solid/Nostr server
38+
you have trusted; the data goes to that server, under your control, not to
39+
Podkey.
40+
41+
## Permissions, and why they are needed
42+
43+
- **`storage`** — to keep the encrypted key, your public key, and your trusted
44+
sites locally, as described above.
45+
- **Host access (`<all_urls>`)** — Podkey is a NIP-07 signer, so it injects a
46+
`window.nostr` provider into pages and, for sites you have trusted, attaches
47+
NIP-98 auth to your own requests. This requires script access to the pages
48+
where you use a Nostr/Solid app. Podkey **does not read, collect, or transmit
49+
page content or browsing history**; host access is used solely to expose the
50+
signer and to intercept auth for origins you have explicitly approved.
51+
52+
## Data sharing and retention
53+
54+
- Podkey **does not sell or share** any data. There is no data broker, no
55+
third-party processor, and no off-device transfer of your key or activity.
56+
- All data remains on your device until you delete it. **Removing the
57+
extension, or using "Forget key" in the popup, erases the stored vault, public
58+
key, and trusted sites.** Back up your private key before doing so — without a
59+
backup it cannot be recovered.
60+
61+
## Your control
62+
63+
- Unlock / lock your key on demand from the popup.
64+
- Review and revoke trusted sites at any time.
65+
- Export your private key (after unlocking) to back it up.
66+
- Forget the key entirely to start over.
67+
68+
## Contact
69+
70+
Podkey is open source under AGPL-3.0. Questions, issues, or security reports:
71+
<https://github.com/JavaScriptSolidServer/podkey/issues>.
72+
73+
This policy may be updated as the extension evolves; the "Last updated" date
74+
above reflects the current version. Material changes will be noted in the
75+
project [CHANGELOG](CHANGELOG.md).

README.md

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> Browser extension for **did:nostr** and **Solid** authentication
44
5-
[![Version](https://img.shields.io/badge/version-0.0.7-blue.svg)](https://github.com/JavaScriptSolidServer/podkey/releases)
5+
[![Version](https://img.shields.io/badge/version-0.0.8-blue.svg)](https://github.com/JavaScriptSolidServer/podkey/releases)
66
[![License](https://img.shields.io/badge/license-AGPL--3.0-green.svg)](LICENSE)
77
[![NIP-07](https://img.shields.io/badge/NIP--07-compatible-purple.svg)](https://github.com/nostr-protocol/nips/blob/master/07.md)
88
[![Test Page](https://img.shields.io/badge/test--page-live-brightgreen)](https://javascriptsolidserver.github.io/podkey/test-page/)
@@ -29,9 +29,14 @@ stays inside the extension and never reaches the page.
2929

3030
## Security model
3131

32-
- The private key lives in `chrome.storage.session`: held in memory, cleared
33-
when the browser closes, never copied to the page. Signing, NIP-44 and NIP-98
34-
all run in the background service worker.
32+
- **Encrypted at rest.** The private key is persisted only as an AES-256-GCM
33+
ciphertext in `chrome.storage.local`, wrapped by a key derived from your
34+
passphrase with scrypt. The raw key is never written to disk.
35+
- **Unlocked in memory.** When you unlock with your passphrase, the decrypted
36+
key is cached in `chrome.storage.session` for the browser session so signing
37+
is fast; it is cleared when the browser closes, so you re-unlock next time. It
38+
is never copied to the page — signing, NIP-44 and NIP-98 all run in the
39+
background service worker.
3540
- A site you have not approved raises a consent popup on its first request.
3641
Closing the popup, or a 60-second timeout, denies it. Approving grants
3742
per-origin trust that you can revoke at any time from the popup.
@@ -69,7 +74,9 @@ npm run build # bundles @noble deps into src/background.bundle.js
6974
Then load the `podkey` directory as an unpacked extension (steps 2–4 above).
7075

7176
Pin the toolbar icon (🔑), open it, and generate or import a 64-character hex
72-
key. The [test page](https://javascriptsolidserver.github.io/podkey/test-page/)
77+
key, choosing an **encryption passphrase**. The key is sealed under that
78+
passphrase (see [Security model](#security-model)); you unlock it once per
79+
browser session. The [test page](https://javascriptsolidserver.github.io/podkey/test-page/)
7380
detects the extension and runs live signing checks.
7481

7582
## Usage
@@ -122,9 +129,10 @@ const plaintext = await window.nostr.nip44.decrypt(peer, payload)
122129
│ management, identity display, consent │
123130
│ │
124131
│ Background worker (src/) │
125-
│ key storage (storage.js), signing & │
126-
│ NIP-44 (crypto.js, nip44.js), NIP-98 │
127-
│ auth, per-origin permission gate │
132+
│ encrypted key vault (vault.js) + session│
133+
│ cache (storage.js), signing & NIP-44 │
134+
│ (crypto.js, nip44.js), NIP-98 auth, │
135+
│ per-origin permission gate │
128136
│ │
129137
│ Page bridge (src/injected.js) │
130138
│ injects window.nostr, relays requests │
@@ -150,12 +158,36 @@ const did = `did:nostr:${pubkey}`
150158
That identifier authenticates you to Solid pods and travels across any
151159
NIP-07-aware app.
152160

161+
## Where Podkey fits
162+
163+
Podkey sits at the join of two mature, independently-built ecosystems and
164+
consolidates them behind one key:
165+
166+
- **Nostr signing already exists.** NIP-07 browser signers (nos2x, Alby, and
167+
others) are well-established. Podkey is fully NIP-07 compatible, so every
168+
existing Nostr client works with it unchanged — it does not reinvent that
169+
surface.
170+
- **[did:nostr](https://github.com/topics/did-nostr)** is an emerging ecosystem
171+
of decentralized-identity tooling built on Nostr keys. Podkey treats your
172+
public key as a first-class `did:nostr` identifier rather than just a signing
173+
key.
174+
- **[Solid](https://solidproject.org)** (the W3C-aligned personal-data-pod
175+
standard) is highly mature but has historically required OIDC/WebID identity
176+
providers. Podkey authenticates to Solid pods over NIP-98 keyed to your
177+
did:nostr — no OAuth redirect, no IdP account.
178+
179+
The novel part is the **consolidation**: one locally-held, encrypted key that
180+
is simultaneously your Nostr signer, your `did:nostr` identity, and your Solid
181+
login. Podkey extends what existing Nostr signers do (NIP-07) with did:nostr
182+
identity and Solid/NIP-98 authentication, and adds an encrypted-at-rest vault on
183+
top.
184+
153185
## Development
154186

155187
```bash
156188
npm install
157189
npm run build # bundle dependencies into the service worker
158-
npm test # node --test, 133 cases
190+
npm test # node --test, 141 cases (incl. vault crypto)
159191
npm run lint # eslint, no-unused-vars as error
160192
```
161193

@@ -167,7 +199,8 @@ podkey/
167199
│ ├── crypto.js # key generation & Schnorr signing
168200
│ ├── nip44.js # NIP-44 v2 encrypt/decrypt
169201
│ ├── nip98-interceptor.js # page-context NIP-98 fetch/XHR auth
170-
│ ├── storage.js # session-only key + trusted-origin storage
202+
│ ├── vault.js # AES-GCM encrypted-at-rest key vault (scrypt)
203+
│ ├── storage.js # session key cache + trusted-origin storage
171204
│ ├── injected.js # content-script page bridge
172205
│ └── nostr-provider.js # window.nostr implementation
173206
├── popup/ # popup + approval UI
@@ -196,18 +229,20 @@ uploads a sideloadable extension zip.
196229
3. Run `npm test` and `npm run lint` until both pass.
197230
4. Open a pull request.
198231

199-
Good first contributions: extension icons (16/48/128px), test coverage, NIP-04,
200-
i18n, and documentation.
232+
Good first contributions: test coverage, NIP-04, i18n, `nsec`/`npub` Bech32
233+
display, and documentation.
201234

202235
## Troubleshooting
203236

204237
**`window.nostr` is undefined.** Reload the page after installing, confirm the
205238
extension is enabled, and check for another Nostr extension claiming
206239
`window.nostr`.
207240

208-
**Events will not sign.** Generate or import a key first, and check the service
209-
worker console (the "service worker" link on `chrome://extensions`) for a
210-
blocked consent prompt.
241+
**Events will not sign.** Generate or import a key first. If the popup shows
242+
**Unlock**, the vault is locked (e.g. after a browser restart) — enter your
243+
passphrase to unlock for the session. Also check the service worker console
244+
(the "service worker" link on `chrome://extensions`) for a blocked consent
245+
prompt.
211246

212247
**Build errors.** Reinstall dependencies (`npm install`) and confirm Node.js
213248
18 or newer.
@@ -220,8 +255,9 @@ AGPL-3.0. See [LICENSE](LICENSE).
220255

221256
- **Repository**: https://github.com/JavaScriptSolidServer/podkey
222257
- **Issues**: https://github.com/JavaScriptSolidServer/podkey/issues
258+
- **Privacy policy**: [PRIVACY.md](PRIVACY.md)
223259
- **Test page**: https://javascriptsolidserver.github.io/podkey/test-page/
224-
- **did:nostr**: https://nostrcg.github.io/did-nostr/
260+
- **did:nostr**: https://nostrcg.github.io/did-nostr/ · [ecosystem](https://github.com/topics/did-nostr)
225261
- **NIP-07**: https://github.com/nostr-protocol/nips/blob/master/07.md
226262
- **NIP-98**: https://github.com/nostr-protocol/nips/blob/master/98.md
227263
- **Solid**: https://solidproject.org/

USAGE.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,27 @@ This will:
4646

4747
1. Click the **Podkey icon** (🔑) in your browser toolbar
4848
2. Click **"✨ Generate New Key"**
49-
3. Your new Nostr identity is ready! 🎉
49+
3. Choose an **encryption passphrase** (at least 8 characters) and confirm it
50+
4. Your new Nostr identity is ready, sealed under your passphrase! 🎉
5051

5152
### Import an Existing Key
5253

5354
1. Click the **Podkey icon** (🔑) in your browser toolbar
5455
2. Click **"📥 Import Existing Key"**
5556
3. Paste your 64-character hexadecimal private key
56-
4. Click **"Import"**
57+
4. Choose an **encryption passphrase** (at least 8 characters) and confirm it
58+
5. Click **"Import"**
5759

5860
⚠️ **Warning**: Never share your private key with anyone!
5961

62+
### Unlocking after a browser restart
63+
64+
Your key is encrypted at rest, so when you restart the browser the popup shows
65+
an **Unlock** screen. Enter your passphrase to unlock it for the session — until
66+
you do, signing requests show a clear "Podkey is locked" prompt rather than
67+
failing silently. Your passphrase is never stored and cannot be recovered, so
68+
keep a backup of your private key (and the passphrase).
69+
6070
---
6171

6272
## Using Podkey with Nostr Apps
@@ -180,11 +190,14 @@ This enables:
180190

181191
⚠️ **DANGER**: Only do this if you need to backup your key!
182192

183-
1. Click the Podkey icon
193+
1. Click the Podkey icon (unlock it first if it shows the Unlock screen)
184194
2. Click **"Export Key"** in the footer
185195
3. Confirm the warning
186196
4. Your private key will be shown (keep it safe!)
187197

198+
You can also **Lock** the key on demand from the footer, or **Forget key** from
199+
the Unlock screen to wipe the encrypted vault and start over.
200+
188201
---
189202

190203
## Development Workflow

site/icon.svg

Lines changed: 17 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)