Field feedback on 0.5.28: the standalone Backup/Signer screens were hard to read over the synthwave background, back left the app instead of the menu, and they broke the hub's one-container interaction model. Both are now hub sub-pages exactly like Nodes/FIPS: - BackupSection / SignerSection (ui/components) render inside the NESMenu panel with the menu's own dark glass surface, scrim, and palette — the readability and theming problem disappears with the standalone surface. - The header back arrow returns to the hub card page (same as Nodes). - The panel height cap drops from 92% to 70% of the screen — ~15% breathing margin top and bottom; content scrolls inside. - The pairing QR scanner is hosted by NESMenu OUTSIDE the panel (QrGlassModal is a full-screen Box, not a Dialog — inside the panel's scroll it would clip), and decoded nostrconnect:// URIs funnel into the signer section through the same latch as the deep link. - The nostrconnect:// deep link now routes to the session and pops the hub open on the signer sub-page (SignerLaunch singleton) instead of a dedicated route; standalone screens and routes removed. - BunkerManager.refreshState is now a proper suspend fun (was runBlocking on the caller's dispatcher). Docs updated to the new locations. Rebuilt for on-device testing (v0.5.28-debug/vc48, same signing cert).
5.5 KiB
Companion NIP-46 remote signer — the phone side of Nostr Bunker (#139)
Status: shipped in companion 0.5.28 (vc48). Issue: #139 ("Remote signer
with companion app?"). Background research:
nostr-signer-login-research.md — flow B of
that document is exactly the flow this implements, with the companion playing
the role the research assigned to Amber.
What shipped: the phone IS the bunker (remote signer)
The companion holds a nostr key (generate or import an nsec) and speaks
NIP-46 as the remote signer:
- A NIP-46 client — the node's login page, per the research doc's flow B,
or any
nostrconnect://-emitting app — shows its pairing QR. - The phone scans it (hub → Remote Signer → Scan pairing QR), or any
QR-scanner app hands the
nostrconnect://URI over as a deep link (registered in the manifest). - The phone connects to the client's relay(s), subscribes to kind-24133
events p-tagged to its own key, and sends the
connectrequest carrying the secret — the same handshake direction rust-nostr's reference bunker uses (NostrConnectRemoteSigner::send_connect_ack), which is what the node's eventual nostr-connect client will wait for. - Requests arrive NIP-44-encrypted. Handled methods:
connect→ "ack" (validates our pubkey + the pairing secret)get_public_key→ our pubkeydescribe→ method listping→ "pong"sign_event→ an approve/deny card — kind label, content, tags, time. Nothing signs without a thumb on Approve. Deny replies"denied"; a second request while one is pending replies"busy"instead of replacing the visible card.- anything else →
"not authorized"(nip04/nip44 encrypt/decrypt are deliberately NOT granted in v1).
- Responses go back over the same encrypted kind-24133 channel.
The session lives while the app does (the login handshake takes seconds); remembered-session auto-reconnect is the research doc's deferred flow C, and stays deferred. NIP-04 is accepted on receive as a fallback (deprecated but still spoken by real clients); all sending is NIP-44 v2.
Where the code lives
- Crypto:
Android/rust/archy-fips-core/src/nostr.rs— nsec/npub bech32 keys, BIP-340 schnorr event signing (NIP-01 id serialization), NIP-44 v2 payloads, NIP-04 fallback,nostrconnect://parsing. Hostcargo testruns the official NIP-44 vectors (conversation/message keys, padded lengths, byte-exact encrypt vectors), the official BIP-340 sign vectors, and round-trip/tamper/failure cases. - JNI:
com.archipelago.app.NativeCore(same .so as the FIPS mesh). - Session:
nostr/BunkerManager.kt— OkHttp WebSocket relay client, JSON-RPC dispatch, approve/deny state. - UI: a hub sub-page (
ui/components/SignerSection.kt, opened from the three-finger hub menu like Nodes/FIPS) — key setup, npub/nsec display, pairing scan, session status, the approve/deny card. The full-screen pairing scanner (QrGlassModal) is hosted by NESMenu so it isn't clipped to the panel's bounds. Thenostrconnect://deep link routes to the session and pops the hub open on the signer sub-page (SignerLaunch).
Security notes (conscious deviations, reviewed)
- Incoming events are not signature-verified before decryption — the same choice rust-nostr's reference bunker makes. The NIP-44 MAC is the actual gate: forging content that decrypts with a valid MAC requires one of the two conversation secrets. A future hardening pass may add event verification first.
- The signer secret lives in app-private DataStore (same storage model as
the FIPS secret and node login passwords). It can additionally be sealed
inside an encrypted backup (see
companion-backup-restore.md). sign_eventapproval is per-request and per-screen; there is no "remember this client" auto-approve in v1.
End-to-end test harness (the node side doesn't exist yet)
Android/tools/nip46-test-client.py plays the node's role: generates the
pairing QR in your terminal, runs the full handshake, requests
get_public_key + sign_event, and verifies the returned signature with an
independent pure-Python BIP-340 implementation (no code shared with the
phone's Rust core; both are pinned to the same official test vectors).
python3 -m venv /tmp/nip46env
/tmp/nip46env/bin/pip install websockets qrcode
/tmp/nip46env/bin/python Android/tools/nip46-test-client.py # --relay to override
Then on the phone: hub → Remote Signer → Generate key (once) → Scan pairing
QR → point at the terminal QR → Approve the incoming request. The harness
prints END-TO-END PASS when the phone-signed event verifies.
Test checklist (on-device)
- Generate key → npub shows, copy works; import nsec → same npub.
- Harness handshake: pair → ack →
get_public_keyreturns the phone's npub. sign_eventrequest shows a legible card (kind label, content, tags); Approve → harness verifies the schnorr signature; Deny → harness sees"denied".- Deep link: open a
nostrconnect://…URI from a QR app → SignerScreen with the pairing already starting. - Wrong/foreign QR → clear error, no state change.
Roadmap (node-side, tracked separately)
The node-side bunker hosting/login flow (research doc flows A+B, the
auth.login.nostr slot, relay topology on the node's own strfry) is roadmap
territory via the companion-agent-labeled tracker issues; when it ships,
the phone side here already speaks its language.