From 5a4e185a612d87e667ee62a329f10bfd01c23179 Mon Sep 17 00:00:00 2001
From: Haining Chen <hainingc@google.com>
Date: Wed, 17 Jan 2024 11:26:04 -0800
Subject: [PATCH] Report primary auth attempts from LockSettingsService

Report authentication attempts with PIN/pattern/password from
LockSettingsService as the source of truth. Also add a new listener to
allow other system components to response to primary auth results

Bug: 285053096
Test: m -j
Test: atest LockSettingsServiceTests
Change-Id: I9115b70742cb724b373c028d8e00d178aa1a8096
---
 .../widget/ILockSettingsStateListener.aidl    | 36 ++++++++++
 .../internal/widget/LockSettingsInternal.java | 12 ++++
 .../locksettings/LockSettingsService.java     | 46 ++++++++++++
 .../LockSettingsServiceTests.java             | 70 +++++++++++++++++++
 4 files changed, 164 insertions(+)
 create mode 100644 core/java/com/android/internal/widget/ILockSettingsStateListener.aidl

diff --git a/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl b/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl
new file mode 100644
index 000000000000..25e30034fe8f
--- /dev/null
+++ b/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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.internal.widget;
+
+/**
+ * Callback interface between LockSettingService and other system services to be notified about the
+ * state of primary authentication (i.e. PIN/pattern/password).
+ * @hide
+ */
+oneway interface ILockSettingsStateListener {
+    /**
+     * Defines behavior in response to a successful authentication
+     * @param userId The user Id for the requested authentication
+     */
+    void onAuthenticationSucceeded(int userId);
+
+    /**
+     * Defines behavior in response to a failed authentication
+     * @param userId The user Id for the requested authentication
+     */
+    void onAuthenticationFailed(int userId);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/widget/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java
index 8114e1fd3bb0..627e8779f9d0 100644
--- a/core/java/com/android/internal/widget/LockSettingsInternal.java
+++ b/core/java/com/android/internal/widget/LockSettingsInternal.java
@@ -166,4 +166,16 @@ public abstract class LockSettingsInternal {
      * Refreshes pending strong auth timeout with the latest admin requirement set by device policy.
      */
     public abstract void refreshStrongAuthTimeout(int userId);
+
+    /**
+     * Register a LockSettingsStateListener
+     * @param listener The listener to be registered
+     */
+    public abstract void registerLockSettingsStateListener(ILockSettingsStateListener listener);
+
+    /**
+     * Unregister a LockSettingsStateListener
+     * @param listener The listener to be unregistered
+     */
+    public abstract void unregisterLockSettingsStateListener(ILockSettingsStateListener listener);
 }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index ad090829a2f6..c81ce26b6403 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.locksettings;
 
+import static android.security.Flags.reportPrimaryAuthAttempts;
 import static android.Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE;
 import static android.Manifest.permission.MANAGE_BIOMETRIC;
 import static android.Manifest.permission.SET_AND_VERIFY_LOCKSCREEN_CREDENTIALS;
@@ -90,6 +91,7 @@ import android.os.Handler;
 import android.os.IBinder;
 import android.os.IProgressListener;
 import android.os.Process;
+import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
@@ -135,6 +137,7 @@ import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
 import com.android.internal.widget.ICheckCredentialProgressCallback;
 import com.android.internal.widget.ILockSettings;
+import com.android.internal.widget.ILockSettingsStateListener;
 import com.android.internal.widget.IWeakEscrowTokenActivatedListener;
 import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
 import com.android.internal.widget.LockPatternUtils;
@@ -327,6 +330,9 @@ public class LockSettingsService extends ILockSettings.Stub {
 
     private HashMap<UserHandle, UserManager> mUserManagerCache = new HashMap<>();
 
+    private final RemoteCallbackList<ILockSettingsStateListener> mLockSettingsStateListeners =
+            new RemoteCallbackList<>();
+
     // This class manages life cycle events for encrypted users on File Based Encryption (FBE)
     // devices. The most basic of these is to show/hide notifications about missing features until
     // the user unlocks the account and credential-encrypted storage is available.
@@ -2342,9 +2348,37 @@ public class LockSettingsService extends ILockSettings.Stub {
                 requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId);
             }
         }
+        if (reportPrimaryAuthAttempts()) {
+            final boolean success =
+                    response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK;
+            notifyLockSettingsStateListeners(success, userId);
+        }
         return response;
     }
 
+    private void notifyLockSettingsStateListeners(boolean success, int userId) {
+        int i = mLockSettingsStateListeners.beginBroadcast();
+        try {
+            while (i > 0) {
+                i--;
+                try {
+                    if (success) {
+                        mLockSettingsStateListeners.getBroadcastItem(i)
+                                .onAuthenticationSucceeded(userId);
+                    } else {
+                        mLockSettingsStateListeners.getBroadcastItem(i)
+                                .onAuthenticationFailed(userId);
+                    }
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Exception while notifying LockSettingsStateListener:"
+                            + " success = " + success + ", userId = " + userId, e);
+                }
+            }
+        } finally {
+            mLockSettingsStateListeners.finishBroadcast();
+        }
+    }
+
     @Override
     public VerifyCredentialResponse verifyTiedProfileChallenge(LockscreenCredential credential,
             int userId, @LockPatternUtils.VerifyFlag int flags) {
@@ -3662,6 +3696,18 @@ public class LockSettingsService extends ILockSettings.Stub {
         public void refreshStrongAuthTimeout(int userId) {
             mStrongAuth.refreshStrongAuthTimeout(userId);
         }
+
+        @Override
+        public void registerLockSettingsStateListener(
+                @NonNull ILockSettingsStateListener listener) {
+            mLockSettingsStateListeners.register(listener);
+        }
+
+        @Override
+        public void unregisterLockSettingsStateListener(
+                @NonNull ILockSettingsStateListener listener) {
+            mLockSettingsStateListeners.unregister(listener);
+        }
     }
 
     private class RebootEscrowCallbacks implements RebootEscrowManager.Callbacks {
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index 5a62d92e8e12..5081198f0058 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -16,6 +16,8 @@
 
 package com.android.server.locksettings;
 
+import static android.security.Flags.FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS;
+
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
@@ -30,25 +32,30 @@ import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.PropertyInvalidatedCache;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.gatekeeper.GateKeeperResponse;
 import android.text.TextUtils;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.widget.ILockSettingsStateListener;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.LockscreenCredential;
 import com.android.internal.widget.VerifyCredentialResponse;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -59,6 +66,7 @@ import org.junit.runner.RunWith;
 @Presubmit
 @RunWith(AndroidJUnit4.class)
 public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Before
     public void setUp() {
@@ -398,6 +406,60 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
                         eq(CREDENTIAL_TYPE_PASSWORD), any(), eq(MANAGED_PROFILE_USER_ID));
     }
 
+    @Test
+    public void testVerifyCredential_notifyLockSettingsStateListeners_whenGoodPassword()
+            throws Exception {
+        mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS);
+        final LockscreenCredential password = newPassword("password");
+        setCredential(PRIMARY_USER_ID, password);
+        final ILockSettingsStateListener listener = mockLockSettingsStateListener();
+        mLocalService.registerLockSettingsStateListener(listener);
+
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */)
+                        .getResponseCode());
+
+        verify(listener).onAuthenticationSucceeded(PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testVerifyCredential_notifyLockSettingsStateListeners_whenBadPassword()
+            throws Exception {
+        mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS);
+        final LockscreenCredential password = newPassword("password");
+        setCredential(PRIMARY_USER_ID, password);
+        final LockscreenCredential badPassword = newPassword("badPassword");
+        final ILockSettingsStateListener listener = mockLockSettingsStateListener();
+        mLocalService.registerLockSettingsStateListener(listener);
+
+        assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
+                mService.verifyCredential(badPassword, PRIMARY_USER_ID, 0 /* flags */)
+                        .getResponseCode());
+
+        verify(listener).onAuthenticationFailed(PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testLockSettingsStateListener_registeredThenUnregistered() throws Exception {
+        mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS);
+        final LockscreenCredential password = newPassword("password");
+        setCredential(PRIMARY_USER_ID, password);
+        final LockscreenCredential badPassword = newPassword("badPassword");
+        final ILockSettingsStateListener listener = mockLockSettingsStateListener();
+
+        mLocalService.registerLockSettingsStateListener(listener);
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */)
+                        .getResponseCode());
+        verify(listener).onAuthenticationSucceeded(PRIMARY_USER_ID);
+
+        mLocalService.unregisterLockSettingsStateListener(listener);
+        assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
+                mService.verifyCredential(badPassword, PRIMARY_USER_ID, 0 /* flags */)
+                        .getResponseCode());
+        verify(listener, never()).onAuthenticationFailed(PRIMARY_USER_ID);
+    }
+
     @Test
     public void testSetCredentialNotPossibleInSecureFrpModeDuringSuw() {
         setUserSetupComplete(false);
@@ -537,4 +599,12 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
             assertNotEquals(0, mGateKeeperService.getSecureUserId(userId));
         }
     }
+
+    private ILockSettingsStateListener mockLockSettingsStateListener() {
+        ILockSettingsStateListener listener = mock(ILockSettingsStateListener.Stub.class);
+        IBinder binder = mock(IBinder.class);
+        when(binder.isBinderAlive()).thenReturn(true);
+        when(listener.asBinder()).thenReturn(binder);
+        return listener;
+    }
 }
-- 
GitLab