| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.car.connecteddevice.util; |
| |
| import static com.android.car.connecteddevice.util.SafeLog.logw; |
| |
| import android.bluetooth.le.ScanResult; |
| |
| import androidx.annotation.NonNull; |
| |
| import java.math.BigInteger; |
| |
| /** |
| * Analyzer of {@link ScanResult} data to identify an Apple device that is advertising from the |
| * background. |
| */ |
| public class ScanDataAnalyzer { |
| |
| private static final String TAG = "ScanDataAnalyzer"; |
| |
| private static final byte IOS_OVERFLOW_LENGTH = (byte) 0x14; |
| private static final byte IOS_ADVERTISING_TYPE = (byte) 0xff; |
| private static final int IOS_ADVERTISING_TYPE_LENGTH = 1; |
| private static final long IOS_OVERFLOW_CUSTOM_ID = 0x4c0001; |
| private static final int IOS_OVERFLOW_CUSTOM_ID_LENGTH = 3; |
| private static final int IOS_OVERFLOW_CONTENT_LENGTH = |
| IOS_OVERFLOW_LENGTH - IOS_OVERFLOW_CUSTOM_ID_LENGTH - IOS_ADVERTISING_TYPE_LENGTH; |
| |
| private ScanDataAnalyzer() { } |
| |
| /** |
| * Returns {@code true} if the given bytes from a [ScanResult] contains service UUIDs once the |
| * given serviceUuidMask is applied. |
| * |
| * When an iOS peripheral device goes into a background state, the service UUIDs and other |
| * identifying information are removed from the advertising data and replaced with a hashed |
| * bit in a special "overflow" area. There is no documentation on the layout of this area, |
| * and the below was compiled from experimentation and examples from others who have worked |
| * on reverse engineering iOS background peripherals. |
| * |
| * My best guess is Apple is taking the service UUID and hashing it into a bloom filter. This |
| * would allow any device with the same hashing function to filter for all devices that |
| * might contain the desired service. Since we do not have access to this hashing function, |
| * we must first advertise our service from an iOS device and manually inspect the bit that |
| * is flipped. Once known, it can be passed to serviceUuidMask and used as a filter. |
| * |
| * EXAMPLE |
| * |
| * Foreground contents: |
| * 02011A1107FB349B5F8000008000100000C53A00000709546573746572000000000000000000000000000000000000000000000000000000000000000000 |
| * |
| * Background contents: |
| * 02011A14FF4C0001000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000 |
| * |
| * The overflow bytes are comprised of four parts: |
| * Length -> 14 |
| * Advertising type -> FF |
| * Id custom to Apple -> 4C0001 |
| * Contents where hashed values are stored -> 00000000000000000000000000200000 |
| * |
| * Apple's documentation on advertising from the background: |
| * https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html#//apple_ref/doc/uid/TP40013257-CH7-SW9 |
| * |
| * Other similar reverse engineering: |
| * http://www.pagepinner.com/2014/04/how-to-get-ble-overflow-hash-bit-from.html |
| */ |
| public static boolean containsUuidsInOverflow(@NonNull byte[] scanData, |
| @NonNull BigInteger serviceUuidMask) { |
| byte[] overflowBytes = new byte[IOS_OVERFLOW_CONTENT_LENGTH]; |
| int overflowPtr = 0; |
| int outPtr = 0; |
| try { |
| while (overflowPtr < scanData.length - IOS_OVERFLOW_LENGTH) { |
| byte length = scanData[overflowPtr++]; |
| if (length == 0) { |
| break; |
| } else if (length != IOS_OVERFLOW_LENGTH) { |
| continue; |
| } |
| |
| if (scanData[overflowPtr++] != IOS_ADVERTISING_TYPE) { |
| return false; |
| } |
| |
| byte[] idBytes = new byte[IOS_OVERFLOW_CUSTOM_ID_LENGTH]; |
| for (int i = 0; i < IOS_OVERFLOW_CUSTOM_ID_LENGTH; i++) { |
| idBytes[i] = scanData[overflowPtr++]; |
| } |
| |
| if (!new BigInteger(idBytes).equals(BigInteger.valueOf(IOS_OVERFLOW_CUSTOM_ID))) { |
| return false; |
| } |
| |
| for (outPtr = 0; outPtr < IOS_OVERFLOW_CONTENT_LENGTH; outPtr++) { |
| overflowBytes[outPtr] = scanData[overflowPtr++]; |
| } |
| break; |
| } |
| |
| if (outPtr == IOS_OVERFLOW_CONTENT_LENGTH) { |
| BigInteger overflowBytesValue = new BigInteger(overflowBytes); |
| return overflowBytesValue.and(serviceUuidMask).signum() == 1; |
| } |
| |
| } catch (ArrayIndexOutOfBoundsException e) { |
| logw(TAG, "Inspecting advertisement overflow bytes went out of bounds."); |
| } |
| |
| return false; |
| } |
| } |