The last couples of days I looked a bit into Bluetooth, and then I wondered if it is possible to scan or "sniff" for Bluetooth packages that were send out by an phone which has the Corona Warn App installed.  Yes it's possible and quite east, and no you can't track anyone with it, because the contact tracing protocol is in my opinion well made.

For that I used an ESP32 to look for all Bluetooth packages that contain a specific identifier. Every phone with an Contact-Tracing-App installed sends out those packages.

With the ESP32 you receive all the packages in your vicinity. I used an Bluetooth Low Energy library. After that I changed the BLE_scan.ino a little bit and I came up with this:

Advertised Device: Address: 3e:fd:69:FF:FF:FF, serviceUUID: 0000fd6f 0000-0000-0000-000000000000
Advertised Device: Address: 4f:55:16:FF:FF:FF, manufacturer data: xxx, txPower: 12
Advertised Device: Address: 50:de:ec:FF:FF:FF, manufacturer data: xxx
Advertised Device: Address: 43:dc:e8:FF:FF:FF, manufacturer data: ,txPower: 12
Advertised Device: Address: 78:bd:bc:FF:FF:FF, manufacturer data: xxx
Advertised Device: Address: 64:24:fb:FF:FF:FF, serviceUUID: 0000fd6f 0000-0000-0000-000000000000

The above output was from an scan at my apartment. The interesting thing you see here is the device with the serviceUUID = 0xfd6f.

If you look at the Contact Tracing Bluetooth Specification from Apple:

Complete 16-bit Service UUID Section: the UUID is 0xFD6F, and shall precede the Service Data section.

+-----------------------+
| Flags                 |
+--------+------+-------+
| Length | Type | Flags |
| 0x02   | 0x01 | 0x1A  |
+--------+------+-------+

+-----------------------------+
| Complete 16-bit ServiceUUID |
+-----------------------------+
| Length | Type | ServiceUUID |
| 0x03   | 0x03 | 0xFD6F      |
+-----------------------------+

+------------------------------------------------------------+
| Service Data 16-bit UUID                                   |
+--------+------+-------------+------------------------------+
| Length | Type | ServiceData | 16 Bytes                     |
| 0x13   | 0x16 | 0xFD6F      | Rolling Proximity Identifier |
+------------------------------------------------------------+

Every BLE device has such an identifier. For example my indoor rower also has one:

Advertised Device: Name: PM5 430000000, Address: de:6b:cb:FF:FF:FF, appearance: 0, serviceUUID: 00001826-0000-0000-0000-000000000, txPower: 4

in this case: 0x1826

For the ESP32 I used this library:  https://github.com/nkolban/ESP32_BLE_Arduino. It includes an scanner example. I changed it a little bit so I only get devices with the Contact Tracing Identifier.

Advertised Device: Name: , Address: 09:1c:ed:f3:f1:ca, serviceUUID: 0000fd6f-0000-1000-8000-00805f9b34fb 
Advertised Device: Name: , Address: 09:1c:ed:f3:f1:ca, serviceUUID: 0000fd6f-0000-1000-8000-00805f9b34fb 
Advertised Device: Name: , Address: 09:1c:ed:f3:f1:ca, serviceUUID: 0000fd6f-0000-1000-8000-00805f9b34fb 
Advertised Device: Name: , Address: 09:1c:ed:f3:f1:ca, serviceUUID: 0000fd6f-0000-1000-8000-00805f9b34fb 
Advertised Device: Name: , Address: 09:1c:ed:f3:f1:ca, serviceUUID: 0000fd6f-0000-1000-8000-00805f9b34fb 
Advertised Device: Name: , Address: 09:1c:ed:f3:f1:ca, serviceUUID: 0000fd6f-0000-1000-8000-00805f9b34fb

Randomized Bluetooth MAC Address on iOS

The next thing I was wondering, if the Bluetooth MAC Address gets randomized. First I checked if the data was really from my iPhone. I turned Bluetooth off, activated flight mode. And indeed it was my iPhone. But it wasn't my real MAC Address.

Randomized MAC Address: 09:1c:ed:f3:f1:ca.

Then I restarted my iPhone and I got: 35:e5:53:73:ae:6d. (My real MAC Address begins with the Apple Identifier: E4:E4:AB)

So in iOS 13.5.1 the Bluetooth MAC Address gets randomized. I'm really glad :)

The MAC Address gets changed over time:

Advertised Device: Name: , Address: 29:34:e1:f3:2e:54, serviceUUID: 0000fd6f-0000-1000-8000-00805f9b34fb 
Advertised Device: Name: , Address: 0b:7e:c1:c2:05:74, serviceUUID: 0000fd6f-0000-1000-8000-00805f9b34fb 
The advertiser address and RollingProximityIdentifier shall be changed synchronously so address and RollingProximityIdentifier can not be linked.

Still have to check the claim above but everything looks fine so far.

Arduino Sketch:

#include <Arduino.h>

#include <LinkedList.h>

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

#include <string>

int scanTime = 5;
BLEScan* pBLEScan;
#define CONTACT_TRACING_UUID 0xFD6F

LinkedList<std::string> list{};
LinkedList<std::string> dev_list{};

unsigned long last_millis = 0;

void add_unique(LinkedList<std::string> &list, std::string str) {
  bool can_add = true;
  for(size_t i = 0; i < list.size(); i++) {
    if(strcmp(list.get(i).c_str(), str.c_str()) == 0) {
      can_add = false;
      break;
    }
  }
  if(can_add) {
    list.add(str);
  }
}

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      if(advertisedDevice.haveServiceUUID()) {
        BLEUUID serviceUUID = advertisedDevice.getServiceUUID();       
        if(serviceUUID.bitSize() == 16) {
          esp_bt_uuid_t* raw_uuid_p = serviceUUID.getNative();
          uint16_t uuid = raw_uuid_p->uuid.uuid16;
          if(uuid == CONTACT_TRACING_UUID) {
            BLEAddress addr = advertisedDevice.getAddress();
            Serial.printf("Advertised Device: %s ", advertisedDevice.toString().c_str());
            add_unique(list, addr.toString());
          }
        }
      }
      add_unique(dev_list, advertisedDevice.getAddress().toString());
    }
};

void setup() { 
  Serial.begin(115200);
  Serial.println("setup...");
  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->setWindow(99);
}

void print_list(LinkedList<std::string> &list) {
  //print list
  Serial.print("{");
  for(size_t i = 0; i < list.size(); i++) {    
    Serial.print(list.get(i).c_str());
    if(i != list.size()-1 && list.size() > 1) {
      Serial.print(",");
    }
  }
  Serial.print("}");
}

void loop() {
  BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
  pBLEScan->clearResults();
  Serial.print("Free Heap: ");
  Serial.println(ESP.getFreeHeap());
  Serial.print(list.size());
  Serial.print("/");
  Serial.print(dev_list.size());
  Serial.println(" using Corona-Warn-App.");

  Serial.print("{");
  print_list(list);
  Serial.print(",");
  print_list(dev_list);
  Serial.println("}");
  
  if(list.size() > 100) {
    list.pop();
  }
  if(dev_list.size() > 100) {
    dev_list.pop();
  }

  unsigned long now = millis();
  if(now > (last_millis + 30*1000)) {
    last_millis = now;
    list.clear();
    dev_list.clear();
  }
}

If you run this sketch on an ESP32 it will show the devices (up to a 100) which are running the Corona-Warn-App compared to ALL other Bluetooth devices. Including Tablets, Laptops, Smartwatches, Coffee-Machines... Bluetooth is everywhere ;)

Sample output:

1/5 using Corona-Warn-App.
{{0a:a6:c9:cd:91:ce},{50:de:ec:2c:db:51,54:ce:2e:b7:23:27,0a:a6:c9:cd:91:ce,60:ba:1d:98:a9:2d,78:bd:bc:9a:bf:a8}}
Advertised Device: Name: , Address: 0a:a6:c9:cd:91:ce, serviceUUID: 0000fd6f-0000-1000-8000-00805f9b34fb

It resets the device count every 30 seconds so you get the devices from about ~6 full scans. Sometimes a device will change the MAC Address in the 30 seconds window. It that happens you'll get a misreading. Worst case: For a short time double the devices that are actually there. But those spikes can be filtered out.

ToDo

So tomorrow maybe I'll solder an OLED on top of my ESP32. I'm using an ESP32 where you can put an 18650 cell on the back.

  • OLED Display
  • SD-Card logging

Update (27.Juli.2020):

I've added an OLED-Display