1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.connecteddevice.util; 18 19 import static com.android.car.connecteddevice.util.SafeLog.logw; 20 21 import android.annotation.NonNull; 22 import android.bluetooth.le.ScanResult; 23 24 import java.math.BigInteger; 25 26 /** 27 * Analyzer of {@link ScanResult} data to identify an Apple device that is advertising from the 28 * background. 29 */ 30 public class ScanDataAnalyzer { 31 32 private static final String TAG = "ScanDataAnalyzer"; 33 34 private static final byte IOS_OVERFLOW_LENGTH = (byte) 0x14; 35 private static final byte IOS_ADVERTISING_TYPE = (byte) 0xff; 36 private static final int IOS_ADVERTISING_TYPE_LENGTH = 1; 37 private static final long IOS_OVERFLOW_CUSTOM_ID = 0x4c0001; 38 private static final int IOS_OVERFLOW_CUSTOM_ID_LENGTH = 3; 39 private static final int IOS_OVERFLOW_CONTENT_LENGTH = 40 IOS_OVERFLOW_LENGTH - IOS_OVERFLOW_CUSTOM_ID_LENGTH - IOS_ADVERTISING_TYPE_LENGTH; 41 ScanDataAnalyzer()42 private ScanDataAnalyzer() { } 43 44 /** 45 * Returns {@code true} if the given bytes from a [ScanResult] contains service UUIDs once the 46 * given serviceUuidMask is applied. 47 * 48 * When an iOS peripheral device goes into a background state, the service UUIDs and other 49 * identifying information are removed from the advertising data and replaced with a hashed 50 * bit in a special "overflow" area. There is no documentation on the layout of this area, 51 * and the below was compiled from experimentation and examples from others who have worked 52 * on reverse engineering iOS background peripherals. 53 * 54 * My best guess is Apple is taking the service UUID and hashing it into a bloom filter. This 55 * would allow any device with the same hashing function to filter for all devices that 56 * might contain the desired service. Since we do not have access to this hashing function, 57 * we must first advertise our service from an iOS device and manually inspect the bit that 58 * is flipped. Once known, it can be passed to serviceUuidMask and used as a filter. 59 * 60 * EXAMPLE 61 * 62 * Foreground contents: 63 * 02011A1107FB349B5F8000008000100000C53A00000709546573746572000000000000000000000000000000000000000000000000000000000000000000 64 * 65 * Background contents: 66 * 02011A14FF4C0001000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000 67 * 68 * The overflow bytes are comprised of four parts: 69 * Length -> 14 70 * Advertising type -> FF 71 * Id custom to Apple -> 4C0001 72 * Contents where hashed values are stored -> 00000000000000000000000000200000 73 * 74 * Apple's documentation on advertising from the background: 75 * https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html#//apple_ref/doc/uid/TP40013257-CH7-SW9 76 * 77 * Other similar reverse engineering: 78 * http://www.pagepinner.com/2014/04/how-to-get-ble-overflow-hash-bit-from.html 79 */ containsUuidsInOverflow(@onNull byte[] scanData, @NonNull BigInteger serviceUuidMask)80 public static boolean containsUuidsInOverflow(@NonNull byte[] scanData, 81 @NonNull BigInteger serviceUuidMask) { 82 byte[] overflowBytes = new byte[IOS_OVERFLOW_CONTENT_LENGTH]; 83 int overflowPtr = 0; 84 int outPtr = 0; 85 try { 86 while (overflowPtr < scanData.length - IOS_OVERFLOW_LENGTH) { 87 byte length = scanData[overflowPtr++]; 88 if (length == 0) { 89 break; 90 } else if (length != IOS_OVERFLOW_LENGTH) { 91 continue; 92 } 93 94 if (scanData[overflowPtr++] != IOS_ADVERTISING_TYPE) { 95 return false; 96 } 97 98 byte[] idBytes = new byte[IOS_OVERFLOW_CUSTOM_ID_LENGTH]; 99 for (int i = 0; i < IOS_OVERFLOW_CUSTOM_ID_LENGTH; i++) { 100 idBytes[i] = scanData[overflowPtr++]; 101 } 102 103 if (!new BigInteger(idBytes).equals(BigInteger.valueOf(IOS_OVERFLOW_CUSTOM_ID))) { 104 return false; 105 } 106 107 for (outPtr = 0; outPtr < IOS_OVERFLOW_CONTENT_LENGTH; outPtr++) { 108 overflowBytes[outPtr] = scanData[overflowPtr++]; 109 } 110 break; 111 } 112 113 if (outPtr == IOS_OVERFLOW_CONTENT_LENGTH) { 114 BigInteger overflowBytesValue = new BigInteger(overflowBytes); 115 return overflowBytesValue.and(serviceUuidMask).signum() == 1; 116 } 117 118 } catch (ArrayIndexOutOfBoundsException e) { 119 logw(TAG, "Inspecting advertisement overflow bytes went out of bounds."); 120 } 121 122 return false; 123 } 124 } 125