Skip to content

Commit 384918d

Browse files
authored
Consolidated Podkey security hardening (#22)
Consolidated hardening of the Podkey key-holding signer: brings seven in-flight security/feature branches plus residual gap-fills, a polished CSP-clean UI, a CI build/test/lint gate, and a 133-test suite into one reviewable PR. The fix set was independently re-derived from a full audit of the extension and every item below was verified fixed *in the final tree* by an adversarial review pass (not just claimed). **Supersedes** (folded in here; safe to close in favour of this PR): #12, #14, #17, #18, #19, #20, #21. ## Security posture — before → after | Area | Before | After | |---|---|---| | Consent | Auto-approve everything (`showPermissionPrompt` hard-returned `true`) | **Explicit per-origin consent** popup that blocks on the user's decision; closing or 60s timeout denies | | Key at rest | `storage.local` (persisted plaintext) | **`storage.session` only** (in-memory, cleared on browser close); raw key never crosses to the page | | Crypto | A fake `crypto-browser.js` in the tree (`pubkey = sha256(privkey)`) | Deleted; **real `@noble` crypto** only, with `signEvent` schnorr self-verify before return | | NIP-98 | Signed-event cache → byte-identical id → replayable | **Fresh per-token 16-byte nonce**, redirect-aware `u` tag, page-side body-hash binding | | Solid-host trust | Substring match (`inrupt.net.evil.com` accepted) | **Exact-host match**, case-insensitive; lookalikes rejected | | Interception | Double fetch/XHR interception + double 401-retry | **Single interception path**, one retry | | Message channel | Page could inject arbitrary fields / origin / `privateKey` | Content script **whitelists 4 message types, forwards only safe fields, enforces the real origin** | | Provider surface | Advertised `nip04`/`getRelays` that throw / return `{}` | **Honest**: `getPublicKey`, `signEvent`, `nip44.{encrypt,decrypt}` only | | UI | Inline scripts + `innerHTML` XSS sinks | **CSP `script-src 'self'`**; all DOM via `textContent`/`createElement` | | Auto-sign | Defaulted **ON** (first-run silent trust + NIP-98 mint) | Defaults **OFF** — silent trusted-origin Solid signing and NIP-98 auto-trust are strictly opt-in | ## Notes for review - **Consent timeout:** `approve.js` actively sends `approved:false` + closes at the 60s mark (one-shot `decisionSent` guard) so the visible countdown is truthful; the background `chrome.windows` 60s timer remains a backstop. If you'd prefer the popup stay passive and let the background own the timeout, that is the single behavioural line to reconsider — everything else on the UI is restyling. - The page-context `nip98-interceptor.js` still duplicates a couple of `auth-header-utils.js` helpers because page context can't ESM-import the module; intentional, tracked separately. ## Verification - `npm run build` ✓ · `npm test` → **133 pass / 0 fail** ✓ · `npm run lint` → **0 errors** (now `no-unused-vars: error`) ✓ - New `.github/workflows/ci.yml` runs build → test → lint on every PR and push to `main`. 🤖 Generated by Claude Code
1 parent ef27ea6 commit 384918d

31 files changed

Lines changed: 4176 additions & 1641 deletions

.eslintrc.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"root": true,
3+
"env": {
4+
"browser": true,
5+
"webextensions": true,
6+
"serviceworker": true,
7+
"es2022": true
8+
},
9+
"parserOptions": {
10+
"ecmaVersion": 2022,
11+
"sourceType": "module"
12+
},
13+
"extends": "eslint:recommended",
14+
"rules": {
15+
"no-eval": "error",
16+
"no-implied-eval": "error",
17+
"no-undef": "error",
18+
"no-unused-vars": [
19+
"error",
20+
{
21+
"argsIgnorePattern": "^_",
22+
"varsIgnorePattern": "^_",
23+
"caughtErrorsIgnorePattern": "^_"
24+
}
25+
]
26+
},
27+
"ignorePatterns": [
28+
"node_modules/",
29+
"dist/",
30+
"**/*.bundle.js"
31+
],
32+
"overrides": [
33+
{
34+
"files": ["test/**/*.js", "scripts/**/*.js"],
35+
"env": {
36+
"node": true,
37+
"browser": false
38+
}
39+
}
40+
]
41+
}

.github/workflows/ci.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
build-test-lint:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Setup Node.js
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: '20'
23+
cache: 'npm'
24+
25+
- name: Install dependencies
26+
run: npm ci
27+
28+
- name: Build
29+
run: npm run build
30+
31+
- name: Test
32+
run: npm test
33+
34+
- name: Lint
35+
run: npm run lint
36+
37+
- name: Package extension (sideloadable unpacked zip)
38+
run: |
39+
mkdir -p dist
40+
zip -r dist/podkey-extension.zip manifest.json src popup $([ -d icons ] && echo icons) -x '*.test.js'
41+
42+
- name: Upload extension artifact
43+
uses: actions/upload-artifact@v4
44+
with:
45+
name: podkey-extension
46+
path: dist/podkey-extension.zip
47+
if-no-files-found: error

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to Podkey will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
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.
19+
820
## [0.0.7] - 2024-12-XX
921

1022
### Changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
-**Key Generation** - Secure cryptographic key generation using `@noble/secp256k1`
6969
-**Key Import** - Import existing 64-char hex private keys
7070
-**Event Signing** - Sign Nostr events with Schnorr signatures
71+
-**NIP-44 Encryption** - v2 `encrypt`/`decrypt` for NIP-17/NIP-59 gift-wrapped DMs (key never leaves the background)
7172
-**Trust Management** - Per-origin permissions with auto-approval
7273
-**Beautiful UI** - Soft gradients, smooth animations
7374
-**64-char Hex Keys** - Proper did:nostr format compatibility
@@ -261,6 +262,19 @@ const signed = await window.nostr.signEvent({
261262
})
262263
```
263264

265+
### `window.nostr.nip44.encrypt(pubkey, plaintext)` / `window.nostr.nip44.decrypt(pubkey, ciphertext)`
266+
267+
NIP-44 (v2) encryption, used by NIP-17 / NIP-59 gift-wrapped direct messages.
268+
The private key never leaves the background service worker — encryption is
269+
performed there and only the resulting base64 payload (or decrypted plaintext)
270+
crosses to the page, mirroring the existing signing message path.
271+
272+
```javascript
273+
const peer = '<64-char hex pubkey>'
274+
const payload = await window.nostr.nip44.encrypt(peer, 'hello')
275+
const plaintext = await window.nostr.nip44.decrypt(peer, payload)
276+
```
277+
264278
### `window.nostr.getRelays()`
265279

266280
Returns relay configuration (coming soon).

package-lock.json

Lines changed: 16 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"dev": "echo 'Load extension from chrome://extensions in developer mode'",
99
"build": "npm run bundle",
1010
"bundle": "node scripts/bundle.js",
11-
"test": "node --test test/**/*.test.js",
11+
"test": "node --test test/*.test.js",
1212
"lint": "eslint src/"
1313
},
1414
"keywords": [
@@ -34,6 +34,7 @@
3434
},
3535
"homepage": "https://github.com/JavaScriptSolidServer/podkey#readme",
3636
"dependencies": {
37+
"@noble/ciphers": "^2.2.0",
3738
"@noble/hashes": "^1.3.3",
3839
"@noble/secp256k1": "^3.0.0"
3940
},
@@ -44,4 +45,4 @@
4445
"engines": {
4546
"node": ">=18.0.0"
4647
}
47-
}
48+
}

0 commit comments

Comments
 (0)