Biometric time-based resetLockout for multi-biometric devices

On devices with multiple sensors, lockout should be reset whenever
any strong sensor authenticates.

Bug: 163058911
Test: atest CtsBiometricsTestCases

Change-Id: I4bcdc0279cdaa444e59cf0fac6645d9a67e7d11d
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index 1fdce5e..acfad13 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -29,6 +29,7 @@
 import android.annotation.SystemService;
 import android.annotation.TestApi;
 import android.content.Context;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.security.keystore.KeyProperties;
 import android.util.Slog;
@@ -410,6 +411,36 @@
     }
 
     /**
+     * Requests all other biometric sensors to resetLockout. Note that this is a "time bound"
+     * See the {@link android.hardware.biometrics.fingerprint.ISession#resetLockout(int,
+     * HardwareAuthToken)} and {@link android.hardware.biometrics.face.ISession#resetLockout(int,
+     * HardwareAuthToken)} documentation for complete details.
+     *
+     * @param token A binder from the caller, for the service to linkToDeath
+     * @param opPackageName Caller's package name
+     * @param fromSensorId The originating sensor that just authenticated. Note that this MUST
+     *                     be a sensor that meets {@link Authenticators#BIOMETRIC_STRONG} strength.
+     *                     The strength will also be enforced on the BiometricService side.
+     * @param userId The user that authentication succeeded for, and also the user that resetLockout
+     *               should be applied to.
+     * @param hardwareAuthToken A valid HAT generated upon successful biometric authentication. Note
+     *                          that it is not necessary for the HAT to contain a challenge.
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void resetLockoutTimeBound(IBinder token, String opPackageName, int fromSensorId,
+            int userId, byte[] hardwareAuthToken) {
+        if (mService != null) {
+            try {
+                mService.resetLockoutTimeBound(token, opPackageName, fromSensorId, userId,
+                        hardwareAuthToken);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
      * Provides a localized string that may be used as the label for a button that invokes
      * {@link BiometricPrompt}.
      *
diff --git a/core/java/android/hardware/biometrics/IAuthService.aidl b/core/java/android/hardware/biometrics/IAuthService.aidl
index 1472bb9..86df099 100644
--- a/core/java/android/hardware/biometrics/IAuthService.aidl
+++ b/core/java/android/hardware/biometrics/IAuthService.aidl
@@ -69,6 +69,10 @@
     // land as SIDs, and are used during key generation.
     long[] getAuthenticatorIds();
 
+    // See documentation in BiometricManager.
+    void resetLockoutTimeBound(IBinder token, String opPackageName, int fromSensorId, int userId,
+            in byte[] hardwareAuthToken);
+
     // Provides a localized string that may be used as the label for a button that invokes
     // BiometricPrompt.
     CharSequence getButtonLabel(int userId, String opPackageName, int authenticators);
diff --git a/core/java/android/hardware/biometrics/IBiometricAuthenticator.aidl b/core/java/android/hardware/biometrics/IBiometricAuthenticator.aidl
index 7639c5d..059bf26 100644
--- a/core/java/android/hardware/biometrics/IBiometricAuthenticator.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricAuthenticator.aidl
@@ -70,4 +70,8 @@
 
     // Gets the authenticator ID representing the current set of enrolled templates
     long getAuthenticatorId(int callingUserId);
+
+    // Requests the sensor to reset its lockout state
+    void resetLockout(IBinder token, String opPackageName, int userId,
+            in byte[] hardwareAuthToken);
 }
diff --git a/core/java/android/hardware/biometrics/IBiometricService.aidl b/core/java/android/hardware/biometrics/IBiometricService.aidl
index 6d8bf0f..64b5118 100644
--- a/core/java/android/hardware/biometrics/IBiometricService.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricService.aidl
@@ -74,6 +74,10 @@
     // land as SIDs, and are used during key generation.
     long[] getAuthenticatorIds(int callingUserId);
 
+    // See documentation in BiometricManager.
+    void resetLockoutTimeBound(IBinder token, String opPackageName, int fromSensorId, int userId,
+            in byte[] hardwareAuthToken);
+
     int getCurrentStrength(int sensorId);
 
     // Returns a bit field of the modality (or modalities) that are will be used for authentication.
diff --git a/core/proto/android/server/biometrics.proto b/core/proto/android/server/biometrics.proto
index 900235e..bbb0edd 100644
--- a/core/proto/android/server/biometrics.proto
+++ b/core/proto/android/server/biometrics.proto
@@ -125,6 +125,13 @@
 
     // User states for this sensor.
     repeated UserStateProto user_states = 4;
+
+    // True if resetLockout requires a HAT to be verified in the TEE or equivalent.
+    optional bool reset_lockout_requires_hardware_auth_token = 5;
+
+    // True if a HAT is required (field above) AND a challenge needs to be generated by the
+    // biometric TEE (or equivalent), and wrapped within the HAT.
+    optional bool reset_lockout_requires_challenge = 6;
 }
 
 // State of a specific user for a specific sensor.
diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index 285f318..5b9fa79 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -340,6 +340,20 @@
         }
 
         @Override
+        public void resetLockoutTimeBound(IBinder token, String opPackageName, int fromSensorId,
+                int userId, byte[] hardwareAuthToken) throws RemoteException {
+            checkInternalPermission();
+
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mBiometricService.resetLockoutTimeBound(token, opPackageName, fromSensorId, userId,
+                        hardwareAuthToken);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public CharSequence getButtonLabel(
                 int userId,
                 String opPackageName,
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index a888209..63e7b4b 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -754,7 +754,7 @@
             }
         }
 
-        @Override
+        @Override // Binder call
         public void invalidateAuthenticatorIds(int userId, int fromSensorId,
                 IInvalidationCallback callback) {
             checkInternalPermission();
@@ -790,6 +790,45 @@
         }
 
         @Override // Binder call
+        public void resetLockoutTimeBound(IBinder token, String opPackageName, int fromSensorId,
+                int userId, byte[] hardwareAuthToken) {
+            checkInternalPermission();
+
+            // Check originating strength
+            if (!Utils.isAtLeastStrength(getSensorForId(fromSensorId).getCurrentStrength(),
+                    Authenticators.BIOMETRIC_STRONG)) {
+                Slog.w(TAG, "Sensor: " + fromSensorId + " is does not meet the required strength to"
+                        + " request resetLockout");
+                return;
+            }
+
+            // Request resetLockout for applicable sensors
+            for (BiometricSensor sensor : mSensors) {
+                if (sensor.id == fromSensorId) {
+                    continue;
+                }
+                try {
+                    final SensorPropertiesInternal props = sensor.impl
+                            .getSensorProperties(getContext().getOpPackageName());
+                    final boolean supportsChallengelessHat =
+                            props.resetLockoutRequiresHardwareAuthToken
+                            && !props.resetLockoutRequiresChallenge;
+                    final boolean doesNotRequireHat = !props.resetLockoutRequiresHardwareAuthToken;
+
+                    if (supportsChallengelessHat || doesNotRequireHat) {
+                        Slog.d(TAG, "resetLockout from: " + fromSensorId
+                                + ", for: " + sensor.id
+                                + ", userId: " + userId);
+                        sensor.impl.resetLockout(token, opPackageName, userId,
+                                hardwareAuthToken);
+                    }
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Remote exception", e);
+                }
+            }
+        }
+
+        @Override // Binder call
         public int getCurrentStrength(int sensorId) {
             checkInternalPermission();
 
@@ -1294,6 +1333,16 @@
         }
     }
 
+    @Nullable
+    private BiometricSensor getSensorForId(int sensorId) {
+        for (BiometricSensor sensor : mSensors) {
+            if (sensor.id == sensorId) {
+                return sensor;
+            }
+        }
+        return null;
+    }
+
     private void dumpInternal(PrintWriter pw) {
         pw.println("Sensors:");
         for (BiometricSensor sensor : mSensors) {
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index d9e21a8..f4cb387 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -176,7 +176,8 @@
      * @param requestedStrength the strength that it must meet
      * @return true only if the sensor is at least as strong as the requested strength
      */
-    public static boolean isAtLeastStrength(int sensorStrength, int requestedStrength) {
+    public static boolean isAtLeastStrength(@Authenticators.Types int sensorStrength,
+            @Authenticators.Types int requestedStrength) {
         // Clear out any bits that are not reserved for biometric
         sensorStrength &= Authenticators.BIOMETRIC_MIN_STRENGTH;
 
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 b31a54b..9617bb0 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -26,6 +26,7 @@
 import android.content.pm.ApplicationInfo;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
+import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -50,6 +51,7 @@
     private final boolean mIsStrongBiometric;
     private final boolean mRequireConfirmation;
     private final ActivityTaskManager mActivityTaskManager;
+    private final BiometricManager mBiometricManager;
     @Nullable private final TaskStackListener mTaskStackListener;
     private final LockoutTracker mLockoutTracker;
     private final boolean mIsRestricted;
@@ -73,6 +75,7 @@
         mOperationId = operationId;
         mRequireConfirmation = requireConfirmation;
         mActivityTaskManager = ActivityTaskManager.getInstance();
+        mBiometricManager = context.getSystemService(BiometricManager.class);
         mTaskStackListener = taskStackListener;
         mLockoutTracker = lockoutTracker;
         mIsRestricted = restricted;
@@ -207,6 +210,13 @@
                 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,
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceAuthenticator.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceAuthenticator.java
index 06b049b..2926260 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceAuthenticator.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceAuthenticator.java
@@ -104,4 +104,11 @@
     public long getAuthenticatorId(int callingUserId) throws RemoteException {
         return mFaceService.getAuthenticatorId(mSensorId, callingUserId);
     }
+
+    @Override
+    public void resetLockout(IBinder token, String opPackageName, int userId,
+            byte[] hardwareAuthToken) throws RemoteException {
+        mFaceService.resetLockout(token, mSensorId, userId, hardwareAuthToken,
+                opPackageName);
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
index 3434acb..c7d2f0f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
@@ -495,6 +495,15 @@
         Slog.w(mTag, "setTestHalEnabled: " + enabled);
         if (enabled != mTestHalEnabled) {
             // The framework should retrieve a new session from the HAL.
+            try {
+                if (mCurrentSession != null && mCurrentSession.mSession != null) {
+                    // TODO(181984005): This should be scheduled instead of directly invoked
+                    Slog.d(mTag, "Closing old session");
+                    mCurrentSession.mSession.close(888 /* cookie */);
+                }
+            } catch (RemoteException e) {
+                Slog.e(mTag, "RemoteException", e);
+            }
             mCurrentSession = null;
         }
         mTestHalEnabled = enabled;
@@ -519,6 +528,11 @@
             proto.end(userToken);
         }
 
+        proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_HARDWARE_AUTH_TOKEN,
+                mSensorProperties.resetLockoutRequiresHardwareAuthToken);
+        proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_CHALLENGE,
+                mSensorProperties.resetLockoutRequiresChallenge);
+
         proto.end(sensorToken);
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
index afe7f24..40c050f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
@@ -798,6 +798,11 @@
             proto.end(userToken);
         }
 
+        proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_HARDWARE_AUTH_TOKEN,
+                mSensorProperties.resetLockoutRequiresHardwareAuthToken);
+        proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_CHALLENGE,
+                mSensorProperties.resetLockoutRequiresChallenge);
+
         proto.end(sensorToken);
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticator.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticator.java
index 32e9409..9e82ffc 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticator.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticator.java
@@ -105,4 +105,11 @@
     public long getAuthenticatorId(int callingUserId) throws RemoteException {
         return mFingerprintService.getAuthenticatorId(mSensorId, callingUserId);
     }
+
+    @Override
+    public void resetLockout(IBinder token, String opPackageName, int userId,
+            byte[] hardwareAuthToken) throws RemoteException {
+        mFingerprintService.resetLockout(token, mSensorId, userId, hardwareAuthToken,
+                opPackageName);
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
index a98e7db..b9dee7d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
@@ -475,6 +475,15 @@
         Slog.w(mTag, "setTestHalEnabled: " + enabled);
         if (enabled != mTestHalEnabled) {
             // The framework should retrieve a new session from the HAL.
+            try {
+                if (mCurrentSession != null && mCurrentSession.mSession != null) {
+                    // TODO(181984005): This should be scheduled instead of directly invoked
+                    Slog.d(mTag, "Closing old session");
+                    mCurrentSession.mSession.close(999 /* cookie */);
+                }
+            } catch (RemoteException e) {
+                Slog.e(mTag, "RemoteException", e);
+            }
             mCurrentSession = null;
         }
         mTestHalEnabled = enabled;
@@ -499,6 +508,11 @@
             proto.end(userToken);
         }
 
+        proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_HARDWARE_AUTH_TOKEN,
+                mSensorProperties.resetLockoutRequiresHardwareAuthToken);
+        proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_CHALLENGE,
+                mSensorProperties.resetLockoutRequiresChallenge);
+
         proto.end(sensorToken);
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
index 7ff5ec3..e737677 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
@@ -761,6 +761,11 @@
             proto.end(userToken);
         }
 
+        proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_HARDWARE_AUTH_TOKEN,
+                mSensorProperties.resetLockoutRequiresHardwareAuthToken);
+        proto.write(SensorStateProto.RESET_LOCKOUT_REQUIRES_CHALLENGE,
+                mSensorProperties.resetLockoutRequiresChallenge);
+
         proto.end(sensorToken);
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/iris/IrisAuthenticator.java b/services/core/java/com/android/server/biometrics/sensors/iris/IrisAuthenticator.java
index f44e069..95915b7 100644
--- a/services/core/java/com/android/server/biometrics/sensors/iris/IrisAuthenticator.java
+++ b/services/core/java/com/android/server/biometrics/sensors/iris/IrisAuthenticator.java
@@ -95,4 +95,9 @@
     public long getAuthenticatorId(int callingUserId) throws RemoteException {
         return 0;
     }
+
+    @Override
+    public void resetLockout(IBinder token, String opPackageName, int userId,
+            byte[] hardwareAuthToken) throws RemoteException {
+    }
 }
\ No newline at end of file