Loading core/res/res/values/config.xml +6 −0 Original line number Diff line number Diff line Loading @@ -5336,6 +5336,12 @@ <!-- Default value for performant auth feature. --> <bool name="config_performantAuthDefault">false</bool> <!-- Threshold for false rejection rate (FRR) of biometric authentication. Applies for both fingerprint and face. If a dual-modality device only enrolled a single biometric and experiences high FRR (above threshold), system notification will be sent to encourage user to enroll the other eligible biometric. --> <fraction name="config_biometricNotificationFrrThreshold">30%</fraction> <!-- The component name for the default profile supervisor, which can be set as a profile owner even after user setup is complete. The defined component should be used for supervision purposes only. The component must be part of a system app. --> Loading core/res/res/values/symbols.xml +3 −0 Original line number Diff line number Diff line Loading @@ -2581,6 +2581,9 @@ <java-symbol type="string" name="biometric_error_device_not_secured" /> <java-symbol type="string" name="biometric_error_generic" /> <!-- Biometric FRR config --> <java-symbol type="fraction" name="config_biometricNotificationFrrThreshold" /> <!-- Device credential strings for BiometricManager --> <java-symbol type="string" name="screen_lock_app_setting_name" /> <java-symbol type="string" name="screen_lock_dialog_default_subtitle" /> Loading services/core/java/com/android/server/biometrics/AuthenticationStats.java 0 → 100644 +90 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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; /** * Utility class for on-device biometric authentication data, including total authentication, * rejections, and the number of sent enrollment notifications. */ public class AuthenticationStats { private final int mUserId; private int mTotalAttempts; private int mRejectedAttempts; private int mEnrollmentNotifications; private final int mModality; public AuthenticationStats(final int userId, int totalAttempts, int rejectedAttempts, int enrollmentNotifications, final int modality) { mUserId = userId; mTotalAttempts = totalAttempts; mRejectedAttempts = rejectedAttempts; mEnrollmentNotifications = enrollmentNotifications; mModality = modality; } public AuthenticationStats(final int userId, final int modality) { mUserId = userId; mTotalAttempts = 0; mRejectedAttempts = 0; mEnrollmentNotifications = 0; mModality = modality; } public int getUserId() { return mUserId; } public int getTotalAttempts() { return mTotalAttempts; } public int getRejectedAttempts() { return mRejectedAttempts; } public int getEnrollmentNotifications() { return mEnrollmentNotifications; } public int getModality() { return mModality; } /** Calculate FRR. */ public float getFrr() { if (mTotalAttempts > 0) { return mRejectedAttempts / (float) mTotalAttempts; } else { return -1.0f; } } /** Update total authentication attempts and rejections. */ public void authenticate(boolean authenticated) { if (!authenticated) { mRejectedAttempts++; } mTotalAttempts++; } /** Reset total authentication attempts and rejections. */ public void resetData() { mTotalAttempts = 0; mRejectedAttempts = 0; } } services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java 0 → 100644 +106 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.HashMap; import java.util.Map; /** * Calculate and collect on-device False Rejection Rates (FRR). * FRR = All [given biometric modality] unlock failures / all [given biometric modality] unlock * attempts. */ public class AuthenticationStatsCollector { private static final String TAG = "AuthenticationStatsCollector"; // The minimum number of attempts that will calculate the FRR and trigger the notification. private static final int MINIMUM_ATTEMPTS = 500; // The maximum number of eligible biometric enrollment notification can be sent. private static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 2; private final float mThreshold; private final int mModality; @NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap; public AuthenticationStatsCollector(@NonNull Context context, int modality) { mThreshold = context.getResources() .getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1); mUserAuthenticationStatsMap = new HashMap<>(); mModality = modality; } /** Update total authentication and rejected attempts. */ public void authenticate(int userId, boolean authenticated) { // Check if this is a new user. if (!mUserAuthenticationStatsMap.containsKey(userId)) { mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality)); } AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); authenticationStats.authenticate(authenticated); persistDataIfNeeded(userId); sendNotificationIfNeeded(userId); } private void sendNotificationIfNeeded(int userId) { AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); if (authenticationStats.getTotalAttempts() >= MINIMUM_ATTEMPTS) { // Send notification if FRR exceeds the threshold if (authenticationStats.getEnrollmentNotifications() < MAXIMUM_ENROLLMENT_NOTIFICATIONS && authenticationStats.getFrr() >= mThreshold) { // TODO(wenhuiy): Send notifications. } authenticationStats.resetData(); } } private void persistDataIfNeeded(int userId) { AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); if (authenticationStats.getTotalAttempts() % 50 == 0) { // TODO(wenhuiy): Persist data. } } /** * Only being used in tests. Callers should not make any changes to the returned * authentication stats. * * @return AuthenticationStats of the user, or null if the stats doesn't exist. */ @Nullable @VisibleForTesting AuthenticationStats getAuthenticationStatsForUser(int userId) { return mUserAuthenticationStatsMap.getOrDefault(userId, null); } @VisibleForTesting void setAuthenticationStatsForUser(int userId, AuthenticationStats authenticationStats) { mUserAuthenticationStatsMap.put(userId, authenticationStats); } } services/core/java/com/android/server/biometrics/log/BiometricLogger.java +16 −4 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.Utils; /** Loading @@ -41,6 +42,7 @@ public class BiometricLogger { private final int mStatsAction; private final int mStatsClient; private final BiometricFrameworkStatsLogger mSink; @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector; @NonNull private final ALSProbe mALSProbe; private long mFirstAcquireTimeMs; Loading @@ -49,7 +51,8 @@ public class BiometricLogger { /** Get a new logger with all unknown fields (for operations that do not require logs). */ public static BiometricLogger ofUnknown(@NonNull Context context) { return new BiometricLogger(context, BiometricsProtoEnums.MODALITY_UNKNOWN, BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN); BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN, null /* AuthenticationStatsCollector */); } /** Loading @@ -64,26 +67,32 @@ public class BiometricLogger { * @param statsClient One of {@link BiometricsProtoEnums} CLIENT_* constants. */ public BiometricLogger( @NonNull Context context, int statsModality, int statsAction, int statsClient) { @NonNull Context context, int statsModality, int statsAction, int statsClient, AuthenticationStatsCollector authenticationStatsCollector) { this(statsModality, statsAction, statsClient, BiometricFrameworkStatsLogger.getInstance(), authenticationStatsCollector, context.getSystemService(SensorManager.class)); } @VisibleForTesting BiometricLogger( int statsModality, int statsAction, int statsClient, BiometricFrameworkStatsLogger logSink, SensorManager sensorManager) { BiometricFrameworkStatsLogger logSink, @NonNull AuthenticationStatsCollector statsCollector, SensorManager sensorManager) { mStatsModality = statsModality; mStatsAction = statsAction; mStatsClient = statsClient; mSink = logSink; mAuthenticationStatsCollector = statsCollector; mALSProbe = new ALSProbe(sensorManager); } /** Creates a new logger with the action replaced with the new action. */ public BiometricLogger swapAction(@NonNull Context context, int statsAction) { return new BiometricLogger(context, mStatsModality, statsAction, mStatsClient); return new BiometricLogger(context, mStatsModality, statsAction, mStatsClient, null /* AuthenticationStatsCollector */); } /** Disable logging metrics and only log critical events, such as system health issues. */ Loading Loading @@ -192,10 +201,13 @@ public class BiometricLogger { public void logOnAuthenticated(Context context, OperationContextExt operationContext, boolean authenticated, boolean requireConfirmation, int targetUserId, boolean isBiometricPrompt) { // Do not log metrics when fingerprint enrollment reason is ENROLL_FIND_SENSOR if (!mShouldLogMetrics) { return; } mAuthenticationStatsCollector.authenticate(targetUserId, authenticated); int authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__UNKNOWN; if (!authenticated) { authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__REJECTED; Loading Loading
core/res/res/values/config.xml +6 −0 Original line number Diff line number Diff line Loading @@ -5336,6 +5336,12 @@ <!-- Default value for performant auth feature. --> <bool name="config_performantAuthDefault">false</bool> <!-- Threshold for false rejection rate (FRR) of biometric authentication. Applies for both fingerprint and face. If a dual-modality device only enrolled a single biometric and experiences high FRR (above threshold), system notification will be sent to encourage user to enroll the other eligible biometric. --> <fraction name="config_biometricNotificationFrrThreshold">30%</fraction> <!-- The component name for the default profile supervisor, which can be set as a profile owner even after user setup is complete. The defined component should be used for supervision purposes only. The component must be part of a system app. --> Loading
core/res/res/values/symbols.xml +3 −0 Original line number Diff line number Diff line Loading @@ -2581,6 +2581,9 @@ <java-symbol type="string" name="biometric_error_device_not_secured" /> <java-symbol type="string" name="biometric_error_generic" /> <!-- Biometric FRR config --> <java-symbol type="fraction" name="config_biometricNotificationFrrThreshold" /> <!-- Device credential strings for BiometricManager --> <java-symbol type="string" name="screen_lock_app_setting_name" /> <java-symbol type="string" name="screen_lock_dialog_default_subtitle" /> Loading
services/core/java/com/android/server/biometrics/AuthenticationStats.java 0 → 100644 +90 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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; /** * Utility class for on-device biometric authentication data, including total authentication, * rejections, and the number of sent enrollment notifications. */ public class AuthenticationStats { private final int mUserId; private int mTotalAttempts; private int mRejectedAttempts; private int mEnrollmentNotifications; private final int mModality; public AuthenticationStats(final int userId, int totalAttempts, int rejectedAttempts, int enrollmentNotifications, final int modality) { mUserId = userId; mTotalAttempts = totalAttempts; mRejectedAttempts = rejectedAttempts; mEnrollmentNotifications = enrollmentNotifications; mModality = modality; } public AuthenticationStats(final int userId, final int modality) { mUserId = userId; mTotalAttempts = 0; mRejectedAttempts = 0; mEnrollmentNotifications = 0; mModality = modality; } public int getUserId() { return mUserId; } public int getTotalAttempts() { return mTotalAttempts; } public int getRejectedAttempts() { return mRejectedAttempts; } public int getEnrollmentNotifications() { return mEnrollmentNotifications; } public int getModality() { return mModality; } /** Calculate FRR. */ public float getFrr() { if (mTotalAttempts > 0) { return mRejectedAttempts / (float) mTotalAttempts; } else { return -1.0f; } } /** Update total authentication attempts and rejections. */ public void authenticate(boolean authenticated) { if (!authenticated) { mRejectedAttempts++; } mTotalAttempts++; } /** Reset total authentication attempts and rejections. */ public void resetData() { mTotalAttempts = 0; mRejectedAttempts = 0; } }
services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java 0 → 100644 +106 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.HashMap; import java.util.Map; /** * Calculate and collect on-device False Rejection Rates (FRR). * FRR = All [given biometric modality] unlock failures / all [given biometric modality] unlock * attempts. */ public class AuthenticationStatsCollector { private static final String TAG = "AuthenticationStatsCollector"; // The minimum number of attempts that will calculate the FRR and trigger the notification. private static final int MINIMUM_ATTEMPTS = 500; // The maximum number of eligible biometric enrollment notification can be sent. private static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 2; private final float mThreshold; private final int mModality; @NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap; public AuthenticationStatsCollector(@NonNull Context context, int modality) { mThreshold = context.getResources() .getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1); mUserAuthenticationStatsMap = new HashMap<>(); mModality = modality; } /** Update total authentication and rejected attempts. */ public void authenticate(int userId, boolean authenticated) { // Check if this is a new user. if (!mUserAuthenticationStatsMap.containsKey(userId)) { mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality)); } AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); authenticationStats.authenticate(authenticated); persistDataIfNeeded(userId); sendNotificationIfNeeded(userId); } private void sendNotificationIfNeeded(int userId) { AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); if (authenticationStats.getTotalAttempts() >= MINIMUM_ATTEMPTS) { // Send notification if FRR exceeds the threshold if (authenticationStats.getEnrollmentNotifications() < MAXIMUM_ENROLLMENT_NOTIFICATIONS && authenticationStats.getFrr() >= mThreshold) { // TODO(wenhuiy): Send notifications. } authenticationStats.resetData(); } } private void persistDataIfNeeded(int userId) { AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId); if (authenticationStats.getTotalAttempts() % 50 == 0) { // TODO(wenhuiy): Persist data. } } /** * Only being used in tests. Callers should not make any changes to the returned * authentication stats. * * @return AuthenticationStats of the user, or null if the stats doesn't exist. */ @Nullable @VisibleForTesting AuthenticationStats getAuthenticationStatsForUser(int userId) { return mUserAuthenticationStatsMap.getOrDefault(userId, null); } @VisibleForTesting void setAuthenticationStatsForUser(int userId, AuthenticationStats authenticationStats) { mUserAuthenticationStatsMap.put(userId, authenticationStats); } }
services/core/java/com/android/server/biometrics/log/BiometricLogger.java +16 −4 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; import com.android.server.biometrics.AuthenticationStatsCollector; import com.android.server.biometrics.Utils; /** Loading @@ -41,6 +42,7 @@ public class BiometricLogger { private final int mStatsAction; private final int mStatsClient; private final BiometricFrameworkStatsLogger mSink; @NonNull private final AuthenticationStatsCollector mAuthenticationStatsCollector; @NonNull private final ALSProbe mALSProbe; private long mFirstAcquireTimeMs; Loading @@ -49,7 +51,8 @@ public class BiometricLogger { /** Get a new logger with all unknown fields (for operations that do not require logs). */ public static BiometricLogger ofUnknown(@NonNull Context context) { return new BiometricLogger(context, BiometricsProtoEnums.MODALITY_UNKNOWN, BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN); BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN, null /* AuthenticationStatsCollector */); } /** Loading @@ -64,26 +67,32 @@ public class BiometricLogger { * @param statsClient One of {@link BiometricsProtoEnums} CLIENT_* constants. */ public BiometricLogger( @NonNull Context context, int statsModality, int statsAction, int statsClient) { @NonNull Context context, int statsModality, int statsAction, int statsClient, AuthenticationStatsCollector authenticationStatsCollector) { this(statsModality, statsAction, statsClient, BiometricFrameworkStatsLogger.getInstance(), authenticationStatsCollector, context.getSystemService(SensorManager.class)); } @VisibleForTesting BiometricLogger( int statsModality, int statsAction, int statsClient, BiometricFrameworkStatsLogger logSink, SensorManager sensorManager) { BiometricFrameworkStatsLogger logSink, @NonNull AuthenticationStatsCollector statsCollector, SensorManager sensorManager) { mStatsModality = statsModality; mStatsAction = statsAction; mStatsClient = statsClient; mSink = logSink; mAuthenticationStatsCollector = statsCollector; mALSProbe = new ALSProbe(sensorManager); } /** Creates a new logger with the action replaced with the new action. */ public BiometricLogger swapAction(@NonNull Context context, int statsAction) { return new BiometricLogger(context, mStatsModality, statsAction, mStatsClient); return new BiometricLogger(context, mStatsModality, statsAction, mStatsClient, null /* AuthenticationStatsCollector */); } /** Disable logging metrics and only log critical events, such as system health issues. */ Loading Loading @@ -192,10 +201,13 @@ public class BiometricLogger { public void logOnAuthenticated(Context context, OperationContextExt operationContext, boolean authenticated, boolean requireConfirmation, int targetUserId, boolean isBiometricPrompt) { // Do not log metrics when fingerprint enrollment reason is ENROLL_FIND_SENSOR if (!mShouldLogMetrics) { return; } mAuthenticationStatsCollector.authenticate(targetUserId, authenticated); int authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__UNKNOWN; if (!authenticated) { authState = FrameworkStatsLog.BIOMETRIC_AUTHENTICATED__STATE__REJECTED; Loading