Skip to content

Commit 52a5f62

Browse files
authored
Podkey 0.0.8: general-signer signing, refreshed docs, popup fit, key icons (#23)
* fix(signing): trusted origins sign without a prompt (general NIP-07 signer) handleSignEvent gated auto-signing on `trusted && autoSign && isSolidAuth`, so a trusted origin only signed kind-27235 (Solid auth) silently and re-prompted for every other event kind. A normal Nostr client — the forum publishes kind 0 / 10002 / 22242 on every load — therefore raised three approval popups on each page load, making Podkey unusable as a single general-purpose signer. Align signing with the trust model already used by GET_PUBLIC_KEY and nip44.{encrypt,decrypt}: a trusted origin is sufficient to skip the prompt (decrypting DMs is strictly more sensitive than signing, and is already silent for trusted origins). An untrusted origin still ALWAYS prompts, and approving it establishes revocable trust — no silent first-use. let shouldSign = trusted && autoSign && isSolidAuth; -> let shouldSign = trusted; Tests updated to the general-signer contract (133 pass, lint clean). Co-Authored-By: jjohare <github@thedreamlab.uk> * 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 <github@thedreamlab.uk> * feat(vault): encrypted-at-rest key persistence with passphrase unlock 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 <github@thedreamlab.uk> * 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 384918d commit 52a5f62

21 files changed

Lines changed: 1273 additions & 413 deletions

.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: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- **NIP-44 (v2) encryption**`window.nostr.nip44.encrypt(pubkey, plaintext)`
13-
and `window.nostr.nip44.decrypt(pubkey, ciphertext)`, enabling NIP-17 / NIP-59
14-
gift-wrapped direct messages. Crypto runs in the background service worker
15-
(the private key never reaches the page), reusing the existing message-passing
16-
path. Implemented with `@noble/ciphers` (chacha20) + `@noble/hashes`
17-
(hkdf/hmac/sha256) + `@noble/secp256k1` (ECDH). Verified against the official
18-
NIP-44 spec test vectors.
12+
- **NIP-44 (v2) encryption.** `window.nostr.nip44.encrypt(pubkey, plaintext)`
13+
and `nip44.decrypt(pubkey, ciphertext)` for NIP-17 / NIP-59 gift-wrapped
14+
direct messages. The crypto runs in the background worker (`@noble/ciphers`
15+
chacha20, `@noble/hashes` hkdf/hmac, ECDH via `@noble/secp256k1`) and is
16+
checked against the official NIP-44 test vectors. The private key stays in
17+
the worker.
18+
- **Per-origin signing consent.** The first request from a site opens an
19+
approval popup; closing it or a 60-second timeout denies. Approving grants
20+
per-origin trust that you can revoke from the popup.
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.
28+
- **NIP-98 Solid authentication.** Opt-in HTTP auth to Solid pods. Each token
29+
carries a fresh 16-byte nonce and binds the request body hash and the final
30+
redirect-aware URL, so one token authorises one request. Trusted Solid hosts
31+
are matched exactly.
32+
- **Schnorr self-verify.** Every signature is checked against the public key
33+
before it is returned.
34+
- **Extension icons** at 16, 48 and 128px, and a `script-src 'self'`
35+
content-security policy across the popup and test page.
36+
- **Continuous integration.** `.github/workflows/ci.yml` runs build, test and
37+
lint on every pull request and push to `main`, and uploads a sideloadable
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).
41+
42+
### Changed
43+
44+
- A trusted origin signs any event kind without a prompt, so Podkey works as a
45+
general NIP-07 signer. A normal client publishes kind 0, 10002 and 22242 on
46+
every load; those no longer raise a prompt once you trust the site. Untrusted
47+
origins still prompt, and approving one grants trust.
48+
- The `window.nostr` provider exposes `getPublicKey`, `signEvent` and
49+
`nip44.{encrypt,decrypt}`.
50+
- The popup fits within the browser popup height, keeping the Export and GitHub
51+
links in view.
1952

2053
## [0.0.7] - 2024-12-XX
2154

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).

0 commit comments

Comments
 (0)