Proximity BLE Chat (PBLEC) | Vetch
Skip to main content
Intro
My interest in BLE began when I unsuccessfully tried to create a camera shutter app for my Garmin watch. There are no dedicated system APIs on iOS for this. Selfie sticks solve the problem with a hack. They pair as HID devices, keyboards in particular. The volume button in most camera apps acts as a shutter, including the native iOS camera app. When you use a selfie stick or something similar, its essentially a keyboard with only one key for volume that triggers capture.
Garmin SDK does not allow connection as a secondary peripheral or setting the right HID profile. There are, of course, solutions that try to solve this using a companion app, which needs to be installed beforehand on both the Garmin watch and the phone. Garmin itself could probably do this using private APIs.
Current BLE Chat services
There are multiple apps that do offline messaging over Bluetooth. Briar is one of the most popular, with proven use in protests. There are also more recent ones like BitChat. What most of them have in common is that they rely on pairing and formal connections between devices to send and receive messages, using BLE advertising mainly for discovery.
Initial idea
I wanted to create a messaging service that works like a public broadcast, with no connection to any other device. You send messages to all devices nearby and receive messages from them.
This is a serious constraint because if there is no connection, the only place the message can go is inside the initial BLE advertising packet, which devices send out to announce their existence to nearby devices.
I also wanted the format to be open. Any app or service that follows a similar structure can send and receive messages alongside PBLEC.
So what it is: a public broadcast app where people nearby can communicate without key sharing or pairing. The app does not encrypt messages and should not be used for private messaging. For that, use Briar or something like it.
Creation of the Core app
CoreBluetooth advertises data on a best-effort basis. Apps in the foreground can get up to 28 bytes in the initial advertisement. Out of that, only 26 bytes can be actually used for changing local name and the service UUID.
A service UUID would allow faster filtering of packets but costs 16 bytes. There are also 2 byte UUIDs but they are reserved by the Bluetooth Special Interest Group (SIG) and it costs a decent amount to attain one. I decided the tradeoff was not worth it and put messages in the local name key instead. All messages which are sent from PBLEC have ~ prepended for filtering. I also enabled duplicate advertisements so that deduplication can be handled in-app rather than relying on CoreBluetooth's defaults. Scan responses are left empty.
I used PacketLogger to confirm 26 bytes of local name, whenever a packet is advertised. The byte math came out to be: 3 bytes for Flags AD + 2 bytes of Full Local name header + 26 bytes of message content = Max 31 bytes allowed.
Deduplication took most of the development time. Bluetooth peripherals, iphones included, use an Identity Resolving Key (IRK) to rotate their address roughly every 15 minutes as a privacy measure to prevent tracking. CoreBluetooth includes a peripheral.identifier property that remains stable until the device's IRK rotates, and that is the primary key used for deduplication. If two different people send the same message, both will appear since they have different identifiers. The implications of IRK rotation for deduplication and peer tracking are covered in the building a compatible app section.
Each message broadcasts for 4 seconds with a 2 second cooldown. I also used a sliding window ID cycling through digits 0-9 that gets appended to the ~ prefix on every send. The sliding window is useful for detecting late arriving packets of the same message, and newer messages (even if they are same) passes through as new.
This brings the usable message space to 24 bytes. I had a lot of different ideas on what to do with this, it can be 24 characters in ASCII, or even more if I only allow lowercase a-z, base32 etc. I decided to stick with UTF-8. So the user can get up to 24 characters of ASCII, but they can use emojis or characters of other languages at an additional byte cost. For example, the 😆 emoji costs 4 bytes while its ASCII equivalent "XD" or "xd" costs 2 bytes. The 24 byte limit might encourage users to compress words and use abbreviations, somewhat reminiscent of the SMS era.
As the total time for message broadcast + cooldown is 6 seconds, new messages can be sent by individual users every 6 seconds. This also acts as a sort of rate limit for the feed to remain comprehensible in areas with higher user density.
Flooding is a risk here. Neither iOS nor Android allow apps to change a device's Bluetooth IRK, and PBLEC does check the message id, identifier and the 6 second limit. Even then, someone motivated enough can build a...