Skip to content

Commit e10b48e

Browse files
committed
feat: scaffold platforms/ with working code for all 6 targets
- core: TypeScript types + NSEProvider interface - ios: Swift Package + NSE.swift with Secure Enclave scaffold - android: Kotlin NSE object with StrongBox/TEE scaffold - server: CF Workers + Node.js implementations - browser: WebAuthn + SubtleCrypto scaffold - python: nse-dev package with async API
1 parent d9b5518 commit e10b48e

14 files changed

Lines changed: 655 additions & 6 deletions

File tree

CLAUDE.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@ Public landing page for **NSE (Nostr Secure Enclave)** at [nse.dev](https://nse.
77
## Repo Structure
88

99
```
10-
docs/ ← GitHub Pages source (served from main branch)
11-
index.html ← Single-page site (HTML + inline CSS, no build step)
12-
og-image.png ← 1200x630 OG/Twitter social card
13-
CNAME ← Custom domain: nse.dev
14-
generate-og.py ← Pillow script to regenerate the social card
15-
README.md ← Full project overview + DNS setup
10+
docs/ ← GitHub Pages source (served from main branch)
11+
index.html ← Single-page site (HTML + inline CSS, no build step)
12+
og-image.png ← 1200x630 OG/Twitter social card
13+
CNAME ← Custom domain: nse.dev
14+
platforms/ ← Working code for each target platform
15+
core/ ← @nse-dev/core — shared TypeScript types + NSEProvider interface
16+
ios/ ← @nse-dev/ios — Swift Package (Secure Enclave)
17+
android/ ← @nse-dev/android — Kotlin (StrongBox / TEE)
18+
server/ ← @nse-dev/server — TypeScript (CF Workers + Node.js)
19+
browser/ ← @nse-dev/browser — TypeScript (WebAuthn + SubtleCrypto)
20+
python/ ← nse-dev — Python (PyPI)
21+
generate-og.py ← Pillow script to regenerate the social card
22+
README.md ← Full project overview + DNS setup
1623
```
1724

1825
## How to Work With This Repo

platforms/android/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# @nse-dev/android
2+
3+
Kotlin library for hardware-backed Nostr key management via Android StrongBox / TEE.
4+
5+
## Package: `@nse-dev/android` (Maven)
6+
7+
## How It Works
8+
9+
1. P-256 key generated in StrongBox / TEE via `android.security.keystore`
10+
2. AES-256-GCM key derived from KeyStore P-256 key
11+
3. secp256k1 keypair generated (via secp256k1-kmp or libsecp256k1 JNI)
12+
4. secp256k1 private key encrypted with KeyStore-derived AES key
13+
5. Encrypted blob stored in EncryptedSharedPreferences
14+
6. Biometric-gated unlock via BiometricPrompt
15+
16+
## First Consumer
17+
18+
NostrKeep Signer (`nostrkey.app.android.src`)
19+
20+
## Dependencies
21+
22+
- `android.security.keystore` (Android, built-in)
23+
- `androidx.biometric:biometric` (BiometricPrompt)
24+
- `secp256k1-kmp` or `libsecp256k1` JNI (secp256k1 signing)
25+
- `androidx.security:security-crypto` (EncryptedSharedPreferences)
26+
27+
## Status: Planned (Phase 2)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package dev.nse
2+
3+
import android.content.Context
4+
import android.security.keystore.KeyGenParameterSpec
5+
import android.security.keystore.KeyProperties
6+
import androidx.biometric.BiometricPrompt
7+
import java.security.KeyPairGenerator
8+
import java.security.KeyStore
9+
10+
/**
11+
* Nostr Secure Enclave — Android implementation
12+
* Hardware-backed key management using StrongBox/TEE P-256 to protect secp256k1 keys
13+
*/
14+
object NSE {
15+
16+
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
17+
private const val ENCLAVE_KEY_ALIAS = "dev.nse.enclave.p256"
18+
private const val BLOB_PREF_KEY = "dev.nse.encrypted.secp256k1"
19+
20+
/**
21+
* Generate a new secp256k1 keypair, protected by a StrongBox/TEE P-256 key
22+
*/
23+
suspend fun generate(context: Context): KeyInfo {
24+
// TODO: Phase 2 implementation
25+
// 1. Generate P-256 key in StrongBox/TEE (setIsStrongBoxBacked, biometric-gated)
26+
// 2. Generate secp256k1 keypair
27+
// 3. Derive AES-256-GCM key from P-256 via ECDH + HKDF
28+
// 4. Encrypt secp256k1 private key with AES key
29+
// 5. Store encrypted blob in EncryptedSharedPreferences
30+
// 6. Zero the plaintext: privateKeyBytes.fill(0)
31+
// 7. Return pubkey + npub
32+
throw NotImplementedError("Not yet implemented")
33+
}
34+
35+
/**
36+
* Sign a Nostr event
37+
* Biometric unlock → decrypt secp256k1 key → Schnorr sign → zero memory
38+
*/
39+
suspend fun sign(event: NostrEvent, promptInfo: BiometricPrompt.PromptInfo): SignedEvent {
40+
// TODO: Phase 2 implementation
41+
// 1. Biometric unlock via BiometricPrompt
42+
// 2. Access KeyStore P-256 key
43+
// 3. Derive AES key
44+
// 4. Decrypt secp256k1 private key into memory
45+
// 5. Schnorr sign the event (BIP-340)
46+
// 6. Zero the plaintext: privateKeyBytes.fill(0)
47+
// 7. Return signed event
48+
throw NotImplementedError("Not yet implemented")
49+
}
50+
51+
/** Get the hex pubkey (does not require biometric unlock) */
52+
suspend fun getPublicKey(context: Context): String {
53+
throw NotImplementedError("Not yet implemented")
54+
}
55+
56+
/** Get the bech32 npub */
57+
suspend fun getNpub(context: Context): String {
58+
throw NotImplementedError("Not yet implemented")
59+
}
60+
61+
/** Check if a key exists */
62+
fun exists(): Boolean {
63+
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER)
64+
keyStore.load(null)
65+
return keyStore.containsAlias(ENCLAVE_KEY_ALIAS)
66+
}
67+
68+
/** Wipe all key material */
69+
suspend fun destroy(context: Context) {
70+
// TODO: Delete KeyStore entry + EncryptedSharedPreferences blob
71+
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER)
72+
keyStore.load(null)
73+
keyStore.deleteEntry(ENCLAVE_KEY_ALIAS)
74+
}
75+
76+
// Types
77+
78+
data class KeyInfo(
79+
val pubkey: String,
80+
val npub: String,
81+
val createdAt: Long,
82+
val hardwareBacked: Boolean
83+
)
84+
85+
data class NostrEvent(
86+
val kind: Int,
87+
val content: String,
88+
val tags: List<List<String>>,
89+
val createdAt: Long
90+
)
91+
92+
data class SignedEvent(
93+
val id: String,
94+
val pubkey: String,
95+
val sig: String,
96+
val kind: Int,
97+
val content: String,
98+
val tags: List<List<String>>,
99+
val createdAt: Long
100+
)
101+
}

platforms/browser/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# @nse-dev/browser
2+
3+
TypeScript library for browser-based Nostr key management via WebAuthn + SubtleCrypto.
4+
5+
## Package: `@nse-dev/browser` (npm)
6+
7+
## How It Works
8+
9+
1. SubtleCrypto P-256 key generation
10+
2. AES-GCM wrapping of secp256k1 key
11+
3. Encrypted blob stored in IndexedDB or localStorage
12+
4. Biometric prompt per sign operation (WebAuthn)
13+
14+
## Open Questions
15+
16+
- Is WebAuthn biometric per-sign acceptable UX?
17+
- Or is NIP-46 to a mobile signer always better?
18+
19+
## Status: Research (Phase 5)

platforms/browser/src/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @nse-dev/browser — Browser Nostr Secure Enclave
3+
* WebAuthn + SubtleCrypto P-256 key wrapping for secp256k1
4+
*/
5+
6+
import type { NSEProvider, NSEEvent, NSESignedEvent, NSEKeyInfo } from '../../core/src/index.js';
7+
8+
/**
9+
* Browser implementation using SubtleCrypto + WebAuthn
10+
* P-256 key wraps secp256k1 via AES-GCM, stored in IndexedDB
11+
*/
12+
export class NSEBrowser implements NSEProvider {
13+
// TODO: Phase 5 implementation
14+
// Research: Is WebAuthn biometric per-sign acceptable UX?
15+
16+
async generate(): Promise<NSEKeyInfo> {
17+
throw new Error('Not yet implemented');
18+
}
19+
20+
async sign(event: NSEEvent): Promise<NSESignedEvent> {
21+
throw new Error('Not yet implemented');
22+
}
23+
24+
async getPublicKey(): Promise<string> {
25+
throw new Error('Not yet implemented');
26+
}
27+
28+
async getNpub(): Promise<string> {
29+
throw new Error('Not yet implemented');
30+
}
31+
32+
async exists(): Promise<boolean> {
33+
throw new Error('Not yet implemented');
34+
}
35+
36+
async destroy(): Promise<void> {
37+
throw new Error('Not yet implemented');
38+
}
39+
}

platforms/core/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# @nse-dev/core
2+
3+
Shared TypeScript interfaces and types for NSE. Every platform implementation conforms to these contracts.
4+
5+
## Package: `@nse-dev/core` (npm)
6+
7+
## Types
8+
9+
```typescript
10+
interface NSEProvider {
11+
generate(): Promise<NSEKeyInfo>;
12+
sign(event: NSEEvent): Promise<NSESignedEvent>;
13+
getPublicKey(): Promise<string>;
14+
getNpub(): Promise<string>;
15+
exists(): Promise<boolean>;
16+
destroy(): Promise<void>;
17+
}
18+
```
19+
20+
## Status: Planned (Phase 4)

platforms/core/src/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @nse-dev/core — Shared types for Nostr Secure Enclave
3+
*/
4+
5+
/** Minimal Nostr event for signing */
6+
export interface NSEEvent {
7+
kind: number;
8+
content: string;
9+
tags: string[][];
10+
created_at: number;
11+
}
12+
13+
/** Signed Nostr event (id + sig populated) */
14+
export interface NSESignedEvent extends NSEEvent {
15+
id: string;
16+
pubkey: string;
17+
sig: string;
18+
}
19+
20+
/** Key info returned by generate() */
21+
export interface NSEKeyInfo {
22+
pubkey: string;
23+
npub: string;
24+
created_at: number;
25+
hardware_backed: boolean;
26+
}
27+
28+
/** Platform-agnostic NSE contract — every implementation conforms to this */
29+
export interface NSEProvider {
30+
/** Generate a new secp256k1 keypair, protected by hardware */
31+
generate(): Promise<NSEKeyInfo>;
32+
33+
/** Sign a Nostr event (biometric unlock → decrypt → sign → zero) */
34+
sign(event: NSEEvent): Promise<NSESignedEvent>;
35+
36+
/** Get the hex pubkey */
37+
getPublicKey(): Promise<string>;
38+
39+
/** Get the bech32 npub */
40+
getNpub(): Promise<string>;
41+
42+
/** Check if a key exists in storage */
43+
exists(): Promise<boolean>;
44+
45+
/** Wipe all key material */
46+
destroy(): Promise<void>;
47+
}

platforms/ios/Package.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// swift-tools-version: 5.9
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "NSE",
7+
platforms: [
8+
.iOS(.v15),
9+
.macOS(.v13)
10+
],
11+
products: [
12+
.library(
13+
name: "NSE",
14+
targets: ["NSE"]
15+
)
16+
],
17+
dependencies: [
18+
// secp256k1 Schnorr signing
19+
.package(url: "https://github.com/21-DOT-DEV/swift-secp256k1", from: "0.17.0")
20+
],
21+
targets: [
22+
.target(
23+
name: "NSE",
24+
dependencies: [
25+
.product(name: "secp256k1", package: "swift-secp256k1")
26+
],
27+
path: "Sources/NSE"
28+
),
29+
.testTarget(
30+
name: "NSETests",
31+
dependencies: ["NSE"],
32+
path: "Tests/NSETests"
33+
)
34+
]
35+
)

platforms/ios/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# @nse-dev/ios
2+
3+
Swift Package for hardware-backed Nostr key management via iOS Secure Enclave.
4+
5+
## Package: `@nse-dev/ios` (Swift Package Manager)
6+
7+
## How It Works
8+
9+
1. P-256 key generated in Secure Enclave via `CryptoKit SecureEnclave.P256`
10+
2. AES-256-GCM key derived from enclave P-256 key (ECDH + HKDF)
11+
3. secp256k1 keypair generated (via K1 or swift-secp256k1)
12+
4. secp256k1 private key encrypted with enclave-derived AES key
13+
5. Encrypted blob stored in Keychain (`kSecAttrAccessibleWhenUnlockedThisDeviceOnly`)
14+
6. Biometric-gated unlock via Face ID / Touch ID (LAContext)
15+
16+
## First Consumer
17+
18+
NostrKeep Signer (`nostrkey.app.ios.src`)
19+
20+
## Dependencies
21+
22+
- `CryptoKit` (Apple, built-in)
23+
- `LocalAuthentication` (Apple, built-in)
24+
- `K1` or `swift-secp256k1` (secp256k1 Schnorr signing)
25+
26+
## Status: Planned (Phase 1)

0 commit comments

Comments
 (0)