|
1 | 1 | # NSE — Nostr Secure Enclave |
2 | 2 |
|
3 | | -Open-source hardware-backed key management for Nostr. |
| 3 | +Open-source hardware-backed key management for Nostr. Your nsec, encrypted at rest by hardware you already own. |
4 | 4 |
|
5 | | -**Website:** [nse.dev](https://nse.dev) · **GitHub:** [HumanjavaEnterprises/nse-dev.web.landingpage.src](https://github.com/HumanjavaEnterprises/nse-dev.web.landingpage.src) |
| 5 | +**Website:** [nse.dev](https://nse.dev) · **npm:** [nostr-secure-enclave](https://www.npmjs.com/package/nostr-secure-enclave) · **PyPI:** [nostr-secure-enclave](https://pypi.org/project/nostr-secure-enclave/) |
6 | 6 |
|
7 | 7 | ## The Problem |
8 | 8 |
|
9 | | -Nostr keys (secp256k1/Schnorr) can't be generated or used directly inside mobile secure enclaves (iOS Secure Enclave, Android StrongBox/TEE) — those only support P-256. Every Nostr app today stores keys in software. If the device is compromised, the key is gone. |
| 9 | +Nostr keys (secp256k1/Schnorr) can't be generated or used directly inside mobile secure enclaves (iOS Secure Enclave, Android StrongBox/TEE) — those only support P-256. Most Nostr apps today store keys in software. If the device is compromised, the key is gone. |
10 | 10 |
|
11 | 11 | ## The Solution |
12 | 12 |
|
13 | | -NSE wraps the gap: |
| 13 | +NSE uses hardware to **protect** the key, not to sign with it. A P-256 key lives in hardware (non-exportable, biometric-gated). It encrypts the secp256k1 key at rest via AES-256-GCM. At signing time: unlock, decrypt, sign, zero. |
14 | 14 |
|
15 | | -1. **Generate** a secp256k1 keypair |
16 | | -2. **Protect** it with a hardware-backed P-256 key (non-exportable, biometric-gated) |
17 | | -3. **Sign** Nostr events with the secp256k1 key (briefly decrypted in memory, then zeroed) |
18 | | -4. **Expose** a simple API: `generate()`, `sign()`, `getPublicKey()` |
| 15 | +``` |
| 16 | +nse.sign(event) |
| 17 | + ├── Biometric unlock → Secure Enclave access |
| 18 | + ├── Derive AES key from hardware P-256 key |
| 19 | + ├── Decrypt secp256k1 key into memory |
| 20 | + ├── Schnorr sign the event |
| 21 | + ├── Zero plaintext key from memory |
| 22 | + └── Return signed event |
| 23 | +``` |
19 | 24 |
|
20 | | -The nsec never exists unprotected at rest. The P-256 key never leaves hardware. |
| 25 | +## Install |
21 | 26 |
|
22 | | -## Platform Support |
| 27 | +```bash |
| 28 | +# Server / CF Workers / Node.js |
| 29 | +npm install nostr-secure-enclave-server |
23 | 30 |
|
24 | | -| Platform | Hardware | Key Wrapping | Status | |
25 | | -|----------|----------|-------------|--------| |
26 | | -| iOS | Secure Enclave (SEP) | P-256 → AES-GCM → secp256k1 | Planned | |
27 | | -| Android | StrongBox / TEE | KeyStore → AES-GCM → secp256k1 | Planned | |
28 | | -| Server (CF Workers) | `crypto.subtle` | AES-GCM with KV-stored DEK | Planned | |
29 | | -| Server (Node.js) | TPM 2.0 (optional) | AES-GCM with file/env key | Planned | |
30 | | -| Browser | WebAuthn / SubtleCrypto | P-256 → AES-GCM → secp256k1 | Research | |
| 31 | +# Browser extensions / web apps |
| 32 | +npm install nostr-secure-enclave-browser |
31 | 33 |
|
32 | | -## API Surface |
| 34 | +# Python bots, AI entities, MCP servers |
| 35 | +pip install nostr-secure-enclave |
33 | 36 |
|
| 37 | +# Types only (peer dependency, installed automatically) |
| 38 | +npm install nostr-secure-enclave |
34 | 39 | ``` |
35 | | -// All platforms — same interface |
36 | | -nse.generate() → { pubkey, npub } |
37 | | -nse.sign(event) → signed event (id + sig populated) |
38 | | -nse.getPublicKey() → hex pubkey |
39 | | -nse.getNpub() → bech32 npub |
40 | | -nse.exists() → boolean (has a stored key?) |
41 | | -nse.destroy() → wipe key material |
| 40 | + |
| 41 | +## Quick Start |
| 42 | + |
| 43 | +```typescript |
| 44 | +// Server |
| 45 | +import { NSEServer, generateMasterKey } from 'nostr-secure-enclave-server'; |
| 46 | + |
| 47 | +const nse = new NSEServer({ masterKey: process.env.NSE_MASTER_KEY, storage }); |
| 48 | +const { pubkey, npub } = await nse.generate(); |
| 49 | +const signed = await nse.sign({ kind: 1, content: 'hello', tags: [], created_at: now }); |
42 | 50 | ``` |
43 | 51 |
|
44 | | -## Architecture |
| 52 | +```typescript |
| 53 | +// Browser |
| 54 | +import { NSEBrowser, NSEIndexedDBStorage } from 'nostr-secure-enclave-browser'; |
45 | 55 |
|
| 56 | +const nse = new NSEBrowser({ storage: new NSEIndexedDBStorage() }); |
| 57 | +const { pubkey, npub } = await nse.generate(); |
| 58 | +const signed = await nse.sign(event); |
46 | 59 | ``` |
47 | | -┌─────────────────────────────────────┐ |
48 | | -│ Your Nostr App │ |
49 | | -│ nse.sign(event) / nse.generate() │ |
50 | | -└──────────────┬──────────────────────┘ |
51 | | - │ |
52 | | -┌──────────────▼──────────────────────┐ |
53 | | -│ NSE Library │ |
54 | | -│ Platform detection + unified API │ |
55 | | -└──────────────┬──────────────────────┘ |
56 | | - │ |
57 | | - ┌───────────┼───────────┐ |
58 | | - │ │ │ |
59 | | -┌──▼──┐ ┌───▼───┐ ┌───▼───┐ |
60 | | -│ iOS │ │Android│ │Server │ |
61 | | -│ SEP │ │StrongB│ │ TPM/ │ |
62 | | -│P-256│ │ P-256 │ │ KMS │ |
63 | | -└─────┘ └───────┘ └───────┘ |
64 | | - │ │ │ |
65 | | - └───────────┼───────────┘ |
66 | | - │ |
67 | | - AES-GCM encrypted |
68 | | - secp256k1 key blob |
69 | | - (stored in Keychain/ |
70 | | - KeyStore/KV/env) |
| 60 | + |
| 61 | +```python |
| 62 | +# Python |
| 63 | +from nse import NSE |
| 64 | +nse = NSE(master_key=os.environ['NSE_MASTER_KEY']) |
| 65 | +info = nse.generate() |
| 66 | +signed = nse.sign(NostrEvent(kind=1, content="hello", tags=[], created_at=now)) |
71 | 67 | ``` |
72 | 68 |
|
73 | | -## Prior Art |
| 69 | +## Packages |
74 | 70 |
|
75 | | -| Project | Scope | Gap NSE Fills | |
76 | | -|---------|-------|---------------| |
77 | | -| noauth-enclaved | NIP-46 signer in AWS Nitro | Server-only, not mobile | |
78 | | -| keycrux | Key persistence for Nitro enclaves | Server-only | |
79 | | -| K1 (Swift) | secp256k1 Schnorr signing | No enclave key wrapping | |
80 | | -| LNbits NSD | ESP32 hardware signer | DIY device, not phone-native | |
81 | | -| HardKey SDK | Cross-platform hardware keys | P-256 only, no secp256k1 | |
| 71 | +| Package | Platform | Registry | Status | |
| 72 | +|---------|----------|----------|--------| |
| 73 | +| [`nostr-secure-enclave`](https://www.npmjs.com/package/nostr-secure-enclave) | TypeScript types + NSEProvider interface | npm | **Published** | |
| 74 | +| [`nostr-secure-enclave-server`](https://www.npmjs.com/package/nostr-secure-enclave-server) | CF Workers / Node.js | npm | **Published** | |
| 75 | +| [`nostr-secure-enclave-browser`](https://www.npmjs.com/package/nostr-secure-enclave-browser) | WebAuthn + SubtleCrypto | npm | **Published** | |
| 76 | +| [`nostr-secure-enclave`](https://pypi.org/project/nostr-secure-enclave/) | Python (AI entities, bots, MCP) | PyPI | **Published** | |
| 77 | +| `nostr-secure-enclave-ios` | Swift via Secure Enclave | Swift Package | Planned | |
| 78 | +| `nostr-secure-enclave-android` | Kotlin via StrongBox | Maven | Planned | |
82 | 79 |
|
83 | | -## NIP Integration |
| 80 | +## Where NSE Fits |
84 | 81 |
|
85 | | -- **NIP-46** — NSE sits behind the NIP-46 signer interface. The app calls `nse.sign()`, NSE handles hardware unlock + decryption. |
86 | | -- **NIP-49** — NSE replaces ncryptsec for key storage. Instead of passphrase-encrypted keys, the enclave protects them. |
87 | | -- **Future NIP** — We may propose a NIP for hardware-backed key attestation (prove to a relay that a key is hardware-protected). |
| 82 | +NSE is **Level 0 infrastructure** — the cryptographic foundation that makes sovereign key management possible without asking users to understand cryptography. |
88 | 83 |
|
89 | | -## Packages (planned) |
| 84 | +``` |
| 85 | +Level 0 NSE encrypts the key at rest |
| 86 | + └── Browser extension stores wrapped key in IndexedDB |
| 87 | + └── Server process holds encrypted identity in KV |
90 | 88 |
|
91 | | -| Package | Platform | Registry | |
92 | | -|---------|----------|----------| |
93 | | -| `nostr-secure-enclave` | TypeScript types + interface | npm | |
94 | | -| `nostr-secure-enclave-ios` | Swift via Secure Enclave | Swift Package | |
95 | | -| `nostr-secure-enclave-android` | Kotlin via StrongBox | Maven | |
96 | | -| `nostr-secure-enclave-server` | CF Workers / Node.js | npm | |
97 | | -| `nostr-secure-enclave-browser` | WebAuthn + SubtleCrypto | npm | |
98 | | -| `nostr-secure-enclave` | Python wrapper | PyPI | |
| 89 | +Level 1 Mobile app as backup + authenticator |
| 90 | + └── iOS Secure Enclave / Android StrongBox wrap the key |
99 | 91 |
|
100 | | -## Hosting |
| 92 | +Level 2 NIP-46 bunker — keys never leave hardware |
| 93 | + └── NSE signs behind the NIP-46 interface |
| 94 | + └── Remote apps request signatures, never see the nsec |
| 95 | +``` |
101 | 96 |
|
102 | | -- **Pages source:** `main` branch, `/docs` folder |
103 | | -- **Custom domain:** `nse.dev` |
| 97 | +Products like [NostrKey](https://nostrkey.com) use NSE to protect keys in the browser. NIP-46 bunker signers use NSE on the backend. The principle: **Don't explain cryptography. Explain consequences.** |
104 | 98 |
|
105 | | -### DNS Configuration |
| 99 | +## Repo Structure |
106 | 100 |
|
107 | | -**A records** (apex domain): |
108 | 101 | ``` |
109 | | -185.199.108.153 |
110 | | -185.199.109.153 |
111 | | -185.199.110.153 |
112 | | -185.199.111.153 |
| 102 | +docs/ ← GitHub Pages source (nse.dev) |
| 103 | + index.html ← Single-page site (HTML + inline CSS) |
| 104 | + og-image.png ← 1200x630 social card |
| 105 | + CNAME ← Custom domain: nse.dev |
| 106 | +platforms/ ← Working code for each target platform |
| 107 | + core/ ← nostr-secure-enclave — shared types + NSEProvider interface |
| 108 | + server/ ← nostr-secure-enclave-server — AES-256-GCM + nostr-crypto-utils |
| 109 | + browser/ ← nostr-secure-enclave-browser — SubtleCrypto + IndexedDB |
| 110 | + python/ ← nostr-secure-enclave (PyPI) — cryptography + secp256k1 |
| 111 | + ios/ ← Planned — Swift Package (Secure Enclave) |
| 112 | + android/ ← Planned — Kotlin (StrongBox / TEE) |
| 113 | +examples/ ← 7 real-world usage patterns |
| 114 | + server-process-identity.ts |
| 115 | + cloudflare-worker-identity.ts |
| 116 | + netlify-function-identity.ts |
| 117 | + browser-extension-signer.ts |
| 118 | + python-bot-identity.py |
| 119 | + nip46-signer-backend.ts |
| 120 | + multi-key-manager.ts |
113 | 121 | ``` |
114 | 122 |
|
115 | | -**CNAME** (www subdomain): |
| 123 | +## Development |
| 124 | + |
| 125 | +```bash |
| 126 | +cd platforms |
| 127 | +npm install # Links workspaces (core, server, browser) |
| 128 | +npm test # Runs all 82 tests (core + server + browser + python) |
| 129 | +npm run build # Compiles TypeScript to dist/ |
| 130 | +``` |
| 131 | + |
| 132 | +Individual test suites: `npm run test:core`, `npm run test:server`, `npm run test:browser`, `npm run test:python` |
| 133 | + |
| 134 | +Python tests require: `pip install cryptography secp256k1 pytest` |
| 135 | + |
| 136 | +## API |
| 137 | + |
| 138 | +All platforms implement the same `NSEProvider` interface: |
| 139 | + |
116 | 140 | ``` |
117 | | -www → humanjavaenterprises.github.io |
| 141 | +nse.generate() → { pubkey, npub, created_at, hardware_backed } |
| 142 | +nse.sign(event) → signed event (id + pubkey + sig populated) |
| 143 | +nse.getPublicKey() → hex pubkey (no unlock needed) |
| 144 | +nse.getNpub() → bech32 npub (no unlock needed) |
| 145 | +nse.exists() → boolean |
| 146 | +nse.destroy() → wipe all key material |
118 | 147 | ``` |
119 | 148 |
|
120 | | -HTTPS enforced automatically by GitHub Pages. |
| 149 | +## What NSE Is Not |
| 150 | + |
| 151 | +- **Not a remote signer.** NSE is a local library. Use NIP-46 for remote signing. |
| 152 | +- **Not custodial.** Keys never leave your device. |
| 153 | +- **Not a wallet.** No Lightning, no transactions. Just keys and signing. |
| 154 | +- **Not magic.** The secp256k1 key exists briefly in application memory during signing. NSE minimizes that window and zeros the key after — but a rooted/jailbroken device with memory access is out of scope. |
121 | 155 |
|
122 | | -## OG Image |
| 156 | +## Part of the nostr-* Family |
123 | 157 |
|
124 | | -Regenerate the social card: `python3 generate-og.py` |
| 158 | +NSE is built on [`nostr-crypto-utils`](https://www.npmjs.com/package/nostr-crypto-utils) and sits alongside the rest of the [Humanjava nostr-* libraries](https://www.npmjs.com/~vveerrgg). |
125 | 159 |
|
126 | 160 | ## License |
127 | 161 |
|
|
0 commit comments