Skip to content

feat: NIP-AB device pairing — Phase 2 (desktop + mobile UI)#343

Merged
wesbillman merged 9 commits intomainfrom
feat/nip-ab-desktop-mobile-pairing
Apr 16, 2026
Merged

feat: NIP-AB device pairing — Phase 2 (desktop + mobile UI)#343
wesbillman merged 9 commits intomainfrom
feat/nip-ab-desktop-mobile-pairing

Conversation

@wesbillman
Copy link
Copy Markdown
Collaborator

Summary

Integrates the NIP-AB device pairing protocol from #333 into the desktop (source) and mobile (target) apps. Replaces the insecure sprout:// QR code that embedded raw credentials directly with a multi-step encrypted exchange using ephemeral keys, ECDH, NIP-44, and SAS verification.

  • Desktop (source): New pairing.rs backend with 3 Tauri commands + background WebSocket actor. Multi-step React dialog: QR display → SAS verification → encrypted transfer → done
  • Mobile (target): NIP-44 v2 crypto (HKDF, ECDH, ChaCha20+HMAC), NIP-AB protocol implementation (QR parser, session derivations, ephemeral WebSocket), SAS verification UI
  • Bug fixes: Mobile message tagging matched wrong user for @mentions (scoped to channel members), added pubkey display to Settings, fixed logout routing
  • Security hardening from adversarial review: event signature verification, duplicate event tracking, payload gated on user SAS confirmation, Zeroizing for nsec in memory, abort on transcript mismatch

Known issues (not in scope)

  • Post-pairing channel message loading requires hot-reload (pre-existing WebSocket session timing issue)
  • Token not revoked on pairing cancel (follow-up)
  • Mobile needs mention autocomplete UI like desktop (quick fix uses channel-member-scoped resolution)

Test plan

  • Desktop: click Pair → QR code displays with nostrpair:// URI
  • Mobile: scan/paste QR → SAS code appears on both devices
  • Confirm SAS on desktop → credentials transfer → mobile authenticates
  • Mobile: send @agent message → agent responds (p-tag correct)
  • Mobile: Settings shows correct pubkey matching desktop
  • Legacy sprout:// URIs still work on mobile
  • Cancel pairing mid-flow → both sides clean up
  • cargo test -p sprout-core --lib pairing — 69 tests pass

🤖 Generated with Claude Code

wesbillman and others added 9 commits April 16, 2026 14:28
Integrate the NIP-AB pairing protocol from PR #333 into the desktop
(source) and mobile (target) apps, replacing the insecure sprout:// QR
code that embedded raw credentials directly.

Desktop (source side):
- New pairing.rs with 3 Tauri commands (start_pairing, confirm_pairing_sas,
  cancel_pairing) and a background WebSocket actor that drives the protocol
- Multi-step PairingDialog: QR display → SAS verification → transfer → done
- Token minting refactored to expose mint_token_internal for reuse
- Uses nostr-compat (0.36) alias for sprout-core type compatibility

Mobile (target side):
- NIP-44 v2 encrypt/decrypt implementation using pointycastle
- HKDF-SHA256 and secp256k1 ECDH crypto modules
- NIP-AB protocol: QR URI parsing, session ID/SAS/transcript derivations
- Ephemeral WebSocket with NIP-42 auth for pairing relay
- SAS verification UI with large code display and confirm/deny
- Backward-compatible: still accepts legacy sprout:// URIs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove extra author p-tag from sent messages and match desktop tag
  order (h-tag first). The mobile was prepending ['p', authorPubkey]
  before the channel h-tag, which differs from the desktop format and
  caused agents to not respond to mobile messages.

- Add pubkey display with copy button to Settings page so users can
  verify their identity matches the desktop.

- Fix logout routing: pop all navigator routes back to root before
  signing out so the app returns to the pairing/welcome page instead
  of staying on the settings screen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The mobile's text-based @mention resolver was matching against the
global user cache, which picked the wrong user when multiple users
share a display name (e.g. two "rick"s). This caused agents to not
respond to mobile messages — the p-tag pointed to the wrong pubkey.

Now fetches channel members from the relay API and only matches
@mentions against members of the current channel. Falls back to the
full cache if the member fetch fails.

Also adds pubkey display to Settings and fixes logout routing
(popUntil root before sign-out).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ChannelMessagesNotifier returned AsyncData([]) (empty list) when the
WebSocket session wasn't connected yet. After first-time pairing, the
user would see channel names but empty message lists until the session
connected. Changed to AsyncLoading() so the UI shows a spinner, and
the provider rebuilds automatically once the WebSocket connects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After first-time pairing, the WebSocket session takes a moment to
connect. Previously the channel messages provider returned an empty
list (or infinite spinner) while waiting. Now it falls back to the
REST API (GET /api/channels/{id}/messages) to show content immediately.
Once the WebSocket connects, the provider rebuilds and switches to
the full live subscription flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts 0b93236 and c3ee567. The HTTP fallback and AsyncLoading
changes didn't fix the post-pairing channel loading issue — the
real cause is likely the WebSocket session not initializing properly
after first-time authentication. Needs a proper fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes from security review:

Critical:
- Gate payload acceptance on PairingStatus.transferring only, not
  confirmingSas. Prevents credentials from being stored before the
  user explicitly confirms the SAS codes match. (C3)
- Wrap desktop payload field in Zeroizing<String> so nsec is cleared
  from heap memory on drop. (C2)

High:
- Add NIP-01 event signature verification on mobile via
  Event.fromJson(verify: true) before processing any pairing event. (C1)
- Send abort event with reason "sas_mismatch" when transcript hash
  verification fails, per NIP-AB spec requirement. (H2)
- Add duplicate event ID tracking on mobile to prevent replay. (H3)

Medium:
- Validate event kind == 24134 before processing. (M4)
- Remove dead code branch in _handleSasConfirm. (M8)
- Clear _processedEventIds and _sasConfirmReceived in _cleanup. (M6)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The security fix (C3) gated payload acceptance on transferring status,
but the desktop sends sas-confirm + payload back-to-back. The payload
arrives while the mobile is still in confirmingSas (waiting for user
tap), gets silently discarded, and the flow hangs.

Now buffers the payload when it arrives before user confirmation and
processes it immediately when confirmSas() is called.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keep main's tts.rs limit bump (1030) and our pairing overrides.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@wesbillman wesbillman merged commit fc67dac into main Apr 16, 2026
10 checks passed
@wesbillman wesbillman deleted the feat/nip-ab-desktop-mobile-pairing branch April 16, 2026 22:25
tlongwell-block added a commit that referenced this pull request Apr 17, 2026
* origin/main:
  nip-ab: clarify transcript_hash role and fix protocol diagram (#346)
  chore: fix deprecation warnings and decompose AgentsView (#347)
  chore: improve thread panel inline replies and nesting behavior (#339)
  feat: NIP-AB device pairing — Phase 2 (desktop + mobile UI) (#343)
  fix(huddle): prevent phantom huddle from late-arriving relay events (#344)
  perf(tts): reduce Kokoro time-to-first-audio with session warmup and threading (#342)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant