From 3135431870351161002a5d2e79c32b768a9f0752 Mon Sep 17 00:00:00 2001 From: Beverly Date: Wed, 5 Jan 2022 16:46:02 -0500 Subject: [PATCH] Log biometric lockout events Test: manually checked events using go/aster-event-viewing Test: atest KeyguardBiometricLockoutLoggerTest Fixes: 205762820 Bug: 213483562 Bug: 213484709 Change-Id: Id639e5bdc834ee72fdea4877b73b3e8c63170bfb --- packages/SystemUI/res/values/config.xml | 1 + .../KeyguardBiometricLockoutLogger.kt | 176 ++++++++++++++++ .../keyguard/KeyguardUpdateMonitor.java | 13 +- .../systemui/dagger/SystemUIBinder.java | 8 + .../phone/BiometricUnlockController.java | 2 +- .../KeyguardBiometricLockoutLoggerTest.kt | 194 ++++++++++++++++++ 6 files changed, 389 insertions(+), 5 deletions(-) create mode 100644 packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt create mode 100644 packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 7b8f349777a60..56517ccb873f9 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -294,6 +294,7 @@ com.android.systemui.util.NotificationChannels com.android.systemui.keyguard.KeyguardViewMediator + com.android.keyguard.KeyguardBiometricLockoutLogger com.android.systemui.recents.Recents com.android.systemui.volume.VolumeUI com.android.systemui.statusbar.phone.StatusBar diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt new file mode 100644 index 0000000000000..214b284ac4b96 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt @@ -0,0 +1,176 @@ +/* + * 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.keyguard + +import android.content.Context +import android.hardware.biometrics.BiometricSourceType +import com.android.internal.annotations.VisibleForTesting +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE +import com.android.keyguard.KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import java.io.FileDescriptor +import java.io.PrintWriter +import javax.inject.Inject + +/** + * Logs events when primary authentication requirements change. Primary authentication is considered + * authentication using pin/pattern/password input. + * + * See [PrimaryAuthRequiredEvent] for all the events and their descriptions. + */ +@SysUISingleton +class KeyguardBiometricLockoutLogger @Inject constructor( + context: Context?, + private val uiEventLogger: UiEventLogger, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val dumpManager: DumpManager +) : CoreStartable(context) { + private var fingerprintLockedOut = false + private var faceLockedOut = false + private var encryptedOrLockdown = false + private var unattendedUpdate = false + private var timeout = false + + override fun start() { + dumpManager.registerDumpable(this) + mKeyguardUpdateMonitorCallback.onStrongAuthStateChanged( + KeyguardUpdateMonitor.getCurrentUser()) + keyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback) + } + + private val mKeyguardUpdateMonitorCallback: KeyguardUpdateMonitorCallback = + object : KeyguardUpdateMonitorCallback() { + override fun onLockedOutStateChanged(biometricSourceType: BiometricSourceType) { + if (biometricSourceType == BiometricSourceType.FINGERPRINT) { + val lockedOut = keyguardUpdateMonitor.isFingerprintLockedOut + if (lockedOut && !fingerprintLockedOut) { + uiEventLogger.log( + PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT) + } else if (!lockedOut && fingerprintLockedOut) { + uiEventLogger.log( + PrimaryAuthRequiredEvent + .PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT_RESET) + } + fingerprintLockedOut = lockedOut + } else if (biometricSourceType == BiometricSourceType.FACE) { + val lockedOut = keyguardUpdateMonitor.isFaceLockedOut + if (lockedOut && !faceLockedOut) { + uiEventLogger.log( + PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT) + } else if (!lockedOut && faceLockedOut) { + uiEventLogger.log( + PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT_RESET) + } + faceLockedOut = lockedOut + } + } + + override fun onStrongAuthStateChanged(userId: Int) { + if (userId != KeyguardUpdateMonitor.getCurrentUser()) { + return + } + val strongAuthFlags = keyguardUpdateMonitor.strongAuthTracker + .getStrongAuthForUser(userId) + + val newEncryptedOrLockdown = keyguardUpdateMonitor.isEncryptedOrLockdown(userId) + if (newEncryptedOrLockdown && !encryptedOrLockdown) { + uiEventLogger.log( + PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_ENCRYPTED_OR_LOCKDOWN) + } + encryptedOrLockdown = newEncryptedOrLockdown + + val newUnattendedUpdate = isUnattendedUpdate(strongAuthFlags) + if (newUnattendedUpdate && !unattendedUpdate) { + uiEventLogger.log(PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_UNATTENDED_UPDATE) + } + unattendedUpdate = newUnattendedUpdate + + val newTimeout = isStrongAuthTimeout(strongAuthFlags) + if (newTimeout && !timeout) { + uiEventLogger.log(PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_TIMEOUT) + } + timeout = newTimeout + } + } + + private fun isUnattendedUpdate( + @LockPatternUtils.StrongAuthTracker.StrongAuthFlags flags: Int + ) = containsFlag(flags, STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE) + + private fun isStrongAuthTimeout( + @LockPatternUtils.StrongAuthTracker.StrongAuthFlags flags: Int + ) = containsFlag(flags, STRONG_AUTH_REQUIRED_AFTER_TIMEOUT) || + containsFlag(flags, STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT) + + override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array) { + pw.println(" mFingerprintLockedOut=$fingerprintLockedOut") + pw.println(" mFaceLockedOut=$faceLockedOut") + pw.println(" mIsEncryptedOrLockdown=$encryptedOrLockdown") + pw.println(" mIsUnattendedUpdate=$unattendedUpdate") + pw.println(" mIsTimeout=$timeout") + } + + /** + * Events pertaining to whether primary authentication (pin/pattern/password input) is required + * for device entry. + */ + @VisibleForTesting + enum class PrimaryAuthRequiredEvent(private val mId: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "Fingerprint cannot be used to authenticate for device entry. This" + + "can persist until the next primary auth or may timeout.") + PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT(924), + + @UiEvent(doc = "Fingerprint can be used to authenticate for device entry.") + PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT_RESET(925), + + @UiEvent(doc = "Face cannot be used to authenticate for device entry.") + PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT(926), + + @UiEvent(doc = "Face can be used to authenticate for device entry.") + PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT_RESET(927), + + @UiEvent(doc = "Device is encrypted (ie: after reboot) or device is locked down by DPM " + + "or a manual user lockdown.") + PRIMARY_AUTH_REQUIRED_ENCRYPTED_OR_LOCKDOWN(928), + + @UiEvent(doc = "Primary authentication is required because it hasn't been used for a " + + "time required by a device admin or because primary auth hasn't been used for a " + + "time after a non-strong biometric (weak or convenience) is used to unlock the " + + "device.") + PRIMARY_AUTH_REQUIRED_TIMEOUT(929), + + @UiEvent(doc = "Strong authentication is required to prepare for unattended upgrade.") + PRIMARY_AUTH_REQUIRED_UNATTENDED_UPDATE(931); + + override fun getId(): Int { + return mId + } + } + + companion object { + private fun containsFlag(strongAuthFlags: Int, flagCheck: Int): Boolean { + return strongAuthFlags and flagCheck != 0 + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 98721fd78a931..042d945b6ff03 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -183,7 +183,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private static final int MSG_USER_STOPPED = 340; private static final int MSG_USER_REMOVED = 341; private static final int MSG_KEYGUARD_GOING_AWAY = 342; - private static final int MSG_LOCK_SCREEN_MODE = 343; private static final int MSG_TIME_FORMAT_UPDATE = 344; private static final int MSG_REQUIRE_NFC_UNLOCK = 345; @@ -221,7 +220,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private static final int BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED = -1; public static final int BIOMETRIC_HELP_FACE_NOT_RECOGNIZED = -2; - private static final int DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT = 5000000; /** * If no cancel signal has been received after this amount of time, set the biometric running * state to stopped to allow Keyguard to retry authentication. @@ -231,7 +229,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private static final ComponentName FALLBACK_HOME_COMPONENT = new ComponentName( "com.android.settings", "com.android.settings.FallbackHome"); - /** * If true, the system is in the half-boot-to-decryption-screen state. * Prudently disable lockscreen. @@ -1250,7 +1247,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); } - private boolean isEncryptedOrLockdown(int userId) { + /** + * Returns true if primary authentication is required for the given user due to lockdown + * or encryption after reboot. + */ + public boolean isEncryptedOrLockdown(int userId) { final int strongAuth = mStrongAuthTracker.getStrongAuthForUser(userId); final boolean isLockDown = containsFlag(strongAuth, STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW) @@ -2514,6 +2515,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return mFingerprintLockedOut || mFingerprintLockedOutPermanent; } + public boolean isFaceLockedOut() { + return mFaceLockedOutPermanent; + } + /** * If biometrics hardware is available, not disabled, and user has enrolled templates. * This does NOT check if the device is encrypted or in lockdown. diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java index 9dddbb1d67c6c..c0da57f580432 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java @@ -16,6 +16,7 @@ package com.android.systemui.dagger; +import com.android.keyguard.KeyguardBiometricLockoutLogger; import com.android.systemui.CoreStartable; import com.android.systemui.LatencyTester; import com.android.systemui.ScreenDecorations; @@ -90,6 +91,13 @@ public abstract class SystemUIBinder { @ClassKey(KeyguardViewMediator.class) public abstract CoreStartable bindKeyguardViewMediator(KeyguardViewMediator sysui); + /** Inject into KeyguardBiometricLockoutLogger. */ + @Binds + @IntoMap + @ClassKey(KeyguardBiometricLockoutLogger.class) + public abstract CoreStartable bindKeyguardBiometricLockoutLogger( + KeyguardBiometricLockoutLogger sysui); + /** Inject into LatencyTests. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index bfa4a245070c7..dee1b334182af 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -11,7 +11,7 @@ * 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 + * limitations under the License. */ package com.android.systemui.statusbar.phone; diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt new file mode 100644 index 0000000000000..6bc65054d8301 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt @@ -0,0 +1,194 @@ +/* + * 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.keyguard + +import android.hardware.biometrics.BiometricSourceType +import org.mockito.Mockito.verify +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class KeyguardBiometricLockoutLoggerTest : SysuiTestCase() { + @Mock + lateinit var uiEventLogger: UiEventLogger + @Mock + lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock + lateinit var dumpManager: DumpManager + @Mock + lateinit var strongAuthTracker: KeyguardUpdateMonitor.StrongAuthTracker + + @Captor + lateinit var updateMonitorCallbackCaptor: ArgumentCaptor + lateinit var updateMonitorCallback: KeyguardUpdateMonitorCallback + + lateinit var keyguardBiometricLockoutLogger: KeyguardBiometricLockoutLogger + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(keyguardUpdateMonitor.strongAuthTracker).thenReturn(strongAuthTracker) + keyguardBiometricLockoutLogger = KeyguardBiometricLockoutLogger( + mContext, + uiEventLogger, + keyguardUpdateMonitor, + dumpManager) + } + + @Test + fun test_logsOnStart() { + // GIVEN is encrypted / lockdown before start + whenever(keyguardUpdateMonitor.isEncryptedOrLockdown(anyInt())) + .thenReturn(true) + + // WHEN start + keyguardBiometricLockoutLogger.start() + + // THEN encrypted / lockdown state is logged + verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent + .PRIMARY_AUTH_REQUIRED_ENCRYPTED_OR_LOCKDOWN) + } + + @Test + fun test_logTimeoutChange() { + keyguardBiometricLockoutLogger.start() + captureUpdateMonitorCallback() + + // GIVEN primary auth required b/c timeout + whenever(strongAuthTracker.getStrongAuthForUser(anyInt())) + .thenReturn(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT) + + // WHEN primary auth requirement changes + updateMonitorCallback.onStrongAuthStateChanged(0) + + // THEN primary auth required state is logged + verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent + .PRIMARY_AUTH_REQUIRED_TIMEOUT) + } + + @Test + fun test_logUnattendedUpdate() { + keyguardBiometricLockoutLogger.start() + captureUpdateMonitorCallback() + + // GIVEN primary auth required b/c unattended update + whenever(strongAuthTracker.getStrongAuthForUser(anyInt())) + .thenReturn(STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE) + + // WHEN primary auth requirement changes + updateMonitorCallback.onStrongAuthStateChanged(0) + + // THEN primary auth required state is logged + verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent + .PRIMARY_AUTH_REQUIRED_UNATTENDED_UPDATE) + } + + @Test + fun test_logMultipleChanges() { + keyguardBiometricLockoutLogger.start() + captureUpdateMonitorCallback() + + // GIVEN primary auth required b/c timeout + whenever(strongAuthTracker.getStrongAuthForUser(anyInt())) + .thenReturn(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT + or STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE) + + // WHEN primary auth requirement changes + updateMonitorCallback.onStrongAuthStateChanged(0) + + // THEN primary auth required state is logged with all the reasons + verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent + .PRIMARY_AUTH_REQUIRED_TIMEOUT) + verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent + .PRIMARY_AUTH_REQUIRED_UNATTENDED_UPDATE) + + // WHEN onStrongAuthStateChanged is called again + updateMonitorCallback.onStrongAuthStateChanged(0) + + // THEN no more events are sent since there haven't been any changes + verifyNoMoreInteractions(uiEventLogger) + } + + @Test + fun test_logFaceLockout() { + keyguardBiometricLockoutLogger.start() + captureUpdateMonitorCallback() + + // GIVEN primary auth required b/c face lock + whenever(keyguardUpdateMonitor.isFaceLockedOut).thenReturn(true) + + // WHEN lockout state changes + updateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FACE) + + // THEN primary auth required state is logged + verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent + .PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT) + + // WHEN face lockout is reset + whenever(keyguardUpdateMonitor.isFaceLockedOut).thenReturn(false) + updateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FACE) + + // THEN primary auth required state is logged + verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent + .PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT_RESET) + } + + @Test + fun test_logFingerprintLockout() { + keyguardBiometricLockoutLogger.start() + captureUpdateMonitorCallback() + + // GIVEN primary auth required b/c fingerprint lock + whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true) + + // WHEN lockout state changes + updateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT) + + // THEN primary auth required state is logged + verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent + .PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT) + + // WHEN fingerprint lockout is reset + whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false) + updateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT) + + // THEN primary auth required state is logged + verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent + .PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT_RESET) + } + + fun captureUpdateMonitorCallback() { + verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallbackCaptor.capture()) + updateMonitorCallback = updateMonitorCallbackCaptor.value + } +} \ No newline at end of file