diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index 3e6602e3175ad..28c949d4ed870 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -138,141 +138,160 @@ public abstract class AuthenticationClient extends AcquisitionClient final ClientMonitorCallbackConverter listener = getListener(); - try { - if (DEBUG) Slog.v(TAG, "onAuthenticated(" + authenticated + ")" - + ", ID:" + identifier.getBiometricId() - + ", Owner: " + getOwnerString() - + ", isBP: " + isBiometricPrompt() - + ", listener: " + listener - + ", requireConfirmation: " + mRequireConfirmation - + ", user: " + getTargetUserId()); + if (DEBUG) Slog.v(TAG, "onAuthenticated(" + authenticated + ")" + + ", ID:" + identifier.getBiometricId() + + ", Owner: " + getOwnerString() + + ", isBP: " + isBiometricPrompt() + + ", listener: " + listener + + ", requireConfirmation: " + mRequireConfirmation + + ", user: " + getTargetUserId()); - final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId()); - if (isCryptoOperation()) { - pm.incrementCryptoAuthForUser(getTargetUserId(), authenticated); + final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId()); + if (isCryptoOperation()) { + pm.incrementCryptoAuthForUser(getTargetUserId(), authenticated); + } else { + pm.incrementAuthForUser(getTargetUserId(), authenticated); + } + + if (mAllowBackgroundAuthentication) { + Slog.w(TAG, "Allowing background authentication," + + " this is allowed only for platform or test invocations"); + } + + // Ensure authentication only succeeds if the client activity is on top. + boolean isBackgroundAuth = false; + if (!mAllowBackgroundAuthentication && authenticated + && !Utils.isKeyguard(getContext(), getOwnerString()) + && !Utils.isSystem(getContext(), getOwnerString())) { + final List tasks = + mActivityTaskManager.getTasks(1); + if (tasks == null || tasks.isEmpty()) { + Slog.e(TAG, "No running tasks reported"); + isBackgroundAuth = true; } else { - pm.incrementAuthForUser(getTargetUserId(), authenticated); - } - - if (mAllowBackgroundAuthentication) { - Slog.w(TAG, "Allowing background authentication," - + " this is allowed only for platform or test invocations"); - } - - // Ensure authentication only succeeds if the client activity is on top. - boolean isBackgroundAuth = false; - if (!mAllowBackgroundAuthentication && authenticated - && !Utils.isKeyguard(getContext(), getOwnerString()) - && !Utils.isSystem(getContext(), getOwnerString())) { - final List tasks = - mActivityTaskManager.getTasks(1); - if (tasks == null || tasks.isEmpty()) { - Slog.e(TAG, "No running tasks reported"); + final ComponentName topActivity = tasks.get(0).topActivity; + if (topActivity == null) { + Slog.e(TAG, "Unable to get top activity"); isBackgroundAuth = true; } else { - final ComponentName topActivity = tasks.get(0).topActivity; - if (topActivity == null) { - Slog.e(TAG, "Unable to get top activity"); + final String topPackage = topActivity.getPackageName(); + if (!topPackage.contentEquals(getOwnerString())) { + Slog.e(TAG, "Background authentication detected, top: " + topPackage + + ", client: " + getOwnerString()); isBackgroundAuth = true; - } else { - final String topPackage = topActivity.getPackageName(); - if (!topPackage.contentEquals(getOwnerString())) { - Slog.e(TAG, "Background authentication detected, top: " + topPackage - + ", client: " + getOwnerString()); - isBackgroundAuth = true; - } } } } + } - // Fail authentication if we can't confirm the client activity is on top. + // Fail authentication if we can't confirm the client activity is on top. + if (isBackgroundAuth) { + Slog.e(TAG, "Failing possible background authentication"); + authenticated = false; + + // SafetyNet logging for exploitation attempts of b/159249069. + final ApplicationInfo appInfo = getContext().getApplicationInfo(); + EventLog.writeEvent(0x534e4554, "159249069", appInfo != null ? appInfo.uid : -1, + "Attempted background authentication"); + } + + if (authenticated) { + // SafetyNet logging for b/159249069 if constraint is violated. if (isBackgroundAuth) { - Slog.e(TAG, "Failing possible background authentication"); - authenticated = false; - - // SafetyNet logging for exploitation attempts of b/159249069. final ApplicationInfo appInfo = getContext().getApplicationInfo(); EventLog.writeEvent(0x534e4554, "159249069", appInfo != null ? appInfo.uid : -1, - "Attempted background authentication"); + "Successful background authentication!"); } - if (authenticated) { - // SafetyNet logging for b/159249069 if constraint is violated. - if (isBackgroundAuth) { - final ApplicationInfo appInfo = getContext().getApplicationInfo(); - EventLog.writeEvent(0x534e4554, "159249069", appInfo != null ? appInfo.uid : -1, - "Successful background authentication!"); - } + mAlreadyDone = true; - mAlreadyDone = true; + if (mTaskStackListener != null) { + mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); + } - if (listener != null && mShouldVibrate) { - vibrateSuccess(); - } + final byte[] byteToken = new byte[hardwareAuthToken.size()]; + for (int i = 0; i < hardwareAuthToken.size(); i++) { + byteToken[i] = hardwareAuthToken.get(i); + } - if (mTaskStackListener != null) { - mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); - } + if (mIsStrongBiometric) { + mBiometricManager.resetLockoutTimeBound(getToken(), + getContext().getOpPackageName(), + getSensorId(), getTargetUserId(), byteToken); + } - final byte[] byteToken = new byte[hardwareAuthToken.size()]; - for (int i = 0; i < hardwareAuthToken.size(); i++) { - byteToken[i] = hardwareAuthToken.get(i); - } - - if (mIsStrongBiometric) { - mBiometricManager.resetLockoutTimeBound(getToken(), - getContext().getOpPackageName(), - getSensorId(), getTargetUserId(), byteToken); - } - - if (isBiometricPrompt() && listener != null) { - // BiometricService will add the token to keystore - listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken, - getTargetUserId(), mIsStrongBiometric); - } else if (!isBiometricPrompt() && listener != null) { - if (mIsStrongBiometric) { + final CoexCoordinator coordinator = CoexCoordinator.getInstance(); + coordinator.onAuthenticationSucceeded(this, new CoexCoordinator.Callback() { + @Override + public void sendAuthenticationResult(boolean addAuthTokenIfStrong) { + if (addAuthTokenIfStrong && mIsStrongBiometric) { final int result = KeyStore.getInstance().addAuthToken(byteToken); Slog.d(TAG, "addAuthToken: " + result); } else { Slog.d(TAG, "Skipping addAuthToken"); } - // Explicitly have if/else here to make it super obvious in case the code is - // touched in the future. - if (!mIsRestricted) { - listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken, - getTargetUserId(), mIsStrongBiometric); - } else { - listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */, - byteToken, getTargetUserId(), mIsStrongBiometric); - } - - } else { - // Client not listening - Slog.w(TAG, "Client not listening"); - } - } else { - if (listener != null && mShouldVibrate) { - vibrateError(); - } - - // Allow system-defined limit of number of attempts before giving up - final @LockoutTracker.LockoutMode int lockoutMode = - handleFailedAttempt(getTargetUserId()); - if (lockoutMode == LockoutTracker.LOCKOUT_NONE) { - // Don't send onAuthenticationFailed if we're in lockout, it causes a - // janky UI on Keyguard/BiometricPrompt since "authentication failed" - // will show briefly and be replaced by "device locked out" message. if (listener != null) { - listener.onAuthenticationFailed(getSensorId()); + try { + // Explicitly have if/else here to make it super obvious in case the + // code is touched in the future. + if (!mIsRestricted) { + listener.onAuthenticationSucceeded(getSensorId(), + identifier, + byteToken, + getTargetUserId(), + mIsStrongBiometric); + } else { + listener.onAuthenticationSucceeded(getSensorId(), + null /* identifier */, + byteToken, + getTargetUserId(), + mIsStrongBiometric); + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to notify listener", e); + } + } else { + Slog.w(TAG, "Client not listening"); } - } else { - mAlreadyDone = true; } + + @Override + public void sendHapticFeedback() { + if (listener != null && mShouldVibrate) { + vibrateSuccess(); + } + } + }); + } else { + // Allow system-defined limit of number of attempts before giving up + final @LockoutTracker.LockoutMode int lockoutMode = + handleFailedAttempt(getTargetUserId()); + if (lockoutMode != LockoutTracker.LOCKOUT_NONE) { + mAlreadyDone = true; } - } catch (RemoteException e) { - Slog.e(TAG, "Unable to notify listener, finishing", e); - mCallback.onClientFinished(this, false /* success */); + + final CoexCoordinator coordinator = CoexCoordinator.getInstance(); + coordinator.onAuthenticationRejected(this, lockoutMode, + new CoexCoordinator.Callback() { + @Override + public void sendAuthenticationResult(boolean addAuthTokenIfStrong) { + if (listener != null) { + try { + listener.onAuthenticationFailed(getSensorId()); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to notify listener", e); + } + } + } + + @Override + public void sendHapticFeedback() { + if (listener != null && mShouldVibrate) { + vibrateError(); + } + } + }); } } diff --git a/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java new file mode 100644 index 0000000000000..cccb6e2d3c906 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2021 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.sensors; + +import android.annotation.NonNull; + +/** + * Singleton that contains the core logic for determining if haptics and authentication callbacks + * should be sent to receivers. Note that this class is used even when coex is not required (e.g. + * single sensor devices, or multi-sensor devices where only a single sensor is authenticating). + * This allows us to have all business logic in one testable place. + */ +public class CoexCoordinator { + + private static final String TAG = "BiometricCoexCoordinator"; + + /** + * Callback interface notifying the owner of "results" from the CoexCoordinator's business + * logic. + */ + interface Callback { + /** + * Requests the owner to send the result (success/reject) and any associated info to the + * receiver (e.g. keyguard, BiometricService, etc). + */ + void sendAuthenticationResult(boolean addAuthTokenIfStrong); + + /** + * Requests the owner to initiate a vibration for this event. + */ + void sendHapticFeedback(); + } + + private static CoexCoordinator sInstance; + + private CoexCoordinator() { + // Singleton + } + + @NonNull + static CoexCoordinator getInstance() { + if (sInstance == null) { + sInstance = new CoexCoordinator(); + } + return sInstance; + } + + public void onAuthenticationSucceeded(@NonNull AuthenticationClient client, + @NonNull Callback callback) { + if (client.isBiometricPrompt()) { + callback.sendHapticFeedback(); + // For BP, BiometricService will add the authToken to Keystore. + callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */); + } else { + // Keyguard, FingerprintManager, FaceManager, etc + callback.sendHapticFeedback(); + callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); + } + } + + public void onAuthenticationRejected(@NonNull AuthenticationClient client, + @LockoutTracker.LockoutMode int lockoutMode, + @NonNull Callback callback) { + callback.sendHapticFeedback(); + if (lockoutMode == LockoutTracker.LOCKOUT_NONE) { + // Don't send onAuthenticationFailed if we're in lockout, it causes a + // janky UI on Keyguard/BiometricPrompt since "authentication failed" + // will show briefly and be replaced by "device locked out" message. + callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java new file mode 100644 index 0000000000000..589463e55f4fc --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 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.sensors; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@Presubmit +@SmallTest +public class CoexCoordinatorTest { + + private CoexCoordinator mCoexCoordinator; + + @Mock + private CoexCoordinator.Callback mCallback; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mCoexCoordinator = CoexCoordinator.getInstance(); + } + + @Test + public void testBiometricPrompt_authSuccess() { + AuthenticationClient client = mock(AuthenticationClient.class); + when(client.isBiometricPrompt()).thenReturn(true); + + mCoexCoordinator.onAuthenticationSucceeded(client, mCallback); + verify(mCallback).sendHapticFeedback(); + verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); + } + + @Test + public void testBiometricPrompt_authReject_whenNotLockedOut() { + AuthenticationClient client = mock(AuthenticationClient.class); + when(client.isBiometricPrompt()).thenReturn(true); + + mCoexCoordinator.onAuthenticationRejected(client, LockoutTracker.LOCKOUT_NONE, mCallback); + verify(mCallback).sendHapticFeedback(); + verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); + } + + @Test + public void testBiometricPrompt_authReject_whenLockedOut() { + AuthenticationClient client = mock(AuthenticationClient.class); + when(client.isBiometricPrompt()).thenReturn(true); + + mCoexCoordinator.onAuthenticationRejected(client, LockoutTracker.LOCKOUT_TIMED, mCallback); + verify(mCallback).sendHapticFeedback(); + verify(mCallback, never()).sendAuthenticationResult(anyBoolean()); + } +}