blob: 273f8a545db55873fed3d6676a86bca2aaff2cee [file] [log] [blame]
/*
* 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.sensors.fingerprint.hidl;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.trust.TrustManager;
import android.content.ContentResolver;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback;
import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.IUdfpsOverlayController;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Slog;
import android.util.SparseBooleanArray;
import com.android.internal.R;
import com.android.server.biometrics.sensors.AuthenticationConsumer;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import com.android.server.biometrics.sensors.fingerprint.FingerprintStateCallback;
import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* A mockable/testable provider of the {@link android.hardware.biometrics.fingerprint.V2_3} HIDL
* interface. This class is intended simulate UDFPS logic for devices that do not have an actual
* fingerprint@2.3 HAL (where UDFPS starts to become supported)
*
* UDFPS "accept" can only happen within a set amount of time after a sensor authentication. This is
* specified by {@link MockHalResultController#AUTH_VALIDITY_MS}. Touches after this duration will
* be treated as "reject".
*
* This class provides framework logic to emulate, for testing only, the UDFPS functionalies below:
*
* 1) IF either A) the caller is keyguard, and the device is not in a trusted state (authenticated
* via biometric sensor or unlocked with a trust agent {@see android.app.trust.TrustManager}, OR
* B) the caller is not keyguard, and regardless of trusted state, AND (following applies to both
* (A) and (B) above) {@link FingerprintManager#onFingerDown(int, int, float, float)} is
* received, this class will respond with {@link AuthenticationCallback#onAuthenticationFailed()}
* after a tunable flat_time + variance_time.
*
* In the case above (1), callers must not receive a successful authentication event here because
* the sensor has not actually been authenticated.
*
* 2) IF A) the caller is keyguard and the device is not in a trusted state, OR B) the caller is not
* keyguard and regardless of trusted state, AND (following applies to both (A) and (B)) the
* sensor is touched and the fingerprint is accepted by the HAL, and then
* {@link FingerprintManager#onFingerDown(int, int, float, float)} is received, this class will
* respond with {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)}
* after a tunable flat_time + variance_time. Note that the authentication callback from the
* sensor is held until {@link FingerprintManager#onFingerDown(int, int, float, float)} is
* invoked.
*
* In the case above (2), callers can receive a successful authentication callback because the
* real sensor was authenticated. Note that even though the real sensor was touched, keyguard
* fingerprint authentication does not put keyguard into a trusted state because the
* authentication callback is held until onFingerDown was invoked. This allows callers such as
* keyguard to simulate a realistic path.
*
* 3) IF the caller is keyguard AND the device in a trusted state and then
* {@link FingerprintManager#onFingerDown(int, int, float, float)} is received, this class will
* respond with {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)}
* after a tunable flat_time + variance time.
*
* In the case above (3), since the device is already unlockable via trust agent, it's fine to
* simulate the successful auth path. Non-keyguard clients will fall into either (1) or (2)
* above.
*
* This class currently does not simulate false rejection. Conversely, this class relies on the
* real hardware sensor so does not affect false acceptance.
*/
@SuppressWarnings("deprecation")
public class Fingerprint21UdfpsMock extends Fingerprint21 implements TrustManager.TrustListener {
private static final String TAG = "Fingerprint21UdfpsMock";
// Secure setting integer. If true, the system will load this class to enable udfps testing.
public static final String CONFIG_ENABLE_TEST_UDFPS =
"com.android.server.biometrics.sensors.fingerprint.test_udfps.enable";
// Secure setting integer. A fixed duration intended to simulate something like the duration
// required for image capture.
private static final String CONFIG_AUTH_DELAY_PT1 =
"com.android.server.biometrics.sensors.fingerprint.test_udfps.auth_delay_pt1";
// Secure setting integer. A fixed duration intended to simulate something like the duration
// required for template matching to complete.
private static final String CONFIG_AUTH_DELAY_PT2 =
"com.android.server.biometrics.sensors.fingerprint.test_udfps.auth_delay_pt2";
// Secure setting integer. A random value between [-randomness, randomness] will be added to the
// capture delay above for each accept/reject.
private static final String CONFIG_AUTH_DELAY_RANDOMNESS =
"com.android.server.biometrics.sensors.fingerprint.test_udfps.auth_delay_randomness";
private static final int DEFAULT_AUTH_DELAY_PT1_MS = 300;
private static final int DEFAULT_AUTH_DELAY_PT2_MS = 400;
private static final int DEFAULT_AUTH_DELAY_RANDOMNESS_MS = 100;
@NonNull private final TestableBiometricScheduler mScheduler;
@NonNull private final Handler mHandler;
@NonNull private final FingerprintSensorPropertiesInternal mSensorProperties;
@NonNull private final MockHalResultController mMockHalResultController;
@NonNull private final TrustManager mTrustManager;
@NonNull private final SparseBooleanArray mUserHasTrust;
@NonNull private final Random mRandom;
@NonNull private final FakeRejectRunnable mFakeRejectRunnable;
@NonNull private final FakeAcceptRunnable mFakeAcceptRunnable;
@NonNull private final RestartAuthRunnable mRestartAuthRunnable;
private static class TestableBiometricScheduler extends BiometricScheduler {
@NonNull private Fingerprint21UdfpsMock mFingerprint21;
TestableBiometricScheduler(@NonNull String tag, @NonNull Handler handler,
@Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
super(tag, BiometricScheduler.SENSOR_TYPE_FP_OTHER, gestureAvailabilityDispatcher);
}
void init(@NonNull Fingerprint21UdfpsMock fingerprint21) {
mFingerprint21 = fingerprint21;
}
}
/**
* All of the mocking/testing should happen in here. This way we don't need to modify the
* {@link BaseClientMonitor} implementations and can run the
* real path there.
*/
private static class MockHalResultController extends HalResultController {
// Duration for which a sensor authentication can be treated as UDFPS success.
private static final int AUTH_VALIDITY_MS = 10 * 1000; // 10 seconds
static class LastAuthArgs {
@NonNull final AuthenticationConsumer lastAuthenticatedClient;
final long deviceId;
final int fingerId;
final int groupId;
@Nullable final ArrayList<Byte> token;
LastAuthArgs(@NonNull AuthenticationConsumer authenticationConsumer, long deviceId,
int fingerId, int groupId, @Nullable ArrayList<Byte> token) {
lastAuthenticatedClient = authenticationConsumer;
this.deviceId = deviceId;
this.fingerId = fingerId;
this.groupId = groupId;
if (token == null) {
this.token = null;
} else {
// Store a copy so the owner can be GC'd
this.token = new ArrayList<>(token);
}
}
}
// Initialized after the constructor, but before it's ever used.
@NonNull private RestartAuthRunnable mRestartAuthRunnable;
@NonNull private Fingerprint21UdfpsMock mFingerprint21;
@Nullable private LastAuthArgs mLastAuthArgs;
MockHalResultController(int sensorId, @NonNull Context context, @NonNull Handler handler,
@NonNull BiometricScheduler scheduler) {
super(sensorId, context, handler, scheduler);
}
void init(@NonNull RestartAuthRunnable restartAuthRunnable,
@NonNull Fingerprint21UdfpsMock fingerprint21) {
mRestartAuthRunnable = restartAuthRunnable;
mFingerprint21 = fingerprint21;
}
@Nullable AuthenticationConsumer getLastAuthenticatedClient() {
return mLastAuthArgs != null ? mLastAuthArgs.lastAuthenticatedClient : null;
}
/**
* Intercepts the HAL authentication and holds it until the UDFPS simulation decides
* that authentication finished.
*/
@Override
public void onAuthenticated(long deviceId, int fingerId, int groupId,
ArrayList<Byte> token) {
mHandler.post(() -> {
final BaseClientMonitor client = mScheduler.getCurrentClient();
if (!(client instanceof AuthenticationConsumer)) {
Slog.e(TAG, "Non authentication consumer: " + client);
return;
}
final boolean accepted = fingerId != 0;
if (accepted) {
mFingerprint21.setDebugMessage("Finger accepted");
} else {
mFingerprint21.setDebugMessage("Finger rejected");
}
final AuthenticationConsumer authenticationConsumer =
(AuthenticationConsumer) client;
mLastAuthArgs = new LastAuthArgs(authenticationConsumer, deviceId, fingerId,
groupId, token);
// Remove any existing restart runnbables since auth just started, and put a new
// one on the queue.
mHandler.removeCallbacks(mRestartAuthRunnable);
mRestartAuthRunnable.setLastAuthReference(authenticationConsumer);
mHandler.postDelayed(mRestartAuthRunnable, AUTH_VALIDITY_MS);
});
}
/**
* Calls through to the real interface and notifies clients of accept/reject.
*/
void sendAuthenticated(long deviceId, int fingerId, int groupId,
ArrayList<Byte> token) {
Slog.d(TAG, "sendAuthenticated: " + (fingerId != 0));
mFingerprint21.setDebugMessage("Udfps match: " + (fingerId != 0));
super.onAuthenticated(deviceId, fingerId, groupId, token);
}
}
public static Fingerprint21UdfpsMock newInstance(@NonNull Context context,
@NonNull FingerprintStateCallback fingerprintStateCallback,
@NonNull FingerprintSensorPropertiesInternal sensorProps,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
Slog.d(TAG, "Creating Fingerprint23Mock!");
final Handler handler = new Handler(Looper.getMainLooper());
final TestableBiometricScheduler scheduler =
new TestableBiometricScheduler(TAG, handler, gestureAvailabilityDispatcher);
final MockHalResultController controller =
new MockHalResultController(sensorProps.sensorId, context, handler, scheduler);
return new Fingerprint21UdfpsMock(context, fingerprintStateCallback, sensorProps, scheduler,
handler, lockoutResetDispatcher, controller);
}
private static abstract class FakeFingerRunnable implements Runnable {
private long mFingerDownTime;
private int mCaptureDuration;
/**
* @param fingerDownTime System time when onFingerDown occurred
* @param captureDuration Duration that the finger needs to be down for
*/
void setSimulationTime(long fingerDownTime, int captureDuration) {
mFingerDownTime = fingerDownTime;
mCaptureDuration = captureDuration;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isImageCaptureComplete() {
return System.currentTimeMillis() - mFingerDownTime > mCaptureDuration;
}
}
private final class FakeRejectRunnable extends FakeFingerRunnable {
@Override
public void run() {
mMockHalResultController.sendAuthenticated(0, 0, 0, null);
}
}
private final class FakeAcceptRunnable extends FakeFingerRunnable {
@Override
public void run() {
if (mMockHalResultController.mLastAuthArgs == null) {
// This can happen if the user has trust agents enabled, which make lockscreen
// dismissable. Send a fake non-zero (accept) finger.
Slog.d(TAG, "Sending fake finger");
mMockHalResultController.sendAuthenticated(1 /* deviceId */,
1 /* fingerId */, 1 /* groupId */, null /* token */);
} else {
mMockHalResultController.sendAuthenticated(
mMockHalResultController.mLastAuthArgs.deviceId,
mMockHalResultController.mLastAuthArgs.fingerId,
mMockHalResultController.mLastAuthArgs.groupId,
mMockHalResultController.mLastAuthArgs.token);
}
}
}
/**
* The fingerprint HAL allows multiple (5) fingerprint attempts per HIDL invocation of the
* authenticate method. However, valid fingerprint authentications are invalidated after
* {@link MockHalResultController#AUTH_VALIDITY_MS}, meaning UDFPS touches will be reported as
* rejects if touched after that duration. However, since a valid fingerprint was detected, the
* HAL and FingerprintService will not look for subsequent fingerprints.
*
* In order to keep the FingerprintManager API consistent (that multiple fingerprint attempts
* are allowed per auth lifecycle), we internally cancel and restart authentication so that the
* sensor is responsive again.
*/
private static final class RestartAuthRunnable implements Runnable {
@NonNull private final Fingerprint21UdfpsMock mFingerprint21;
@NonNull private final TestableBiometricScheduler mScheduler;
// Store a reference to the auth consumer that should be invalidated.
private AuthenticationConsumer mLastAuthConsumer;
RestartAuthRunnable(@NonNull Fingerprint21UdfpsMock fingerprint21,
@NonNull TestableBiometricScheduler scheduler) {
mFingerprint21 = fingerprint21;
mScheduler = scheduler;
}
void setLastAuthReference(AuthenticationConsumer lastAuthConsumer) {
mLastAuthConsumer = lastAuthConsumer;
}
@Override
public void run() {
final BaseClientMonitor client = mScheduler.getCurrentClient();
// We don't care about FingerprintDetectClient, since accept/rejects are both OK. UDFPS
// rejects will just simulate the path where non-enrolled fingers are presented.
if (!(client instanceof FingerprintAuthenticationClient)) {
Slog.e(TAG, "Non-FingerprintAuthenticationClient client: " + client);
return;
}
// Perhaps the runnable is stale, or the user stopped/started auth manually. Do not
// restart auth in this case.
if (client != mLastAuthConsumer) {
Slog.e(TAG, "Current client: " + client
+ " does not match mLastAuthConsumer: " + mLastAuthConsumer);
return;
}
Slog.d(TAG, "Restarting auth, current: " + client);
mFingerprint21.setDebugMessage("Auth timed out");
final FingerprintAuthenticationClient authClient =
(FingerprintAuthenticationClient) client;
// Store the authClient parameters so it can be rescheduled
final IBinder token = client.getToken();
final long operationId = authClient.getOperationId();
final int user = client.getTargetUserId();
final int cookie = client.getCookie();
final ClientMonitorCallbackConverter listener = client.getListener();
final String opPackageName = client.getOwnerString();
final boolean restricted = authClient.isRestricted();
final int statsClient = client.getStatsClient();
final boolean isKeyguard = authClient.isKeyguard();
// Don't actually send cancel() to the HAL, since successful auth already finishes
// HAL authenticate() lifecycle. Just
mScheduler.getInternalCallback().onClientFinished(client, true /* success */);
// Schedule this only after we invoke onClientFinished for the previous client, so that
// internal preemption logic is not run.
mFingerprint21.scheduleAuthenticate(mFingerprint21.mSensorProperties.sensorId, token,
operationId, user, cookie, listener, opPackageName, restricted, statsClient,
isKeyguard);
}
}
private Fingerprint21UdfpsMock(@NonNull Context context,
@NonNull FingerprintStateCallback fingerprintStateCallback,
@NonNull FingerprintSensorPropertiesInternal sensorProps,
@NonNull TestableBiometricScheduler scheduler,
@NonNull Handler handler,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull MockHalResultController controller) {
super(context, fingerprintStateCallback, sensorProps, scheduler, handler,
lockoutResetDispatcher, controller);
mScheduler = scheduler;
mScheduler.init(this);
mHandler = handler;
// resetLockout is controlled by the framework, so hardwareAuthToken is not required
final boolean resetLockoutRequiresHardwareAuthToken = false;
final int maxTemplatesAllowed = mContext.getResources()
.getInteger(R.integer.config_fingerprintMaxTemplatesPerUser);
mSensorProperties = new FingerprintSensorPropertiesInternal(sensorProps.sensorId,
sensorProps.sensorStrength, maxTemplatesAllowed, sensorProps.componentInfo,
FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
resetLockoutRequiresHardwareAuthToken, sensorProps.getAllLocations());
mMockHalResultController = controller;
mUserHasTrust = new SparseBooleanArray();
mTrustManager = context.getSystemService(TrustManager.class);
mTrustManager.registerTrustListener(this);
mRandom = new Random();
mFakeRejectRunnable = new FakeRejectRunnable();
mFakeAcceptRunnable = new FakeAcceptRunnable();
mRestartAuthRunnable = new RestartAuthRunnable(this, mScheduler);
// We can't initialize this during MockHalresultController's constructor due to a circular
// dependency.
mMockHalResultController.init(mRestartAuthRunnable, this);
}
@Override
public void onTrustChanged(boolean enabled, int userId, int flags) {
mUserHasTrust.put(userId, enabled);
}
@Override
public void onTrustManagedChanged(boolean enabled, int userId) {
}
@Override
public void onTrustError(CharSequence message) {
}
@Override
@NonNull
public List<FingerprintSensorPropertiesInternal> getSensorProperties() {
final List<FingerprintSensorPropertiesInternal> properties = new ArrayList<>();
properties.add(mSensorProperties);
return properties;
}
@Override
public void onPointerDown(int sensorId, int x, int y, float minor, float major) {
mHandler.post(() -> {
Slog.d(TAG, "onFingerDown");
final AuthenticationConsumer lastAuthenticatedConsumer =
mMockHalResultController.getLastAuthenticatedClient();
final BaseClientMonitor currentScheduledClient = mScheduler.getCurrentClient();
if (currentScheduledClient == null) {
Slog.d(TAG, "Not authenticating");
return;
}
mHandler.removeCallbacks(mFakeRejectRunnable);
mHandler.removeCallbacks(mFakeAcceptRunnable);
// The sensor was authenticated, is still the currently scheduled client, and the
// user touched the UDFPS affordance. Pretend that auth succeeded.
final boolean authenticatedClientIsCurrent = lastAuthenticatedConsumer != null
&& lastAuthenticatedConsumer == currentScheduledClient;
// User is unlocked on keyguard via Trust Agent
final boolean keyguardAndTrusted;
if (currentScheduledClient instanceof FingerprintAuthenticationClient) {
keyguardAndTrusted = ((FingerprintAuthenticationClient) currentScheduledClient)
.isKeyguard()
&& mUserHasTrust.get(currentScheduledClient.getTargetUserId(), false);
} else {
keyguardAndTrusted = false;
}
final int captureDuration = getNewCaptureDuration();
final int matchingDuration = getMatchingDuration();
final int totalDuration = captureDuration + matchingDuration;
setDebugMessage("Duration: " + totalDuration
+ " (" + captureDuration + " + " + matchingDuration + ")");
if (authenticatedClientIsCurrent || keyguardAndTrusted) {
mFakeAcceptRunnable.setSimulationTime(System.currentTimeMillis(), captureDuration);
mHandler.postDelayed(mFakeAcceptRunnable, totalDuration);
} else if (currentScheduledClient instanceof AuthenticationConsumer) {
// Something is authenticating but authentication has not succeeded yet. Pretend
// that auth rejected.
mFakeRejectRunnable.setSimulationTime(System.currentTimeMillis(), captureDuration);
mHandler.postDelayed(mFakeRejectRunnable, totalDuration);
}
});
}
@Override
public void onPointerUp(int sensorId) {
mHandler.post(() -> {
Slog.d(TAG, "onFingerUp");
// Only one of these can be on the handler at any given time (see onFingerDown). If
// image capture is not complete, send ACQUIRED_TOO_FAST and remove the runnable from
// the handler. Image capture (onFingerDown) needs to happen again.
if (mHandler.hasCallbacks(mFakeRejectRunnable)
&& !mFakeRejectRunnable.isImageCaptureComplete()) {
mHandler.removeCallbacks(mFakeRejectRunnable);
mMockHalResultController.onAcquired(0 /* deviceId */,
FingerprintManager.FINGERPRINT_ACQUIRED_TOO_FAST,
0 /* vendorCode */);
} else if (mHandler.hasCallbacks(mFakeAcceptRunnable)
&& !mFakeAcceptRunnable.isImageCaptureComplete()) {
mHandler.removeCallbacks(mFakeAcceptRunnable);
mMockHalResultController.onAcquired(0 /* deviceId */,
FingerprintManager.FINGERPRINT_ACQUIRED_TOO_FAST,
0 /* vendorCode */);
}
});
}
private int getNewCaptureDuration() {
final ContentResolver contentResolver = mContext.getContentResolver();
final int captureTime = Settings.Secure.getIntForUser(contentResolver,
CONFIG_AUTH_DELAY_PT1,
DEFAULT_AUTH_DELAY_PT1_MS,
UserHandle.USER_CURRENT);
final int randomDelayRange = Settings.Secure.getIntForUser(contentResolver,
CONFIG_AUTH_DELAY_RANDOMNESS,
DEFAULT_AUTH_DELAY_RANDOMNESS_MS,
UserHandle.USER_CURRENT);
final int randomDelay = mRandom.nextInt(randomDelayRange * 2) - randomDelayRange;
// Must be at least 0
return Math.max(captureTime + randomDelay, 0);
}
private int getMatchingDuration() {
final int matchingTime = Settings.Secure.getIntForUser(mContext.getContentResolver(),
CONFIG_AUTH_DELAY_PT2,
DEFAULT_AUTH_DELAY_PT2_MS,
UserHandle.USER_CURRENT);
// Must be at least 0
return Math.max(matchingTime, 0);
}
private void setDebugMessage(String message) {
try {
final IUdfpsOverlayController controller = getUdfpsOverlayController();
// Things can happen before SysUI loads and sets the controller.
if (controller != null) {
Slog.d(TAG, "setDebugMessage: " + message);
controller.setDebugMessage(mSensorProperties.sensorId, message);
}
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception when sending message: " + message, e);
}
}
}