Move OOB flow to new OobAssociationSecureChannel
Fixes: 158024972
Test: Unit tests and oob association succeeds
Change-Id: I2b3c70d830f9e4d182778c52c0ee187560d2cc22
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ble/AssociationSecureChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/AssociationSecureChannel.java
index a13e690..dbe0701 100644
--- a/connected-device-lib/src/com/android/car/connecteddevice/ble/AssociationSecureChannel.java
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/AssociationSecureChannel.java
@@ -98,33 +98,20 @@
logd(TAG, "Continuing handshake.");
HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
mState = handshakeMessage.getHandshakeState();
-
- switch (mState) {
- case HandshakeState.VERIFICATION_NEEDED:
- String code = handshakeMessage.getVerificationCode();
- if (code == null) {
- loge(TAG, "Unable to get verification code.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
- return;
- }
- processVerificationCode(code);
- return;
- case HandshakeState.OOB_VERIFICATION_NEEDED:
- byte[] oobCode = handshakeMessage.getOobVerificationCode();
- if (oobCode == null) {
- loge(TAG, "Unable to get out of band verification code.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
- return;
- }
- processVerificationCode(new String(oobCode));
- return;
- default:
- loge(TAG, "processHandshakeInProgress: Encountered unexpected handshake state: "
- + mState + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
+ if (mState != HandshakeState.VERIFICATION_NEEDED) {
+ loge(TAG, "processHandshakeInProgress: Encountered unexpected handshake state: "
+ + mState + ".");
+ notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
+ return;
}
-
+ String code = handshakeMessage.getVerificationCode();
+ if (code == null) {
+ loge(TAG, "Unable to get verification code.");
+ notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
+ return;
+ }
+ processVerificationCode(code);
}
private void processVerificationCode(@NonNull String code) {
@@ -217,6 +204,15 @@
sendHandshakeMessage(ByteUtils.uuidToBytes(uniqueId), /* isEncrypted= */ true);
}
+ @HandshakeState
+ int getState() {
+ return mState;
+ }
+
+ void setState(@HandshakeState int state) {
+ mState = state;
+ }
+
/** Listener that will be invoked to display verification code. */
interface ShowVerificationCodeListener {
/**
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBlePeripheralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBlePeripheralManager.java
index ebe05dc..ef69664 100644
--- a/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBlePeripheralManager.java
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBlePeripheralManager.java
@@ -16,11 +16,8 @@
package com.android.car.connecteddevice.ble;
-import static android.car.encryptionrunner.EncryptionRunnerFactory.EncryptionRunnerType.OOB_UKEY2;
-
import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_INVALID_HANDSHAKE;
import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_UNEXPECTED_DISCONNECTION;
-import static com.android.car.connecteddevice.ble.SecureBleChannel.CHANNEL_ERROR_INVALID_HANDSHAKE;
import static com.android.car.connecteddevice.util.SafeLog.logd;
import static com.android.car.connecteddevice.util.SafeLog.loge;
import static com.android.car.connecteddevice.util.SafeLog.logw;
@@ -35,7 +32,6 @@
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
-import android.car.encryptionrunner.EncryptionRunnerFactory;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelUuid;
@@ -48,8 +44,6 @@
import com.android.car.connecteddevice.util.EventLog;
import com.android.internal.annotations.VisibleForTesting;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
import java.time.Duration;
import java.util.Arrays;
import java.util.UUID;
@@ -58,9 +52,6 @@
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-
/**
* Communication manager that allows for targeted connections to a specific device in the car.
*/
@@ -76,8 +67,6 @@
// fails.
private static final long ASSOCIATE_ADVERTISING_DELAY_MS = 10L;
- private static final Duration OOB_CODE_EXCHANGE_TIMEOUT = Duration.ofSeconds(5);
-
private static final UUID CLIENT_CHARACTERISTIC_CONFIG =
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
@@ -125,6 +114,8 @@
private AdvertiseCallback mAdvertiseCallback;
+ private OobConnectionManager mOobConnectionManager;
+
/**
* Initialize a new instance of manager.
*
@@ -212,6 +203,7 @@
mConnectedDevices.clear();
mReconnectDeviceId = null;
mReconnectChallenge = null;
+ mOobConnectionManager = null;
}
/** Attempt to connect to device with provided id. */
@@ -335,86 +327,12 @@
logd(TAG, "Starting out of band association.");
reset();
mAssociationCallback = callback;
+ mOobConnectionManager = oobConnectionManager;
addConnectedDevice(bluetoothDevice, /* isReconnect= */ false, /* isOob= */ true);
BleDevice connectedDevice = getConnectedDevice();
if (connectedDevice == null || connectedDevice.mSecureChannel == null) {
loge(TAG, "Connected device or secure channel are null");
- return;
}
-
- logd(TAG, "Setting out of band verification listener.");
- ((AssociationSecureChannel) connectedDevice.mSecureChannel)
- .setShowVerificationCodeListener(
- code -> {
- logd(TAG, "onShowVerificationCode called for out of band exchange");
- if (!isAssociating()) {
- loge(TAG, "No valid callback for association.");
- return;
- }
-
- performOutOfBandVerification(code.getBytes(), connectedDevice,
- oobConnectionManager);
- });
- }
-
- private void performOutOfBandVerification(byte[] code, BleDevice connectedDevice,
- OobConnectionManager oobConnectionManager) {
- byte[] encryptedCode;
- try {
- encryptedCode = oobConnectionManager.encryptVerificationCode(code);
- } catch (InvalidKeyException | InvalidAlgorithmParameterException
- | IllegalBlockSizeException | BadPaddingException e) {
- loge(TAG, "Encryption failed for verification code exchange.", e);
- connectedDevice.mSecureChannel.notifySecureChannelFailure(
- CHANNEL_ERROR_INVALID_HANDSHAKE);
- return;
- }
-
- Handler handler = new Handler(Looper.getMainLooper());
- SecureBleChannel.Callback oobExchangeCallback = new SecureBleChannel.Callback() {
- @Override
- public void onMessageReceived(DeviceMessage deviceMessage) {
- handler.removeCallbacksAndMessages(null);
- connectedDevice.mSecureChannel.unregisterCallback(this);
- connectedDevice.mSecureChannel.registerCallback(mSecureChannelCallback);
-
- byte[] serverEncryptedCode = deviceMessage.getMessage();
- byte[] decryptedCode;
- try {
- decryptedCode =
- oobConnectionManager.decryptVerificationCode(serverEncryptedCode);
- } catch (InvalidKeyException | InvalidAlgorithmParameterException
- | IllegalBlockSizeException | BadPaddingException e) {
- loge(TAG, "Decryption failed for verification code exchange", e);
- connectedDevice.mSecureChannel.notifySecureChannelFailure(
- CHANNEL_ERROR_INVALID_HANDSHAKE);
- return;
- }
-
- if (!Arrays.equals(code, decryptedCode)) {
- loge(TAG, "Exchanged verification codes don't match. Code is "
- + new String(code) + "and decrypted code is "
- + new String(decryptedCode));
- connectedDevice.mSecureChannel.notifySecureChannelFailure(
- CHANNEL_ERROR_INVALID_HANDSHAKE);
- }
- }
- };
-
- connectedDevice.mSecureChannel.unregisterCallback(mSecureChannelCallback);
- connectedDevice.mSecureChannel.registerCallback(oobExchangeCallback);
-
- handler.postDelayed(() -> {
- connectedDevice.mSecureChannel.unregisterCallback(oobExchangeCallback);
- connectedDevice.mSecureChannel.registerCallback(mSecureChannelCallback);
-
- loge(TAG, "Verification code exchange failed due to a timeout.");
- connectedDevice.mSecureChannel.notifySecureChannelFailure(
- CHANNEL_ERROR_INVALID_HANDSHAKE);
- }, OOB_CODE_EXCHANGE_TIMEOUT.toMillis());
-
- connectedDevice.mSecureChannel.sendHandshakeMessage(encryptedCode,
- /* isEncrypted= */ false);
}
private void attemptAssociationAdvertising(@NonNull String adapterName,
@@ -554,13 +472,12 @@
disconnectWithError("Error occurred in stream: " + exception.getMessage());
});
SecureBleChannel secureChannel;
- // TODO(b/157492943): Define an out of band version of ReconnectSecureChannel
if (isReconnect) {
secureChannel = new ReconnectSecureChannel(secureStream, mStorage, mReconnectDeviceId,
mReconnectChallenge);
} else if (isOob) {
- secureChannel = new AssociationSecureChannel(secureStream, mStorage,
- EncryptionRunnerFactory.newRunner(OOB_UKEY2));
+ secureChannel = new OobAssociationSecureChannel(secureStream, mStorage,
+ mOobConnectionManager);
} else {
secureChannel = new AssociationSecureChannel(secureStream, mStorage);
}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ble/OobAssociationSecureChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/OobAssociationSecureChannel.java
new file mode 100644
index 0000000..d607f0b
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/OobAssociationSecureChannel.java
@@ -0,0 +1,131 @@
+/*
+ * 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.ble;
+
+import static android.car.encryptionrunner.HandshakeMessage.HandshakeState;
+
+import static com.android.car.connecteddevice.util.SafeLog.loge;
+
+import android.annotation.NonNull;
+import android.car.encryptionrunner.EncryptionRunner;
+import android.car.encryptionrunner.EncryptionRunnerFactory;
+import android.car.encryptionrunner.HandshakeException;
+import android.car.encryptionrunner.HandshakeMessage;
+
+import com.android.car.connecteddevice.oob.OobConnectionManager;
+import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+
+/**
+ * A secure channel established with the association flow with an out-of-band verification.
+ */
+class OobAssociationSecureChannel extends AssociationSecureChannel {
+
+ private static final String TAG = "OobAssociationSecureChannel";
+
+ private final OobConnectionManager mOobConnectionManager;
+
+ private byte[] mOobCode;
+
+ OobAssociationSecureChannel(
+ BleDeviceMessageStream stream,
+ ConnectedDeviceStorage storage,
+ OobConnectionManager oobConnectionManager) {
+ this(stream, storage, oobConnectionManager, EncryptionRunnerFactory.newRunner(
+ EncryptionRunnerFactory.EncryptionRunnerType.OOB_UKEY2));
+ }
+
+ OobAssociationSecureChannel(
+ BleDeviceMessageStream stream,
+ ConnectedDeviceStorage storage,
+ OobConnectionManager oobConnectionManager,
+ EncryptionRunner encryptionRunner) {
+ super(stream, storage, encryptionRunner);
+ mOobConnectionManager = oobConnectionManager;
+ }
+
+ @Override
+ void processHandshake(@NonNull byte[] message) throws HandshakeException {
+ switch (getState()) {
+ case HandshakeState.IN_PROGRESS:
+ processHandshakeInProgress(message);
+ break;
+ case HandshakeState.OOB_VERIFICATION_NEEDED:
+ processHandshakeOobVerificationNeeded(message);
+ break;
+ default:
+ super.processHandshake(message);
+ }
+ }
+
+ private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
+ HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
+ setState(handshakeMessage.getHandshakeState());
+ int state = getState();
+ if (state != HandshakeState.OOB_VERIFICATION_NEEDED) {
+ loge(TAG, "processHandshakeInProgress: Encountered unexpected handshake state: "
+ + state + ".");
+ notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
+ return;
+ }
+
+ mOobCode = handshakeMessage.getOobVerificationCode();
+ if (mOobCode == null) {
+ loge(TAG, "Unable to get out of band verification code.");
+ notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
+ return;
+ }
+
+ byte[] encryptedCode;
+ try {
+ encryptedCode = mOobConnectionManager.encryptVerificationCode(mOobCode);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException
+ | IllegalBlockSizeException | BadPaddingException e) {
+ loge(TAG, "Encryption failed for verification code exchange.", e);
+ notifySecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
+ return;
+ }
+
+ sendHandshakeMessage(encryptedCode, /* isEncrypted= */ false);
+ }
+
+ private void processHandshakeOobVerificationNeeded(@NonNull byte[] message) {
+ byte[] decryptedCode;
+ try {
+ decryptedCode = mOobConnectionManager.decryptVerificationCode(message);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException
+ | IllegalBlockSizeException | BadPaddingException e) {
+ loge(TAG, "Decryption failed for verification code exchange", e);
+ notifySecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
+ return;
+ }
+
+ if (!Arrays.equals(mOobCode, decryptedCode)) {
+ loge(TAG, "Exchanged verification codes do not match. Aborting secure channel.");
+ notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
+ return;
+ }
+
+ notifyOutOfBandAccepted();
+ }
+}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/AssociationSecureChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/AssociationSecureChannelTest.java
index d9cab5e..1a4941c 100644
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/AssociationSecureChannelTest.java
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/AssociationSecureChannelTest.java
@@ -119,38 +119,6 @@
}
@Test
- public void testEncryptionHandshake_oobAssociation() throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
- ChannelCallback callbackSpy = spy(new ChannelCallback(semaphore));
- setupAssociationSecureChannel(callbackSpy, EncryptionRunnerFactory::newOobDummyRunner);
- ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
- ArgumentCaptor<DeviceMessage> messageCaptor =
- ArgumentCaptor.forClass(DeviceMessage.class);
-
- initHandshakeMessage();
- verify(mStreamMock).writeMessage(messageCaptor.capture(), any());
- byte[] response = messageCaptor.getValue().getMessage();
- assertThat(response).isEqualTo(DummyEncryptionRunner.INIT_RESPONSE.getBytes());
-
- respondToContinueMessage();
- verify(mShowVerificationCodeListenerMock).showVerificationCode(anyString());
-
- mChannel.notifyOutOfBandAccepted();
- sendDeviceId();
- assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
- verify(callbackSpy).onDeviceIdReceived(deviceIdCaptor.capture());
- verify(mStreamMock, times(2)).writeMessage(messageCaptor.capture(), any());
- byte[] deviceIdMessage = messageCaptor.getValue().getMessage();
- assertThat(deviceIdMessage).isEqualTo(ByteUtils.uuidToBytes(SERVER_DEVICE_ID));
- assertThat(deviceIdCaptor.getValue()).isEqualTo(CLIENT_DEVICE_ID.toString());
- verify(mStorageMock).saveEncryptionKey(eq(CLIENT_DEVICE_ID.toString()), any());
- verify(mStorageMock).saveChallengeSecret(CLIENT_DEVICE_ID.toString(), CLIENT_SECRET);
-
- assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
- verify(callbackSpy).onSecureChannelEstablished();
- }
-
- @Test
public void testEncryptionHandshake_Association_wrongInitHandshakeMessage()
throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/CarBlePeripheralManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/CarBlePeripheralManagerTest.java
index 967caa0..431aa29 100644
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/CarBlePeripheralManagerTest.java
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/CarBlePeripheralManagerTest.java
@@ -16,8 +16,6 @@
package com.android.car.connecteddevice.ble;
-import static com.android.car.connecteddevice.ble.SecureBleChannel.CHANNEL_ERROR_INVALID_HANDSHAKE;
-
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -41,7 +39,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.BleStreamProtos.BleOperationProto;
import com.android.car.connecteddevice.model.AssociatedDevice;
import com.android.car.connecteddevice.oob.OobConnectionManager;
import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
@@ -235,105 +232,6 @@
.stopAdvertising(any(AdvertiseCallback.class));
}
- @Test
- public void startOutOfBandAssociation_success() throws Exception {
- BluetoothDevice bluetoothDevice = BluetoothAdapter.getDefaultAdapter()
- .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
-
- Semaphore semaphore = new Semaphore(0);
- AssociationCallback callback = createAssociationCallback(semaphore);
- mCarBlePeripheralManager.startOutOfBandAssociation(bluetoothDevice,
- mMockOobConnectionManager, callback);
-
- AssociationSecureChannel associationSecureChannel =
- (AssociationSecureChannel) mCarBlePeripheralManager.getConnectedDeviceChannel();
- associationSecureChannel.getShowVerificationCodeListener().showVerificationCode(
- TEST_VERIFICATION_CODE);
-
- ArgumentCaptor<byte[]> codeArgumentCaptor = ArgumentCaptor.forClass(byte[].class);
- verify(mMockOobConnectionManager).encryptVerificationCode(codeArgumentCaptor.capture());
- assertThat(codeArgumentCaptor.getValue()).isEqualTo(TEST_VERIFICATION_CODE.getBytes());
-
- associationSecureChannel.onMessageReceived(
- new DeviceMessage(null, false, TEST_ENCRYPTED_VERIFICATION_CODE.getBytes()),
- BleOperationProto.OperationType.CLIENT_MESSAGE);
-
- verify(mMockOobConnectionManager, timeout(100)).decryptVerificationCode(
- codeArgumentCaptor.capture());
- assertThat(codeArgumentCaptor.getValue()).isEqualTo(
- TEST_ENCRYPTED_VERIFICATION_CODE.getBytes());
-
- SecureBleChannel.Callback channelCallback = associationSecureChannel.getCallback();
- assertThat(channelCallback).isNotNull();
- channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
- channelCallback.onSecureChannelEstablished();
- ArgumentCaptor<AssociatedDevice> deviceCaptor =
- ArgumentCaptor.forClass(AssociatedDevice.class);
- verify(mMockStorage).addAssociatedDeviceForActiveUser(deviceCaptor.capture());
- AssociatedDevice device = deviceCaptor.getValue();
- assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString());
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(callback).onAssociationCompleted(eq(TEST_REMOTE_DEVICE_ID.toString()));
- }
-
- @Test
- public void startOutOfBandAssociation_onInvalidAuthTokenReceived_secureChannelFailure()
- throws Exception {
- when(mMockOobConnectionManager.decryptVerificationCode(
- TEST_ENCRYPTED_VERIFICATION_CODE.getBytes())).thenReturn("invalidCode".getBytes());
- BluetoothDevice bluetoothDevice = BluetoothAdapter.getDefaultAdapter()
- .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
-
- Semaphore semaphore = new Semaphore(0);
- AssociationCallback callback = createAssociationCallback(semaphore);
- mCarBlePeripheralManager.startOutOfBandAssociation(bluetoothDevice,
- mMockOobConnectionManager, callback);
-
- AssociationSecureChannel associationSecureChannel =
- (AssociationSecureChannel) mCarBlePeripheralManager.getConnectedDeviceChannel();
- associationSecureChannel.getShowVerificationCodeListener().showVerificationCode(
- TEST_VERIFICATION_CODE);
-
- ArgumentCaptor<byte[]> codeArgumentCaptor = ArgumentCaptor.forClass(byte[].class);
- verify(mMockOobConnectionManager).encryptVerificationCode(codeArgumentCaptor.capture());
- assertThat(codeArgumentCaptor.getValue()).isEqualTo(TEST_VERIFICATION_CODE.getBytes());
-
- associationSecureChannel.onMessageReceived(
- new DeviceMessage(null, false, TEST_ENCRYPTED_VERIFICATION_CODE.getBytes()),
- BleOperationProto.OperationType.CLIENT_MESSAGE);
-
- verify(mMockOobConnectionManager, timeout(100)).decryptVerificationCode(
- codeArgumentCaptor.capture());
- assertThat(codeArgumentCaptor.getValue()).isEqualTo(
- TEST_ENCRYPTED_VERIFICATION_CODE.getBytes());
-
- assertThat(tryAcquire(semaphore)).isTrue();
- verify(callback).onAssociationError(CHANNEL_ERROR_INVALID_HANDSHAKE);
- }
-
- @Test
- public void startOutOfBandAssociation_onTimeout_secureChannelFailure() throws Exception {
- BluetoothDevice bluetoothDevice = BluetoothAdapter.getDefaultAdapter()
- .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
-
- Semaphore semaphore = new Semaphore(0);
- AssociationCallback callback = createAssociationCallback(semaphore);
- mCarBlePeripheralManager.startOutOfBandAssociation(bluetoothDevice,
- mMockOobConnectionManager, callback);
-
- AssociationSecureChannel associationSecureChannel =
- (AssociationSecureChannel) mCarBlePeripheralManager.getConnectedDeviceChannel();
- associationSecureChannel.getShowVerificationCodeListener().showVerificationCode(
- TEST_VERIFICATION_CODE);
-
- ArgumentCaptor<byte[]> codeArgumentCaptor = ArgumentCaptor.forClass(byte[].class);
- verify(mMockOobConnectionManager).encryptVerificationCode(codeArgumentCaptor.capture());
- assertThat(codeArgumentCaptor.getValue()).isEqualTo(TEST_VERIFICATION_CODE.getBytes());
-
- assertThat(semaphore.tryAcquire(5100, TimeUnit.MILLISECONDS)).isTrue();
- verify(callback).onAssociationError(CHANNEL_ERROR_INVALID_HANDSHAKE);
- }
-
private BlePeripheralManager.Callback startAssociation(AssociationCallback callback,
String deviceName) {
ArgumentCaptor<BlePeripheralManager.Callback> callbackCaptor =
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/OobAssociationSecureChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/OobAssociationSecureChannelTest.java
new file mode 100644
index 0000000..d586dde
--- /dev/null
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/OobAssociationSecureChannelTest.java
@@ -0,0 +1,228 @@
+/*
+ * 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.ble;
+
+import static com.android.car.connecteddevice.BleStreamProtos.BleOperationProto.OperationType;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mockitoSession;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.car.encryptionrunner.DummyEncryptionRunner;
+import android.car.encryptionrunner.EncryptionRunnerFactory;
+
+import com.android.car.connecteddevice.oob.OobConnectionManager;
+import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
+import com.android.car.connecteddevice.util.ByteUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.util.UUID;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+
+public class OobAssociationSecureChannelTest {
+ private static final UUID CLIENT_DEVICE_ID =
+ UUID.fromString("a5645523-3280-410a-90c1-582a6c6f4969");
+
+ private static final UUID SERVER_DEVICE_ID =
+ UUID.fromString("a29f0c74-2014-4b14-ac02-be6ed15b545a");
+
+ private static final byte[] CLIENT_SECRET = ByteUtils.randomBytes(32);
+
+ @Mock
+ private BleDeviceMessageStream mStreamMock;
+
+ @Mock
+ private ConnectedDeviceStorage mStorageMock;
+
+ @Mock
+ private OobConnectionManager mOobConnectionManagerMock;
+
+ private OobAssociationSecureChannel mChannel;
+
+ private BleDeviceMessageStream.MessageReceivedListener mMessageReceivedListener;
+
+ private MockitoSession mMockitoSession;
+
+ @Before
+ public void setUp() {
+ mMockitoSession = mockitoSession()
+ .initMocks(this)
+ .strictness(Strictness.WARN)
+ .startMocking();
+ when(mStorageMock.getUniqueId()).thenReturn(SERVER_DEVICE_ID);
+ }
+
+ @After
+ public void tearDown() {
+ if (mMockitoSession != null) {
+ mMockitoSession.finishMocking();
+ }
+ }
+
+ @Test
+ public void testEncryptionHandshake_oobAssociation() throws InterruptedException {
+ Semaphore semaphore = new Semaphore(0);
+ ChannelCallback
+ callbackSpy = spy(new ChannelCallback(semaphore));
+ setupOobAssociationSecureChannel(callbackSpy);
+ ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor<DeviceMessage> messageCaptor =
+ ArgumentCaptor.forClass(DeviceMessage.class);
+
+ initHandshakeMessage();
+ verify(mStreamMock).writeMessage(messageCaptor.capture(), any());
+ byte[] response = messageCaptor.getValue().getMessage();
+ assertThat(response).isEqualTo(DummyEncryptionRunner.INIT_RESPONSE.getBytes());
+ reset(mStreamMock);
+ respondToContinueMessage();
+ verify(mStreamMock).writeMessage(messageCaptor.capture(), any());
+ byte[] oobCodeResponse = messageCaptor.getValue().getMessage();
+ assertThat(oobCodeResponse).isEqualTo(DummyEncryptionRunner.VERIFICATION_CODE.getBytes());
+ respondToOobCode();
+ sendDeviceId();
+ assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
+ verify(callbackSpy).onDeviceIdReceived(deviceIdCaptor.capture());
+ verify(mStreamMock, times(2)).writeMessage(messageCaptor.capture(), any());
+ byte[] deviceIdMessage = messageCaptor.getValue().getMessage();
+ assertThat(deviceIdMessage).isEqualTo(ByteUtils.uuidToBytes(SERVER_DEVICE_ID));
+ assertThat(deviceIdCaptor.getValue()).isEqualTo(CLIENT_DEVICE_ID.toString());
+ verify(mStorageMock).saveEncryptionKey(eq(CLIENT_DEVICE_ID.toString()), any());
+ verify(mStorageMock).saveChallengeSecret(CLIENT_DEVICE_ID.toString(), CLIENT_SECRET);
+
+ assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
+ verify(callbackSpy).onSecureChannelEstablished();
+ }
+
+ private void setupOobAssociationSecureChannel(ChannelCallback callback) {
+ mChannel = new OobAssociationSecureChannel(mStreamMock, mStorageMock,
+ mOobConnectionManagerMock, EncryptionRunnerFactory.newOobDummyRunner());
+ mChannel.registerCallback(callback);
+ ArgumentCaptor<BleDeviceMessageStream.MessageReceivedListener> listenerCaptor =
+ ArgumentCaptor.forClass(BleDeviceMessageStream.MessageReceivedListener.class);
+ verify(mStreamMock).setMessageReceivedListener(listenerCaptor.capture());
+ mMessageReceivedListener = listenerCaptor.getValue();
+ try {
+ when(mOobConnectionManagerMock.encryptVerificationCode(any()))
+ .thenReturn(DummyEncryptionRunner.VERIFICATION_CODE.getBytes());
+ } catch (InvalidAlgorithmParameterException | BadPaddingException | InvalidKeyException
+ | IllegalBlockSizeException e) {
+ }
+ try {
+ when(mOobConnectionManagerMock.decryptVerificationCode(any()))
+ .thenReturn(DummyEncryptionRunner.VERIFICATION_CODE.getBytes());
+ } catch (InvalidAlgorithmParameterException | BadPaddingException | InvalidKeyException
+ | IllegalBlockSizeException e) {
+ }
+ }
+
+ private void sendDeviceId() {
+ DeviceMessage message = new DeviceMessage(
+ /* recipient= */ null,
+ /* isMessageEncrypted= */ true,
+ ByteUtils.concatByteArrays(ByteUtils.uuidToBytes(CLIENT_DEVICE_ID), CLIENT_SECRET)
+ );
+ mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
+ }
+
+ private void initHandshakeMessage() {
+ DeviceMessage message = new DeviceMessage(
+ /* recipient= */ null,
+ /* isMessageEncrypted= */ false,
+ DummyEncryptionRunner.INIT.getBytes()
+ );
+ mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
+ }
+
+ private void respondToContinueMessage() {
+ DeviceMessage message = new DeviceMessage(
+ /* recipient= */ null,
+ /* isMessageEncrypted= */ false,
+ DummyEncryptionRunner.CLIENT_RESPONSE.getBytes()
+ );
+ mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
+ }
+
+ private void respondToOobCode() {
+ DeviceMessage message = new DeviceMessage(
+ /* recipient= */ null,
+ /* isMessageEncrypted= */ false,
+ DummyEncryptionRunner.VERIFICATION_CODE.getBytes()
+ );
+ mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
+ }
+
+ /**
+ * Add the thread control logic into {@link SecureBleChannel.Callback} only for spy purpose.
+ *
+ * <p>The callback will release the semaphore which hold by one test when this callback
+ * is called, telling the test that it can verify certain behaviors which will only occurred
+ * after the callback is notified. This is needed mainly because of the callback is notified
+ * in a different thread.
+ */
+ private static class ChannelCallback implements SecureBleChannel.Callback {
+ private final Semaphore mSemaphore;
+
+ ChannelCallback(Semaphore semaphore) {
+ mSemaphore = semaphore;
+ }
+
+ @Override
+ public void onSecureChannelEstablished() {
+ mSemaphore.release();
+ }
+
+ @Override
+ public void onEstablishSecureChannelFailure(int error) {
+ mSemaphore.release();
+ }
+
+ @Override
+ public void onMessageReceived(DeviceMessage deviceMessage) {
+ mSemaphore.release();
+ }
+
+ @Override
+ public void onMessageReceivedError(Exception exception) {
+ mSemaphore.release();
+ }
+
+ @Override
+ public void onDeviceIdReceived(String deviceId) {
+ mSemaphore.release();
+ }
+ }
+}