Skip to main content

Proximity BLE Chat (PBLEC)

· 8 min read
Download on the App Store

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 workaround to these checks by spoofing IRKs, giving the impression of new devices to PBLEC and flooding the feed.

The Notify feature

The app still did not feel complete to me. For Briar or similar apps, usage in protests makes sense because users are expected to have the app open for communication.

How would PBLEC users know that there is a conversation going on nearby? How would a PBLEC user notify others that they want to chat? Mentioning it in person or telling someone to hop on the chat defeats the purpose of the app.

iBeacon, although a type of Bluetooth advertisement, is part of the CoreLocation framework and solves this. iBeacon detection on iOS allows the app to register a UUID for the OS to track. If there is a match, the app will be woken up in the background regardless of previous status (even if it was terminated).

For PBLEC, if the app is woken up and the iBeacon packet is not a duplicate (major/minor values checked), the app will push a notification.

This is still an experimental feature as iBeacon detection depends heavily on physical motion. A device in motion is more likely to get the iBeacon notification than a stationary one. To increase the chance of detection, the broadcast time of iBeacons is set to 60 seconds.

The Notify feature does not carry the same flooding risks as the core chat. Each user is limited to 1 broadcast per day. Users can also set how many notifications they want to receive per day (default is 2). The notification cap on the receiving end limits iBeacon advertisements. All packets after the notification count limit is reached are dropped.

Building a PBLEC compatible app

The core things to keep in mind here is the 6 second message cycle: 4 seconds of broadcast followed by 2 second cooldown. The format for a PBLEC packet is ~<digit><UTF-8 text>, where ~ is used for filtering for app traffic, the digit is the sliding window ID (0-9) and the remaining bytes carry the message text.

Device identifiers are useful for differentiating between peers within a session, but Bluetooth addresses rotate roughly every 15 minutes due to IRK cycling. PBLEC prunes peer entries after 60 seconds of inactivity for two reasons: to remove stale peers who have left range, and to clean up identifiers that may have rotated in the meantime.

The UUID of my iBeacons is: 40B74E09-F1E9-4992-87AD-77E4882EECF3

Some additional things to keep in mind: The stop advertising call in CoreBluetooth is asynchronous and can take multiple seconds, if not minutes to effect. The workaround used in PBLEC is to overwrite the current message with an empty string before calling stop. This could be similar or different for Android and should be tested. The app is foreground only.

Please ensure that your app gives users control over how many notifications they receive, including the option to receive none.

What's next

Subscribe to my RSS feed to be the first to hear about Vetch, the productivity tool I'm building.