Merge "Update LockSettingsService to support hardened FRP" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index f93eab5..20a621ba 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3683,6 +3683,7 @@
     field public static final String ACTION_INSTANT_APP_RESOLVER_SETTINGS = "android.intent.action.INSTANT_APP_RESOLVER_SETTINGS";
     field @Deprecated public static final String ACTION_INTENT_FILTER_NEEDS_VERIFICATION = "android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION";
     field public static final String ACTION_LOAD_DATA = "android.intent.action.LOAD_DATA";
+    field @FlaggedApi("android.security.frp_enforcement") public static final String ACTION_MAIN_USER_LOCKSCREEN_KNOWLEDGE_FACTOR_CHANGED = "android.intent.action.MAIN_USER_LOCKSCREEN_KNOWLEDGE_FACTOR_CHANGED";
     field @RequiresPermission(android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS) public static final String ACTION_MANAGE_APP_PERMISSION = "android.intent.action.MANAGE_APP_PERMISSION";
     field @Deprecated public static final String ACTION_MANAGE_APP_PERMISSIONS = "android.intent.action.MANAGE_APP_PERMISSIONS";
     field @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public static final String ACTION_MANAGE_DEFAULT_APP = "android.intent.action.MANAGE_DEFAULT_APP";
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index e8031a3..0bcbb8e 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -19,6 +19,7 @@
 import static android.app.sdksandbox.SdkSandboxManager.ACTION_START_SANDBOXED_ACTIVITY;
 import static android.content.ContentProvider.maybeAddUserId;
 import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
+import static android.security.Flags.FLAG_FRP_ENFORCEMENT;
 import static android.service.chooser.Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA;
 
 import android.Manifest;
@@ -3902,6 +3903,26 @@
             "android.intent.action.ACTION_IDLE_MAINTENANCE_END";
 
     /**
+     * Broadcast Action: A broadcast sent to the main user when the main user changes their
+     * Lock Screen Knowledge Factor, either because they changed the current value, or because
+     * they added or removed it.
+     *
+     * <p class="note">At present, this intent is only broadcast to listeners with the
+     * CONFIGURE_FACTORY_RESET_PROTECTION signature|privileged permiession.</p>
+     *
+     * <p class="note">This is a protected intent that can only be sent by the system.</p>
+     *
+     * @hide
+     */
+    @FlaggedApi(FLAG_FRP_ENFORCEMENT)
+    @SystemApi
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @BroadcastBehavior(protectedBroadcast = true)
+    public static final String
+            ACTION_MAIN_USER_LOCKSCREEN_KNOWLEDGE_FACTOR_CHANGED =
+            "android.intent.action.MAIN_USER_LOCKSCREEN_KNOWLEDGE_FACTOR_CHANGED";
+
+    /**
      * Broadcast Action: a remote intent is to be broadcasted.
      *
      * A remote intent is used for remote RPC between devices. The remote intent
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index fb82edc..e7df19c 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -836,6 +836,7 @@
     <protected-broadcast android:name="android.intent.action.PROFILE_AVAILABLE" />
     <protected-broadcast android:name="android.intent.action.PROFILE_UNAVAILABLE" />
     <protected-broadcast android:name="android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED" />
+    <protected-broadcast android:name="android.intent.action.MAIN_USER_LOCKSCREEN_KNOWLEDGE_FACTOR_CHANGED" />
 
     <!-- ====================================================================== -->
     <!--                          RUNTIME PERMISSIONS                           -->
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 7fb3e00..9a76ebd 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -18,6 +18,7 @@
 
 import static android.security.Flags.reportPrimaryAuthAttempts;
 import static android.Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE;
+import static android.Manifest.permission.CONFIGURE_FACTORY_RESET_PROTECTION;
 import static android.Manifest.permission.MANAGE_BIOMETRIC;
 import static android.Manifest.permission.SET_AND_VERIFY_LOCKSCREEN_CREDENTIALS;
 import static android.Manifest.permission.SET_INITIAL_LOCK;
@@ -27,6 +28,7 @@
 import static android.app.admin.DevicePolicyResources.Strings.Core.PROFILE_ENCRYPTED_MESSAGE;
 import static android.app.admin.DevicePolicyResources.Strings.Core.PROFILE_ENCRYPTED_TITLE;
 import static android.content.Context.KEYGUARD_SERVICE;
+import static android.content.Intent.ACTION_MAIN_USER_LOCKSCREEN_KNOWLEDGE_FACTOR_CHANGED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.UserHandle.USER_ALL;
 import static android.os.UserHandle.USER_SYSTEM;
@@ -1201,8 +1203,9 @@
 
         final boolean inSetupWizard = Settings.Secure.getIntForUser(cr,
                 Settings.Secure.USER_SETUP_COMPLETE, 0, mainUserId) == 0;
-        final boolean secureFrp = Settings.Global.getInt(cr,
-                Settings.Global.SECURE_FRP_MODE, 0) == 1;
+        final boolean secureFrp = android.security.Flags.frpEnforcement()
+                ? mStorage.isFactoryResetProtectionActive()
+                : (Settings.Global.getInt(cr, Settings.Global.SECURE_FRP_MODE, 0) == 1);
 
         if (inSetupWizard && secureFrp) {
             throw new SecurityException("Cannot change credential in SUW while factory reset"
@@ -2332,8 +2335,13 @@
 
         synchronized (mSpManager) {
             if (isSpecialUserId(userId)) {
-                return mSpManager.verifySpecialUserCredential(userId, getGateKeeperService(),
+                response = mSpManager.verifySpecialUserCredential(userId, getGateKeeperService(),
                         credential, progressCallback);
+                if (android.security.Flags.frpEnforcement() && response.isMatched()
+                        && userId == USER_FRP) {
+                    mStorage.deactivateFactoryResetProtectionWithoutSecret();
+                }
+                return response;
             }
 
             long protectorId = getCurrentLskfBasedProtectorId(userId);
@@ -3054,6 +3062,7 @@
         setCurrentLskfBasedProtectorId(newProtectorId, userId);
         LockPatternUtils.invalidateCredentialTypeCache();
         synchronizeUnifiedChallengeForProfiles(userId, profilePasswords);
+        sendMainUserCredentialChangedNotificationIfNeeded(userId);
 
         setUserPasswordMetrics(credential, userId);
         mUnifiedProfilePasswordCache.removePassword(userId);
@@ -3071,6 +3080,24 @@
         return newProtectorId;
     }
 
+    private void sendMainUserCredentialChangedNotificationIfNeeded(int userId) {
+        if (!android.security.Flags.frpEnforcement()) {
+            return;
+        }
+
+        if (userId != mInjector.getUserManagerInternal().getMainUserId()) {
+            return;
+        }
+
+        sendBroadcast(new Intent(ACTION_MAIN_USER_LOCKSCREEN_KNOWLEDGE_FACTOR_CHANGED),
+                UserHandle.of(userId), CONFIGURE_FACTORY_RESET_PROTECTION);
+    }
+
+    @VisibleForTesting
+    void sendBroadcast(Intent intent, UserHandle userHandle, String permission) {
+        mContext.sendBroadcastAsUser(intent, userHandle, permission, /* options */ null);
+    }
+
     private void removeBiometricsForUser(int userId) {
         removeAllFingerprintForUser(userId);
         removeAllFaceForUser(userId);
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index 6d123cc..158d444 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -34,6 +34,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.service.persistentdata.PersistentDataBlockManager;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.AtomicFile;
@@ -587,6 +588,10 @@
         return mPersistentDataBlockManagerInternal;
     }
 
+    /**
+     * Writes main user credential handle to the persistent data block, to enable factory reset
+     * protection to be deactivated with the credential.
+     */
     public void writePersistentDataBlock(int persistentType, int userId, int qualityForUi,
             byte[] payload) {
         PersistentDataBlockManagerInternal persistentDataBlock = getPersistentDataBlockManager();
@@ -610,6 +615,31 @@
         }
     }
 
+    public void deactivateFactoryResetProtectionWithoutSecret() {
+        PersistentDataBlockManagerInternal persistentDataBlock = getPersistentDataBlockManager();
+        if (persistentDataBlock != null) {
+            persistentDataBlock.deactivateFactoryResetProtectionWithoutSecret();
+        } else {
+            Slog.wtf(TAG, "Failed to get PersistentDataBlockManagerInternal");
+        }
+    }
+
+    public boolean isFactoryResetProtectionActive() {
+        PersistentDataBlockManager persistentDataBlockManager =
+                mContext.getSystemService(PersistentDataBlockManager.class);
+        if (persistentDataBlockManager != null) {
+            return persistentDataBlockManager.isFactoryResetProtectionActive();
+        } else {
+            Slog.wtf(TAG, "Failed to get PersistentDataBlockManager");
+            // This should never happen, but in the event it does, let's not block the user.  This
+            // may be the wrong call, since if an attacker can find a way to prevent us from
+            // getting the PersistentDataBlockManager they can defeat FRP, but if they can block
+            // access to PersistentDataBlockManager they must have compromised the system and we've
+            // probably already lost this battle.
+            return false;
+        }
+    }
+
     /**
      * Provides a concrete data structure to represent the minimal information from
      * a user's LSKF-based SP protector that is needed to verify the user's LSKF,
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index 6986cab..e59b5ea 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -270,6 +270,9 @@
     }
 
     protected void setSecureFrpMode(boolean secure) {
+        if (android.security.Flags.frpEnforcement()) {
+            mStorage.setTestFactoryResetProtectionState(secure);
+        }
         Settings.Secure.putIntForUser(mContext.getContentResolver(),
                 Settings.Secure.SECURE_FRP_MODE, secure ? 1 : 0, UserHandle.USER_SYSTEM);
     }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
index ee076c6..296d2cb 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
@@ -21,12 +21,14 @@
 import android.app.IActivityManager;
 import android.app.admin.DeviceStateCache;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.UserInfo;
 import android.hardware.authsecret.IAuthSecret;
 import android.os.Handler;
 import android.os.Parcel;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.os.storage.IStorageManager;
 import android.security.KeyStore;
 import android.security.keystore.KeyPermanentlyInvalidatedException;
@@ -41,6 +43,9 @@
 import java.io.FileNotFoundException;
 
 public class LockSettingsServiceTestable extends LockSettingsService {
+    private Intent mSavedFrpNotificationIntent = null;
+    private UserHandle mSavedFrpNotificationUserHandle = null;
+    private String mSavedFrpNotificationPermission = null;
 
     public static class MockInjector extends LockSettingsService.Injector {
 
@@ -218,4 +223,29 @@
             mAuthSecret = null;
         }
     }
+
+    @Override
+    void sendBroadcast(Intent intent, UserHandle userHandle, String permission) {
+        mSavedFrpNotificationIntent = intent;
+        mSavedFrpNotificationUserHandle = userHandle;
+        mSavedFrpNotificationPermission = permission;
+    }
+
+    String getSavedFrpNotificationPermission() {
+        return mSavedFrpNotificationPermission;
+    }
+
+    UserHandle getSavedFrpNotificationUserHandle() {
+        return mSavedFrpNotificationUserHandle;
+    }
+
+    Intent getSavedFrpNotificationIntent() {
+        return mSavedFrpNotificationIntent;
+    }
+
+    void clearRecordedFrpNotificationData() {
+        mSavedFrpNotificationIntent = null;
+        mSavedFrpNotificationPermission = null;
+        mSavedFrpNotificationUserHandle = null;
+    }
 }
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 7053597..4b22652 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,7 @@
 
 package com.android.server.locksettings;
 
+import static android.Manifest.permission.CONFIGURE_FACTORY_RESET_PROTECTION;
 import static android.security.Flags.FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS;
 
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
@@ -39,7 +40,9 @@
 import static org.mockito.Mockito.when;
 
 import android.app.PropertyInvalidatedCache;
+import android.content.Intent;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.gatekeeper.GateKeeperResponse;
@@ -239,6 +242,12 @@
     }
 
     @Test
+    public void testSetLockCredential_forPrimaryUser_sendsFrpNotification() throws Exception {
+        setCredential(PRIMARY_USER_ID, newPassword("password"));
+        checkRecordedFrpNotificationIntent();
+    }
+
+    @Test
     public void testSetLockCredential_forPrimaryUser_sendsCredentials() throws Exception {
         setCredential(PRIMARY_USER_ID, newPassword("password"));
         verify(mRecoverableKeyStoreManager)
@@ -323,6 +332,15 @@
     }
 
     @Test
+    public void testClearLockCredential_sendsFrpNotification() throws Exception {
+        setCredential(PRIMARY_USER_ID, newPassword("password"));
+        checkRecordedFrpNotificationIntent();
+        mService.clearRecordedFrpNotificationData();
+        clearCredential(PRIMARY_USER_ID, newPassword("password"));
+        checkRecordedFrpNotificationIntent();
+    }
+
+    @Test
     public void testSetLockCredential_forUnifiedToSeparateChallengeProfile_sendsNewCredentials()
             throws Exception {
         final LockscreenCredential parentPassword = newPassword("parentPassword");
@@ -519,6 +537,23 @@
         mService.setString(null, "value", 0);
     }
 
+    private void checkRecordedFrpNotificationIntent() {
+        if (android.security.Flags.frpEnforcement()) {
+            Intent savedNotificationIntent = mService.getSavedFrpNotificationIntent();
+            assertNotNull(savedNotificationIntent);
+            UserHandle userHandle = mService.getSavedFrpNotificationUserHandle();
+            assertEquals(userHandle,
+                    UserHandle.of(mInjector.getUserManagerInternal().getMainUserId()));
+
+            String permission = mService.getSavedFrpNotificationPermission();
+            assertEquals(CONFIGURE_FACTORY_RESET_PROTECTION, permission);
+        } else {
+            assertNull(mService.getSavedFrpNotificationIntent());
+            assertNull(mService.getSavedFrpNotificationUserHandle());
+            assertNull(mService.getSavedFrpNotificationPermission());
+        }
+    }
+
     private void checkPasswordHistoryLength(int userId, int expectedLen) {
         String history = mService.getString(LockPatternUtils.PASSWORD_HISTORY_KEY, "", userId);
         String[] hashes = TextUtils.split(history, LockPatternUtils.PASSWORD_HISTORY_DELIMITER);
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
index fa3c7a4c..c01d0f6 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
@@ -35,6 +35,7 @@
     public final File mStorageDir;
     public PersistentDataBlockManagerInternal mPersistentDataBlockManager;
     private byte[] mPersistentData;
+    private boolean mIsFactoryResetProtectionActive = false;
 
     public LockSettingsStorageTestable(Context context, File storageDir) {
         super(context);
@@ -63,6 +64,10 @@
         }).when(mPersistentDataBlockManager).getFrpCredentialHandle();
     }
 
+    void setTestFactoryResetProtectionState(boolean active) {
+        mIsFactoryResetProtectionActive = active;
+    }
+
     @Override
     File getChildProfileLockFile(int userId) {
         return remapToStorageDir(super.getChildProfileLockFile(userId));
@@ -101,4 +106,9 @@
         mappedPath.getParentFile().mkdirs();
         return mappedPath;
     }
+
+    @Override
+    public boolean isFactoryResetProtectionActive() {
+        return mIsFactoryResetProtectionActive;
+    }
 }