blob: fcd6dc54bf2b6ef8ca1b584540b21f210021a56e [file] [log] [blame]
/*
* 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;
}
}