Merge "3/n: Add CoexCoordinator" into sc-dev
This commit is contained in:
@@ -138,141 +138,160 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T>
|
|||||||
|
|
||||||
final ClientMonitorCallbackConverter listener = getListener();
|
final ClientMonitorCallbackConverter listener = getListener();
|
||||||
|
|
||||||
try {
|
if (DEBUG) Slog.v(TAG, "onAuthenticated(" + authenticated + ")"
|
||||||
if (DEBUG) Slog.v(TAG, "onAuthenticated(" + authenticated + ")"
|
+ ", ID:" + identifier.getBiometricId()
|
||||||
+ ", ID:" + identifier.getBiometricId()
|
+ ", Owner: " + getOwnerString()
|
||||||
+ ", Owner: " + getOwnerString()
|
+ ", isBP: " + isBiometricPrompt()
|
||||||
+ ", isBP: " + isBiometricPrompt()
|
+ ", listener: " + listener
|
||||||
+ ", listener: " + listener
|
+ ", requireConfirmation: " + mRequireConfirmation
|
||||||
+ ", requireConfirmation: " + mRequireConfirmation
|
+ ", user: " + getTargetUserId());
|
||||||
+ ", user: " + getTargetUserId());
|
|
||||||
|
|
||||||
final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId());
|
final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId());
|
||||||
if (isCryptoOperation()) {
|
if (isCryptoOperation()) {
|
||||||
pm.incrementCryptoAuthForUser(getTargetUserId(), authenticated);
|
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<ActivityManager.RunningTaskInfo> tasks =
|
||||||
|
mActivityTaskManager.getTasks(1);
|
||||||
|
if (tasks == null || tasks.isEmpty()) {
|
||||||
|
Slog.e(TAG, "No running tasks reported");
|
||||||
|
isBackgroundAuth = true;
|
||||||
} else {
|
} else {
|
||||||
pm.incrementAuthForUser(getTargetUserId(), authenticated);
|
final ComponentName topActivity = tasks.get(0).topActivity;
|
||||||
}
|
if (topActivity == null) {
|
||||||
|
Slog.e(TAG, "Unable to get top activity");
|
||||||
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<ActivityManager.RunningTaskInfo> tasks =
|
|
||||||
mActivityTaskManager.getTasks(1);
|
|
||||||
if (tasks == null || tasks.isEmpty()) {
|
|
||||||
Slog.e(TAG, "No running tasks reported");
|
|
||||||
isBackgroundAuth = true;
|
isBackgroundAuth = true;
|
||||||
} else {
|
} else {
|
||||||
final ComponentName topActivity = tasks.get(0).topActivity;
|
final String topPackage = topActivity.getPackageName();
|
||||||
if (topActivity == null) {
|
if (!topPackage.contentEquals(getOwnerString())) {
|
||||||
Slog.e(TAG, "Unable to get top activity");
|
Slog.e(TAG, "Background authentication detected, top: " + topPackage
|
||||||
|
+ ", client: " + getOwnerString());
|
||||||
isBackgroundAuth = true;
|
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) {
|
if (isBackgroundAuth) {
|
||||||
Slog.e(TAG, "Failing possible background authentication");
|
|
||||||
authenticated = false;
|
|
||||||
|
|
||||||
// SafetyNet logging for exploitation attempts of b/159249069.
|
|
||||||
final ApplicationInfo appInfo = getContext().getApplicationInfo();
|
final ApplicationInfo appInfo = getContext().getApplicationInfo();
|
||||||
EventLog.writeEvent(0x534e4554, "159249069", appInfo != null ? appInfo.uid : -1,
|
EventLog.writeEvent(0x534e4554, "159249069", appInfo != null ? appInfo.uid : -1,
|
||||||
"Attempted background authentication");
|
"Successful background authentication!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authenticated) {
|
mAlreadyDone = true;
|
||||||
// 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;
|
if (mTaskStackListener != null) {
|
||||||
|
mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
|
||||||
|
}
|
||||||
|
|
||||||
if (listener != null && mShouldVibrate) {
|
final byte[] byteToken = new byte[hardwareAuthToken.size()];
|
||||||
vibrateSuccess();
|
for (int i = 0; i < hardwareAuthToken.size(); i++) {
|
||||||
}
|
byteToken[i] = hardwareAuthToken.get(i);
|
||||||
|
}
|
||||||
|
|
||||||
if (mTaskStackListener != null) {
|
if (mIsStrongBiometric) {
|
||||||
mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
|
mBiometricManager.resetLockoutTimeBound(getToken(),
|
||||||
}
|
getContext().getOpPackageName(),
|
||||||
|
getSensorId(), getTargetUserId(), byteToken);
|
||||||
|
}
|
||||||
|
|
||||||
final byte[] byteToken = new byte[hardwareAuthToken.size()];
|
final CoexCoordinator coordinator = CoexCoordinator.getInstance();
|
||||||
for (int i = 0; i < hardwareAuthToken.size(); i++) {
|
coordinator.onAuthenticationSucceeded(this, new CoexCoordinator.Callback() {
|
||||||
byteToken[i] = hardwareAuthToken.get(i);
|
@Override
|
||||||
}
|
public void sendAuthenticationResult(boolean addAuthTokenIfStrong) {
|
||||||
|
if (addAuthTokenIfStrong && mIsStrongBiometric) {
|
||||||
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 int result = KeyStore.getInstance().addAuthToken(byteToken);
|
final int result = KeyStore.getInstance().addAuthToken(byteToken);
|
||||||
Slog.d(TAG, "addAuthToken: " + result);
|
Slog.d(TAG, "addAuthToken: " + result);
|
||||||
} else {
|
} else {
|
||||||
Slog.d(TAG, "Skipping addAuthToken");
|
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) {
|
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);
|
final CoexCoordinator coordinator = CoexCoordinator.getInstance();
|
||||||
mCallback.onClientFinished(this, false /* success */);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 */);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user