How Bluetooth is Used for Digital Contact Tracing

At the time of this writing (December 2020), the world is at the peak of the first wave of the Covid-19 pandemic, with vaccines on the near horizon but still months away from making a substantial impact on the spread. Near the beginning of the outbreak, leading tech companies turned resources towards how they could help curtail the outbreak, and the concept of Digital Contact Tracing was an early answer.

What is Digital Contact Tracing?

Digital Contact Tracing apps (sometimes known as Exposure Notification apps) are Bluetooth Beacons that run on your smart phone, offering an encrypted unique identifier to other phones in near proximity, and storing the similar IDs they scan in memory. This identifier is encrypted with a key controlled by the regional health department and registered in their database. When a person reports a positive Covid-19 exposure over their Digital Contact app, it sends the list of recent IDs it stored to the health department, where they are decrypted and mapped to the associated individuals so they can be alerted through their device apps. Anonymity is a feature, as every step is designed to only transmit encrypted rolling identifiers, rather than personal information.

These apps provide a great resource for performing a hands-on exploration of Bluetooth Low Energy (BLE) technology: they are freely downloadable, have publicly available specifications, don’t require any hardware other than your smart phone, and (at the moment) are of timely interest.

This article will discuss two leading notification technologies and the BLE service behind them: the Exposure Notifications System developed jointly by Google and Apple and widely used by state health departments in the USA, and the BlueTrace Protocol, designed by the Singapore government and adopted by the Australian Department of Health.

Looking for more information on what Bluetooth Low Energy (BLE) is? See this BLE post to learn more including how the related GAP and GATT profiles work.

Comparing Linux BLE Sniffing Tools

The go-to solution for Linux-based Bluetooth work is Bluez, the official Linux Bluetooth protocol stack. It is available as a prebuilt package for most distributions:

Distro Package Installation
Debian, Ubuntu bluez, bluez-hcidump
RedHat EL, CentOS bluez, bluez-hcidump
Raspbian bluez source

The command line tools used here are provided in the Bluez package. Compared to other Linux commands, documentation tends to be sparse and spread throughout various How-To guides. This article will discuss a small subset of their capabilities, focused on sniffing BLE GAP and GATT signals.

Hcitool Cmd Example

The hcitool command is the wrapper for most Linux Bluetooth operations, offering numerous subcommands for configuring the local device and probing remote devices. To begin, make sure your system has a Bluetooth device that can be recognized:

% sudo hcitool dev
        hci0    00:11:22:33:44:55

If just one device is reported, the instructions here should work as detailed. If multiple devices are reported, the flag “-i hci” can be added to specify a particular one, or the first found device will be used. If no devices are reported, the system may not have Bluetooth capability – many inexpensive (less than $10 USD) and generally compatible Bluetooth adapters are easily available.

In the last couple of years, hcitool has been deprecated in favor of bluetoothctl (below), which is smarter about interfacing to a Bluetooth daemon rather than accessing the device hardware directly. Since then, some users have reported I/O errors with later Unix kernels when using hcitool due to this difference. However, other users have reported problems with using bluetoothctl for device scanning and its interface is harder to control by script. For its simplicity, hcitool will be used here.

bluetoothctl Example

The bluetoothctl command is also part of the bluez package, and is the officially-favored central command, deprecating the older hcitool. The descriptions below detail the older but easier-to-use hcitool lescan for remote device scanning, but in case that doesn’t work on newer systems, the bluetoothctl method is included here:

% sudo bluetoothctl
[NEW] Controller 00:11:22:33:44:55 localhost [default]
Agent registered
[bluetooth]# power on
Changing power on succeeded
[bluetooth]# scan on
Discovery started
[CHG] Controller 00:11:22:33:44:55 Discovering: yes
[NEW] Device 44:55:66:77:88:99 44-55-66-77-88-99
[bluetooth]# devices
Device 44:55:66:77:88:99 44-55-66-77-88-99

Hcidump Example

The hcidump command is useful to run in the background or in a separate terminal window, as it provides a continuous running output and parsing of the Bluetooth Host Controller Interface (HCI) data between the stack and device. This allows one to understand how an hcitool operation is translated into actual Bluetooth protocol data. By default, it will recognize many protocol patterns and convert them to human-readable summaries of the data format. With the optional “–raw” argument, it will output the literal packet data instead.

% sudo hcidump
HCI sniffer - Bluetooth packet analyzer ver 5.48
device: hci0 snap_len: 1500 filter: 0xffffffffffffffff
< HCI Command: LE Set Scan Parameters (0x08|0x000b) plen 7 type 0x01 (active) interval 10.000ms window 10.000ms own address: 0x00 (Public) policy: All > HCI Event: Command Complete (0x0e) plen 4
    LE Set Scan Parameters (0x08|0x000b) ncmd 1
    status 0x00
% sudo hcidump --raw
HCI sniffer - Bluetooth packet analyzer ver 5.48
device: hci0 snap_len: 1500 filter: 0xffffffffffffffff
< 01 0B 20 07 01 10 00 10 00 00 00 > 04 0E 04 01 0B 20 00

In the sniffing operations below, the hcidump output in raw form will be used to report the GAP advertising and scan-response packets containing the tracing ID output by the Exposure Notification apps.


The gatttool command is specifically useful in communicating with a remote device over the GATT protocol, to query the list of Services and Characteristics provided by a remote device, and to respond likewise.

This command in particular suffers from a lack of online documentation. NOTE that one useful facility is that most of its interactive operations are also provided as command line flag arguments, and they can be queried as follows:

% gatttool --help-all
  gatttool [OPTION?]

Help Options:
  -h, --help                                Show help options
  --help-all                                Show all help options
  --help-gatt                               Show all GATT commands
  --help-params                             Show all Primary Services/Characteristics arguments
  --help-char-read-write                    Show all Characteristics Value/Descriptor Read/Write arguments

GATT commands
  --primary                                 Primary Service Discovery
  --characteristics                         Characteristics Discovery
  --char-read                               Characteristics Value/Descriptor Read
  --char-write                              Characteristics Value Write Without Response (Write Command)
  --char-write-req                          Characteristics Value Write (Write Request)
  --char-desc                               Characteristics Descriptor Discovery
  --listen                                  Listen for notifications and indications

Primary Services/Characteristics arguments
  -s, --start=0x0001                        Starting handle(optional)
  -e, --end=0xffff                          Ending handle(optional)
  -u, --uuid=0x1801                         UUID16 or UUID128(optional)

Characteristics Value/Descriptor Read/Write arguments
  -a, --handle=0x0001                       Read/Write characteristic by handle(required)
  -n, --value=0x0001                        Write characteristic value (required for write operation)

Application Options:
  -i, --adapter=hciX                        Specify local adapter interface
  -b, --device=MAC                          Specify remote Bluetooth address
  -t, --addr-type=[public | random]         Set LE address type. Default: public
  -m, --mtu=MTU                             Specify the MTU size
  -p, --psm=PSM                             Specify the PSM for GATT/ATT over BR/EDR
  -l, --sec-level=[low | medium | high]     Set security level. Default: low
  -I, --interactive                         Use interactive mode

In the steps further below, the gatttool command will be used to query the GATT Service Characteristic that BlueTrace uses to deliver the tracing ID to observers.

Other BLE Scanning Platform Tools

The above operations to query GAP advertising packets and GATT characteristics can also more or less be performed by free smart phone apps. Of particular note are nRF Connect and LightBlue. These apps provide a convenient way to query and display BLE-capable devices in the vicinity, but options to capture and parse that data are more limited.

Exposure Notification Apps

For observing the Google/Apple Exposure Notification protocol, the Colorado Department of Public Health & Environment app CO Exposure Notifications was used. (As of this writing, there is no national United States app available, so many individual states have written an app based on the free template provided by the Exposure Notification project.) Like other state apps, this one maintains its own independent registry of users, not shared with other states. There were no obstacles in registering on this app as an out-of-state experimenter.

For observing the BlueTrace protocol, the Singapore Government Ministry of Health app TraceTogether was used. The app was available on the Google Play Store in the USA, and registering as an out-of-country visitor with a passport number provided no issues. Also available and using the same protocol is the Australian Government Department of Health COVIDSafe app; however, registration without having an Australian address did not seem to be an option.

Currently, user adoption of these apps in the USA has been low, and many states have been slow to respond with their own.

GAP and the Google/Apple Exposure Notification Model

The Exposure Notification model of digital contract tracing uses BLE GAP, the Generic Access Profile. In this form, the smart phone periodically issues an advertising packet while in a non-connection Broadcaster role. The payload of this packet contains a record of type 0x03 (Complete 16-bit Service UUID), Service UUID 0xFD6F, with 20 bytes of data (16 bytes Rolling Proximity Identifier plus 4 bytes encrypted metadata).

Because the contact identifier is contained exclusively in the broadcast advertising packet, there is no need for a proximate device to make a BLE GAP connection to receive the data. This simplifies the examination, and only the hcidump tool needs to be involved in the analysis.

(NOTE: For this and all following exercises, it will help to have as few Bluetooth devices discoverable as possible to eliminate confusion between them. Phones may have Bluetooth completely turned off from their Settings app; speakers/mice/etc may need to be completely powered off or moved out of the vicinity.)

In one terminal, launch the hcidump tool in normal, non-raw mode to get a textual representation of the data received from the device:

% sudo hcidump

From a second terminal, invoke hcitool operation lescan to perform a BLE scan of advertising devices. This will pick up any devices advertising in the environment and report their MAC bdaddr (along with their cosmetic name, if advertised). The lescan operation will also broadcast a scan-request packet for each device scanned, often receiving the reciprocal scan-response packet, which explains the duplicated entries:

% sudo hcitool lescan
LE Scan . . .
AA:BB:CC:DD:EE:FF (unknown)
AA:BB:CC:DD:EE:FF (unknown)
. . . .

While this scan is going on, we can see the resulting textual summary of the received data in the hcidump window:

% sudo hcidump
. . . .
> HCI Event: LE Meta Event (0x3e) plen 40
    LE Advertising Report
      ADV_SCAN_IND - Scannable undirected advertising (2)
      bdaddr AA:BB:CC:DD:EE:FF (Random)
      Complete service classes: 0xfd6f
      Unknown type 0x16 with 22 bytes data
      RSSI: -87
. . . .

This output is telling us the lescan operation scanned an advertising packet from a Random (rather than Public) MAC address of AA:BB:CC:DD:EE:FF. (The Random designation indicates this device is performing Random Resolvable Addressing to prevent address tracking, which is typical for a modern smart phone or portable Bluetooth device.) In this case, the payload contains one record, a Complete Service Class of type 0x16 with UUID 0xFD6F followed by 22 bytes of data. The Exposure Notifications System spec tells us that this is the identifier for their marker. The Bluetooth Special Interest Group (SIG) 16-bit UUID Numbers Document confirms that this UUID is registered to Apple, Inc.

However, this top-level text summary does not return the details or data of the packet received. For a deeper examination, the hcidump tool should be re-invoked in Raw mode, and the lescan operation repeated:

% sudo hcidump
. . . .
% sudo hcidump --raw

% sudo hcitool lescan
LE Scan  . . .
AA:BB:CC:DD:EE:FF (unknown)
AA:BB:CC:DD:EE:FF (unknown)
. . . .

(NOTE: Don’t be surprised when repeated runs of the lescan operation return different MAC addresses than previously seen. A key aspect of the Random address is that it changes every 15 minutes or so to confuse snoopers.)

% sudo hcidump --raw
. . . .
> 04 3E 28 02 01 02 01 AA BB CC DD EE FF 1C 03 03 6F FD 17 16
  6F FD 59 7D E2 8F 70 28 21 3E 8B 40 08 AA 15 81 70 1B 02 A5
  EC 7F A9
. . . .

From a general point of view, this data is much less descriptive, but is useful for doing a bitwise examination of the protocol.

Let’s break down the content of this advertising packet against the general BLE GAP spec and the Exposure Notifications System spec:

# GAP packet header:
04 3E # LE Advertising Report event
28    # Length of data to follow, 0x28 == 40 bytes/octets
02    # Subevent code for the HCI_LE_Advertising_Report event
01    # Number (of following) Reports: 1
02    # Event Type[1]: Scannable undirected advertising (ADV_SCAN_IND) tx:public, rx:public
01    # Address Type[1]: Random Device Address
AA BB CC DD EE FF # Address[1]: Random MAC bdaddr
# Begin Advertising Data (AD) Structure records:
1C    # AD Structure length: 0x1c = 28 bytes/octets
03    # AD Type: 0x03 (Complete 16-bit service UUID)
03    # Flags: 16-bit service UUID to follow
6F FD # Service UUID: FD6F
17    # Length: 0x17 == 23 bytes/octets
16    # Service Data: 16-bit UUID
6F FD # Service Type: Exposure Notification Service (FD6F)
59 7D E2 8F # 16 bytes Rolling Proximity Identifier
70 28 21 3E # ^ . . .
8B 40 08 AA # ^ . . .
15 81 70 1B # ^ . . .
02 A5 EC 7F # 4 bytes Associated Encrypted Metadata
A9    # RSSI (signal strength) dB: 0xA9 == 169 == -87 signed

What we see reported here is a standard BLE GAP advertising header containing subtype fields and the broadcasting MAC address, followed by one Advertising Data Structure record, ending with the received signal strength. The UUID of the record data matches the Type 0x16, UUID 0xFD6F we were expecting from the spec, and now the actual data contained in the record can be observed. This comprises 16 bytes of encrypted device identifier, plus 4 bytes of encrypted meta data (versioning, power level). Unfortunately, this data is not able to be decrypted without access to the associated health department’s key.

Since the hcitool lescan operation issues a scan-request packet for each received advertising packet, a similar advertising Scan Response packet is also seen from the device, of the same format but with a slightly different header. In this case, the scan-response packet has trivial contents – zero length of payload containing no records:

% hcidump --raw
. . . .
> 04 3E 0C 02 01 04 01 AA BB CC DD EE FF 00 A8
. . . .
# GAP packet header:
04 3E # LE Advertising Report event
0C    # Length of data to follow, 0x0C == 12 bytes/octets
02    # Subevent code for the HCI_LE_Advertising_Report event
01    # Number (of following) Reports: 1
04    # Event Type: Scan Response (SCAN_RSP) tx:public, rx:public
01    # Address Type: Random Device Address
AA BB CC DD EE FF # Address: Random MAC bdaddr
# Begin AD Structure records:
00    # AD Structure length: 0 bytes/octets
A8    # RSSI (signal strength) dB: 0xA9 == 168 == -88 signed

If the smart phone is advertising other services, it is possible that those would be contained in the initial broadcast and the 0xFD6F Exposure Notification Service record would be contained in the scan response packet.

Online resources are also available to decode these raw packets. The site has a convenient way to paste in the data, and returns the following parsing of the above advertising packet:

  "type": "ADVA-48",
  "value": "ffeeddccbbaa",
  "advHeader": {
    "type": "ADV_NONCONNECT_IND",
    "length": 1,
    "txAdd": "public",
    "rxAdd": "public"
  "advData": {
  "complete16BitUUIDs": "7feca5021b708115aa08408b3e2128708fe27d59fd6f1617fd6f03"

To wrap up, by using hcidump in raw output form, the BLE GAP advertising packet and scan response data can be observed and decoded against the public specs to parse the data they emit. In this case, the Apple/Google Exposure Notification System is using the facility to broadcast a 16-byte encrypted Rolling Proximity Identifier in conjunction with a 4-byte encrypted metadata field defined as UUID 0xFD6F (Apple, Inc.) of type 0x16, along with the necessary framing with type and length fields. The encrypted data has no avenues to be decrypted without the health department’s encryption key, which is the intent to prevent ID tracking by peer devices.

GATT and BlueTrace (the Singapore/Australia Model)

The BlueTrace model of digital contract tracing uses BLE GATT, the Generic Attribute Profile. In this form, the smart phone configures GAP as a connection-oriented Peripheral in order to make connections with peer devices, allowing an exchange of GATT attribute lists. The contents of this data involves various Service header declarations before detailing a Characteristic with 128-bit UUID of 0x117bdd58-57ce-4e7a-8e87-7cccdda2a804, containing a JSON record of encrypted unique device ID and other miscellaneous fields.

Since obtaining this GATT Service/Attribute/Characteristic data requires a connection between devices, rather than just an observation of a broadcast, the gatttool application becomes involved to parse and extract this data.

As in the above GAP example, the hcitool lescan operation is invoked to scan for the list of available device addresses to identify the one to make a connection with:

% sudo hcitool lescan
LE Scan . . .
AA:BB:CC:DD:EE:FF (unknown)
AA:BB:CC:DD:EE:FF (unknown)
. . . .

Since this device is running the Singapore TraceTogether app, gatttool can be given its address (“-b “) to attempt a connection for the querying of GATT data. Since the smart phone is most likely using a Random address (see GAP and hcidump above), that condition is flagged to support that connection negotiation (“-t random”), and interactive use is requested (“-I”):

% sudo gatttool -t random -b AA:BB:CC:DD:EE:FF -I

In interactive mode, a connection is manually performed by the connect operation:

[AA:BB:CC:DD:EE:FF][LE]> connect
Attempting to connect to AA:BB:CC:DD:EE:FF
Connection successful

GATT defines a two-step hierarchy out of the ordered list of ATT attributes by defining one attribute type as a Service marker; any attributes occurring after that marker are assumed to be Characteristic attributes of that Service. To examine this list of Services offered by the device, invoke the gatttool primary command:

[AA:BB:CC:DD:EE:FFB][LE]> primary
attr handle: 0x0001, end grp handle: 0x0003 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x0014, end grp handle: 0x001a uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x0028, end grp handle: 0x002e uuid: 594a34fc-31db-11ea-978f-2e728ce88125
attr handle: 0x002f, end grp handle: 0xffff uuid: b82ab3fc-1595-4f6a-80f0-fe094cc218f9

Of particular interest here is the 128-bit UUID 0xb82ab3fc-1595-4f6a-80f0-fe094cc218f9, which reverse engineering of the template app has discovered as the BlueTrace Service ID. (This value was not listed in the BlueTrace spec.)

The list of Characteristic property and value handles can be obtained by the characteristics command:

[AA:BB:CC:DD:EE:FFB][LE]> characteristics
handle: 0x0002, char properties: 0x20, char value handle: 0x0003, uuid: 00002a05-0000-1000-8000-00805f9b34fb
handle: 0x0015, char properties: 0x02, char value handle: 0x0016, uuid: 00002a00-0000-1000-8000-00805f9b34fb
handle: 0x0017, char properties: 0x02, char value handle: 0x0018, uuid: 00002a01-0000-1000-8000-00805f9b34fb
handle: 0x0019, char properties: 0x02, char value handle: 0x001a, uuid: 00002aa6-0000-1000-8000-00805f9b34fb
handle: 0x0029, char properties: 0x28, char value handle: 0x002a, uuid: 594a36e6-31db-11ea-978f-2e728ce88125
handle: 0x002c, char properties: 0x0c, char value handle: 0x002d, uuid: 594a3010-31db-11ea-978f-2e728ce88125
handle: 0x0030, char properties: 0x0a, char value handle: 0x0031, uuid: b82ab3fc-1595-4f6a-80f0-fe094cc218f9
handle: 0x0032, char properties: 0x0a, char value handle: 0x0033, uuid: 117bdd58-57ce-4e7a-8e87-7cccdda2a804

Following the understood BlueTrace Service UUID 0xb82ab3fc-1595-4f6a-80f0-fe094cc218f9 is the single BlueTrace Service Characteristic ID 0x117bdd58-57ce-4e7a-8e87-7cccdda2a804. Again, reverse engineering has shown that this is the key attribute holding the desired identification data.

To read the value associated with this Characteristic attribute, the given shortcut handle can be used with the char-read-hnd command (the full UUID could be used instead with the char-read-uuid command, if desired):

[AA:BB:CC:DD:EE:FF][LE]> char-read-hnd 0x0033
Characteristic value/descriptor: 7b 22 69 64 22 3a 22 33 2b 35 35 58 70 76 58 50 52 34 76 51 4f 49 63 67 6f 44 4f 68 45 65 6c 48 73 55 61 54 75 37 32 6e 44 4b 32 31 52 71 73 78 50 70 79 61 77 39 76 54 33 68 62 78 6e 4f 4e 62 31 57 34 47 78 31 47 4a 33 41 4a 58 71 31 58 45 35 2f 59 31 58 42 70 6b 58 54 43 58 67 3d 3d 22 2c 22 6d 70 22 3a 22 53 4d 2d 47 39 36 30 55 22 2c 22 6f 22 3a 22 53 47 5f 4d 4f 48 22 2c 22 76 22 3a 32 7d

The resulting hexadecimal return values can now be parsed against a spec that defines their format. In the case of BlueTrace, the spec calls for a JSON return packet. Observing that many of the resulting values are within the range of printable ASCII characters, we pipe it into one of many online Hex-to-Ascii websites to examine textually what is returned (in this case was used):


Success! The hexadecimal data here was able to decode into an JSON structure that matches the BlueTrace spec:

  # ID: encrypted, rolling Temporary device ID of the peripheral:
  "id" : "3+55XpvXPR4vQOIcgoDOhEelHsUaTu72nDK21RqsxPpyaw9vT3hbxnONb1W4Gx1GJ3AJXq1XE5/Y1XBpkXTCXg==",
  # MP: device model of the peripheral (Samsung Galaxy S9)
  "mp" : "SM-G960U",
  # O: Issuing organization code (Singapore Government Ministry of Health)
  "o" : "SG_MOH",
  # V: BlueTrace protocol version 2
  "v" : 2

As with the Exposure Notification System ID above, the BlueTrace device ID is encrypted by a key controlled by the health department and cannot be further decoded here.

If desired, gatttool can be run in non-interactive mode by omitting the “-I” flag and providing the documented flag equivalents of the interactive command. In this case, either of these commands will return the same attribute value:

% gatttool -t random -b AA:BB:CC:DD:EE:FF --char-read --handle=0x0033
% gatttool -t random -b AA:BB:CC:DD:EE:FF --char-read --uuid=117bdd58-57ce-4e7a-8e87-7cccdda2a804

The gatttool command can also be used to write characteristic values, although that operation is not needed or documented here for examination of BlueTrace values.

If you are looking for a partner for your digital product design services, please reach out.