Platform & Server

BLE Sync & Pairing

Face-to-face handshakes, GATT service attributes, seed derivations, and proximity gates.

Overview

Wiltkey implements in-person, face-to-face pairing over Bluetooth Low Energy (BLE). This guarantees that initial key exchanges cannot be intercepted remotely. The pairing flow enforces a proximity gate (RSSI strength) so that only devices physically touching or extremely close can complete a handshake.

Pairing Handshake Flow

Initiator Client Scanning (RSSI > -85) Responder Client Advertising GATT 1. Write: pairing_request (JSON) 2. Read: accepted + public key (JSON) derivedSeed = SHA-256(sorted_pubkeys)

How it Works

  1. BLE Advertising & Proximity Check: The responder advertises a local name with format:
    • WK:<shortNick>:<shortPubKeyHash> (1-on-1)
    • WKG:<groupName>:<shortPubKeyHash> (Group Invite)
    The initiator scans for devices matching these prefixes. To pair, the signal strength must exceed the proximity threshold:
    kNearRssi = -85 dBm
  2. GATT Handshake Channel: Pairing utilizes a dedicated GATT service and characteristic:
    • Service UUID: 4f7091f6-7ccf-f798-f5b2-098ed89c5b2d
    • Characteristic UUID: 098ed89c-5b2d-4f70-91f6-7ccff798f5b2 (properties: Read/Write/Notify)
    The initiator writes a JSON payload of type pairing_request containing their device name, nickname, profile image, public key, and requested buffer size to the characteristic.
  3. Seed Derivation & Confirmation: The responder displays an unlock confirmation dialogue. If accepted, the responder:
    1. Sorts the public key strings alphabetically: [myPubKey, peerPubKey]..sort()
    2. Derives the pairwise seed: derivedSeed = SHA-256(sorted_keys)
    3. Writes a JSON response mapping back to the characteristic containing its own public key and nickname.
    The initiator reads this response, performs the same key sorting, and derives the identical seed. Both clients then execute addOrRechargeContact to generate their local pads and provision the secure session.
  4. Group Invite Seed Distribution: If the pairing type is a group invite, the host generates the response with pairing_type: "group_invite" and encrypts the group's seed by XORing it with the derived pairwise seed:
    encryptedSeed = groupSeed XOR SHA-256(derivedSeed)
    The responder also sends the assigned lane slot_index. The joiner decrypts the seed using the derived seed and registers the group.

Key Files & Symbols

File Path Symbol Name Description
lib/features/proximity/controllers/ble_pairing_manager.dart BlePairingManager State controller managing Bluetooth scanning, advertising, and GATT handshakes.
lib/features/proximity/controllers/ble_pairing_manager.dart respondToPairRequest() Builds derived seeds, encrypts group seeds, and writes GATT responses.
lib/features/proximity/controllers/ble_pairing_manager.dart _encryptGroupSeed() Encrypts the group seed with the pairwise derived key.
lib/features/proximity/controllers/ble_pairing_manager.dart _decryptGroupSeed() Decrypts the group seed with the pairwise derived key.

Gotchas & Edge Cases

⚠️ SIGNAL HYSTERESIS
To prevent scanning devices from rapidly blinking on and off the interface due to signal fluctuations near the boundary, BlePairingManager uses smoothed RSSI averages and a grace window of 12 seconds (_inRangeGrace) before evicting a device from the range filter.
🛑 STRICT 512-BYTE GATT MTU LIMIT
Bluetooth GATT operations are strictly bound by the maximum negotiated Attribute MTU. In BlePairingManager, we call device.requestMtu(512) on Android (the highest safe limit for standard attribute reads/writes). If the JSON pairing payload exceeds 512 bytes, the write is truncated or rejected, causing the pairing process to fail silently.

Current Sizing Metrics (Theoretical & Practical limits):
  • 1-on-1 Chat Pairing Request: Currently runs at roughly 350 to 400 bytes. It contains the initiator's ED25519 public key (64 hex characters), user ID (64 hex characters), device name, nickname, and a base64-encoded pixel art avatar (typically 100-200 bytes). If the user name is unusually long or if the avatar resolution is increased, it will overflow the 512-byte MTU limit.
  • Group Chat Join Response: Currently runs at roughly 380 to 450 bytes. It contains the group ID (64 hex characters), encrypted group seed (64 hex characters), host public key (64 hex characters), group name, lane configurations, and slot indexes. This is extremely tight and lies within 60-130 bytes of the absolute 512-byte limit. Long group names, long host names, or metadata expansion will easily exceed the MTU and cause pairing to fail.