| /* |
| * 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.server.biometrics; |
| |
| import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; |
| import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; |
| import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_NEGATIVE; |
| |
| import static com.android.server.biometrics.BiometricServiceStateProto.*; |
| |
| import static junit.framework.Assert.assertEquals; |
| import static junit.framework.Assert.assertFalse; |
| import static junit.framework.Assert.assertTrue; |
| |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyBoolean; |
| 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.verify; |
| import static org.mockito.Mockito.when; |
| |
| import android.annotation.NonNull; |
| import android.app.admin.DevicePolicyManager; |
| import android.app.trust.ITrustManager; |
| import android.content.Context; |
| import android.hardware.biometrics.BiometricManager.Authenticators; |
| import android.hardware.biometrics.ComponentInfoInternal; |
| import android.hardware.biometrics.IBiometricAuthenticator; |
| import android.hardware.biometrics.IBiometricSensorReceiver; |
| import android.hardware.biometrics.IBiometricServiceReceiver; |
| import android.hardware.biometrics.IBiometricSysuiReceiver; |
| import android.hardware.biometrics.PromptInfo; |
| import android.hardware.biometrics.SensorProperties; |
| import android.hardware.fingerprint.FingerprintSensorProperties; |
| import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; |
| import android.os.Binder; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.platform.test.annotations.Presubmit; |
| import android.security.KeyStore; |
| |
| import androidx.test.filters.SmallTest; |
| |
| import com.android.internal.statusbar.IStatusBarService; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Random; |
| import java.util.function.Consumer; |
| |
| @Presubmit |
| @SmallTest |
| public class AuthSessionTest { |
| |
| private static final String TEST_PACKAGE = "test_package"; |
| private static final long TEST_REQUEST_ID = 22; |
| |
| @Mock private Context mContext; |
| @Mock private ITrustManager mTrustManager; |
| @Mock private DevicePolicyManager mDevicePolicyManager; |
| @Mock private BiometricService.SettingObserver mSettingObserver; |
| @Mock private IBiometricSensorReceiver mSensorReceiver; |
| @Mock private IBiometricServiceReceiver mClientReceiver; |
| @Mock private IStatusBarService mStatusBarService; |
| @Mock private IBiometricSysuiReceiver mSysuiReceiver; |
| @Mock private KeyStore mKeyStore; |
| @Mock private AuthSession.ClientDeathReceiver mClientDeathReceiver; |
| |
| private Random mRandom; |
| private IBinder mToken; |
| |
| // Assume all tests can be done with the same set of sensors for now. |
| @NonNull private List<BiometricSensor> mSensors; |
| @NonNull private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProps; |
| |
| @Before |
| public void setUp() throws Exception { |
| MockitoAnnotations.initMocks(this); |
| when(mClientReceiver.asBinder()).thenReturn(mock(Binder.class)); |
| mRandom = new Random(); |
| mToken = new Binder(); |
| mSensors = new ArrayList<>(); |
| mFingerprintSensorProps = new ArrayList<>(); |
| } |
| |
| @Test |
| public void testNewAuthSession_eligibleSensorsSetToStateUnknown() throws RemoteException { |
| setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_REAR); |
| setupFace(1 /* id */, false /* confirmationAlwaysRequired */, |
| mock(IBiometricAuthenticator.class)); |
| |
| final AuthSession session = createAuthSession(mSensors, |
| false /* checkDevicePolicyManager */, |
| Authenticators.BIOMETRIC_STRONG, |
| TEST_REQUEST_ID, |
| 0 /* operationId */, |
| 0 /* userId */); |
| |
| for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { |
| assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState()); |
| } |
| } |
| |
| @Test |
| public void testStartNewAuthSession() throws RemoteException { |
| setupFace(0 /* id */, false /* confirmationAlwaysRequired */, |
| mock(IBiometricAuthenticator.class)); |
| setupFingerprint(1 /* id */, FingerprintSensorProperties.TYPE_REAR); |
| |
| final boolean requireConfirmation = true; |
| final long operationId = 123; |
| final int userId = 10; |
| |
| final AuthSession session = createAuthSession(mSensors, |
| false /* checkDevicePolicyManager */, |
| Authenticators.BIOMETRIC_STRONG, |
| TEST_REQUEST_ID, |
| operationId, |
| userId); |
| assertEquals(mSensors.size(), session.mPreAuthInfo.eligibleSensors.size()); |
| |
| for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { |
| assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState()); |
| assertEquals(0, sensor.getCookie()); |
| } |
| |
| session.goToInitialState(); |
| for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { |
| assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState()); |
| assertTrue("Cookie must be >0", sensor.getCookie() > 0); |
| verify(sensor.impl).prepareForAuthentication( |
| eq(sensor.confirmationSupported() && requireConfirmation), |
| eq(mToken), |
| eq(operationId), |
| eq(userId), |
| eq(mSensorReceiver), |
| eq(TEST_PACKAGE), |
| eq(TEST_REQUEST_ID), |
| eq(sensor.getCookie()), |
| anyBoolean() /* allowBackgroundAuthentication */); |
| } |
| |
| final int cookie1 = session.mPreAuthInfo.eligibleSensors.get(0).getCookie(); |
| session.onCookieReceived(cookie1); |
| for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { |
| if (cookie1 == sensor.getCookie()) { |
| assertEquals(BiometricSensor.STATE_COOKIE_RETURNED, sensor.getSensorState()); |
| } else { |
| assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState()); |
| } |
| } |
| assertFalse(session.allCookiesReceived()); |
| |
| final int cookie2 = session.mPreAuthInfo.eligibleSensors.get(1).getCookie(); |
| session.onCookieReceived(cookie2); |
| assertTrue(session.allCookiesReceived()); |
| |
| |
| // for multi-sensor face then fingerprint is the default policy |
| for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { |
| if (sensor.modality == TYPE_FACE) { |
| verify(sensor.impl).startPreparedClient(eq(sensor.getCookie())); |
| assertEquals(BiometricSensor.STATE_AUTHENTICATING, sensor.getSensorState()); |
| } else if (sensor.modality == TYPE_FINGERPRINT) { |
| assertEquals(BiometricSensor.STATE_COOKIE_RETURNED, sensor.getSensorState()); |
| } |
| } |
| } |
| |
| @Test |
| public void testCancelReducesAppetiteForCookies() throws Exception { |
| setupFace(0 /* id */, false /* confirmationAlwaysRequired */, |
| mock(IBiometricAuthenticator.class)); |
| setupFingerprint(1 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); |
| |
| final AuthSession session = createAuthSession(mSensors, |
| false /* checkDevicePolicyManager */, |
| Authenticators.BIOMETRIC_STRONG, |
| TEST_REQUEST_ID, |
| 44 /* operationId */, |
| 2 /* userId */); |
| |
| session.goToInitialState(); |
| |
| for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { |
| assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState()); |
| } |
| |
| session.onCancelAuthSession(false /* force */); |
| |
| for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { |
| session.onCookieReceived(sensor.getCookie()); |
| assertEquals(BiometricSensor.STATE_CANCELING, sensor.getSensorState()); |
| } |
| } |
| |
| @Test |
| public void testMultiAuth_singleSensor_fingerprintSensorStartsAfterDialogAnimationCompletes() |
| throws Exception { |
| setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); |
| testMultiAuth_fingerprintSensorStartsAfterUINotifies(); |
| } |
| |
| @Test |
| public void testMultiAuth_fingerprintSensorStartsAfterDialogAnimationCompletes() |
| throws Exception { |
| setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); |
| setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class)); |
| testMultiAuth_fingerprintSensorStartsAfterUINotifies(); |
| } |
| |
| public void testMultiAuth_fingerprintSensorStartsAfterUINotifies() |
| throws Exception { |
| final long operationId = 123; |
| final int userId = 10; |
| final int fingerprintSensorId = mSensors.stream() |
| .filter(s -> s.modality == TYPE_FINGERPRINT) |
| .map(s -> s.id) |
| .findFirst() |
| .orElse(-1); |
| |
| final AuthSession session = createAuthSession(mSensors, |
| false /* checkDevicePolicyManager */, |
| Authenticators.BIOMETRIC_STRONG, |
| TEST_REQUEST_ID, |
| operationId, |
| userId); |
| assertEquals(mSensors.size(), session.mPreAuthInfo.eligibleSensors.size()); |
| |
| for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { |
| assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState()); |
| assertEquals(0, sensor.getCookie()); |
| } |
| |
| session.goToInitialState(); |
| |
| for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { |
| assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState()); |
| session.onCookieReceived( |
| session.mPreAuthInfo.eligibleSensors.get(sensor.id).getCookie()); |
| if (fingerprintSensorId == sensor.id) { |
| assertEquals(BiometricSensor.STATE_COOKIE_RETURNED, sensor.getSensorState()); |
| } else { |
| assertEquals(BiometricSensor.STATE_AUTHENTICATING, sensor.getSensorState()); |
| } |
| } |
| assertTrue(session.allCookiesReceived()); |
| |
| // fingerprint sensor does not start even if all cookies are received |
| assertEquals(STATE_AUTH_STARTED, session.getState()); |
| verify(mStatusBarService).showAuthenticationDialog(any(), any(), any(), |
| anyBoolean(), anyBoolean(), anyInt(), anyLong(), any(), anyLong(), anyInt()); |
| |
| // Notify AuthSession that the UI is shown. Then, fingerprint sensor should be started. |
| session.onDialogAnimatedIn(); |
| assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); |
| assertEquals(BiometricSensor.STATE_AUTHENTICATING, |
| session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState()); |
| } |
| |
| @Test |
| public void testCancelAuthentication_whenStateAuthCalled_invokesCancel() |
| throws RemoteException { |
| testInvokesCancel(session -> session.onCancelAuthSession(false /* force */)); |
| } |
| |
| @Test |
| public void testCancelAuthentication_whenStateAuthForcedCalled_invokesCancel() |
| throws RemoteException { |
| testInvokesCancel(session -> session.onCancelAuthSession(true /* force */)); |
| } |
| |
| @Test |
| public void testCancelAuthentication_whenDialogDismissed() throws RemoteException { |
| testInvokesCancel(session -> session.onDialogDismissed(DISMISSED_REASON_NEGATIVE, null)); |
| } |
| |
| // TODO (b/208484275) : Enable these tests |
| // @Test |
| // public void testPreAuth_canAuthAndPrivacyDisabled() throws Exception { |
| // SensorPrivacyManager manager = ExtendedMockito.mock(SensorPrivacyManager.class); |
| // when(manager |
| // .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, anyInt())) |
| // .thenReturn(false); |
| // when(mContext.getSystemService(SensorPrivacyManager.class)) |
| // .thenReturn(manager); |
| // setupFace(1 /* id */, false /* confirmationAlwaysRequired */, |
| // mock(IBiometricAuthenticator.class)); |
| // final PromptInfo promptInfo = createPromptInfo(Authenticators.BIOMETRIC_STRONG); |
| // final PreAuthInfo preAuthInfo = createPreAuthInfo(mSensors, 0, promptInfo, false); |
| // assertEquals(BiometricManager.BIOMETRIC_SUCCESS, preAuthInfo.getCanAuthenticateResult()); |
| // for (BiometricSensor sensor : preAuthInfo.eligibleSensors) { |
| // assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState()); |
| // } |
| // } |
| |
| // @Test |
| // public void testPreAuth_cannotAuthAndPrivacyEnabled() throws Exception { |
| // SensorPrivacyManager manager = ExtendedMockito.mock(SensorPrivacyManager.class); |
| // when(manager |
| // .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, anyInt())) |
| // .thenReturn(true); |
| // when(mContext.getSystemService(SensorPrivacyManager.class)) |
| // .thenReturn(manager); |
| // setupFace(1 /* id */, false /* confirmationAlwaysRequired */, |
| // mock(IBiometricAuthenticator.class)); |
| // final PromptInfo promptInfo = createPromptInfo(Authenticators.BIOMETRIC_STRONG); |
| // final PreAuthInfo preAuthInfo = createPreAuthInfo(mSensors, 0, promptInfo, false); |
| // assertEquals(BiometricManager.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED, |
| // preAuthInfo.getCanAuthenticateResult()); |
| // // Even though canAuth returns privacy enabled, we should still be able to authenticate. |
| // for (BiometricSensor sensor : preAuthInfo.eligibleSensors) { |
| // assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState()); |
| // } |
| // } |
| |
| // @Test |
| // public void testPreAuth_canAuthAndPrivacyEnabledCredentialEnabled() throws Exception { |
| // SensorPrivacyManager manager = ExtendedMockito.mock(SensorPrivacyManager.class); |
| // when(manager |
| // .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, anyInt())) |
| // .thenReturn(true); |
| // when(mContext.getSystemService(SensorPrivacyManager.class)) |
| // .thenReturn(manager); |
| // setupFace(1 /* id */, false /* confirmationAlwaysRequired */, |
| // mock(IBiometricAuthenticator.class)); |
| // final PromptInfo promptInfo = |
| // createPromptInfo(Authenticators.BIOMETRIC_STRONG |
| // | Authenticators. DEVICE_CREDENTIAL); |
| // final PreAuthInfo preAuthInfo = createPreAuthInfo(mSensors, 0, promptInfo, false); |
| // assertEquals(BiometricManager.BIOMETRIC_SUCCESS, preAuthInfo.getCanAuthenticateResult()); |
| // for (BiometricSensor sensor : preAuthInfo.eligibleSensors) { |
| // assertEquals(BiometricSensor.STATE_UNKNOWN, sensor.getSensorState()); |
| // } |
| // } |
| |
| private void testInvokesCancel(Consumer<AuthSession> sessionConsumer) throws RemoteException { |
| final IBiometricAuthenticator faceAuthenticator = mock(IBiometricAuthenticator.class); |
| |
| setupFace(0 /* id */, false /* confirmationAlwaysRequired */, faceAuthenticator); |
| final AuthSession session = createAuthSession(mSensors, |
| false /* checkDevicePolicyManager */, |
| Authenticators.BIOMETRIC_STRONG, |
| TEST_REQUEST_ID, |
| 0 /* operationId */, |
| 0 /* userId */); |
| |
| session.goToInitialState(); |
| assertEquals(STATE_AUTH_CALLED, session.getState()); |
| |
| sessionConsumer.accept(session); |
| |
| verify(faceAuthenticator).cancelAuthenticationFromService( |
| eq(mToken), eq(TEST_PACKAGE), eq(TEST_REQUEST_ID)); |
| } |
| |
| private PreAuthInfo createPreAuthInfo(List<BiometricSensor> sensors, int userId, |
| PromptInfo promptInfo, boolean checkDevicePolicyManager) throws RemoteException { |
| return PreAuthInfo.create(mTrustManager, |
| mDevicePolicyManager, |
| mSettingObserver, |
| sensors, |
| userId, |
| promptInfo, |
| TEST_PACKAGE, |
| checkDevicePolicyManager, |
| mContext); |
| } |
| |
| private AuthSession createAuthSession(List<BiometricSensor> sensors, |
| boolean checkDevicePolicyManager, @Authenticators.Types int authenticators, |
| long requestId, long operationId, int userId) throws RemoteException { |
| |
| final PromptInfo promptInfo = createPromptInfo(authenticators); |
| |
| final PreAuthInfo preAuthInfo = createPreAuthInfo(sensors, userId, promptInfo, |
| checkDevicePolicyManager); |
| return new AuthSession(mContext, mStatusBarService, mSysuiReceiver, mKeyStore, |
| mRandom, mClientDeathReceiver, preAuthInfo, mToken, requestId, operationId, userId, |
| mSensorReceiver, mClientReceiver, TEST_PACKAGE, promptInfo, |
| false /* debugEnabled */, mFingerprintSensorProps); |
| } |
| |
| private PromptInfo createPromptInfo(@Authenticators.Types int authenticators) { |
| PromptInfo promptInfo = new PromptInfo(); |
| promptInfo.setAuthenticators(authenticators); |
| return promptInfo; |
| } |
| |
| private void setupFingerprint(int id, @FingerprintSensorProperties.SensorType int type) |
| throws RemoteException { |
| IBiometricAuthenticator fingerprintAuthenticator = mock(IBiometricAuthenticator.class); |
| when(fingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); |
| when(fingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); |
| mSensors.add(new BiometricSensor(mContext, id, |
| TYPE_FINGERPRINT /* modality */, |
| Authenticators.BIOMETRIC_STRONG /* strength */, |
| fingerprintAuthenticator) { |
| @Override |
| boolean confirmationAlwaysRequired(int userId) { |
| return false; // no-op / unsupported |
| } |
| |
| @Override |
| boolean confirmationSupported() { |
| return false; // fingerprint does not support confirmation |
| } |
| }); |
| |
| final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); |
| componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, |
| "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, |
| "00000001" /* serialNumber */, "" /* softwareVersion */)); |
| componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, |
| "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, |
| "vendor/version/revision" /* softwareVersion */)); |
| |
| mFingerprintSensorProps.add(new FingerprintSensorPropertiesInternal(id, |
| SensorProperties.STRENGTH_STRONG, |
| 5 /* maxEnrollmentsPerUser */, |
| componentInfo, |
| type, |
| false /* resetLockoutRequiresHardwareAuthToken */)); |
| |
| when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); |
| } |
| |
| private void setupFace(int id, boolean confirmationAlwaysRequired, |
| IBiometricAuthenticator authenticator) throws RemoteException { |
| when(authenticator.isHardwareDetected(any())).thenReturn(true); |
| when(authenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); |
| mSensors.add(new BiometricSensor(mContext, id, |
| TYPE_FACE /* modality */, |
| Authenticators.BIOMETRIC_STRONG /* strength */, |
| authenticator) { |
| @Override |
| boolean confirmationAlwaysRequired(int userId) { |
| return confirmationAlwaysRequired; |
| } |
| |
| @Override |
| boolean confirmationSupported() { |
| return true; |
| } |
| }); |
| |
| when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); |
| } |
| } |