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
How it Works
-
BLE Advertising & Proximity Check:
The responder advertises a local name with format:
WK:<shortNick>:<shortPubKeyHash>(1-on-1)WKG:<groupName>:<shortPubKeyHash>(Group Invite)
kNearRssi = -85 dBm -
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)
pairing_requestcontaining their device name, nickname, profile image, public key, and requested buffer size to the characteristic. - Service UUID:
-
Seed Derivation & Confirmation:
The responder displays an unlock confirmation dialogue. If accepted, the responder:
- Sorts the public key strings alphabetically:
[myPubKey, peerPubKey]..sort() - Derives the pairwise seed:
derivedSeed = SHA-256(sorted_keys) - Writes a JSON response mapping back to the characteristic containing its own public key and nickname.
addOrRechargeContactto generate their local pads and provision the secure session. - Sorts the public key strings alphabetically:
-
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 laneslot_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,
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
Current Sizing Metrics (Theoretical & Practical limits):
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.