blob: e53e60f3892d0b4a4aaef26fecbac47e8511d7c5 [file] [log] [blame]
/*
* Copyright (C) 2022 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.server.nearby.common.bluetooth.fastpair;
import static com.android.server.nearby.common.bluetooth.fastpair.Constants.BLUETOOTH_ADDRESS_LENGTH;
import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC;
import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.DEVICE_ACTION;
import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING;
import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME;
import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DISCOVERABLE;
import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR;
import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.ADDITIONAL_DATA_TYPE_INDEX;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.when;
import android.platform.test.annotations.Presubmit;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import com.android.server.nearby.common.bluetooth.BluetoothException;
import com.android.server.nearby.common.bluetooth.BluetoothGattException;
import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request;
import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
import com.android.server.nearby.intdefs.NearbyEventIntDefs;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.util.Arrays;
/**
* Unit tests for {@link HandshakeHandler}.
*/
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public class HandshakeHandlerTest {
public static final byte[] PUBLIC_KEY =
BaseEncoding.base64().decode(
"d2JTfvfdS6u7LmGfMOmco3C7ra3lW1k17AOly0LrBydDZURacfTY"
+ "IMmo5K1ejfD9e8b6qHsDTNzselhifi10kQ==");
private static final String SEEKER_ADDRESS = "A1:A2:A3:A4:A5:A6";
private static final String PROVIDER_BLE_ADDRESS = "11:22:33:44:55:66";
/**
* The random-resolvable private address (RPA) is sometimes used when advertising over BLE, to
* hide the static public address (otherwise, the Fast Pair device would b
* identifiable/trackable whenever it's BLE advertising).
*/
private static final String RANDOM_PRIVATE_ADDRESS = "BB:BB:BB:BB:BB:1E";
private static final byte[] SHARED_SECRET =
BaseEncoding.base16().decode("0123456789ABCDEF0123456789ABCDEF");
@Mock EventLoggerWrapper mEventLoggerWrapper;
@Mock BluetoothGattConnection mBluetoothGattConnection;
@Mock BluetoothGattConnection.ChangeObserver mChangeObserver;
@Mock private Consumer<Integer> mRescueFromError;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void handshakeGattError_noRetryError_failed() throws BluetoothException {
HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
new HandshakeHandler.KeyBasedPairingRequest.Builder()
.setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
.build();
BluetoothGattException exception =
new BluetoothGattException("Exception for no retry", 257);
when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
GattConnectionManager gattConnectionManager =
createGattConnectionManager(Preferences.builder(), () -> {});
gattConnectionManager.setGattConnection(mBluetoothGattConnection);
when(mBluetoothGattConnection.enableNotification(any(), any()))
.thenReturn(mChangeObserver);
InOrder inOrder = inOrder(mEventLoggerWrapper);
assertThrows(
BluetoothGattException.class,
() ->
getHandshakeHandler(gattConnectionManager, address -> address)
.doHandshakeWithRetryAndSignalLostCheck(
PUBLIC_KEY,
keyBasedPairingRequest,
mRescueFromError));
inOrder.verify(mEventLoggerWrapper).setCurrentEvent(
NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
inOrder.verifyNoMoreInteractions();
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void handshakeGattError_retryAndNoCount_throwException() throws BluetoothException {
HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
new HandshakeHandler.KeyBasedPairingRequest.Builder()
.setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
.build();
BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
GattConnectionManager gattConnectionManager =
createGattConnectionManager(Preferences.builder(), () -> {});
gattConnectionManager.setGattConnection(mBluetoothGattConnection);
when(mBluetoothGattConnection.enableNotification(any(), any()))
.thenReturn(mChangeObserver);
InOrder inOrder = inOrder(mEventLoggerWrapper);
HandshakeHandler.HandshakeException handshakeException =
assertThrows(
HandshakeHandler.HandshakeException.class,
() -> getHandshakeHandler(gattConnectionManager, address -> address)
.doHandshakeWithRetryAndSignalLostCheck(
PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
inOrder.verify(mEventLoggerWrapper)
.setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
inOrder.verify(mEventLoggerWrapper)
.setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
inOrder.verify(mEventLoggerWrapper)
.setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
inOrder.verify(mEventLoggerWrapper)
.setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
inOrder.verifyNoMoreInteractions();
assertThat(handshakeException.getOriginalException()).isEqualTo(exception);
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void handshakeGattError_noRetryOnTimeout_throwException() throws BluetoothException {
HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
new HandshakeHandler.KeyBasedPairingRequest.Builder()
.setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
.build();
BluetoothOperationExecutor.BluetoothOperationTimeoutException exception =
new BluetoothOperationExecutor.BluetoothOperationTimeoutException("Test timeout");
when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
GattConnectionManager gattConnectionManager =
createGattConnectionManager(Preferences.builder(), () -> {});
gattConnectionManager.setGattConnection(mBluetoothGattConnection);
when(mBluetoothGattConnection.enableNotification(any(), any()))
.thenReturn(mChangeObserver);
InOrder inOrder = inOrder(mEventLoggerWrapper);
assertThrows(
HandshakeHandler.HandshakeException.class,
() ->
new HandshakeHandler(
gattConnectionManager,
PROVIDER_BLE_ADDRESS,
Preferences.builder().setRetrySecretHandshakeTimeout(false).build(),
mEventLoggerWrapper,
address -> address)
.doHandshakeWithRetryAndSignalLostCheck(
PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
inOrder.verify(mEventLoggerWrapper)
.setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
inOrder.verifyNoMoreInteractions();
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void handshakeGattError_signalLost() throws BluetoothException {
HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
new HandshakeHandler.KeyBasedPairingRequest.Builder()
.setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
.build();
BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
GattConnectionManager gattConnectionManager =
createGattConnectionManager(Preferences.builder(), () -> {});
gattConnectionManager.setGattConnection(mBluetoothGattConnection);
when(mBluetoothGattConnection.enableNotification(any(), any()))
.thenReturn(mChangeObserver);
InOrder inOrder = inOrder(mEventLoggerWrapper);
SignalLostException signalLostException =
assertThrows(
SignalLostException.class,
() -> getHandshakeHandler(gattConnectionManager, address -> null)
.doHandshakeWithRetryAndSignalLostCheck(
PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
inOrder.verify(mEventLoggerWrapper)
.setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
assertThat(signalLostException).hasCauseThat().isEqualTo(exception);
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void handshakeGattError_addressRotate() throws BluetoothException {
HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
new HandshakeHandler.KeyBasedPairingRequest.Builder()
.setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
.build();
BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
GattConnectionManager gattConnectionManager =
createGattConnectionManager(Preferences.builder(), () -> {});
gattConnectionManager.setGattConnection(mBluetoothGattConnection);
when(mBluetoothGattConnection.enableNotification(any(), any()))
.thenReturn(mChangeObserver);
InOrder inOrder = inOrder(mEventLoggerWrapper);
SignalRotatedException signalRotatedException =
assertThrows(
SignalRotatedException.class,
() -> getHandshakeHandler(
gattConnectionManager, address -> "AA:BB:CC:DD:EE:FF")
.doHandshakeWithRetryAndSignalLostCheck(
PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
inOrder.verify(mEventLoggerWrapper).setCurrentEvent(
NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
assertThat(signalRotatedException.getNewAddress()).isEqualTo("AA:BB:CC:DD:EE:FF");
assertThat(signalRotatedException).hasCauseThat().isEqualTo(exception);
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void constructBytes_setRetroactiveFlag_decodeCorrectly() throws
GeneralSecurityException {
HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
new HandshakeHandler.KeyBasedPairingRequest.Builder()
.setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
.addFlag(REQUEST_RETROACTIVE_PAIR)
.setSeekerPublicAddress(BluetoothAddress.decode(SEEKER_ADDRESS))
.build();
byte[] encryptedRawMessage =
AesEcbSingleBlockEncryption.encrypt(
SHARED_SECRET, keyBasedPairingRequest.getBytes());
HandshakeRequest handshakeRequest =
new HandshakeRequest(SHARED_SECRET, encryptedRawMessage);
assertThat(handshakeRequest.getType())
.isEqualTo(HandshakeRequest.Type.KEY_BASED_PAIRING_REQUEST);
assertThat(handshakeRequest.requestRetroactivePair()).isTrue();
assertThat(handshakeRequest.getVerificationData())
.isEqualTo(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS));
assertThat(handshakeRequest.getSeekerPublicAddress())
.isEqualTo(BluetoothAddress.decode(SEEKER_ADDRESS));
assertThat(handshakeRequest.requestDeviceName()).isFalse();
assertThat(handshakeRequest.requestDiscoverable()).isFalse();
assertThat(handshakeRequest.requestProviderInitialBonding()).isFalse();
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void getTimeout_notOverShortRetryMaxSpentTime_getShort() {
Preferences preferences = Preferences.builder().build();
assertThat(getHandshakeHandler(/* getEnable128BitCustomGattCharacteristicsId= */ true)
.getTimeoutMs(
preferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
- 1))
.isEqualTo(preferences.getSecretHandshakeShortTimeoutMs());
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void getTimeout_overShortRetryMaxSpentTime_getLong() {
Preferences preferences = Preferences.builder().build();
assertThat(getHandshakeHandler(/* getEnable128BitCustomGattCharacteristicsId= */ true)
.getTimeoutMs(
preferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
+ 1))
.isEqualTo(preferences.getSecretHandshakeLongTimeoutMs());
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void getTimeout_retryNotEnabled_getOrigin() {
Preferences preferences = Preferences.builder().build();
assertThat(
new HandshakeHandler(
createGattConnectionManager(Preferences.builder(), () -> {}),
PROVIDER_BLE_ADDRESS,
Preferences.builder()
.setRetryGattConnectionAndSecretHandshake(false).build(),
mEventLoggerWrapper,
/* fastPairSignalChecker= */ null)
.getTimeoutMs(0))
.isEqualTo(Duration.ofSeconds(
preferences.getGattOperationTimeoutSeconds()).toMillis());
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void triggersActionOverBle_notCrash() {
HandshakeHandler.ActionOverBle.Builder actionOverBleBuilder =
new HandshakeHandler.ActionOverBle.Builder()
.addFlag(
Constants.FastPairService.KeyBasedPairingCharacteristic
.ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC)
.setVerificationData(BluetoothAddress.decode(RANDOM_PRIVATE_ADDRESS))
.setAdditionalDataType(AdditionalDataType.PERSONALIZED_NAME)
.setEvent(0, 0)
.setEventAdditionalData(new byte[]{1})
.getThis();
HandshakeHandler.ActionOverBle actionOverBle = actionOverBleBuilder.build();
assertThat(actionOverBle.getBytes().length).isEqualTo(16);
assertThat(
Arrays.equals(
Arrays.copyOfRange(actionOverBle.getBytes(), 0, 12),
new byte[]{
(byte) 16, (byte) -64, (byte) -69, (byte) -69,
(byte) -69, (byte) -69, (byte) -69, (byte) 30,
(byte) 0, (byte) 0, (byte) 1, (byte) 1}))
.isTrue();
}
private GattConnectionManager createGattConnectionManager(
Preferences.Builder prefs, ToggleBluetoothTask toggleBluetooth) {
return new GattConnectionManager(
ApplicationProvider.getApplicationContext(),
prefs.build(),
new EventLoggerWrapper(null),
BluetoothAdapter.getDefaultAdapter(),
toggleBluetooth,
PROVIDER_BLE_ADDRESS,
new TimingLogger("GattConnectionManager", prefs.build()),
/* fastPairSignalChecker= */ null,
/* setMtu= */ false);
}
private HandshakeHandler getHandshakeHandler(
GattConnectionManager gattConnectionManager,
@Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker) {
return new HandshakeHandler(
gattConnectionManager,
PROVIDER_BLE_ADDRESS,
Preferences.builder()
.setGattConnectionAndSecretHandshakeNoRetryGattError(ImmutableSet.of(257))
.setRetrySecretHandshakeTimeout(true)
.build(),
mEventLoggerWrapper,
fastPairSignalChecker);
}
private HandshakeHandler getHandshakeHandler(
boolean getEnable128BitCustomGattCharacteristicsId) {
return new HandshakeHandler(
createGattConnectionManager(Preferences.builder(), () -> {}),
PROVIDER_BLE_ADDRESS,
Preferences.builder()
.setGattOperationTimeoutSeconds(5)
.setEnable128BitCustomGattCharacteristicsId(
getEnable128BitCustomGattCharacteristicsId)
.build(),
mEventLoggerWrapper,
/* fastPairSignalChecker= */ null);
}
private static class HandshakeRequest {
/**
* 16 bytes data: 1-byte for type, 1-byte for flags, 6-bytes for provider's BLE address, 8
* bytes optional data.
*
* @see {go/fast-pair-spec-handshake-message1}
*/
private final byte[] mDecryptedMessage;
HandshakeRequest(byte[] key, byte[] encryptedPairingRequest)
throws GeneralSecurityException {
mDecryptedMessage = AesEcbSingleBlockEncryption.decrypt(key, encryptedPairingRequest);
}
/**
* Gets the type of this handshake request. Currently, we have 2 types: 0x00 for Key-based
* Pairing Request and 0x10 for Action Request.
*/
public Type getType() {
return Type.valueOf(mDecryptedMessage[Request.TYPE_INDEX]);
}
/**
* Gets verification data of this handshake request.
* Currently, we use Provider's BLE address.
*/
public byte[] getVerificationData() {
return Arrays.copyOfRange(
mDecryptedMessage,
Request.VERIFICATION_DATA_INDEX,
Request.VERIFICATION_DATA_INDEX + Request.VERIFICATION_DATA_LENGTH);
}
/** Gets Seeker's public address of the handshake request. */
public byte[] getSeekerPublicAddress() {
return Arrays.copyOfRange(
mDecryptedMessage,
Request.SEEKER_PUBLIC_ADDRESS_INDEX,
Request.SEEKER_PUBLIC_ADDRESS_INDEX + BLUETOOTH_ADDRESS_LENGTH);
}
/** Checks whether the Seeker request discoverability from flags byte. */
public boolean requestDiscoverable() {
return (getFlags() & REQUEST_DISCOVERABLE) != 0;
}
/**
* Checks whether the Seeker requests that the Provider shall initiate bonding from
* flags byte.
*/
public boolean requestProviderInitialBonding() {
return (getFlags() & PROVIDER_INITIATES_BONDING) != 0;
}
/** Checks whether the Seeker requests that the Provider shall notify the existing name. */
public boolean requestDeviceName() {
return (getFlags() & REQUEST_DEVICE_NAME) != 0;
}
/** Checks whether this is for retroactively writing account key. */
public boolean requestRetroactivePair() {
return (getFlags() & REQUEST_RETROACTIVE_PAIR) != 0;
}
/** Gets the flags of this handshake request. */
private byte getFlags() {
return mDecryptedMessage[Request.FLAGS_INDEX];
}
/** Checks whether the Seeker requests a device action. */
public boolean requestDeviceAction() {
return (getFlags() & DEVICE_ACTION) != 0;
}
/**
* Checks whether the Seeker requests an action which will be followed by an additional
* data.
*/
public boolean requestFollowedByAdditionalData() {
return (getFlags() & ADDITIONAL_DATA_CHARACTERISTIC) != 0;
}
/** Gets the {@link AdditionalDataType} of this handshake request. */
@AdditionalDataType
public int getAdditionalDataType() {
if (!requestFollowedByAdditionalData()
|| mDecryptedMessage.length <= ADDITIONAL_DATA_TYPE_INDEX) {
return AdditionalDataType.UNKNOWN;
}
return mDecryptedMessage[ADDITIONAL_DATA_TYPE_INDEX];
}
/** Enumerates the handshake message types. */
public enum Type {
KEY_BASED_PAIRING_REQUEST(Request.TYPE_KEY_BASED_PAIRING_REQUEST),
ACTION_OVER_BLE(Request.TYPE_ACTION_OVER_BLE),
UNKNOWN((byte) 0xFF);
private final byte mValue;
Type(byte type) {
mValue = type;
}
public static Type valueOf(byte value) {
for (Type type : Type.values()) {
if (type.getValue() == value) {
return type;
}
}
return UNKNOWN;
}
public byte getValue() {
return mValue;
}
}
}
}