Files
sovereign_browser/plans/nostr-login-integration.md
T

302 lines
14 KiB
Markdown

# Nostr Login Integration — Plan
## Goal
When the browser opens, the first thing the user sees is a Nostr login screen.
After authenticating, the browser has the user's Nostr identity (pubkey + signing
capability) and injects `window.nostr` into every page it loads.
We want all the functionality of `nostr_login_lite` — local key, seed phrase,
read-only, NIP-46 remote signer, n_signer hardware — as login methods.
## Reference implementations
- **`nostr_core_lib/examples/note_poster.c`** — shows local key + all n_signer
transport modes (unix, serial, tcp, fds) via `nostr_signer_t`.
- **`n_signer/client/demo_c99.c`** — comprehensive C99 demo: qrexec transport,
`nostr_signer_nsigner_set_nostr_index()`, getPublicKey + signEvent + nip44
encrypt/decrypt round-trip. This is the pattern for n_signer login.
- **`nostr_core_lib/examples/nip46_remote_signer.c`** — NIP-46 session pattern.
- **`nostr_core_lib/examples/mnemonic_generation.c`** / **`mnemonic_derivation.c`**
— seed phrase key generation/derivation pattern.
## Key insight: nostr_core_lib already has everything
`nostr_core_lib` (at `~/lt/nostr_core_lib`) is a C library that already
implements all the crypto and protocol functionality that `nostr_login_lite`
does in JavaScript:
| Login method | nostr_core_lib C API |
|---|---|
| **Local key** (generate / paste nsec) | `nostr_generate_keypair()`, `nostr_decode_nsec()`, `nostr_key_to_bech32()` |
| **Seed phrase** (BIP-39) | `nostr_generate_mnemonic_and_keys()`, `nostr_derive_keys_from_mnemonic()` |
| **Read-only** (npub only) | `nostr_decode_npub()` |
| **NIP-46 remote signer** | `nostr_nip46_parse_bunker_url()`, `nostr_nip46_client_session_t`, full request/response event handling |
| **n_signer hardware** | `nostr_signer_nsigner_unix()`, `nostr_signer_nsigner_serial()`, `nostr_signer_nsigner_tcp()`, `nostr_signer_nsigner_qrexec()` — direct transport (USB serial, UNIX socket, TCP, Qubes qrexec), **better** than WebUSB. Key selection via `nostr_signer_nsigner_set_nostr_index()`. See `n_signer/client/demo_c99.c` for the full pattern. |
| **Schnorr signing** | `nostr_create_and_sign_event()`, `nostr_signer_sign_event()` |
| **NIP-04 encrypt/decrypt** | `nostr_nip04_encrypt()`, `nostr_nip04_decrypt()` |
| **NIP-44 encrypt/decrypt** | `nostr_nip44_encrypt()`, `nostr_nip44_decrypt()` |
| **Unified signer** | `nostr_signer_t` — local + n_signer backends, all NIP-07 verbs (getPublicKey, signEvent, nip04/nip44) |
The `nostr_signer_t` abstraction is the key: it provides a unified interface
across local keys and n_signer hardware. The browser creates a signer at login
time and routes all `window.nostr` calls through it.
**What nostr_login_lite adds that is NOT in nostr_core_lib:**
1. **UI** — modal dialog, floating tab, themes (we build a GTK dialog instead)
2. **Orchestration** — method selection, persistence, session restore (straightforward C)
3. **window.nostr facade** — JS shim injected into pages (we inject via WebKitGTK)
## Decision: Native C99 login using nostr_core_lib
Since nostr_core_lib provides all the functionality, we build the login as a
**native GTK dialog** that calls nostr_core_lib directly. No JavaScript login
page needed. This is the purest C99 path and avoids the WebKitGTK WebUSB/WebSerial
limitation entirely — n_signer works via direct serial/socket transport in C.
### Architecture
```mermaid
flowchart TB
subgraph C99Host[sovereign_browser C99 host]
LoginDlg[GTK Login Dialog]
KeyStore[C Key Store - file persistence]
Signer[nostr_signer_t from nostr_core_lib]
NostrInject[window.nostr JS injector]
Bridge[C to JS bridge - sovereign:// scheme]
end
subgraph NostrCoreLib[nostr_core_lib - linked static lib]
NIP01[NIP-01 Event signing]
NIP06[NIP-06 Seed phrase derivation]
NIP19[NIP-19 bech32 nsec/npub]
NIP04[NIP-04 Encryption]
NIP44[NIP-44 Encryption]
NIP46[NIP-46 Remote signer]
NSigner[n_signer transport - serial/socket/TCP]
end
subgraph WebKit[WebKitGTK Web View]
Pages[Web pages with window.nostr]
end
LoginDlg -- user picks method --> Signer
LoginDlg -- saves identity --> KeyStore
Signer --> NIP01
Signer --> NIP06
Signer --> NIP19
Signer --> NIP04
Signer --> NIP44
Signer --> NIP46
Signer --> NSigner
KeyStore -- restore on startup --> Signer
Signer -- handles sign requests --> Bridge
Bridge -- marshals calls --> NostrInject
NostrInject -- injects into --> Pages
```
### Login flow
```mermaid
flowchart TD
Start[Browser starts] --> CheckKeyStore{Saved key in store?}
CheckKeyStore -- yes --> RestoreSigner[Restore signer from saved key]
CheckKeyStore -- no --> ShowLogin[Show GTK Login Dialog]
ShowLogin --> MethodSelect{User selects method}
MethodSelect -- Local key --> EnterNsec[Paste nsec or generate new]
MethodSelect -- Seed phrase --> EnterSeed[Enter BIP-39 mnemonic]
MethodSelect -- Read-only --> EnterNpub[Paste npub]
MethodSelect -- NIP-46 --> EnterBunker[Paste bunker:// URL]
MethodSelect -- n_signer USB --> PickSerial[Select /dev/ttyACM* device]
EnterNsec --> CreateSigner[Create nostr_signer_local]
EnterSeed --> DeriveKeys[nostr_derive_keys_from_mnemonic]
DeriveKeys --> CreateSigner
EnterNpub --> CreateReadonly[Store pubkey only, no signer]
EnterBunker --> ConnectNIP46[Parse URL, connect to relay]
ConnectNIP46 --> CreateSigner
PickSerial --> CreateNSigner[Create nostr_signer_nsigner_serial]
CreateNSigner --> CreateSigner
CreateSigner --> SaveKeyStore[Save identity to key store]
CreateReadonly --> SaveKeyStore
SaveKeyStore --> LoadBrowser[Load start URL in web view]
RestoreSigner --> LoadBrowser
LoadBrowser --> InjectNostr[Inject window.nostr into all pages]
```
### window.nostr injection — `sovereign://` URI scheme bridge
After login, every page loaded in the web view gets a `window.nostr` object
injected via `WebKitUserContentManager` + `webkit_user_content_manager_add_script()`.
The injected JS shim exposes the NIP-07 API:
```javascript
window.nostr = {
getPublicKey: () => bridgeCall('getPublicKey'),
signEvent: (event) => bridgeCall('signEvent', event),
getRelays: () => bridgeCall('getRelays'),
nip04: { encrypt: (pubkey, text) => bridgeCall('nip04_encrypt', {pubkey, text}),
decrypt: (pubkey, ct) => bridgeCall('nip04_decrypt', {pubkey, ct}) },
nip44: { encrypt: (pubkey, text) => bridgeCall('nip44_encrypt', {pubkey, text}),
decrypt: (pubkey, ct) => bridgeCall('nip44_decrypt', {pubkey, ct}) }
};
```
`bridgeCall()` marshals to C via a custom `sovereign://` URI scheme fetch:
`fetch('sovereign://nostr/getPublicKey')` etc. The C scheme handler receives
the request, calls the appropriate `nostr_signer_t` function, and returns the
result as the HTTP response body.
This is the same nos2x surface, so existing Nostr web apps work without an
extension.
### Architecture decision: URI scheme now, WebExtension later
Two approaches were considered for the JS→C bridge:
1. **`sovereign://` URI scheme** (chosen for now) — JS does `fetch()` to a
custom scheme, C handles it. Simple, one callback, no separate library.
2. **`WebKitWebExtension` + JSC** (future) — a `.so` loaded into the web
process that registers C functions as JS globals. The "proper" WebKitGTK
way, supports synchronous calls, process isolation.
**Why start with the URI scheme:** it serves double duty. The same
`webkit_web_context_register_uri_scheme()` infrastructure is needed for:
- `sovereign://nostr/*` — window.nostr bridge (Phase 2)
- `sovereign://settings`, `sovereign://about` — browser-internal pages
- `fips://` / `*.fips` — FIPS mesh routing (roadmap item 3)
- `nostr://` — Nostr content scheme (roadmap item 5)
Building the scheme handler infrastructure once covers all these use cases.
**When to add WebExtension:** when we need:
- Synchronous NIP-07 calls (some older Nostr apps expect sync `signEvent`)
- Security stripping (CORS/SOP removal in the web process, before scripts run)
- Fine-grained request interception or content injection
The URI scheme is the **network/content layer** (controls what bytes load).
The WebExtension is the **script manipulation layer** (controls what happens
inside the web process). They serve different layers and will coexist.
## Implementation plan
### Phase 1 — Link nostr_core_lib + key store + local key login
1. **Update Makefile** to link against `libnostr_core_x64.a` and its dependencies
(`-lsecp256k1 -lssl -lcrypto -lcurl -lz -ldl -lpthread -lm`).
2. **Create `src/key_store.h` / `src/key_store.c`** — a C module that:
- Saves/loads the user's identity to `~/.sovereign_browser/identity.json`
- Stores: `{ method, pubkey_hex, private_key_hex?, nsigner_device?,
nip46_session? }`
- On startup, checks if a saved identity exists and restores the signer.
3. **Create `src/login_dialog.h` / `src/login_dialog.c`** — a GTK dialog with:
- Method selection buttons: Local Key, Seed Phrase, Read-only, NIP-46,
n_signer Hardware
- Input fields per method (nsec entry, mnemonic entry, npub entry, bunker
URL entry, serial device dropdown)
- "Generate new key" button for local key method
- "Generate new mnemonic" button for seed phrase method
- Calls nostr_core_lib functions to create the signer
- Returns a `nostr_signer_t*` (or NULL for read-only) + pubkey
4. **Modify `src/main.c`** — on startup:
- Try to restore identity from key store
- If no saved identity, show login dialog before creating the web view
- Store the signer globally for the window.nostr bridge
5. **Test**: launch browser, see login dialog, paste an nsec, verify the pubkey
is derived correctly, browser opens.
### Phase 2 — window.nostr injection + sovereign:// bridge
1. **Register `sovereign://` URI scheme** via
`webkit_web_context_register_uri_scheme()`.
2. **Create `src/nostr_bridge.h` / `src/nostr_bridge.c`** — handles
`sovereign://nostr/<method>` requests:
- `getPublicKey` → `nostr_signer_get_public_key()`
- `signEvent` → `nostr_signer_sign_event()`
- `nip04_encrypt` / `nip04_decrypt` → `nostr_signer_nip04_*()`
- `nip44_encrypt` / `nip44_decrypt` → `nostr_signer_nip44_*()`
- Returns results as JSON in the scheme response stream.
3. **Create `src/nostr_inject.h` / `src/nostr_inject.c`** — builds the JS shim
string and injects it via `WebKitUserContentManager` into all frames before
page scripts run.
4. **Test**: load a Nostr web app (e.g. a test page that calls
`window.nostr.getPublicKey()`), verify it gets the pubkey.
### Phase 3 — Seed phrase + read-only + NIP-46 login
1. **Seed phrase screen** — text area for mnemonic, calls
`nostr_derive_keys_from_mnemonic()`. Add "generate new mnemonic" button
using `nostr_generate_mnemonic_and_keys()`.
2. **Read-only screen** — npub entry, calls `nostr_decode_npub()`. No signer
created; `window.nostr.signEvent()` returns an error.
3. **NIP-46 screen** — bunker:// URL entry, calls
`nostr_nip46_parse_bunker_url()`, establishes a WebSocket connection to the
relay, sends a connect request. Uses `nostr_nip46_client_session_t`.
4. **Test**: each login method end-to-end.
### Phase 4 — n_signer hardware login
1. **n_signer screen** — transport selection:
- **Serial**: enumerate via `nsigner_transport_list_serial()`, dropdown
- **UNIX socket**: enumerate via `nsigner_transport_list_unix()`, dropdown
- **TCP**: host + port entry fields
- **Qubes qrexec**: target qube + service name entry (for Qubes OS)
- **nostr_index** selector (NIP-06 m/44'/1237'/N'/0/0) — numeric input, default 0
2. **Create signer** — `nostr_signer_nsigner_serial/unix/tcp/qrexec(...)`, then
`nostr_signer_nsigner_set_nostr_index(signer, index)`.
3. **Get pubkey** — `nostr_signer_get_public_key()` to verify the device and
show the npub to the user.
4. **Persist** — save the transport type + connection params + nostr_index (not
a private key) in the key store. On restore, reopen the transport.
5. **Test**: connect to n_signer via each transport, verify pubkey, sign an
event from a web page. Follow the pattern in `n_signer/client/demo_c99.c`.
### Phase 5 — Persistence + polish
1. **Encrypted key file** — encrypt the private key in the key store with a
user-supplied password (AES-256-GCM via OpenSSL).
2. **Session lock** — menu option to lock the session (clear signer, require
re-auth).
3. **Auto-restore on startup** — if a saved identity exists, skip the login
dialog and restore the signer directly.
4. **Login dialog styling** — match the browser's aesthetic (monospace, dark
theme option).
5. **Error handling** — clear error messages for invalid keys, connection
failures, device not found, etc.
## File structure after implementation
```
src/
main.c — modified: login flow + signer lifecycle
version.h — existing
key_store.h — new: identity persistence
key_store.c — new
login_dialog.h — new: GTK login dialog
login_dialog.c — new
nostr_bridge.h — new: sovereign:// scheme handler for window.nostr
nostr_bridge.c — new
nostr_inject.h — new: JS shim injection
nostr_inject.c — new
Makefile — modified: link nostr_core_lib + deps
```
## Dependencies to add
- `libnostr_core_x64.a` (or arm64) — static library from nostr_core_lib
- `-lsecp256k1` — Schnorr signing
- `-lssl -lcrypto` — OpenSSL (AES, SHA, HMAC)
- `-lcurl` — HTTP client (NIP-05, relay queries)
- `-lz` — compression
- All already required by nostr_core_lib; see its
[README](../nostr_core_lib/README.md:164) for the full link line.
## What we do NOT need
- **nostr_login_lite JS** — not needed; nostr_core_lib covers all functionality
- **nostr-tools JS bundle** — not needed
- **WebUSB / WebSerial** — not needed; n_signer works via direct serial/socket
- **A web server** — not needed; login is a native GTK dialog
- **JS-to-C bridge for login** — not needed; login is pure C + GTK
- **JS-to-C bridge for signing** — needed, but only for the window.nostr
injection into web pages (Phase 2), not for the login itself