302 lines
14 KiB
Markdown
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
|