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