blob: 1118dd6e7314968d598d978803c09e75abdcc78c [file] [log] [blame]
package com.google.android.connecteddevice.trust;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.room.Room;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.connecteddevice.api.Connector;
import com.google.android.connecteddevice.api.FakeConnector;
import com.google.android.connecteddevice.model.ConnectedDevice;
import com.google.android.connecteddevice.trust.api.IOnTrustedDeviceEnrollmentNotificationRequestListener;
import com.google.android.connecteddevice.trust.api.IOnTrustedDevicesRetrievedListener;
import com.google.android.connecteddevice.trust.api.ITrustedDeviceAgentDelegate;
import com.google.android.connecteddevice.trust.api.ITrustedDeviceCallback;
import com.google.android.connecteddevice.trust.api.ITrustedDeviceEnrollmentCallback;
import com.google.android.connecteddevice.trust.api.TrustedDevice;
import com.google.android.connecteddevice.trust.proto.PhoneAuthProto.PhoneCredentials;
import com.google.android.connecteddevice.trust.proto.TrustedDeviceMessageProto.TrustedDeviceMessage;
import com.google.android.connecteddevice.trust.proto.TrustedDeviceMessageProto.TrustedDeviceMessage.MessageType;
import com.google.android.connecteddevice.trust.proto.TrustedDeviceMessageProto.TrustedDeviceState;
import com.google.android.connecteddevice.trust.storage.FeatureStateEntity;
import com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase;
import com.google.android.connecteddevice.trust.storage.TrustedDeviceEntity;
import com.google.android.connecteddevice.trust.storage.TrustedDeviceTokenEntity;
import com.google.android.connecteddevice.util.ByteUtils;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class)
public final class TrustedDeviceManagerTest {
private static final String DEFAULT_DEVICE_ID = UUID.randomUUID().toString();
// Note: This token needs to be of length 8 to be valid.
private static final byte[] FAKE_TOKEN = "12345678".getBytes(UTF_8);
// Note: The value of this state is arbitrary.
private static final byte[] FAKE_STATE = "state".getBytes(UTF_8);
// Note: The value of this handle is arbitrary.
private static final long FAKE_HANDLE = 111L;
// Note: The value of this ID is arbitrary.
private static final int FAKE_USER_ID = 12;
private static final int DEFAULT_USER_ID = ActivityManager.getCurrentUser();
private static final ConnectedDevice SECURE_CONNECTED_DEVICE =
new ConnectedDevice(
DEFAULT_DEVICE_ID,
"secureConnectedDevice",
/* belongsToDriver= */ true,
/* hasSecureChannel= */ true);
private final Connector fakeConnector = spy(new FakeConnector());
@Captor private ArgumentCaptor<List<TrustedDevice>> trustedDeviceListCaptor;
@Mock private ITrustedDeviceEnrollmentCallback enrollmentCallback;
@Mock private ITrustedDeviceCallback trustedDeviceCallback;
@Mock private ITrustedDeviceAgentDelegate trustAgentDelegate;
@Mock private IOnTrustedDevicesRetrievedListener trustedDeviceListener;
@Mock private IOnTrustedDeviceEnrollmentNotificationRequestListener notificationRequestListener;
private TrustedDeviceManager manager;
private TrustedDeviceFeature feature;
private TrustedDeviceDatabase database;
private TrustedDeviceFeature.Callback featureCallback;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
Context context = ApplicationProvider.getApplicationContext();
// Required because ShadowRemoteCallbackList will invoke these methods and requires non-null
// values.
when(enrollmentCallback.asBinder()).thenReturn(mock(IBinder.class));
when(trustedDeviceCallback.asBinder()).thenReturn(mock(IBinder.class));
when(notificationRequestListener.asBinder()).thenReturn(mock(IBinder.class));
database =
Room.inMemoryDatabaseBuilder(context, TrustedDeviceDatabase.class)
.allowMainThreadQueries()
.setQueryExecutor(directExecutor())
.build();
feature = spy(new TrustedDeviceFeature(context, fakeConnector));
manager =
new TrustedDeviceManager(
database,
feature,
/* databaseExecutor= */ directExecutor(),
/* remoteCallbackExecutor= */ directExecutor());
manager.setTrustedDeviceAgentDelegate(trustAgentDelegate);
manager.registerTrustedDeviceEnrollmentCallback(enrollmentCallback);
manager.registerTrustedDeviceCallback(trustedDeviceCallback);
manager.registerTrustedDeviceEnrollmentNotificationRequestListener(notificationRequestListener);
ArgumentCaptor<TrustedDeviceFeature.Callback> captor =
ArgumentCaptor.forClass(TrustedDeviceFeature.Callback.class);
verify(feature).setCallback(captor.capture());
featureCallback = captor.getValue();
}
@After
public void tearDown() {
database.close();
}
@Test
public void testDeviceCanStillConnect_whenNotTrustedDevice() throws RemoteException {
// In this test and next, should populate stateEntity and ensure that
// trustedDeviceFeature.sendMessageSecurely(device, stateEntity.state) get triggered
// (message is sent)
mockNoDevicesConnected();
FeatureStateEntity stateEntity = new FeatureStateEntity(DEFAULT_DEVICE_ID, FAKE_STATE);
database.trustedDeviceDao().addOrReplaceFeatureState(stateEntity);
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
// Make sure message is sent upon successful device connection
verify(feature).sendMessageSecurely(SECURE_CONNECTED_DEVICE, FAKE_STATE);
}
@Test
public void testDeviceRemainsAsTrustedDevice_ifAssociatedWithCurrentUser()
throws RemoteException {
// Insert new phone into database
TrustedDeviceEntity phoneEntity =
new TrustedDeviceEntity(DEFAULT_DEVICE_ID, DEFAULT_USER_ID, FAKE_HANDLE, true);
database.trustedDeviceDao().addOrReplaceTrustedDevice(phoneEntity);
FeatureStateEntity stateEntity = new FeatureStateEntity(DEFAULT_DEVICE_ID, FAKE_STATE);
database.trustedDeviceDao().addOrReplaceFeatureState(stateEntity);
// Run through device connection
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
// Make sure message is sent upon successful device connection
verify(feature).sendMessageSecurely(SECURE_CONNECTED_DEVICE, FAKE_STATE);
}
@Test
public void testDeviceRemovedAsTrustedDevice_ifAssociatedWithOtherUser() throws RemoteException {
// Insert new phone into database with different user ID than default
TrustedDeviceEntity phoneEntity =
new TrustedDeviceEntity(DEFAULT_DEVICE_ID, FAKE_USER_ID, FAKE_HANDLE, true);
database.trustedDeviceDao().addOrReplaceTrustedDevice(phoneEntity);
TrustedDeviceEntity phoneInDb =
database.trustedDeviceDao().getTrustedDeviceIfValid(DEFAULT_DEVICE_ID);
// Make sure phone is in the database
assertThat(phoneInDb).isNotNull();
// onSecureChannelEstablished called here
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
// Grab phone from database now that we connected to different user
phoneInDb = database.trustedDeviceDao().getTrustedDeviceIfValid(DEFAULT_DEVICE_ID);
// Assert that the phone has successfully been removed from database
assertThat(phoneInDb).isNull();
// Code should return before hitting this method in this case.
verify(feature, never()).sendMessageSecurely(SECURE_CONNECTED_DEVICE, FAKE_STATE);
}
@Test
public void testValidEnrollmentFlow_onSecureCar_notifiesCallback() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
TrustedDevice expectedTrustedDevice =
new TrustedDevice(SECURE_CONNECTED_DEVICE.getDeviceId(), DEFAULT_USER_ID, FAKE_HANDLE);
verify(trustedDeviceCallback).onTrustedDeviceAdded(expectedTrustedDevice);
}
@Test
public void testValidEnrollmentFlow_onInsecureCar_notifiesCallback() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnInsecureCar();
TrustedDevice expectedTrustedDevice =
new TrustedDevice(SECURE_CONNECTED_DEVICE.getDeviceId(), DEFAULT_USER_ID, FAKE_HANDLE);
verify(trustedDeviceCallback).onTrustedDeviceAdded(expectedTrustedDevice);
}
@Test
public void testValidEnrollmentFlow_storesTrustedDevice() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
manager.retrieveTrustedDevicesForActiveUser(trustedDeviceListener);
verify(trustedDeviceListener).onTrustedDevicesRetrieved(trustedDeviceListCaptor.capture());
TrustedDevice expectedTrustedDevice =
new TrustedDevice(SECURE_CONNECTED_DEVICE.getDeviceId(), DEFAULT_USER_ID, FAKE_HANDLE);
assertThat(trustedDeviceListCaptor.getValue()).containsExactly(expectedTrustedDevice);
}
@Test
public void enrollment_storesHashedToken() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
TrustedDeviceTokenEntity entity =
database.trustedDeviceDao().getTrustedDeviceHashedToken(DEFAULT_DEVICE_ID);
assertThat(entity).isNotNull();
}
@Test
public void abortEnrollment_addedTokenRemoved() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyTokenAddedInEnrollFlowOnSecureCar();
manager.abortEnrollment();
verify(trustAgentDelegate).removeEscrowToken(FAKE_HANDLE, DEFAULT_USER_ID);
}
@Test
public void testStateSync_removesStoredTrustedDevice() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
manager.featureCallback.onMessageReceived(
SECURE_CONNECTED_DEVICE,
createStateSyncMessage(/* enabled= */ false));
manager.retrieveTrustedDevicesForActiveUser(trustedDeviceListener);
verify(trustedDeviceListener).onTrustedDevicesRetrieved(trustedDeviceListCaptor.capture());
assertThat(trustedDeviceListCaptor.getValue()).isEmpty();
}
@Test
public void testStateSync_ignoresEnabledMessage() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
manager.featureCallback.onMessageReceived(
SECURE_CONNECTED_DEVICE,
createStateSyncMessage(/* enabled= */ true));
manager.retrieveTrustedDevicesForActiveUser(trustedDeviceListener);
verify(trustedDeviceListener).onTrustedDevicesRetrieved(trustedDeviceListCaptor.capture());
TrustedDevice expectedTrustedDevice =
new TrustedDevice(SECURE_CONNECTED_DEVICE.getDeviceId(), DEFAULT_USER_ID, FAKE_HANDLE);
assertThat(trustedDeviceListCaptor.getValue()).containsExactly(expectedTrustedDevice);
}
@Test
public void testStateSync_ignoresStateMessageFromUnenrolledDevice() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
ConnectedDevice unenrolledDevice =
new ConnectedDevice(
"unenrolled", "unenrolled", /* belongsToDriver= */ true, /* hasSecureChannel= */ true);
manager.featureCallback.onMessageReceived(
unenrolledDevice, createStateSyncMessage(/* enabled= */ false));
manager.retrieveTrustedDevicesForActiveUser(trustedDeviceListener);
verify(trustedDeviceListener).onTrustedDevicesRetrieved(trustedDeviceListCaptor.capture());
TrustedDevice expectedTrustedDevice =
new TrustedDevice(SECURE_CONNECTED_DEVICE.getDeviceId(), DEFAULT_USER_ID, FAKE_HANDLE);
// Should not affect the existing trusted device.
assertThat(trustedDeviceListCaptor.getValue()).containsExactly(expectedTrustedDevice);
}
@Test
public void testStateSync_sendsStateMessage() throws RemoteException {
fakeConnector
.getConnectedDevices()
.add(new ConnectedDevice(DEFAULT_DEVICE_ID, null, true, true));
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
TrustedDevice trustedDevice =
new TrustedDevice(DEFAULT_DEVICE_ID, DEFAULT_USER_ID, FAKE_HANDLE);
manager.removeTrustedDevice(trustedDevice);
verify(feature)
.sendMessageSecurely(SECURE_CONNECTED_DEVICE, createStateSyncMessage(/* enabled= */ false));
}
@Test
public void testStateSync_doesNotSendDuplicateStateMessages() throws RemoteException {
when(fakeConnector.getConnectedDeviceById(DEFAULT_DEVICE_ID))
.thenReturn(new ConnectedDevice(DEFAULT_DEVICE_ID, null, true, true));
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
TrustedDevice trustedDevice =
new TrustedDevice(DEFAULT_DEVICE_ID, DEFAULT_USER_ID, FAKE_HANDLE);
manager.removeTrustedDevice(trustedDevice);
verify(feature)
.sendMessageSecurely(SECURE_CONNECTED_DEVICE, createStateSyncMessage(/* enabled= */ false));
mockNoDevicesConnected();
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
// Should not have sent an additional message, so the count remains at 1.
verify(feature)
.sendMessageSecurely(SECURE_CONNECTED_DEVICE, createStateSyncMessage(/* enabled= */ false));
}
@Test
public void testStateSync_savesStateMessageUntilNextConnection() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
TrustedDevice trustedDevice =
new TrustedDevice(DEFAULT_DEVICE_ID, DEFAULT_USER_ID, FAKE_HANDLE);
mockNoDevicesConnected();
manager.removeTrustedDevice(trustedDevice);
verify(feature, never())
.sendMessageSecurely(SECURE_CONNECTED_DEVICE, createStateSyncMessage(/* enabled= */ false));
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
verify(feature)
.sendMessageSecurely(SECURE_CONNECTED_DEVICE, createStateSyncMessage(/* enabled= */ false));
mockNoDevicesConnected();
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
// Should not send an additional message, so the count remains at 1.
verify(feature)
.sendMessageSecurely(SECURE_CONNECTED_DEVICE, createStateSyncMessage(/* enabled= */ false));
}
@Test
public void testRemoveTrustedDevice_trustedDeviceAgentDelegateNotSet() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
TrustedDevice device =
new TrustedDevice(SECURE_CONNECTED_DEVICE.getDeviceId(), DEFAULT_USER_ID, FAKE_HANDLE);
manager.clearTrustedDeviceAgentDelegate(trustAgentDelegate, /* isDeviceSecure= */ false);
List<TrustedDeviceEntity> invalidEntities =
database.trustedDeviceDao().getInvalidTrustedDevicesForUser(DEFAULT_USER_ID);
List<TrustedDeviceEntity> validEntities =
database.trustedDeviceDao().getValidTrustedDevicesForUser(DEFAULT_USER_ID);
TrustedDeviceEntity expectedEntity = new TrustedDeviceEntity(device, /* isValid= */ false);
assertThat(invalidEntities).containsExactly(expectedEntity);
assertThat(validEntities).isEmpty();
verify(trustedDeviceCallback).onTrustedDeviceRemoved(device);
}
@Test
public void testRemoveTrustedDevice_trustedDeviceAgentDelegateSet() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
TrustedDevice device =
new TrustedDevice(SECURE_CONNECTED_DEVICE.getDeviceId(), DEFAULT_USER_ID, FAKE_HANDLE);
manager.removeTrustedDevice(device);
List<TrustedDeviceEntity> invalidEntities =
database.trustedDeviceDao().getInvalidTrustedDevicesForUser(DEFAULT_USER_ID);
List<TrustedDeviceEntity> validEntities =
database.trustedDeviceDao().getValidTrustedDevicesForUser(DEFAULT_USER_ID);
assertThat(invalidEntities).isEmpty();
assertThat(validEntities).isEmpty();
verify(trustedDeviceCallback).onTrustedDeviceRemoved(device);
}
@Test
public void testRemoveEscrowToken_removeEscrowToken() throws RemoteException {
manager.removeEscrowToken(FAKE_HANDLE, DEFAULT_USER_ID);
verify(trustAgentDelegate).removeEscrowToken(eq(FAKE_HANDLE), eq(DEFAULT_USER_ID));
}
@Test
public void testClearTrustedDeviceAgentDelegate_invalidateTrustedDevicesOnLockScreenRemoved()
throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
manager.clearTrustedDeviceAgentDelegate(trustAgentDelegate, /* isDeviceSecure= */ false);
manager.retrieveTrustedDevicesForActiveUser(trustedDeviceListener);
verify(trustedDeviceListener).onTrustedDevicesRetrieved(trustedDeviceListCaptor.capture());
assertThat(trustedDeviceListCaptor.getValue()).isEmpty();
}
@Test
public void testClearTrustedDeviceAgentDelegate_invalidateTrustedDevicesOnLockScreenSet()
throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
manager.clearTrustedDeviceAgentDelegate(trustAgentDelegate, /* isDeviceSecure= */ true);
manager.retrieveTrustedDevicesForActiveUser(trustedDeviceListener);
verify(trustedDeviceListener).onTrustedDevicesRetrieved(trustedDeviceListCaptor.capture());
TrustedDevice expectedTrustedDevice =
new TrustedDevice(SECURE_CONNECTED_DEVICE.getDeviceId(), DEFAULT_USER_ID, FAKE_HANDLE);
assertThat(trustedDeviceListCaptor.getValue()).containsExactly(expectedTrustedDevice);
}
@Test
public void unlock_validTokenPassedToDelegate() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
TrustedDeviceMessage unlockMessage =
TrustedDeviceMessage.newBuilder()
.setType(MessageType.UNLOCK_CREDENTIALS)
.setPayload(
PhoneCredentials.newBuilder()
.setEscrowToken(ByteString.copyFrom(FAKE_TOKEN))
.setHandle(ByteString.copyFrom(ByteUtils.longToBytes(FAKE_HANDLE)))
.build()
.toByteString())
.build();
featureCallback.onMessageReceived(SECURE_CONNECTED_DEVICE, unlockMessage.toByteArray());
verify(trustAgentDelegate).unlockUserWithToken(FAKE_TOKEN, FAKE_HANDLE, DEFAULT_USER_ID);
}
@Test
public void unlock_invalidTokenIsNotPassedToDelegate() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
byte[] incorrectToken = ByteUtils.randomBytes(8);
TrustedDeviceMessage unlockMessage =
TrustedDeviceMessage.newBuilder()
.setType(MessageType.UNLOCK_CREDENTIALS)
.setPayload(
PhoneCredentials.newBuilder()
.setEscrowToken(ByteString.copyFrom(incorrectToken))
.setHandle(ByteString.copyFrom(ByteUtils.longToBytes(FAKE_HANDLE)))
.build()
.toByteString())
.build();
featureCallback.onMessageReceived(SECURE_CONNECTED_DEVICE, unlockMessage.toByteArray());
verify(trustAgentDelegate, never()).unlockUserWithToken(any(), anyLong(), anyInt());
}
@Test
public void unlock_missingHashedTokenIsNotPassedToDelegate() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
database.trustedDeviceDao().removeTrustedDeviceHashedToken(DEFAULT_DEVICE_ID);
TrustedDeviceMessage unlockMessage =
TrustedDeviceMessage.newBuilder()
.setType(MessageType.UNLOCK_CREDENTIALS)
.setPayload(
PhoneCredentials.newBuilder()
.setEscrowToken(ByteString.copyFrom(FAKE_TOKEN))
.setHandle(ByteString.copyFrom(ByteUtils.longToBytes(FAKE_HANDLE)))
.build()
.toByteString())
.build();
featureCallback.onMessageReceived(SECURE_CONNECTED_DEVICE, unlockMessage.toByteArray());
verify(trustAgentDelegate, never()).unlockUserWithToken(any(), anyLong(), anyInt());
}
@Test
public void removeTrustedDevice_removesHashedToken() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
ArgumentCaptor<TrustedDevice> captor = ArgumentCaptor.forClass(TrustedDevice.class);
verify(trustedDeviceCallback).onTrustedDeviceAdded(captor.capture());
TrustedDevice enrolledDevice = captor.getValue();
manager.removeTrustedDevice(enrolledDevice);
TrustedDeviceTokenEntity entity =
database.trustedDeviceDao().getTrustedDeviceHashedToken(DEFAULT_DEVICE_ID);
assertThat(entity).isNull();
}
@Test
public void setTrustedDeviceAgentDelegate_generatesFeatureStateAndRemovesAllInvalidDevices()
throws RemoteException {
String deviceId = UUID.randomUUID().toString();
TrustedDeviceEntity entity =
new TrustedDeviceEntity(
new TrustedDevice(deviceId, DEFAULT_USER_ID, FAKE_HANDLE), /* isValid= */ false);
database.trustedDeviceDao().addOrReplaceTrustedDevice(entity);
TrustedDeviceManager manager =
new TrustedDeviceManager(
database,
feature,
/* databaseExecutor= */ directExecutor(),
/* remoteCallbackExecutor= */ directExecutor());
manager.setTrustedDeviceAgentDelegate(trustAgentDelegate);
assertThat(database.trustedDeviceDao().getFeatureState(deviceId)).isNotNull();
assertThat(database.trustedDeviceDao().getTrustedDevice(deviceId)).isNull();
verify(trustAgentDelegate).removeEscrowToken(FAKE_HANDLE, DEFAULT_USER_ID);
}
@Test
public void testNotifyRemoteEnrollmentCallbacks_callbackNotified() throws RemoteException {
manager.notifyRemoteEnrollmentCallbacks(
callback -> {
try {
callback.onSecureDeviceRequest();
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
});
manager.notifyRemoteEnrollmentCallbacks(
callback -> {
try {
callback.onValidateCredentialsRequest();
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
});
verify(enrollmentCallback).onSecureDeviceRequest();
verify(enrollmentCallback).onValidateCredentialsRequest();
}
@Test
public void testInvalidateTrustedDevice_deviceInvalidated() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlowOnSecureCar();
TrustedDevice testTrustedDevice =
new TrustedDevice(SECURE_CONNECTED_DEVICE.getDeviceId(), DEFAULT_USER_ID, FAKE_HANDLE);
manager.invalidateTrustedDevice(new TrustedDeviceEntity(testTrustedDevice));
manager.retrieveTrustedDevicesForActiveUser(trustedDeviceListener);
verify(trustedDeviceListener).onTrustedDevicesRetrieved(trustedDeviceListCaptor.capture());
assertThat(trustedDeviceListCaptor.getValue()).isEmpty();
}
@Test
public void testGetPendingToken() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyTokenAddedInEnrollFlowOnSecureCar();
assertThat(manager.getPendingToken()).isEqualTo(FAKE_TOKEN);
}
@Test
public void testGetTrustedDeviceDatabase() {
assertThat(manager.getTrustedDeviceDatabase()).isEqualTo(database.trustedDeviceDao());
}
/**
* Runs through adding token flow in enrollment and verifies it is run on a secure car.
*
* <p>At the end of this method, {@link #FAKE_TOKEN} will be added and waiting to be activated for
* {@link #SECURE_CONNECTED_DEVICE}.
*/
private void executeAndVerifyTokenAddedInEnrollFlowOnSecureCar() throws RemoteException {
// First, the phone will send an escrow token to start the enrollment.
manager.featureCallback.onMessageReceived(
SECURE_CONNECTED_DEVICE, createTokenMessage(FAKE_TOKEN));
verify(notificationRequestListener).onTrustedDeviceEnrollmentNotificationRequest();
// The user confirms enrollment through UI on the secure car.
manager.processEnrollment(/* isDeviceSecure= */ true);
verify(trustAgentDelegate).addEscrowToken(FAKE_TOKEN, DEFAULT_USER_ID);
// The system should then let the manager know the add was successful.
manager.onEscrowTokenAdded(DEFAULT_USER_ID, FAKE_HANDLE);
verify(enrollmentCallback).onValidateCredentialsRequest();
}
/**
* Runs through a valid enrollment flow and verifies that the current flow is run on a secure car.
*
* <p>At the end of this method, {@link #SECURE_CONNECTED_DEVICE} will be enrolled and {@link
* #FAKE_TOKEN} will be activated.
*/
private void executeAndVerifyValidEnrollFlowOnSecureCar() throws RemoteException {
// Add escrow token
executeAndVerifyTokenAddedInEnrollFlowOnSecureCar();
// Now notify that the token has been activated by the user entering their credentials.
manager.onEscrowTokenActivated(DEFAULT_USER_ID, FAKE_HANDLE);
}
/**
* Runs through a valid enrollment flow and verifies that the current flow is run on an insecure
* car.
*
* <p>At the end of this method, {@link #SECURE_CONNECTED_DEVICE} will be enrolled and {@link
* #FAKE_TOKEN} will be activated.
*/
private void executeAndVerifyValidEnrollFlowOnInsecureCar() throws RemoteException {
// First, the phone will send an escrow token to start the enrollment.
manager.featureCallback.onMessageReceived(
SECURE_CONNECTED_DEVICE,
createTokenMessage(FAKE_TOKEN));
verify(notificationRequestListener).onTrustedDeviceEnrollmentNotificationRequest();
// The user confirms enrollment through UI on the insecure car.
manager.processEnrollment(/* isDeviceSecure= */ false);
verify(enrollmentCallback).onSecureDeviceRequest();
// The user creates credential and continues enrollment on the secure car.
manager.processEnrollment(/* isDeviceSecure= */ true);
verify(trustAgentDelegate).addEscrowToken(FAKE_TOKEN, DEFAULT_USER_ID);
// The system should then let the manager know the add was successful.
manager.onEscrowTokenAdded(DEFAULT_USER_ID, FAKE_HANDLE);
enrollmentCallback.onValidateCredentialsRequest();
// Now notify that the token has been activated by the user entering their credentials.
manager.onEscrowTokenActivated(DEFAULT_USER_ID, FAKE_HANDLE);
}
private void triggerDeviceConnected(ConnectedDevice device) {
when(fakeConnector.getConnectedDevices()).thenReturn(Arrays.asList(device));
feature.onSecureChannelEstablished(device);
}
private void mockNoDevicesConnected() {
when(fakeConnector.getConnectedDevices()).thenReturn(ImmutableList.of());
}
private static byte[] createTokenMessage(byte[] token) {
return TrustedDeviceMessage.newBuilder()
.setVersion(TrustedDeviceManager.TRUSTED_DEVICE_MESSAGE_VERSION)
.setType(MessageType.ESCROW_TOKEN)
.setPayload(ByteString.copyFrom(token))
.build()
.toByteArray();
}
private static byte[] createStateSyncMessage(boolean enabled) {
TrustedDeviceState state = TrustedDeviceState.newBuilder().setEnabled(enabled).build();
return TrustedDeviceMessage.newBuilder()
.setVersion(TrustedDeviceManager.TRUSTED_DEVICE_MESSAGE_VERSION)
.setType(MessageType.STATE_SYNC)
.setPayload(state.toByteString())
.build()
.toByteArray();
}
}