summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/AuthResult.java40
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java93
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java157
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java49
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java117
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java94
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java100
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java148
8 files changed, 798 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthResult.java b/services/core/java/com/android/server/biometrics/sensors/AuthResult.java
new file mode 100644
index 000000000000..c0ebf6b8b634
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthResult.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 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.hardware.biometrics.BiometricManager;
+
+class AuthResult {
+ static final int FAILED = 0;
+ static final int LOCKED_OUT = 1;
+ static final int AUTHENTICATED = 2;
+ private final int mStatus;
+ private final int mBiometricStrength;
+
+ AuthResult(int status, @BiometricManager.Authenticators.Types int strength) {
+ mStatus = status;
+ mBiometricStrength = strength;
+ }
+
+ int getStatus() {
+ return mStatus;
+ }
+
+ int getBiometricStrength() {
+ return mBiometricStrength;
+ }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java
new file mode 100644
index 000000000000..6d00c3fc15ea
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 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.hardware.biometrics.BiometricManager.Authenticators;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that takes in a series of authentication attempts (successes, failures, lockouts)
+ * across different biometric strengths (convenience, weak, strong) and returns a single AuthResult.
+ *
+ * The AuthResult will be the strongest biometric operation that occurred amongst all reported
+ * operations, and if multiple such operations exist, it will favor a successful authentication.
+ */
+class AuthResultCoordinator {
+
+ private static final String TAG = "AuthResultCoordinator";
+ private final List<AuthResult> mOperations;
+
+ AuthResultCoordinator() {
+ mOperations = new ArrayList<>();
+ }
+
+ /**
+ * Adds auth success for a given strength to the current operation list.
+ */
+ void authenticatedFor(@Authenticators.Types int strength) {
+ mOperations.add(new AuthResult(AuthResult.AUTHENTICATED, strength));
+ }
+
+ /**
+ * Adds auth ended for a given strength to the current operation list.
+ */
+ void authEndedFor(@Authenticators.Types int strength) {
+ mOperations.add(new AuthResult(AuthResult.FAILED, strength));
+ }
+
+ /**
+ * Adds a lock out of a given strength to the current operation list.
+ */
+ void lockedOutFor(@Authenticators.Types int strength) {
+ mOperations.add(new AuthResult(AuthResult.LOCKED_OUT, strength));
+ }
+
+ /**
+ * Obtains an auth result & strength from a current set of biometric operations.
+ */
+ AuthResult getResult() {
+ AuthResult result = new AuthResult(AuthResult.FAILED, Authenticators.BIOMETRIC_CONVENIENCE);
+ return mOperations.stream().filter(
+ (element) -> element.getStatus() != AuthResult.FAILED).reduce(result,
+ ((curr, next) -> {
+ int strengthCompare = curr.getBiometricStrength() - next.getBiometricStrength();
+ if (strengthCompare < 0) {
+ return curr;
+ } else if (strengthCompare == 0) {
+ // Equal level of strength, favor authentication.
+ if (curr.getStatus() == AuthResult.AUTHENTICATED) {
+ return curr;
+ } else {
+ // Either next is Authenticated, or it is not, either way return this
+ // one.
+ return next;
+ }
+ } else {
+ // curr is a weaker biometric
+ return next;
+ }
+ }));
+ }
+
+ void resetState() {
+ mOperations.clear();
+ }
+}
+
+
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
new file mode 100644
index 000000000000..13840ff389d9
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2022 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.hardware.biometrics.BiometricManager.Authenticators;
+import android.util.Slog;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Coordinates lockout counter enforcement for all types of biometric strengths across all users.
+ *
+ * This class is not thread-safe. In general, all calls to this class should be made on the same
+ * handler to ensure no collisions.
+ */
+class AuthSessionCoordinator implements AuthSessionListener {
+ private static final String TAG = "AuthSessionCoordinator";
+
+ private final Set<Integer> mAuthOperations;
+
+ private int mUserId;
+ private boolean mIsAuthenticating;
+ private AuthResultCoordinator mAuthResultCoordinator;
+ private MultiBiometricLockoutState mMultiBiometricLockoutState;
+
+ AuthSessionCoordinator() {
+ mAuthOperations = new HashSet<>();
+ mAuthResultCoordinator = new AuthResultCoordinator();
+ mMultiBiometricLockoutState = new MultiBiometricLockoutState();
+ }
+
+ /**
+ * A Call indicating that an auth session has started
+ */
+ void onAuthSessionStarted(int userId) {
+ mAuthOperations.clear();
+ mUserId = userId;
+ mIsAuthenticating = true;
+ mAuthResultCoordinator.resetState();
+ }
+
+ /**
+ * Ends the current auth session and updates the lockout state.
+ *
+ * This can happen two ways.
+ * 1. Manually calling this API
+ * 2. If authStartedFor() was called, and all authentication attempts finish.
+ */
+ void endAuthSession() {
+ if (mIsAuthenticating) {
+ mAuthOperations.clear();
+ AuthResult res =
+ mAuthResultCoordinator.getResult();
+ if (res.getStatus() == AuthResult.AUTHENTICATED) {
+ mMultiBiometricLockoutState.onUserUnlocked(mUserId, res.getBiometricStrength());
+ } else if (res.getStatus() == AuthResult.LOCKED_OUT) {
+ mMultiBiometricLockoutState.onUserLocked(mUserId, res.getBiometricStrength());
+ }
+ mAuthResultCoordinator.resetState();
+ mIsAuthenticating = false;
+ }
+ }
+
+ /**
+ * @return true if a user can authenticate with a given strength.
+ */
+ boolean getCanAuthFor(int userId, @Authenticators.Types int strength) {
+ return mMultiBiometricLockoutState.canUserAuthenticate(userId, strength);
+ }
+
+ @Override
+ public void authStartedFor(int userId, int sensorId) {
+ if (!mIsAuthenticating) {
+ onAuthSessionStarted(userId);
+ }
+
+ if (mAuthOperations.contains(sensorId)) {
+ Slog.e(TAG, "Error, authStartedFor(" + sensorId + ") without being finished");
+ return;
+ }
+
+ if (mUserId != userId) {
+ Slog.e(TAG, "Error authStartedFor(" + userId + ") Incorrect userId, expected" + mUserId
+ + ", ignoring...");
+ return;
+ }
+
+ mAuthOperations.add(sensorId);
+ }
+
+ @Override
+ public void authenticatedFor(int userId, @Authenticators.Types int biometricStrength,
+ int sensorId) {
+ mAuthResultCoordinator.authenticatedFor(biometricStrength);
+ attemptToFinish(userId, sensorId,
+ "authenticatedFor(userId=" + userId + ", biometricStrength=" + biometricStrength
+ + ", sensorId=" + sensorId + "");
+ }
+
+ @Override
+ public void lockedOutFor(int userId, @Authenticators.Types int biometricStrength,
+ int sensorId) {
+ mAuthResultCoordinator.lockedOutFor(biometricStrength);
+ attemptToFinish(userId, sensorId,
+ "lockOutFor(userId=" + userId + ", biometricStrength=" + biometricStrength
+ + ", sensorId=" + sensorId + "");
+ }
+
+ @Override
+ public void authEndedFor(int userId, @Authenticators.Types int biometricStrength,
+ int sensorId) {
+ mAuthResultCoordinator.authEndedFor(biometricStrength);
+ attemptToFinish(userId, sensorId,
+ "authEndedFor(userId=" + userId + " ,biometricStrength=" + biometricStrength
+ + ", sensorId=" + sensorId);
+ }
+
+ @Override
+ public void resetLockoutFor(int userId, @Authenticators.Types int biometricStrength) {
+ mMultiBiometricLockoutState.onUserUnlocked(userId, biometricStrength);
+ }
+
+ private void attemptToFinish(int userId, int sensorId, String description) {
+ boolean didFail = false;
+ if (!mAuthOperations.contains(sensorId)) {
+ Slog.e(TAG, "Error unable to find auth operation : " + description);
+ didFail = true;
+ }
+ if (userId != mUserId) {
+ Slog.e(TAG, "Error mismatched userId, expected=" + mUserId + " for " + description);
+ didFail = true;
+ }
+ if (didFail) {
+ return;
+ }
+ mAuthOperations.remove(sensorId);
+ if (mIsAuthenticating && mAuthOperations.isEmpty()) {
+ endAuthSession();
+ }
+ }
+
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java
new file mode 100644
index 000000000000..8b1f90af0234
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2022 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.hardware.biometrics.BiometricManager.Authenticators;
+
+/**
+ * An interface that listens to authentication events.
+ */
+interface AuthSessionListener {
+ /**
+ * Indicates an auth operation has started for a given user and sensor.
+ */
+ void authStartedFor(int userId, int sensorId);
+
+ /**
+ * Indicates a successful authentication occurred for a sensor of a given strength.
+ */
+ void authenticatedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId);
+
+ /**
+ * Indicates authentication ended for a sensor of a given strength.
+ */
+ void authEndedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId);
+
+ /**
+ * Indicates a lockout occurred for a sensor of a given strength.
+ */
+ void lockedOutFor(int userId, @Authenticators.Types int biometricStrength, int sensorId);
+
+ /**
+ * Indicates that a reset lockout has happened for a given strength.
+ */
+ void resetLockoutFor(int uerId, @Authenticators.Types int biometricStrength);
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
new file mode 100644
index 000000000000..49dc817c7fd5
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2022 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 android.hardware.biometrics.BiometricManager.Authenticators;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
+
+import android.util.ArrayMap;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class is used as a system to store the state of each
+ * {@link Authenticators.Types} status for every user.
+ */
+class MultiBiometricLockoutState {
+
+ private static final String TAG = "MultiBiometricLockoutState";
+ private static final Map<Integer, List<Integer>> PRECEDENCE;
+
+ static {
+ Map<Integer, List<Integer>> precedence = new ArrayMap<>();
+ precedence.put(Authenticators.BIOMETRIC_STRONG,
+ Arrays.asList(BIOMETRIC_STRONG, BIOMETRIC_WEAK, BIOMETRIC_CONVENIENCE));
+ precedence.put(BIOMETRIC_WEAK, Arrays.asList(BIOMETRIC_WEAK, BIOMETRIC_CONVENIENCE));
+ precedence.put(BIOMETRIC_CONVENIENCE, Arrays.asList(BIOMETRIC_CONVENIENCE));
+ PRECEDENCE = Collections.unmodifiableMap(precedence);
+ }
+
+ private final Map<Integer, Map<Integer, Boolean>> mCanUserAuthenticate;
+
+ @VisibleForTesting
+ MultiBiometricLockoutState() {
+ mCanUserAuthenticate = new HashMap<>();
+ }
+
+ private static Map<Integer, Boolean> createLockedOutMap() {
+ Map<Integer, Boolean> lockOutMap = new HashMap<>();
+ lockOutMap.put(BIOMETRIC_STRONG, false);
+ lockOutMap.put(BIOMETRIC_WEAK, false);
+ lockOutMap.put(BIOMETRIC_CONVENIENCE, false);
+ return lockOutMap;
+ }
+
+ private Map<Integer, Boolean> getAuthMapForUser(int userId) {
+ if (!mCanUserAuthenticate.containsKey(userId)) {
+ mCanUserAuthenticate.put(userId, createLockedOutMap());
+ }
+ return mCanUserAuthenticate.get(userId);
+ }
+
+ /**
+ * Indicates a {@link Authenticators} has been locked for userId.
+ *
+ * @param userId The user.
+ * @param strength The strength of biometric that is requested to be locked.
+ */
+ void onUserLocked(int userId, @Authenticators.Types int strength) {
+ Slog.d(TAG, "onUserLocked(userId=" + userId + ", strength=" + strength + ")");
+ Map<Integer, Boolean> canUserAuthState = getAuthMapForUser(userId);
+ for (int strengthToLockout : PRECEDENCE.get(strength)) {
+ canUserAuthState.put(strengthToLockout, false);
+ }
+ }
+
+ /**
+ * Indicates that a user has unlocked a {@link Authenticators}
+ *
+ * @param userId The user.
+ * @param strength The strength of biometric that is unlocked.
+ */
+ void onUserUnlocked(int userId, @Authenticators.Types int strength) {
+ Slog.d(TAG, "onUserUnlocked(userId=" + userId + ", strength=" + strength + ")");
+ Map<Integer, Boolean> canUserAuthState = getAuthMapForUser(userId);
+ for (int strengthToLockout : PRECEDENCE.get(strength)) {
+ canUserAuthState.put(strengthToLockout, true);
+ }
+ }
+
+ /**
+ * Indicates if a user can perform an authentication operation with a given
+ * {@link Authenticators.Types}
+ *
+ * @param userId The user.
+ * @param strength The strength of biometric that is requested to authenticate.
+ * @return If a user can authenticate with a given biometric of this strength.
+ */
+ boolean canUserAuthenticate(int userId, @Authenticators.Types int strength) {
+ final boolean canAuthenticate = getAuthMapForUser(userId).get(strength);
+ Slog.d(TAG, "canUserAuthenticate(userId=" + userId + ", strength=" + strength + ") ="
+ + canAuthenticate);
+ return canAuthenticate;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java
new file mode 100644
index 000000000000..47b4bf547ff8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 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 com.google.common.truth.Truth.assertThat;
+
+import android.hardware.biometrics.BiometricManager;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class AuthResultCoordinatorTest {
+ private AuthResultCoordinator mAuthResultCoordinator;
+
+ @Before
+ public void setUp() throws Exception {
+ mAuthResultCoordinator = new AuthResultCoordinator();
+ }
+
+ @Test
+ public void testDefaultMessage() {
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.FAILED,
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ }
+
+ @Test
+ public void testSingleMessageCoordinator() {
+ mAuthResultCoordinator.authenticatedFor(
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.AUTHENTICATED,
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ }
+
+ @Test
+ public void testLockout() {
+ mAuthResultCoordinator.lockedOutFor(
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.LOCKED_OUT,
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ }
+
+ @Test
+ public void testHigherStrengthPrecedence() {
+ mAuthResultCoordinator.authenticatedFor(
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ mAuthResultCoordinator.authenticatedFor(
+ BiometricManager.Authenticators.BIOMETRIC_WEAK);
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.AUTHENTICATED,
+ BiometricManager.Authenticators.BIOMETRIC_WEAK);
+
+ mAuthResultCoordinator.authenticatedFor(
+ BiometricManager.Authenticators.BIOMETRIC_STRONG);
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.AUTHENTICATED,
+ BiometricManager.Authenticators.BIOMETRIC_STRONG);
+ }
+
+ @Test
+ public void testAuthPrecedence() {
+ mAuthResultCoordinator.authenticatedFor(
+ BiometricManager.Authenticators.BIOMETRIC_WEAK);
+ mAuthResultCoordinator.lockedOutFor(
+ BiometricManager.Authenticators.BIOMETRIC_WEAK);
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.AUTHENTICATED,
+ BiometricManager.Authenticators.BIOMETRIC_WEAK);
+
+ }
+
+ void checkResult(AuthResult res, int status,
+ @BiometricManager.Authenticators.Types int strength) {
+ assertThat(res.getStatus()).isEqualTo(status);
+ assertThat(res.getBiometricStrength()).isEqualTo(strength);
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java
new file mode 100644
index 000000000000..9bb0f58db520
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 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 android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@Presubmit
+@SmallTest
+public class AuthSessionCoordinatorTest {
+ private static final int PRIMARY_USER = 0;
+ private static final int SECONDARY_USER = 10;
+
+ private AuthSessionCoordinator mCoordinator;
+
+ @Before
+ public void setUp() throws Exception {
+ mCoordinator = new AuthSessionCoordinator();
+ }
+
+ @Test
+ public void testUserUnlocked() {
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+
+ mCoordinator.authStartedFor(PRIMARY_USER, 1);
+ mCoordinator.authenticatedFor(PRIMARY_USER, BIOMETRIC_WEAK, 1);
+
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+ }
+
+ @Test
+ public void testUserCanAuthDuringLockoutOfSameSession() {
+ mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_STRONG);
+
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+
+ mCoordinator.authStartedFor(PRIMARY_USER, 1);
+ mCoordinator.authStartedFor(PRIMARY_USER, 2);
+ mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_WEAK, 2);
+
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ }
+
+ @Test
+ public void testMultiUserAuth() {
+ mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_STRONG);
+
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_STRONG)).isFalse();
+
+ mCoordinator.authStartedFor(PRIMARY_USER, 1);
+ mCoordinator.authStartedFor(PRIMARY_USER, 2);
+ mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_WEAK, 2);
+
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_STRONG)).isFalse();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java
new file mode 100644
index 000000000000..8baa1ce3f6c6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2022 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 android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class MultiBiometricLockoutStateTest {
+ private static final int PRIMARY_USER = 0;
+ private MultiBiometricLockoutState mCoordinator;
+
+ private void unlockAllBiometrics() {
+ unlockAllBiometrics(mCoordinator, PRIMARY_USER);
+ }
+
+ private void lockoutAllBiometrics() {
+ lockoutAllBiometrics(mCoordinator, PRIMARY_USER);
+ }
+
+ private static void unlockAllBiometrics(MultiBiometricLockoutState coordinator, int userId) {
+ coordinator.onUserUnlocked(userId, BIOMETRIC_STRONG);
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_STRONG)).isTrue();
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_WEAK)).isTrue();
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_CONVENIENCE)).isTrue();
+ }
+
+ private static void lockoutAllBiometrics(MultiBiometricLockoutState coordinator, int userId) {
+ coordinator.onUserLocked(userId, BIOMETRIC_STRONG);
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_STRONG)).isFalse();
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_WEAK)).isFalse();
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mCoordinator = new MultiBiometricLockoutState();
+ }
+
+ @Test
+ public void testInitialStateLockedOut() {
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+
+ @Test
+ public void testConvenienceLockout() {
+ unlockAllBiometrics();
+ mCoordinator.onUserLocked(PRIMARY_USER, BIOMETRIC_CONVENIENCE);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+
+ @Test
+ public void testWeakLockout() {
+ unlockAllBiometrics();
+ mCoordinator.onUserLocked(PRIMARY_USER, BIOMETRIC_WEAK);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+
+ @Test
+ public void testStrongLockout() {
+ unlockAllBiometrics();
+ mCoordinator.onUserLocked(PRIMARY_USER, BIOMETRIC_STRONG);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+
+ @Test
+ public void testConvenienceUnlock() {
+ lockoutAllBiometrics();
+ mCoordinator.onUserUnlocked(PRIMARY_USER, BIOMETRIC_CONVENIENCE);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ }
+
+ @Test
+ public void testWeakUnlock() {
+ lockoutAllBiometrics();
+ mCoordinator.onUserUnlocked(PRIMARY_USER, BIOMETRIC_WEAK);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ }
+
+ @Test
+ public void testStrongUnlock() {
+ lockoutAllBiometrics();
+ mCoordinator.onUserUnlocked(PRIMARY_USER, BIOMETRIC_STRONG);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ }
+
+ @Test
+ public void multiUser_userOneDoesNotAffectUserTwo() {
+ final int userOne = 1;
+ final int userTwo = 2;
+ MultiBiometricLockoutState coordinator = new MultiBiometricLockoutState();
+ lockoutAllBiometrics(coordinator, userOne);
+ lockoutAllBiometrics(coordinator, userTwo);
+
+ coordinator.onUserUnlocked(userOne, BIOMETRIC_WEAK);
+ assertThat(coordinator.canUserAuthenticate(userOne, BIOMETRIC_STRONG)).isFalse();
+ assertThat(coordinator.canUserAuthenticate(userOne, BIOMETRIC_WEAK)).isTrue();
+ assertThat(coordinator.canUserAuthenticate(userOne, BIOMETRIC_CONVENIENCE)).isTrue();
+
+ assertThat(coordinator.canUserAuthenticate(userTwo, BIOMETRIC_STRONG)).isFalse();
+ assertThat(coordinator.canUserAuthenticate(userTwo, BIOMETRIC_WEAK)).isFalse();
+ assertThat(coordinator.canUserAuthenticate(userTwo, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+}