Skip to content

feat: iOS BLE support for BitBox02 Nova#1

Draft
TaprootFreak wants to merge 7 commits intotrunkfrom
feature/ios-ble-pairing
Draft

feat: iOS BLE support for BitBox02 Nova#1
TaprootFreak wants to merge 7 commits intotrunkfrom
feature/ios-ble-pairing

Conversation

@TaprootFreak
Copy link
Copy Markdown

@TaprootFreak TaprootFreak commented Mar 28, 2026

Problem

The bitbox_flutter plugin had no iOS support — only Android was implemented. Adding iOS BLE support for BitBox02 Nova required solving several protocol-level issues specific to Bluetooth LE communication.

Key issues discovered and fixed

1. Wrong product mapping (bb02p-multiProductBitBox02Multi)
The BitBox02 Nova is a "Plus" device, but we mapped its BLE product string bb02p-multi to ProductBitBox02Multi (original BitBox02) instead of ProductBitBox02PlusMulti. This caused the Go firmware API to use wrong protocol paths — encrypted queries after pairing (e.g. ETHGetAddress) would hang because the device expected Plus-specific message formats.

2. BLE stale buffer causing panic: unexpected hwwRspBusy
Unlike USB (synchronous request/response), BLE delivers notifications asynchronously. The device sometimes sends hwwRspNotready followed by hwwRspBusy for the same request. The second response would land in the read buffer during the Go retry sleep (200ms). When Go then sent hwwReqRetry and read the response, it consumed the stale hwwRspBusy instead of the actual retry response, causing a panic.

Fix: Clear the read buffer + drain the semaphore before each new u2fhid init frame write. This ensures stale notifications from previous exchanges are discarded. The semaphore signal was also moved inside readBufferLock to prevent a race condition between clearReadBuffer() and the BLE notification handler.

3. InitDevice() panics on error
The Go API's InitDevice() called panic(err) on failure (e.g. device disconnect during pairing), crashing the entire app. Changed to return bool.

4. Blocking main thread
channelHashVerify, getETHAddress, and signETHMessage all send encrypted queries to the device and block until a response arrives. Running these on the Flutter main thread froze the UI. Moved all device-querying methods to DispatchQueue.global().async.

5. Read timeout
readBlocking() used semaphore.wait() without timeout. If the BLE connection dropped without didDisconnectPeripheral being called (e.g. XPC connection interrupted), the app would hang forever. Added a 10-second timeout.

Test plan

  • Connect to BitBox02 Nova via Bluetooth on iOS
  • Complete Noise XX handshake and attestation
  • Verify pairing code on device and in app
  • Retrieve ETH address after pairing
  • Sign ETH message (DFX auth) via BitBox
  • Test disconnect/reconnect behavior
  • Test with BitBox02 Nova BTC-only edition

konstantinullrich and others added 6 commits December 6, 2025 01:15
Implement CoreBluetooth communication layer for BitBox02 Nova (Plus)
hardware wallet on iOS:

- Add Bluetooth.swift with BLE service/characteristic discovery,
  read/write operations, and u2fhid packet handling
- Add BitboxFlutterPlugin.swift methods for iOS: initBitBox,
  getChannelHash, channelHashVerify, getETHAddress (all on background
  threads to avoid blocking UI)
- Fix product mapping: bb02p-* now correctly maps to
  ProductBitBox02PlusMulti/PlusBTCOnly (not the non-Plus variants)
- Fix BLE stale buffer issue: clear read buffer + drain semaphore
  before each new u2fhid init frame to prevent hwwRspBusy panics
- Fix race condition: semaphore.signal() moved inside readBufferLock
- Add 10s timeout on readBlocking to prevent hangs on BLE disconnect
- Change InitDevice() from panic to bool return on error
- Add pre-built Api.xcframework for iOS (gomobile bind)
- Add channelHash and channelHashVerify to Dart platform interface
Prevents blocking the main thread during DFX auth signing
when BitBox device communication is required.
@TaprootFreak
Copy link
Copy Markdown
Author

Code Review

Must fix

  1. signETHRLPTransaction and signETHTypedMessage still on main thread (BitboxFlutterPlugin.swift:327,367)

    • Both send encrypted queries to the BitBox device and block until response. Same DispatchQueue.global().async fix needed as for signETHMessage and channelHashVerify.
  2. GetDeviceWithInfo panics on invalid version (api.go:31)

    • panic(fmt.Sprintf("invalid version: %s", versionStr)) — should return gracefully (e.g. return false or log and use a default) instead of crashing the app.

Should fix

  1. Unused scanTimer property (BitboxFlutterPlugin.swift:8)

    • private var scanTimer: Timer? is declared but never used. Dead code.
  2. updateBackendState() body entirely commented out (Bluetooth.swift:520-531)

    • The method body is MobileserverBluetoothSetState(...) wrapped in comments. Either implement it or remove the dead code.
  3. Verbose print() statements in production code (Bluetooth.swift)

    • Every BLE packet logs its full hex content. Useful for debugging but noisy in production. Consider removing the per-packet logs or gating them behind a debug flag.

Move signETHRLPTransaction and signETHTypedMessage to
DispatchQueue.global().async to prevent blocking the main thread
during device communication.
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.

2 participants