August 29th, 2023

The road to good bluetooth permissions on mobile

Bluetooth and Peer-to-Peer Wi-Fi on mobile never got the permissions they deserved. Let's analyze what went wrong and how to change that.

Tom Karpiniec

Tom Karpiniec

Mesh Network Engineer

Designing a permissions system isn't easy. You must ensure you're tackling the most relevant privacy risks and permitting legitimate use cases, all while keeping the user informed on how you will be utilizing their device, but in a way that they aren't annoyed by a barrage of questions every time they try to use a new app on their phone.

In the case of Bluetooth and Peer-to-Peer WiFi, neither iOS nor Android got it right from the beginning. The evidence for that is made clear by looking at the number of changes that have been made over the years. In my opinion, most of the warts have been fixed now and I'm hopeful we're entering a stable period which will make developers' lives easier. Let's review.

The Apple Way

Support for Bluetooth Low Energy (BLE) arrived staggered across iOS 5 and 6. At this time, both publishing advertisements and scanning for devices were permitted freely while the app was in the foreground; the app was cut off immediately as soon as the user locked their phone or switched to another app.

There were opt-in capabilities, presumably considered during App Store review, to permit both scanning and advertising while the app was in the background. Even if you have this capability, the amount of data that could be advertised was reduced. If the user had switched to another app or the device was locked then you could only broadcast your service UUID, not the local name string.

This by itself is no guarantee of privacy for two reasons. An external device could still connect to your phone based only on the UUID part of the advertisement and communicate with the backgrounded app to read out additional data via characteristics. This enables your phone to transmit large amounts of data while locked—but this is also useful and important for many legitimate apps.

The other reason that dropping the local name isn't so helpful is that you're permitted to use an arbitrary service UUID anyway. A very small part of that 128-bit space is theoretically reserved but it's acceptable for most purposes to just pick and use a random one. An app that wanted to identify itself but also track individuals could use a static 64-bit prefix and a unique 64-bit suffix.

This situation continued until iOS 13 (released 2019). Apple decided to take the privacy risks of this Bluetooth free-for-all seriously and added a runtime permission check. This was quickly proven to be a good idea with many big apps suddenly producing permission prompts, to the surprise of their users. This change owed its success to an important design decision: runtime permission prompts are generated automatically by the OS at the appropriate time, presented over the top of your app without your control.

Meanwhile, Apple has their own flavour of high-speed Peer-to-Peer WiFi called Apple Wireless Direct Link (AWDL). The ability to discover and connect to other devices arrived in the NSNetService API in iOS 7 and has recently been superseded by Apple's new "Network" framework.

This was restricted starting in iOS 14. Any use of service discovery asks the user whether the app should be permitted to access devices on the local network. You are also expected to specify which service types you're searching for in your Info.plist under the NSBonjourServices key.

If all this sounds pretty reasonable, I tend to agree. The multicast debacle notwithstanding, Apple's slow and steady approach to permissions has tightened things up over the years without being overly weird. Android's approach was a bit different.

The Android Way

Similar to Apple's approach, support for BLE was staggered across Android 4 and 5. Like iOS, acting as a BLE central to connect to peripherals was possible first. At this time you needed the BLUETOOTH and BLUETOOTH_ADMIN permissions in your manifest. Otherwise, on Android 5 you were free to use these features, including in the background, if your app was lucky enough to still be running. At this time, Android apps were granted all of their permissions upfront at installation time so users couldn't choose whether or not to grant it later.

For Android 6 (released 2015), they realised that there was a privacy risk in allowing free access to BLE. In particular, you could determine someone's location by scanning for beacons. In a move so pedantic that only a Googler could have come up with it, it became necessary to ask for the ACCESS_COARSE_LOCATION permission to scan for other BLE devices. This is the same permission that allows the app to find out your location directly. This was deeply confusing to users who just wanted to use Bluetooth, because they didn't understand why they were being prompted to grant location access.

This strange situation continued until Android 10 (released 2019) when they decided to review this location permission. Unfortunately, the problem they identified is that Bluetooth could actually be used to figure out your location pretty precisely; now ACCESS_FINE_LOCATION is required, not just coarse. This is not a joke. This was the requirement through Android 11.

In Android 12 (released 2021), they finally realised that this was ridiculous and implemented an elegant solution. There are now fine-grained permissions for BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, and BLUETOOTH_CONNECT. The BLUETOOTH_SCAN one has a sub-property neverForLocation which you can use as a developer to assert that you're not using the technology to figure out the user's location. If you are, then you need to ask for the location permission as well. Very good.

How about Peer-to-Peer WiFi? Well the main technology for that now is Wi-Fi Aware, which has been getting support since Android 8. In the latest iteration of the API it works really well and it's a formidable competitor to AWDL. So what permissions does that one need?

You guessed it—this also started out needing ACCESS_FINE_LOCATION, because detecting nearby WiFi devices could reveal your location, right? This situation continued through Android 12 and only in Android 13 (released Aug 2022) did they split off a new NEARBY_WIFI_DEVICES permission, with one of those neverForLocation subproperties like the ones they have for Bluetooth.

Phew. It took a few years of messing around but everything's great now. Mostly.

One of Android's design decisions is that the OS doesn't prompt automatically for a runtime permission that's needed. You have to figure out what permissions are missing and request those ones specifically. As we have seen, the list of required permissions changes a lot depending on what version of Android the app is running on, and there are rather a lot of Android users on OS versions that are several years old. As an app developer this means you now have a complicated manifest replete with maxSdkVersion properties and complex logic for what to ask for at runtime.

What have we learned?

Not all "location" information is equal. Permissions tend to align better with actions or behaviours than the type of data. This is slowly spreading to other permission types. For example, instead of allowing access to "Photos" you can allow access to "Specific Photos". You could apply the same idea to Contacts permissions.

It's better for both developers and users if the OS takes charge of asking for runtime permissions and simply prompts for whichever ones are needed at the present time. This has implications for the code in the app that wants to take the privileged action. It can't just block waiting for access unless the OS is designed to do that, as is the case in iOS. Android likes its sync APIs, which don't leave time for an async permission request, so the developer is forced to check in advance.

It's smart to tie risky classes of functionality (like background operation) to manifest declarations that can be checked at app review time. It makes it easier for a reviewer to spot an abusive flashlight app or at least limit the damage if the developer has to declare up-front that they're going to use certain APIs.

We can also conclude that users need and expect more privacy from their devices' radios nowadays. Both Apple and Android have improved the level of control and that benefits all of us.

There's still room for innovation. It wouldn't be surprising if Apple adds further restrictions on service UUID, such as having to declare them in Info.plist. Users may also demand direct control over whether an app should be able to use communication features in the foreground vs in the background. With luck though, there won't be any major adjustments for a while.

Get posts in your inbox

Subscribe to updates and we'll send you occasional emails with posts that we think you'll like.