Merge "Harden Factory Reset Protection" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index bfa486b..f41982f 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -40961,6 +40961,14 @@
 
 }
 
+package android.service.persistentdata {
+
+  @FlaggedApi("android.security.frp_enforcement") public class PersistentDataBlockManager {
+    method @FlaggedApi("android.security.frp_enforcement") public boolean isFactoryResetProtectionActive();
+  }
+
+}
+
 package android.service.quickaccesswallet {
 
   public interface GetWalletCardsCallback {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index a942e0d..45d5778 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -113,6 +113,7 @@
     field public static final String CLEAR_APP_USER_DATA = "android.permission.CLEAR_APP_USER_DATA";
     field public static final String COMPANION_APPROVE_WIFI_CONNECTIONS = "android.permission.COMPANION_APPROVE_WIFI_CONNECTIONS";
     field public static final String CONFIGURE_DISPLAY_BRIGHTNESS = "android.permission.CONFIGURE_DISPLAY_BRIGHTNESS";
+    field @FlaggedApi("android.security.frp_enforcement") public static final String CONFIGURE_FACTORY_RESET_PROTECTION = "android.permission.CONFIGURE_FACTORY_RESET_PROTECTION";
     field public static final String CONFIGURE_INTERACT_ACROSS_PROFILES = "android.permission.CONFIGURE_INTERACT_ACROSS_PROFILES";
     field @Deprecated public static final String CONNECTIVITY_INTERNAL = "android.permission.CONNECTIVITY_INTERNAL";
     field public static final String CONNECTIVITY_USE_RESTRICTED_NETWORKS = "android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS";
@@ -12698,13 +12699,15 @@
 
 package android.service.persistentdata {
 
-  public class PersistentDataBlockManager {
+  @FlaggedApi("android.security.frp_enforcement") public class PersistentDataBlockManager {
+    method @FlaggedApi("android.security.frp_enforcement") @RequiresPermission(android.Manifest.permission.CONFIGURE_FACTORY_RESET_PROTECTION) public boolean deactivateFactoryResetProtection(@NonNull byte[]);
     method @RequiresPermission(android.Manifest.permission.ACCESS_PDB_STATE) public int getDataBlockSize();
     method @RequiresPermission(anyOf={android.Manifest.permission.READ_OEM_UNLOCK_STATE, "android.permission.OEM_UNLOCK_STATE"}) public int getFlashLockState();
     method public long getMaximumDataBlockSize();
     method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.READ_OEM_UNLOCK_STATE, "android.permission.OEM_UNLOCK_STATE"}) public boolean getOemUnlockEnabled();
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_PDB_STATE) public String getPersistentDataPackageName();
     method public byte[] read();
+    method @FlaggedApi("android.security.frp_enforcement") public boolean setFactoryResetProtectionSecret(@NonNull byte[]);
     method @Deprecated @RequiresPermission("android.permission.OEM_UNLOCK_STATE") public void setOemUnlockEnabled(boolean);
     method @RequiresPermission("android.permission.OEM_UNLOCK_STATE") public void wipe();
     method public int write(byte[]);
diff --git a/core/java/android/service/persistentdata/IPersistentDataBlockService.aidl b/core/java/android/service/persistentdata/IPersistentDataBlockService.aidl
index 11e5ad8..21801c0 100644
--- a/core/java/android/service/persistentdata/IPersistentDataBlockService.aidl
+++ b/core/java/android/service/persistentdata/IPersistentDataBlockService.aidl
@@ -38,5 +38,33 @@
     int getFlashLockState();
     boolean hasFrpCredentialHandle();
     String getPersistentDataPackageName();
-}
 
+    /**
+     * Returns true if Factory Reset Protection (FRP) is active, meaning the device rebooted and has
+     * not been able to transition to the FRP inactive state.
+     */
+    boolean isFactoryResetProtectionActive();
+
+    /**
+     * Attempts to deactivate Factory Reset Protection (FRP) with the provided secret.  If the
+     * provided secret matches the stored FRP secret, FRP is deactivated and the method returns
+     * true.  Otherwise, FRP state remains unchanged and the method returns false.
+     */
+    boolean deactivateFactoryResetProtection(in byte[] secret);
+
+    /**
+     * Stores the provided Factory Reset Protection (FRP) secret as the secret to be used for future
+     * FRP deactivation.  The secret must be 32 bytes in length.  Setting the all-zeros "default"
+     * value disables the FRP feature entirely.
+     *
+     * It's the responsibility of the caller to ensure that copies of the FRP secret are stored
+     * securely where they can be recovered and used to deactivate FRP after an untrusted reset.
+     * This method will store a copy in /data/system and use that to automatically deactivate FRP
+     * until /data is wiped.
+     *
+     * Note that this method does nothing if FRP is currently active.
+     *
+     * Returns true if the secret was successfully changed, false otherwise.
+     */
+    boolean setFactoryResetProtectionSecret(in byte[] secret);
+}
diff --git a/core/java/android/service/persistentdata/PersistentDataBlockManager.java b/core/java/android/service/persistentdata/PersistentDataBlockManager.java
index 6da3206..9b9cc19 100644
--- a/core/java/android/service/persistentdata/PersistentDataBlockManager.java
+++ b/core/java/android/service/persistentdata/PersistentDataBlockManager.java
@@ -16,6 +16,7 @@
 
 package android.service.persistentdata;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
@@ -24,30 +25,17 @@
 import android.annotation.SystemService;
 import android.content.Context;
 import android.os.RemoteException;
+import android.security.Flags;
 import android.service.oemlock.OemLockManager;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * Interface for reading and writing data blocks to a persistent partition.
- *
- * Allows writing one block at a time. Namely, each time
- * {@link PersistentDataBlockManager#write(byte[])}
- * is called, it will overwite the data that was previously written on the block.
- *
- * Clients can query the size of the currently written block via
- * {@link PersistentDataBlockManager#getDataBlockSize()}.
- *
- * Clients can query the maximum size for a block via
- * {@link PersistentDataBlockManager#getMaximumDataBlockSize()}
- *
- * Clients can read the currently written block by invoking
- * {@link PersistentDataBlockManager#read()}.
- *
- * @hide
+ * Interface to the persistent data partition.  Provides access to information about the state
+ * of factory reset protection.
  */
-@SystemApi
+@FlaggedApi(Flags.FLAG_FRP_ENFORCEMENT)
 @SystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE)
 public class PersistentDataBlockManager {
     private static final String TAG = PersistentDataBlockManager.class.getSimpleName();
@@ -55,18 +43,32 @@
 
     /**
      * Indicates that the device's bootloader lock state is UNKNOWN.
+     *
+     * @hide
      */
+    @SystemApi
     public static final int FLASH_LOCK_UNKNOWN = -1;
     /**
      * Indicates that the device's bootloader is UNLOCKED.
+     *
+     * @hide
      */
+    @SystemApi
     public static final int FLASH_LOCK_UNLOCKED = 0;
     /**
      * Indicates that the device's bootloader is LOCKED.
+     *
+     * @hide
      */
+    @SystemApi
     public static final int FLASH_LOCK_LOCKED = 1;
 
-    /** @removed mistakenly exposed previously */
+    /**
+     * @removed mistakenly exposed previously
+     *
+     * @hide
+     */
+    @SystemApi
     @IntDef(prefix = { "FLASH_LOCK_" }, value = {
             FLASH_LOCK_UNKNOWN,
             FLASH_LOCK_LOCKED,
@@ -75,7 +77,9 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface FlashLockState {}
 
-    /** @hide */
+    /**
+     * @hide
+     */
     public PersistentDataBlockManager(IPersistentDataBlockService service) {
         sService = service;
     }
@@ -91,7 +95,10 @@
      * in which case -1 will be returned.
      *
      * @param data the data to write
+     *
+     * @hide
      */
+    @SystemApi
     @SuppressLint("RequiresPermission")
     public int write(byte[] data) {
         try {
@@ -103,7 +110,10 @@
 
     /**
      * Returns the data block stored on the persistent partition.
+     *
+     * @hide
      */
+    @SystemApi
     @SuppressLint("RequiresPermission")
     public byte[] read() {
         try {
@@ -117,7 +127,10 @@
      * Retrieves the size of the block currently written to the persistent partition.
      *
      * Return -1 on error.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(android.Manifest.permission.ACCESS_PDB_STATE)
     public int getDataBlockSize() {
         try {
@@ -131,7 +144,10 @@
      * Retrieves the maximum size allowed for a data block.
      *
      * Returns -1 on error.
+     *
+     * @hide
      */
+    @SystemApi
     @SuppressLint("RequiresPermission")
     public long getMaximumDataBlockSize() {
         try {
@@ -146,7 +162,10 @@
      * will erase all data written to the persistent data partition.
      * It will also prevent any further {@link #write} operation until reboot,
      * in order to prevent a potential race condition. See b/30352311.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(android.Manifest.permission.OEM_UNLOCK_STATE)
     public void wipe() {
         try {
@@ -160,7 +179,11 @@
      * Writes a byte enabling or disabling the ability to "OEM unlock" the device.
      *
      * @deprecated use {@link OemLockManager#setOemUnlockAllowedByUser(boolean)} instead.
+     *
+     * @hide
      */
+    @SystemApi
+    @Deprecated
     @RequiresPermission(android.Manifest.permission.OEM_UNLOCK_STATE)
     public void setOemUnlockEnabled(boolean enabled) {
         try {
@@ -174,7 +197,11 @@
      * Returns whether or not "OEM unlock" is enabled or disabled on this device.
      *
      * @deprecated use {@link OemLockManager#isOemUnlockAllowedByUser()} instead.
+     *
+     * @hide
      */
+    @SystemApi
+    @Deprecated
     @RequiresPermission(anyOf = {
             android.Manifest.permission.READ_OEM_UNLOCK_STATE,
             android.Manifest.permission.OEM_UNLOCK_STATE
@@ -193,7 +220,10 @@
      * @return {@link #FLASH_LOCK_LOCKED} if device bootloader is locked,
      * {@link #FLASH_LOCK_UNLOCKED} if device bootloader is unlocked, or {@link #FLASH_LOCK_UNKNOWN}
      * if this information cannot be ascertained on this device.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(anyOf = {
             android.Manifest.permission.READ_OEM_UNLOCK_STATE,
             android.Manifest.permission.OEM_UNLOCK_STATE
@@ -222,4 +252,73 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Returns true if FactoryResetProtection (FRP) is active, meaning the device rebooted and has
+     * not been able to deactivate FRP because the deactivation secrets were wiped by an untrusted
+     * factory reset.
+     */
+    @FlaggedApi(Flags.FLAG_FRP_ENFORCEMENT)
+    public boolean isFactoryResetProtectionActive() {
+        try {
+            return sService.isFactoryResetProtectionActive();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Attempt to deactivate FRP with the provided secret.  If the provided secret matches the
+     * stored FRP secret, FRP is deactivated and the method returns true.  Otherwise, FRP state
+     * remains unchanged and the method returns false.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_FRP_ENFORCEMENT)
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.CONFIGURE_FACTORY_RESET_PROTECTION)
+    public boolean deactivateFactoryResetProtection(@NonNull byte[] secret) {
+        try {
+            return sService.deactivateFactoryResetProtection(secret);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Store the provided FRP secret as the secret to be used for future FRP deactivation.  The
+     * secret must be 32 bytes in length.  Setting the all-zeros "default" value disables the FRP
+     * feature entirely.
+     *
+     * To ensure that the device doesn't end up in a bad state if a crash occurs, this method
+     * should be used in a three-step process:
+     *
+     * 1.  Generate a new secret and securely store any necessary copies (e.g. by encrypting them
+     *     and calling #write with a new data block that contains both the old encrypted secret
+     *     copies and the new ones).
+     * 2.  Call this method to set the new FRP secret.  This will also write the copy used during
+     *     normal boot.
+     * 3.  Delete any old FRP secret copies (e.g. by calling #write with a new data block that
+     *     contains only the new encrypted secret copies).
+     *
+     * Note that this method does nothing if FRP is currently active.
+     *
+     * This method does not require any permission, but can be called only by the
+     * PersistentDataBlockService's authorized caller UID.
+     *
+     * Returns true if the new secret was successfully written.  Returns false if FRP is currently
+     * active.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_FRP_ENFORCEMENT)
+    @SystemApi
+    @SuppressLint("RequiresPermission")
+    public boolean setFactoryResetProtectionSecret(@NonNull byte[] secret) {
+        try {
+            return sService.setFactoryResetProtectionSecret(secret);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 374c312..a425bb0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2772,6 +2772,12 @@
     <permission android:name="android.permission.OEM_UNLOCK_STATE"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi Allows configuration of factory reset protection
+         @FlaggedApi("android.security.frp_enforcement")
+         @hide <p>Not for use by third-party applications. -->
+    <permission android:name="android.permission.CONFIGURE_FACTORY_RESET_PROTECTION"
+        android:protectionLevel="signature|privileged" />
+
     <!-- @SystemApi @hide Allows querying state of PersistentDataBlock
    <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.ACCESS_PDB_STATE"
diff --git a/services/core/java/com/android/server/pdb/PersistentDataBlockManagerInternal.java b/services/core/java/com/android/server/pdb/PersistentDataBlockManagerInternal.java
index 66ad716..a56406e 100644
--- a/services/core/java/com/android/server/pdb/PersistentDataBlockManagerInternal.java
+++ b/services/core/java/com/android/server/pdb/PersistentDataBlockManagerInternal.java
@@ -49,4 +49,10 @@
 
     /** Retrieves the UID that can access the persistent data partition. */
     int getAllowedUid();
+
+    /**
+     * Attempt to deactivate Factory Reset Protection (FRP) without a secret.  Returns true if
+     * successful, false if not.
+     */
+    boolean deactivateFactoryResetProtectionWithoutSecret();
 }
diff --git a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
index b9b09fb..133fc8f 100644
--- a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
@@ -18,16 +18,31 @@
 
 import static com.android.internal.util.Preconditions.checkArgument;
 
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.SYNC;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+
 import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.os.Binder;
+import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.ShellCommand;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
+import android.security.Flags;
 import android.service.persistentdata.IPersistentDataBlockService;
 import android.service.persistentdata.PersistentDataBlockManager;
 import android.text.TextUtils;
@@ -56,10 +71,10 @@
 import java.nio.channels.FileChannel;
 import java.nio.file.Files;
 import java.nio.file.Paths;
-import java.nio.file.StandardOpenOption;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
+import java.util.HexFormat;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -85,9 +100,14 @@
  * | --------------------------------------------|
  * | FRP data block length (4 bytes)             |
  * | --------------------------------------------|
- * | FRP data (variable length)                  |
+ * | FRP data (variable length; 100KB max)    |
  * | --------------------------------------------|
  * | ...                                         |
+ * | Empty space.                                |
+ * | ...                                         |
+ * | --------------------------------------------|
+ * | FRP secret magic (8 bytes)                  |
+ * | FRP secret (32 bytes)                       |
  * | --------------------------------------------|
  * | Test mode data block (10000 bytes)          |
  * | --------------------------------------------|
@@ -127,6 +147,14 @@
     /** Maximum size of the FRP credential handle that can be stored. */
     @VisibleForTesting
     static final int MAX_FRP_CREDENTIAL_HANDLE_SIZE = FRP_CREDENTIAL_RESERVED_SIZE - 4;
+    /** Size of the FRP mode deactivation secret, in bytes */
+    @VisibleForTesting
+    static final int FRP_SECRET_SIZE = 32;
+    /** Magic value to identify the FRP secret is present. */
+    @VisibleForTesting
+    static final byte[] FRP_SECRET_MAGIC = {(byte) 0xda, (byte) 0xc2, (byte) 0xfc,
+            (byte) 0xcd, (byte) 0xb9, 0x1b, 0x09, (byte) 0x88};
+
     /**
      * Size of the block reserved for Test Harness Mode data, including 4 bytes for the size header.
      */
@@ -145,21 +173,52 @@
     private static final String FLASH_LOCK_LOCKED = "1";
     private static final String FLASH_LOCK_UNLOCKED = "0";
 
+    /**
+     * Path to FRP secret stored on /data.  This file enables automatic deactivation of FRP mode if
+     * it contains the current FRP secret.  When /data is wiped in an untrusted reset this file is
+     * destroyed, blocking automatic deactivation.
+     */
+    private static final String FRP_SECRET_FILE = "/data/system/frp_secret";
+
+    /**
+     * Path to temp file used when changing the FRP secret.
+     */
+    private static final String FRP_SECRET_TMP_FILE = "/data/system/frp_secret_tmp";
+
+    public static final String BOOTLOADER_LOCK_STATE = "ro.boot.vbmeta.device_state";
+    public static final String VERIFIED_BOOT_STATE = "ro.boot.verifiedbootstate";
+    public static final int INIT_WAIT_TIMEOUT = 10;
+
     private final Context mContext;
     private final String mDataBlockFile;
     private final boolean mIsFileBacked;
     private final Object mLock = new Object();
     private final CountDownLatch mInitDoneSignal = new CountDownLatch(1);
+    private final String mFrpSecretFile;
+    private final String mFrpSecretTmpFile;
 
     private int mAllowedUid = -1;
     private long mBlockDeviceSize = -1; // Load lazily
 
+    private final boolean mFrpEnforced;
+
+    /**
+     * FRP active state.  When true (the default) we may have had an untrusted factory reset. In
+     * that case we block any updates of the persistent data block.  To exit active state, it's
+     * necessary for some caller to provide the FRP secret.
+     */
+    private boolean mFrpActive = false;
+
     @GuardedBy("mLock")
     private boolean mIsWritable = true;
 
     public PersistentDataBlockService(Context context) {
         super(context);
         mContext = context;
+        mFrpEnforced = Flags.frpEnforcement();
+        mFrpActive = mFrpEnforced;
+        mFrpSecretFile = FRP_SECRET_FILE;
+        mFrpSecretTmpFile = FRP_SECRET_TMP_FILE;
         if (SystemProperties.getBoolean(GSI_RUNNING_PROP, false)) {
             mIsFileBacked = true;
             mDataBlockFile = GSI_SANDBOX;
@@ -171,12 +230,17 @@
 
     @VisibleForTesting
     PersistentDataBlockService(Context context, boolean isFileBacked, String dataBlockFile,
-            long blockDeviceSize) {
+            long blockDeviceSize, boolean frpEnabled, String frpSecretFile,
+            String frpSecretTmpFile) {
         super(context);
         mContext = context;
         mIsFileBacked = isFileBacked;
         mDataBlockFile = dataBlockFile;
         mBlockDeviceSize = blockDeviceSize;
+        mFrpEnforced = frpEnabled;
+        mFrpActive = mFrpEnforced;
+        mFrpSecretFile = frpSecretFile;
+        mFrpSecretTmpFile = frpSecretTmpFile;
     }
 
     private int getAllowedUid() {
@@ -206,24 +270,35 @@
         // Do init on a separate thread, will join in PHASE_ACTIVITY_MANAGER_READY
         SystemServerInitThreadPool.submit(() -> {
             enforceChecksumValidity();
-            formatIfOemUnlockEnabled();
+            if (mFrpEnforced) {
+                automaticallyDeactivateFrpIfPossible();
+                setOemUnlockEnabledProperty(doGetOemUnlockEnabled());
+                // Set the SECURE_FRP_MODE flag, for backward compatibility with clients who use it.
+                // They should switch to calling #isFrpActive().
+                Settings.Global.putInt(mContext.getContentResolver(),
+                        Settings.Global.SECURE_FRP_MODE, mFrpActive ? 1 : 0);
+            } else {
+                formatIfOemUnlockEnabled();
+            }
             publishBinderService(Context.PERSISTENT_DATA_BLOCK_SERVICE, mService);
-            mInitDoneSignal.countDown();
+            signalInitDone();
         }, TAG + ".onStart");
     }
 
+    @VisibleForTesting
+    void signalInitDone() {
+        mInitDoneSignal.countDown();
+    }
+
+    private void setOemUnlockEnabledProperty(boolean oemUnlockEnabled) {
+        setProperty(OEM_UNLOCK_PROP, oemUnlockEnabled ? "1" : "0");
+    }
+
     @Override
     public void onBootPhase(int phase) {
         // Wait for initialization in onStart to finish
         if (phase == PHASE_SYSTEM_SERVICES_READY) {
-            try {
-                if (!mInitDoneSignal.await(10, TimeUnit.SECONDS)) {
-                    throw new IllegalStateException("Service " + TAG + " init timeout");
-                }
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                throw new IllegalStateException("Service " + TAG + " init interrupted", e);
-            }
+            waitForInitDoneSignal();
             // The user responsible for FRP should exist by now.
             mAllowedUid = getAllowedUid();
             LocalServices.addService(PersistentDataBlockManagerInternal.class, mInternalService);
@@ -231,6 +306,17 @@
         super.onBootPhase(phase);
     }
 
+    private void waitForInitDoneSignal() {
+        try {
+            if (!mInitDoneSignal.await(INIT_WAIT_TIMEOUT, TimeUnit.SECONDS)) {
+                throw new IllegalStateException("Service " + TAG + " init timeout");
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("Service " + TAG + " init interrupted", e);
+        }
+    }
+
     @VisibleForTesting
     void setAllowedUid(int uid) {
         mAllowedUid = uid;
@@ -243,8 +329,7 @@
                 formatPartitionLocked(true);
             }
         }
-
-        setProperty(OEM_UNLOCK_PROP, enabled ? "1" : "0");
+        setOemUnlockEnabledProperty(enabled);
     }
 
     private void enforceOemUnlockReadPermission() {
@@ -263,9 +348,18 @@
                 "Can't modify OEM unlock state");
     }
 
+    private void enforceConfigureFrpPermission() {
+        if (mFrpEnforced && mContext.checkCallingOrSelfPermission(
+                Manifest.permission.CONFIGURE_FACTORY_RESET_PROTECTION)
+                == PackageManager.PERMISSION_DENIED) {
+            throw new SecurityException(("Can't configure Factory Reset Protection. Requires "
+                    + "CONFIGURE_FACTORY_RESET_PROTECTION"));
+        }
+    }
+
     private void enforceUid(int callingUid) {
-        if (callingUid != mAllowedUid) {
-            throw new SecurityException("uid " + callingUid + " not allowed to access PST");
+        if (callingUid != mAllowedUid && callingUid != UserHandle.AID_ROOT) {
+            throw new SecurityException("uid " + callingUid + " not allowed to access PDB");
         }
     }
 
@@ -315,7 +409,9 @@
 
     @VisibleForTesting
     int getMaximumFrpDataSize() {
-        return (int) (getTestHarnessModeDataOffset() - DIGEST_SIZE_BYTES - HEADER_SIZE);
+        long frpSecretSize = mFrpEnforced ? FRP_SECRET_MAGIC.length + FRP_SECRET_SIZE : 0;
+        return (int) (getTestHarnessModeDataOffset() - DIGEST_SIZE_BYTES - HEADER_SIZE
+                - frpSecretSize);
     }
 
     @VisibleForTesting
@@ -324,6 +420,16 @@
     }
 
     @VisibleForTesting
+    long getFrpSecretMagicOffset() {
+        return getFrpSecretDataOffset() - FRP_SECRET_MAGIC.length;
+    }
+
+    @VisibleForTesting
+    long getFrpSecretDataOffset() {
+        return getTestHarnessModeDataOffset() - FRP_SECRET_SIZE;
+    }
+
+    @VisibleForTesting
     long getTestHarnessModeDataOffset() {
         return getFrpCredentialDataOffset() - TEST_MODE_RESERVED_SIZE;
     }
@@ -349,6 +455,11 @@
     }
 
     private FileChannel getBlockOutputChannel() throws IOException {
+        enforceFactoryResetProtectionInactive();
+        return getBlockOutputChannelIgnoringFrp();
+    }
+
+    private FileChannel getBlockOutputChannelIgnoringFrp() throws FileNotFoundException {
         return new RandomAccessFile(mDataBlockFile, "rw").getChannel();
     }
 
@@ -416,7 +527,7 @@
     @VisibleForTesting
     void formatPartitionLocked(boolean setOemUnlockEnabled) {
 
-        try (FileChannel channel = getBlockOutputChannel()) {
+        try (FileChannel channel = getBlockOutputChannelIgnoringFrp()) {
             // Format the data selectively.
             //
             // 1. write header, set length = 0
@@ -431,12 +542,18 @@
 
             // 2. corrupt the legacy FRP data explicitly
             int payload_size = (int) getBlockDeviceSize() - header_size;
-            buf = ByteBuffer.allocate(payload_size
-                          - TEST_MODE_RESERVED_SIZE - FRP_CREDENTIAL_RESERVED_SIZE - 1);
+            if (mFrpEnforced) {
+                buf = ByteBuffer.allocate(payload_size - TEST_MODE_RESERVED_SIZE
+                        - FRP_SECRET_MAGIC.length - FRP_SECRET_SIZE - FRP_CREDENTIAL_RESERVED_SIZE
+                        - 1);
+            } else {
+                buf = ByteBuffer.allocate(payload_size - TEST_MODE_RESERVED_SIZE
+                        - FRP_CREDENTIAL_RESERVED_SIZE - 1);
+            }
             channel.write(buf);
             channel.force(true);
 
-            // 3. skip the test mode data and leave it unformat
+            // 3. skip the test mode data and leave it unformatted.
             //    This is for a feature that enables testing.
             channel.position(channel.position() + TEST_MODE_RESERVED_SIZE);
 
@@ -451,6 +568,11 @@
             buf.flip();
             channel.write(buf);
             channel.force(true);
+
+            // 6. Write the default FRP secret (all zeros).
+            if (mFrpEnforced) {
+                writeFrpMagicAndDefaultSecret();
+            }
         } catch (IOException e) {
             Slog.e(TAG, "failed to format block", e);
             return;
@@ -460,10 +582,198 @@
         computeAndWriteDigestLocked();
     }
 
+    /**
+     * Try to deactivate FRP by presenting an FRP secret from the data partition, or the default
+     * secret if the secret(s) on the data partition are not present or don't work.
+     */
+    @VisibleForTesting
+    boolean automaticallyDeactivateFrpIfPossible() {
+        synchronized (mLock) {
+            if (deactivateFrpWithFileSecret(mFrpSecretFile)) {
+                return true;
+            }
+
+            Slog.w(TAG, "Failed to deactivate with primary secret file, trying backup.");
+            if (deactivateFrpWithFileSecret(mFrpSecretTmpFile)) {
+                // The backup file has the FRP secret, make it the primary file.
+                moveFrpTempFileToPrimary();
+                return true;
+            }
+
+            Slog.w(TAG, "Failed to deactivate with backup secret file, trying default secret.");
+            if (deactivateFrp(new byte[FRP_SECRET_SIZE])) {
+                return true;
+            }
+
+            // We could not deactivate FRP.  It's possible that we have hit an obscure corner case,
+            // a device that once ran a version of Android that set the FRP magic and a secret,
+            // then downgraded to a version that did not know about FRP, wiping the FRP secrets
+            // files, then upgraded to a version (the current one) that does know about FRP,
+            // potentially leaving the user unable to deactivate FRP because all copies of the
+            // secret are gone.
+            //
+            // To handle this case, we check to see if we have recently upgraded from a pre-V
+            // version.  If so, we deactivate FRP and set the secret to the default value.
+            if (isUpgradingFromPreVRelease()) {
+                Slog.w(TAG, "Upgrading from Android 14 or lower, defaulting FRP secret");
+                writeFrpMagicAndDefaultSecret();
+                mFrpActive = false;
+                return true;
+            }
+
+            Slog.e(TAG, "Did not find valid FRP secret, FRP remains active.");
+            return false;
+        }
+    }
+
+    private boolean deactivateFrpWithFileSecret(String frpSecretFile) {
+        try {
+            return deactivateFrp(Files.readAllBytes(Paths.get(frpSecretFile)));
+        } catch (IOException e) {
+            Slog.w(TAG, "Failed to read FRP secret file: " + frpSecretFile + " "
+                    + e.getClass().getSimpleName());
+            return false;
+        }
+    }
+
+    private void moveFrpTempFileToPrimary() {
+        try {
+            Files.move(Paths.get(mFrpSecretTmpFile), Paths.get(mFrpSecretFile), REPLACE_EXISTING);
+        } catch (IOException e) {
+            Slog.e(TAG, "Error moving FRP backup file to primary (ignored)", e);
+        }
+    }
+
+    @VisibleForTesting
+    boolean isFrpActive() {
+        waitForInitDoneSignal();
+        synchronized (mLock) {
+            return mFrpActive;
+        }
+    }
+
+    /**
+     * Write the provided secret to the FRP secret file in /data and to the /persist partition.
+     *
+     * Writing is a three-step process, to ensure that we can recover from a crash at any point.
+     */
+    private boolean updateFrpSecret(byte[] secret) {
+        // 1.  Write the new secret to a temporary file, and sync the write.
+        try {
+            Files.write(
+                    Paths.get(mFrpSecretTmpFile), secret, WRITE, CREATE, TRUNCATE_EXISTING, SYNC);
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to write FRP secret file", e);
+            return false;
+        }
+
+        // 2.  Write the new secret to /persist, and sync the write.
+        if (!mInternalService.writeDataBuffer(getFrpSecretDataOffset(), ByteBuffer.wrap(secret))) {
+            return false;
+        }
+
+        // 3.  Move the temporary file to the primary file location.  Syncing doesn't matter
+        //     here.  In the event this update doesn't complete it will get done by
+        //     #automaticallyDeactivateFrpIfPossible() during the next boot.
+        moveFrpTempFileToPrimary();
+        return true;
+    }
+
+    /**
+     * Only for testing, activate FRP.
+     */
+    @VisibleForTesting
+    void activateFrp() {
+        synchronized (mLock) {
+            mFrpActive = true;
+        }
+    }
+
+    private boolean hasFrpSecretMagic() {
+        final byte[] frpMagic =
+                readDataBlock(getFrpSecretMagicOffset(), FRP_SECRET_MAGIC.length);
+        if (frpMagic == null) {
+            // Transient read error on the partition?
+            Slog.e(TAG, "Failed to read FRP magic region.");
+            return false;
+        }
+        return Arrays.equals(frpMagic, FRP_SECRET_MAGIC);
+    }
+
+    private byte[] getFrpSecret() {
+        return readDataBlock(getFrpSecretDataOffset(), FRP_SECRET_SIZE);
+    }
+
+    private boolean deactivateFrp(byte[] secret) {
+        if (secret == null || secret.length != FRP_SECRET_SIZE) {
+            Slog.w(TAG, "Attempted to deactivate FRP with a null or incorrectly-sized secret");
+            return false;
+        }
+
+        synchronized (mLock) {
+            if (!hasFrpSecretMagic()) {
+                Slog.i(TAG, "No FRP secret magic, system must have been upgraded.");
+                writeFrpMagicAndDefaultSecret();
+            }
+        }
+
+        final byte[] partitionSecret = getFrpSecret();
+        if (partitionSecret == null || partitionSecret.length != FRP_SECRET_SIZE) {
+            Slog.e(TAG, "Failed to read FRP secret from persistent data partition");
+            return false;
+        }
+
+        // MessageDigest.isEqual is constant-time, to protect secret deduction by timing attack.
+        if (MessageDigest.isEqual(secret, partitionSecret)) {
+            mFrpActive = false;
+            Slog.i(TAG, "FRP secret matched, FRP deactivated.");
+            return true;
+        } else {
+            Slog.e(TAG,
+                    "FRP deactivation failed with secret " + HexFormat.of().formatHex(secret));
+            return false;
+        }
+    }
+
+    private void writeFrpMagicAndDefaultSecret() {
+        try (FileChannel channel = getBlockOutputChannelIgnoringFrp()) {
+            synchronized (mLock) {
+                // Write secret first in case we crash between the writes, causing the first write
+                // to be synced but the second to be lost.
+                Slog.i(TAG, "Writing default FRP secret");
+                channel.position(getFrpSecretDataOffset());
+                channel.write(ByteBuffer.allocate(FRP_SECRET_SIZE));
+                channel.force(true);
+
+                Slog.i(TAG, "Writing FRP secret magic");
+                channel.position(getFrpSecretMagicOffset());
+                channel.write(ByteBuffer.wrap(FRP_SECRET_MAGIC));
+                channel.force(true);
+
+                mFrpActive = false;
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to write FRP magic and default secret", e);
+        }
+    }
+
+    @VisibleForTesting
+    byte[] readDataBlock(long offset, int length) {
+        try (DataInputStream inputStream =
+                     new DataInputStream(new FileInputStream(new File(mDataBlockFile)))) {
+            synchronized (mLock) {
+                inputStream.skip(offset);
+                byte[] bytes = new byte[length];
+                inputStream.readFully(bytes);
+                return bytes;
+            }
+        } catch (IOException e) {
+            throw new IllegalStateException("persistent partition not readable", e);
+        }
+    }
+
     private void doSetOemUnlockEnabledLocked(boolean enabled) {
-
         try (FileChannel channel = getBlockOutputChannel()) {
-
             channel.position(getBlockDeviceSize() - 1);
 
             ByteBuffer data = ByteBuffer.allocate(1);
@@ -475,7 +785,7 @@
             Slog.e(TAG, "unable to access persistent partition", e);
             return;
         } finally {
-            setProperty(OEM_UNLOCK_PROP, enabled ? "1" : "0");
+            setOemUnlockEnabledProperty(enabled);
         }
     }
 
@@ -507,8 +817,10 @@
     }
 
     private long doGetMaximumDataBlockSize() {
-        long actualSize = getBlockDeviceSize() - HEADER_SIZE - DIGEST_SIZE_BYTES
-                - TEST_MODE_RESERVED_SIZE - FRP_CREDENTIAL_RESERVED_SIZE - 1;
+        final long frpSecretSize =
+                mFrpEnforced ? (FRP_SECRET_MAGIC.length + FRP_SECRET_SIZE) : 0;
+        final long actualSize = getBlockDeviceSize() - HEADER_SIZE - DIGEST_SIZE_BYTES
+                - TEST_MODE_RESERVED_SIZE - frpSecretSize - FRP_CREDENTIAL_RESERVED_SIZE - 1;
         return actualSize <= MAX_DATA_BLOCK_SIZE ? actualSize : MAX_DATA_BLOCK_SIZE;
     }
 
@@ -526,6 +838,140 @@
     }
 
     private final IBinder mService = new IPersistentDataBlockService.Stub() {
+        private int printFrpStatus(PrintWriter pw, boolean printSecrets) {
+            enforceUid(Binder.getCallingUid());
+
+            pw.println("FRP state");
+            pw.println("=========");
+            pw.println("Enforcement enabled: " + mFrpEnforced);
+            pw.println("FRP state: " + mFrpActive);
+            printFrpDataFilesContents(pw, printSecrets);
+            printFrpSecret(pw, printSecrets);
+            pw.println("OEM unlock state: " + getOemUnlockEnabled());
+            pw.println("Bootloader lock state: " + getFlashLockState());
+            pw.println("Verified boot state: " + getVerifiedBootState());
+            pw.println("Has FRP credential handle: " + hasFrpCredentialHandle());
+            pw.println("FRP challenge block size: " + getDataBlockSize());
+            return 1;
+        }
+
+        private void printFrpSecret(PrintWriter pw, boolean printSecret) {
+            if (hasFrpSecretMagic()) {
+                if (printSecret) {
+                    pw.println("FRP secret in PDB: " + HexFormat.of().formatHex(
+                            readDataBlock(getFrpSecretDataOffset(), FRP_SECRET_SIZE)));
+                } else {
+                    pw.println("FRP secret present but omitted.");
+                }
+            } else {
+                pw.println("FRP magic not found");
+            }
+        }
+
+        private void printFrpDataFilesContents(PrintWriter pw, boolean printSecrets) {
+            printFrpDataFileContents(pw, mFrpSecretFile, printSecrets);
+            printFrpDataFileContents(pw, mFrpSecretTmpFile, printSecrets);
+        }
+
+        private void printFrpDataFileContents(
+                PrintWriter pw, String frpSecretFile, boolean printSecret) {
+            if (Files.exists(Paths.get(frpSecretFile))) {
+                if (printSecret) {
+                    try {
+                        pw.println("FRP secret in " + frpSecretFile + ": " + HexFormat.of()
+                                .formatHex(Files.readAllBytes(Paths.get(mFrpSecretFile))));
+                    } catch (IOException e) {
+                        Slog.e(TAG, "Failed to read " + frpSecretFile, e);
+                    }
+                } else {
+                    pw.println(
+                            "FRP secret file " + frpSecretFile + " exists, contents omitted.");
+                }
+            }
+        }
+
+        @Override
+        public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
+                @Nullable FileDescriptor err,
+                @NonNull String[] args, @Nullable ShellCallback callback,
+                @NonNull ResultReceiver resultReceiver) throws RemoteException {
+            if (!mFrpEnforced) {
+                super.onShellCommand(in, out, err, args, callback, resultReceiver);
+                return;
+            }
+            new ShellCommand(){
+                @Override
+                public int onCommand(final String cmd) {
+                    if (cmd == null) {
+                        return handleDefaultCommands(cmd);
+                    }
+
+                    final PrintWriter pw = getOutPrintWriter();
+                    return switch (cmd) {
+                        case "status" -> printFrpStatus(pw, /* printSecrets */ !mFrpActive);
+                        case "activate" -> {
+                            activateFrp();
+                            yield printFrpStatus(pw, /* printSecrets */ !mFrpActive);
+                        }
+
+                        case "deactivate" -> {
+                            byte[] secret = hashSecretString(getNextArg());
+                            pw.println("Attempting to deactivate with: " + HexFormat.of().formatHex(
+                                    secret));
+                            pw.println("Deactivation "
+                                    + (deactivateFrp(secret) ? "succeeded" : "failed"));
+                            yield printFrpStatus(pw, /* printSecrets */ !mFrpActive);
+                        }
+
+                        case "auto_deactivate" -> {
+                            boolean result = automaticallyDeactivateFrpIfPossible();
+                            pw.println(
+                                    "Automatic deactivation " + (result ? "succeeded" : "failed"));
+                            yield printFrpStatus(pw, /* printSecrets */ !mFrpActive);
+                        }
+
+                        case "set_secret" -> {
+                            byte[] secret = new byte[FRP_SECRET_SIZE];
+                            String secretString = getNextArg();
+                            if (!secretString.equals("default")) {
+                                secret = hashSecretString(secretString);
+                            }
+                            pw.println("Setting FRP secret to: " + HexFormat.of()
+                                    .formatHex(secret) + " length: " + secret.length);
+                            setFactoryResetProtectionSecret(secret);
+                            yield printFrpStatus(pw, /* printSecrets */ !mFrpActive);
+                        }
+
+                        default -> handleDefaultCommands(cmd);
+                    };
+                }
+
+                @Override
+                public void onHelp() {
+                    final PrintWriter pw = getOutPrintWriter();
+                    pw.println("Commands");
+                    pw.println("status: Print the FRP state and associated information.");
+                    pw.println("activate:  Put FRP into \"active\" mode.");
+                    pw.println("deactivate <secret>:  Deactivate with a hash of 'secret'.");
+                    pw.println("auto_deactivate: Deactivate with the stored secret or the default");
+                    pw.println("set_secret <secret>:  Set the stored secret to a hash of `secret`");
+                }
+
+                private static byte[] hashSecretString(String secretInput) {
+                    try {
+                        // SHA-256 produces 32-byte outputs, same as the FRP secret size, so it's
+                        // a convenient way to "normalize" the length of whatever the user provided.
+                        // Also, hashing makes it difficult for an attacker to set the secret to a
+                        // known value that was randomly generated.
+                        MessageDigest md = MessageDigest.getInstance("SHA-256");
+                        return md.digest(secretInput.getBytes());
+                    } catch (NoSuchAlgorithmException e) {
+                        Slog.e(TAG, "Can't happen", e);
+                        return new byte[FRP_SECRET_SIZE];
+                    }
+                }
+            }.exec(this, in, out, err, args, callback, resultReceiver);
+        }
 
         /**
          * Write the data to the persistent data block.
@@ -545,7 +991,7 @@
             }
 
             ByteBuffer headerAndData = ByteBuffer.allocate(
-                                           data.length + HEADER_SIZE + DIGEST_SIZE_BYTES);
+                    data.length + HEADER_SIZE + DIGEST_SIZE_BYTES);
             headerAndData.put(new byte[DIGEST_SIZE_BYTES]);
             headerAndData.putInt(PARTITION_TYPE_MARKER);
             headerAndData.putInt(data.length);
@@ -619,6 +1065,7 @@
 
         @Override
         public void wipe() {
+            enforceFactoryResetProtectionInactive();
             enforceOemUnlockWritePermission();
 
             synchronized (mLock) {
@@ -626,7 +1073,7 @@
                 if (mIsFileBacked) {
                     try {
                         Files.write(Paths.get(mDataBlockFile), new byte[MAX_DATA_BLOCK_SIZE],
-                                StandardOpenOption.TRUNCATE_EXISTING);
+                                TRUNCATE_EXISTING);
                         ret = 0;
                     } catch (IOException e) {
                         ret = -1;
@@ -685,6 +1132,10 @@
             }
         }
 
+        private static String getVerifiedBootState() {
+            return SystemProperties.get(VERIFIED_BOOT_STATE);
+        }
+
         @Override
         public int getDataBlockSize() {
             enforcePersistentDataBlockAccess();
@@ -716,6 +1167,18 @@
             }
         }
 
+        private void enforceConfigureFrpPermissionOrPersistentDataBlockAccess() {
+            if (!mFrpEnforced) {
+                enforcePersistentDataBlockAccess();
+            } else {
+                if (mContext.checkCallingOrSelfPermission(
+                        Manifest.permission.CONFIGURE_FACTORY_RESET_PROTECTION)
+                        == PackageManager.PERMISSION_DENIED) {
+                    enforcePersistentDataBlockAccess();
+                }
+            }
+        }
+
         @Override
         public long getMaximumDataBlockSize() {
             enforceUid(Binder.getCallingUid());
@@ -724,7 +1187,7 @@
 
         @Override
         public boolean hasFrpCredentialHandle() {
-            enforcePersistentDataBlockAccess();
+            enforceConfigureFrpPermissionOrPersistentDataBlockAccess();
             try {
                 return mInternalService.getFrpCredentialHandle() != null;
             } catch (IllegalStateException e) {
@@ -751,9 +1214,51 @@
             synchronized (mLock) {
                 pw.println("mIsWritable: " + mIsWritable);
             }
+            printFrpStatus(pw, /* printSecrets */ false);
+        }
+
+        @Override
+        public boolean isFactoryResetProtectionActive() {
+            return isFrpActive();
+        }
+
+        @Override
+        public boolean deactivateFactoryResetProtection(byte[] secret) {
+            enforceConfigureFrpPermission();
+            return deactivateFrp(secret);
+        }
+
+        @Override
+        public boolean setFactoryResetProtectionSecret(byte[] secret) {
+            enforceUid(Binder.getCallingUid());
+            if (secret == null || secret.length != FRP_SECRET_SIZE) {
+                throw new IllegalArgumentException(
+                        "Invalid FRP secret: " + HexFormat.of().formatHex(secret));
+            }
+            enforceFactoryResetProtectionInactive();
+            return updateFrpSecret(secret);
         }
     };
 
+    private void enforceFactoryResetProtectionInactive() {
+        if (mFrpEnforced && isFrpActive()) {
+            throw new SecurityException("FRP is active");
+        }
+    }
+
+    @VisibleForTesting
+    boolean isUpgradingFromPreVRelease() {
+        PackageManagerInternal packageManagerInternal =
+                LocalServices.getService(PackageManagerInternal.class);
+        if (packageManagerInternal == null) {
+            Slog.e(TAG, "Unable to retrieve PackageManagerInternal");
+            return false;
+        }
+
+        return packageManagerInternal
+                .isUpgradingFromLowerThan(Build.VERSION_CODES.VANILLA_ICE_CREAM);
+    }
+
     private InternalService mInternalService = new InternalService();
 
     private class InternalService implements PersistentDataBlockManagerInternal {
@@ -792,6 +1297,14 @@
             return mAllowedUid;
         }
 
+        @Override
+        public boolean deactivateFactoryResetProtectionWithoutSecret() {
+            synchronized (mLock) {
+                mFrpActive = false;
+            }
+            return true;
+        }
+
         private void writeInternal(byte[] data, long offset, int dataLength) {
             checkArgument(data == null || data.length > 0, "data must be null or non-empty");
             checkArgument(
@@ -808,10 +1321,10 @@
             writeDataBuffer(offset, dataBuffer);
         }
 
-        private void writeDataBuffer(long offset, ByteBuffer dataBuffer) {
+        private boolean writeDataBuffer(long offset, ByteBuffer dataBuffer) {
             synchronized (mLock) {
                 if (!mIsWritable) {
-                    return;
+                    return false;
                 }
                 try (FileChannel channel = getBlockOutputChannel()) {
                     channel.position(offset);
@@ -819,10 +1332,10 @@
                     channel.force(true);
                 } catch (IOException e) {
                     Slog.e(TAG, "unable to access persistent partition", e);
-                    return;
+                    return false;
                 }
 
-                computeAndWriteDigestLocked();
+                return computeAndWriteDigestLocked();
             }
         }
 
@@ -864,5 +1377,5 @@
                 computeAndWriteDigestLocked();
             }
         }
-    };
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java b/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
index f537efd..da8ec2e 100644
--- a/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
@@ -17,12 +17,14 @@
 package com.android.server.pdb;
 
 import static com.android.server.pdb.PersistentDataBlockService.DIGEST_SIZE_BYTES;
+import static com.android.server.pdb.PersistentDataBlockService.FRP_SECRET_SIZE;
 import static com.android.server.pdb.PersistentDataBlockService.MAX_DATA_BLOCK_SIZE;
 import static com.android.server.pdb.PersistentDataBlockService.MAX_FRP_CREDENTIAL_HANDLE_SIZE;
 import static com.android.server.pdb.PersistentDataBlockService.MAX_TEST_MODE_DATA_SIZE;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doNothing;
@@ -30,7 +32,8 @@
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
-import static org.junit.Assert.assertThrows;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 
 import android.Manifest;
 import android.content.Context;
@@ -45,7 +48,6 @@
 import junitparams.JUnitParamsRunner;
 import junitparams.Parameters;
 
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -54,9 +56,13 @@
 import org.mockito.MockitoAnnotations;
 
 import java.io.File;
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
+import java.nio.file.Files;
 import java.nio.file.StandardOpenOption;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 
 @RunWith(JUnitParamsRunner.class)
 public class PersistentDataBlockServiceTest {
@@ -64,20 +70,31 @@
 
     private static final byte[] SMALL_DATA = "data to write".getBytes();
     private static final byte[] ANOTHER_SMALL_DATA = "something else".getBytes();
+    public static final int DEFAULT_BLOCK_DEVICE_SIZE = -1;
 
     private Context mContext;
     private PersistentDataBlockService mPdbService;
     private IPersistentDataBlockService mInterface;
     private PersistentDataBlockManagerInternal mInternalInterface;
     private File mDataBlockFile;
+    private File mFrpSecretFile;
+    private File mFrpSecretTmpFile;
     private String mOemUnlockPropertyValue;
+    private boolean mIsUpgradingFromPreV = false;
 
     @Mock private UserManager mUserManager;
 
     private class FakePersistentDataBlockService extends PersistentDataBlockService {
+
         FakePersistentDataBlockService(Context context, String dataBlockFile,
-                long blockDeviceSize) {
-            super(context, /* isFileBacked */ true, dataBlockFile, blockDeviceSize);
+                long blockDeviceSize, boolean frpEnabled, String frpSecretFile,
+                String frpSecretTmpFile) {
+            super(context, /* isFileBacked */ true, dataBlockFile, blockDeviceSize, frpEnabled,
+                    frpSecretFile, frpSecretTmpFile);
+            // In the real service, this is done by onStart(), which we don't want to call because
+            // it registers the service, etc.  But we need to signal init done to prevent
+            // `isFrpActive` from blocking.
+            signalInitDone();
         }
 
         @Override
@@ -86,18 +103,25 @@
             assertThat(key).isEqualTo("sys.oem_unlock_allowed");
             mOemUnlockPropertyValue = value;
         }
+
+        @Override
+        boolean isUpgradingFromPreVRelease() {
+            return mIsUpgradingFromPreV;
+        }
     }
 
     @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
 
-    @Before
-    public void setUp() throws Exception {
+    private void setUp(boolean frpEnabled) throws Exception {
         MockitoAnnotations.initMocks(this);
 
         mDataBlockFile = mTemporaryFolder.newFile();
+        mFrpSecretFile = mTemporaryFolder.newFile();
+        mFrpSecretTmpFile = mTemporaryFolder.newFile();
         mContext = spy(ApplicationProvider.getApplicationContext());
         mPdbService = new FakePersistentDataBlockService(mContext, mDataBlockFile.getPath(),
-                /* blockDeviceSize */ -1);
+                DEFAULT_BLOCK_DEVICE_SIZE, frpEnabled, mFrpSecretFile.getPath(),
+                mFrpSecretTmpFile.getPath());
         mPdbService.setAllowedUid(Binder.getCallingUid());
         mPdbService.formatPartitionLocked(/* setOemUnlockEnabled */ false);
         mInterface = mPdbService.getInterfaceForTesting();
@@ -119,9 +143,7 @@
      * a block implementation for the read/write operations.
      */
     public Object[][] getTestParametersForBlocks() {
-        return new Object[][] {
-            {
-                new Block() {
+        Block simpleReadWrite = new Block() {
                     @Override public int write(byte[] data) throws RemoteException {
                         return service.getInterfaceForTesting().write(data);
                     }
@@ -129,10 +151,8 @@
                     @Override public byte[] read() throws RemoteException {
                         return service.getInterfaceForTesting().read();
                     }
-                },
-            },
-            {
-                new Block() {
+                };
+        Block credHandle =  new Block() {
                     @Override public int write(byte[] data) {
                         service.getInternalInterfaceForTesting().setFrpCredentialHandle(data);
                         // The written size isn't returned. Pretend it's fully written in the
@@ -143,10 +163,8 @@
                     @Override public byte[] read() {
                         return service.getInternalInterfaceForTesting().getFrpCredentialHandle();
                     }
-                },
-            },
-            {
-                new Block() {
+                };
+        Block testHarness = new Block() {
                     @Override public int write(byte[] data) {
                         service.getInternalInterfaceForTesting().setTestHarnessModeData(data);
                         // The written size isn't returned. Pretend it's fully written in the
@@ -157,14 +175,21 @@
                     @Override public byte[] read() {
                         return service.getInternalInterfaceForTesting().getTestHarnessModeData();
                     }
-                },
-            },
+                };
+        return new Object[][] {
+                { simpleReadWrite, false },
+                { simpleReadWrite, true },
+                { credHandle, false },
+                { credHandle, true },
+                { testHarness, false },
+                { testHarness, true },
         };
     }
 
     @Test
     @Parameters(method = "getTestParametersForBlocks")
-    public void writeThenRead(Block block) throws Exception {
+    public void writeThenRead(Block block, boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         block.service = mPdbService;
         assertThat(block.write(SMALL_DATA)).isEqualTo(SMALL_DATA.length);
         assertThat(block.read()).isEqualTo(SMALL_DATA);
@@ -172,7 +197,8 @@
 
     @Test
     @Parameters(method = "getTestParametersForBlocks")
-    public void writeWhileAlreadyCorrupted(Block block) throws Exception {
+    public void writeWhileAlreadyCorrupted(Block block, boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         block.service = mPdbService;
         assertThat(block.write(SMALL_DATA)).isEqualTo(SMALL_DATA.length);
         assertThat(block.read()).isEqualTo(SMALL_DATA);
@@ -184,7 +210,9 @@
     }
 
     @Test
-    public void frpWriteOutOfBound() throws Exception {
+    @Parameters({"false", "true"})
+    public void frpWriteOutOfBound(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         byte[] maxData = new byte[mPdbService.getMaximumFrpDataSize()];
         assertThat(mInterface.write(maxData)).isEqualTo(maxData.length);
 
@@ -193,7 +221,9 @@
     }
 
     @Test
-    public void frpCredentialWriteOutOfBound() throws Exception {
+    @Parameters({"false", "true"})
+    public void frpCredentialWriteOutOfBound(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         byte[] maxData = new byte[MAX_FRP_CREDENTIAL_HANDLE_SIZE];
         mInternalInterface.setFrpCredentialHandle(maxData);
 
@@ -203,7 +233,9 @@
     }
 
     @Test
-    public void testHardnessWriteOutOfBound() throws Exception {
+    @Parameters({"false", "true"})
+    public void testHardnessWriteOutOfBound(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         byte[] maxData = new byte[MAX_TEST_MODE_DATA_SIZE];
         mInternalInterface.setTestHarnessModeData(maxData);
 
@@ -213,7 +245,9 @@
     }
 
     @Test
-    public void readCorruptedFrpData() throws Exception {
+    @Parameters({"false", "true"})
+    public void readCorruptedFrpData(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         assertThat(mInterface.write(SMALL_DATA)).isEqualTo(SMALL_DATA.length);
         assertThat(mInterface.read()).isEqualTo(SMALL_DATA);
 
@@ -224,7 +258,9 @@
     }
 
     @Test
-    public void readCorruptedFrpCredentialData() throws Exception {
+    @Parameters({"false", "true"})
+    public void readCorruptedFrpCredentialData(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         mInternalInterface.setFrpCredentialHandle(SMALL_DATA);
         assertThat(mInternalInterface.getFrpCredentialHandle()).isEqualTo(SMALL_DATA);
 
@@ -235,7 +271,9 @@
     }
 
     @Test
-    public void readCorruptedTestHarnessData() throws Exception {
+    @Parameters({"false", "true"})
+    public void readCorruptedTestHarnessData(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         mInternalInterface.setTestHarnessModeData(SMALL_DATA);
         assertThat(mInternalInterface.getTestHarnessModeData()).isEqualTo(SMALL_DATA);
 
@@ -246,14 +284,18 @@
     }
 
     @Test
-    public void nullWrite() throws Exception {
+    @Parameters({"false", "true"})
+    public void nullWrite(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         assertThrows(NullPointerException.class, () -> mInterface.write(null));
         mInternalInterface.setFrpCredentialHandle(null);  // no exception
         mInternalInterface.setTestHarnessModeData(null);  // no exception
     }
 
     @Test
-    public void emptyDataWrite() throws Exception {
+    @Parameters({"false", "true"})
+    public void emptyDataWrite(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         var empty = new byte[0];
         assertThat(mInterface.write(empty)).isEqualTo(0);
 
@@ -264,10 +306,13 @@
     }
 
     @Test
-    public void frpWriteMoreThan100K() throws Exception {
+    @Parameters({"false", "true"})
+    public void frpWriteMoreThan100K(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         File dataBlockFile = mTemporaryFolder.newFile();
         PersistentDataBlockService pdbService = new FakePersistentDataBlockService(mContext,
-                dataBlockFile.getPath(), /* blockDeviceSize */ 128 * 1000);
+                dataBlockFile.getPath(), /* blockDeviceSize */ 128 * 1000, frpEnabled,
+                /* frpSecretFile */ null, /* frpSecretTmpFile */ null);
         pdbService.setAllowedUid(Binder.getCallingUid());
         pdbService.formatPartitionLocked(/* setOemUnlockEnabled */ false);
 
@@ -278,30 +323,39 @@
     }
 
     @Test
-    public void frpBlockReadWriteWithoutPermission() throws Exception {
+    @Parameters({"false", "true"})
+    public void frpBlockReadWriteWithoutPermission(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         mPdbService.setAllowedUid(Binder.getCallingUid() + 1);  // unexpected uid
         assertThrows(SecurityException.class, () -> mInterface.write(SMALL_DATA));
         assertThrows(SecurityException.class, () -> mInterface.read());
     }
 
     @Test
-    public void getMaximumDataBlockSizeDenied() throws Exception {
+    @Parameters({"false", "true"})
+    public void getMaximumDataBlockSizeDenied(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         mPdbService.setAllowedUid(Binder.getCallingUid() + 1);  // unexpected uid
         assertThrows(SecurityException.class, () -> mInterface.getMaximumDataBlockSize());
     }
 
     @Test
-    public void getMaximumDataBlockSize() throws Exception {
+    @Parameters({"false", "true"})
+    public void getMaximumDataBlockSize(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         mPdbService.setAllowedUid(Binder.getCallingUid());
         assertThat(mInterface.getMaximumDataBlockSize())
                 .isEqualTo(mPdbService.getMaximumFrpDataSize());
     }
 
     @Test
-    public void getMaximumDataBlockSizeOfLargerPartition() throws Exception {
+    @Parameters({"false", "true"})
+    public void getMaximumDataBlockSizeOfLargerPartition(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         File dataBlockFile = mTemporaryFolder.newFile();
         PersistentDataBlockService pdbService = new FakePersistentDataBlockService(mContext,
-                dataBlockFile.getPath(), /* blockDeviceSize */ 128 * 1000);
+                dataBlockFile.getPath(), /* blockDeviceSize */ 128 * 1000, frpEnabled,
+                /* frpSecretFile */null, /* mFrpSecretTmpFile */ null);
         pdbService.setAllowedUid(Binder.getCallingUid());
         pdbService.formatPartitionLocked(/* setOemUnlockEnabled */ false);
 
@@ -310,7 +364,9 @@
     }
 
     @Test
-    public void getFrpDataBlockSizeGrantedByUid() throws Exception {
+    @Parameters({"false", "true"})
+    public void getFrpDataBlockSizeGrantedByUid(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         assertThat(mInterface.write(SMALL_DATA)).isEqualTo(SMALL_DATA.length);
 
         mPdbService.setAllowedUid(Binder.getCallingUid());
@@ -323,7 +379,9 @@
     }
 
     @Test
-    public void getFrpDataBlockSizeGrantedByPermission() throws Exception {
+    @Parameters({"false", "true"})
+    public void getFrpDataBlockSizeGrantedByPermission(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         assertThat(mInterface.write(SMALL_DATA)).isEqualTo(SMALL_DATA.length);
 
         mPdbService.setAllowedUid(Binder.getCallingUid() + 1);  // unexpected uid
@@ -338,13 +396,17 @@
     }
 
     @Test
-    public void wipePermissionCheck() throws Exception {
+    @Parameters({"false", "true"})
+    public void wipePermissionCheck(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         denyOemUnlockPermission();
         assertThrows(SecurityException.class, () -> mInterface.wipe());
     }
 
     @Test
-    public void wipeMakesItNotWritable() throws Exception {
+    @Parameters({"false", "true"})
+    public void wipeMakesItNotWritable(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         grantOemUnlockPermission();
         mInterface.wipe();
 
@@ -368,7 +430,9 @@
     }
 
     @Test
-    public void hasFrpCredentialHandleGrantedByUid() throws Exception {
+    @Parameters({"false", "true"})
+    public void hasFrpCredentialHandle_GrantedByUid(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         mPdbService.setAllowedUid(Binder.getCallingUid());
 
         assertThat(mInterface.hasFrpCredentialHandle()).isFalse();
@@ -377,17 +441,51 @@
     }
 
     @Test
-    public void hasFrpCredentialHandleGrantedByPermission() throws Exception {
+    @Parameters({"false", "true"})
+    public void hasFrpCredentialHandle_GrantedByConfigureFrpPermission(boolean frpEnabled)
+            throws Exception {
+        setUp(frpEnabled);
+        grantConfigureFrpPermission();
+
         mPdbService.setAllowedUid(Binder.getCallingUid() + 1);  // unexpected uid
+
+        if (frpEnabled) {
+            assertThat(mInterface.hasFrpCredentialHandle()).isFalse();
+            mInternalInterface.setFrpCredentialHandle(SMALL_DATA);
+            assertThat(mInterface.hasFrpCredentialHandle()).isTrue();
+        } else {
+            assertThrows(SecurityException.class, () -> mInterface.hasFrpCredentialHandle());
+        }
+    }
+
+    @Test
+    @Parameters({"false", "true"})
+    public void hasFrpCredentialHandle_GrantedByAccessPdbStatePermission(boolean frpEnabled)
+            throws Exception {
+        setUp(frpEnabled);
         grantAccessPdbStatePermission();
 
+        mPdbService.setAllowedUid(Binder.getCallingUid() + 1);  // unexpected uid
+
         assertThat(mInterface.hasFrpCredentialHandle()).isFalse();
         mInternalInterface.setFrpCredentialHandle(SMALL_DATA);
         assertThat(mInterface.hasFrpCredentialHandle()).isTrue();
     }
 
     @Test
-    public void clearTestHarnessModeData() throws Exception {
+    @Parameters({"false", "true"})
+    public void hasFrpCredentialHandle_Unauthorized(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
+
+        mPdbService.setAllowedUid(Binder.getCallingUid() + 1);  // unexpected uid
+
+        assertThrows(SecurityException.class, () -> mInterface.hasFrpCredentialHandle());
+    }
+
+    @Test
+    @Parameters({"false", "true"})
+    public void clearTestHarnessModeData(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         mInternalInterface.setTestHarnessModeData(SMALL_DATA);
         mInternalInterface.clearTestHarnessModeData();
 
@@ -397,19 +495,25 @@
     }
 
     @Test
-    public void getAllowedUid() throws Exception {
+    @Parameters({"false", "true"})
+    public void getAllowedUid(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         assertThat(mInternalInterface.getAllowedUid()).isEqualTo(Binder.getCallingUid());
     }
 
     @Test
-    public void oemUnlockWithoutPermission() throws Exception {
+    @Parameters({"false", "true"})
+    public void oemUnlockWithoutPermission(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         denyOemUnlockPermission();
 
         assertThrows(SecurityException.class, () -> mInterface.setOemUnlockEnabled(true));
     }
 
     @Test
-    public void oemUnlockNotAdmin() throws Exception {
+    @Parameters({"false", "true"})
+    public void oemUnlockNotAdmin(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         grantOemUnlockPermission();
         makeUserAdmin(false);
 
@@ -417,7 +521,9 @@
     }
 
     @Test
-    public void oemUnlock() throws Exception {
+    @Parameters({"false", "true"})
+    public void oemUnlock(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         grantOemUnlockPermission();
         makeUserAdmin(true);
 
@@ -427,7 +533,9 @@
     }
 
     @Test
-    public void oemUnlockUserRestriction_OemUnlock() throws Exception {
+    @Parameters({"false", "true"})
+    public void oemUnlockUserRestriction_OemUnlock(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         grantOemUnlockPermission();
         makeUserAdmin(true);
         when(mUserManager.hasUserRestriction(eq(UserManager.DISALLOW_OEM_UNLOCK)))
@@ -437,7 +545,9 @@
     }
 
     @Test
-    public void oemUnlockUserRestriction_FactoryReset() throws Exception {
+    @Parameters({"false", "true"})
+    public void oemUnlockUserRestriction_FactoryReset(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         grantOemUnlockPermission();
         makeUserAdmin(true);
         when(mUserManager.hasUserRestriction(eq(UserManager.DISALLOW_FACTORY_RESET)))
@@ -447,7 +557,9 @@
     }
 
     @Test
-    public void oemUnlockIgnoreTampering() throws Exception {
+    @Parameters({"false", "true"})
+    public void oemUnlockIgnoreTampering(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         grantOemUnlockPermission();
         makeUserAdmin(true);
 
@@ -460,26 +572,37 @@
     }
 
     @Test
-    public void getOemUnlockEnabledPermissionCheck_NoPermission() throws Exception {
+    @Parameters({"false", "true"})
+    public void getOemUnlockEnabledPermissionCheck_NoPermission(boolean frpEnabled)
+            throws Exception {
+        setUp(frpEnabled);
         assertThrows(SecurityException.class, () -> mInterface.getOemUnlockEnabled());
     }
 
     @Test
-    public void getOemUnlockEnabledPermissionCheck_OemUnlcokState() throws Exception {
+    @Parameters({"false", "true"})
+    public void getOemUnlockEnabledPermissionCheck_OemUnlockState(boolean frpEnabled)
+            throws Exception {
+        setUp(frpEnabled);
         doReturn(PackageManager.PERMISSION_GRANTED).when(mContext)
                 .checkCallingOrSelfPermission(eq(Manifest.permission.OEM_UNLOCK_STATE));
         assertThat(mInterface.getOemUnlockEnabled()).isFalse();
     }
 
     @Test
-    public void getOemUnlockEnabledPermissionCheck_ReadOemUnlcokState() throws Exception {
+    @Parameters({"false", "true"})
+    public void getOemUnlockEnabledPermissionCheck_ReadOemUnlockState(boolean frpEnabled)
+            throws Exception {
+        setUp(frpEnabled);
         doReturn(PackageManager.PERMISSION_GRANTED).when(mContext)
                 .checkCallingOrSelfPermission(eq(Manifest.permission.READ_OEM_UNLOCK_STATE));
         assertThat(mInterface.getOemUnlockEnabled()).isFalse();
     }
 
     @Test
-    public void forceOemUnlock_RequiresNoPermission() throws Exception {
+    @Parameters({"false", "true"})
+    public void forceOemUnlock_RequiresNoPermission(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         denyOemUnlockPermission();
 
         mInternalInterface.forceOemUnlockEnabled(true);
@@ -490,24 +613,331 @@
     }
 
     @Test
-    public void getFlashLockStatePermissionCheck_NoPermission() throws Exception {
+    @Parameters({"false", "true"})
+    public void getFlashLockStatePermissionCheck_NoPermission(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
         assertThrows(SecurityException.class, () -> mInterface.getFlashLockState());
     }
 
     @Test
-    public void getFlashLockStatePermissionCheck_OemUnlcokState() throws Exception {
+    @Parameters({"false", "true"})
+    public void getFlashLockStatePermissionCheck_OemUnlockState(boolean frpEnabled)
+            throws Exception {
+        setUp(frpEnabled);
         doReturn(PackageManager.PERMISSION_GRANTED).when(mContext)
                 .checkCallingOrSelfPermission(eq(Manifest.permission.OEM_UNLOCK_STATE));
         mInterface.getFlashLockState();  // Do not throw
     }
 
     @Test
-    public void getFlashLockStatePermissionCheck_ReadOemUnlcokState() throws Exception {
+    @Parameters({"false", "true"})
+    public void getFlashLockStatePermissionCheck_ReadOemUnlockState(boolean frpEnabled)
+            throws Exception {
+        setUp(frpEnabled);
         doReturn(PackageManager.PERMISSION_GRANTED).when(mContext)
                 .checkCallingOrSelfPermission(eq(Manifest.permission.READ_OEM_UNLOCK_STATE));
         mInterface.getFlashLockState();  // Do not throw
     }
 
+    @Test
+    @Parameters({"false", "true"})
+    public void frpMagicTest(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
+        byte[] magicField = mPdbService.readDataBlock(mPdbService.getFrpSecretMagicOffset(),
+                PersistentDataBlockService.FRP_SECRET_MAGIC.length);
+        if (frpEnabled) {
+            assertThat(magicField).isEqualTo(PersistentDataBlockService.FRP_SECRET_MAGIC);
+        } else {
+            assertThat(magicField).isNotEqualTo(PersistentDataBlockService.FRP_SECRET_MAGIC);
+        }
+    }
+
+    @Test
+    public void frpSecret_StartsAsDefault() throws Exception {
+        setUp(/* frpEnabled */ true);
+
+        byte[] secretField = mPdbService.readDataBlock(
+                mPdbService.getFrpSecretDataOffset(), PersistentDataBlockService.FRP_SECRET_SIZE);
+        assertThat(secretField).isEqualTo(new byte[PersistentDataBlockService.FRP_SECRET_SIZE]);
+    }
+
+    @Test
+    public void frpSecret_SetSecret() throws Exception {
+        setUp(/* frpEnforcement */ true);
+        grantConfigureFrpPermission();
+
+        byte[] hashedSecret = hashStringto32Bytes("secret");
+        assertThat(mInterface.setFactoryResetProtectionSecret(hashedSecret)).isTrue();
+
+        byte[] secretField = mPdbService.readDataBlock(
+                mPdbService.getFrpSecretDataOffset(), PersistentDataBlockService.FRP_SECRET_SIZE);
+        assertThat(secretField).isEqualTo(hashedSecret);
+
+        assertThat(mFrpSecretFile.exists()).isTrue();
+        byte[] secretFileData = Files.readAllBytes(mFrpSecretFile.toPath());
+        assertThat(secretFileData).isEqualTo(hashedSecret);
+
+        assertThat(mFrpSecretTmpFile.exists()).isFalse();
+    }
+
+    @Test
+    public void frpSecret_SetSecretByUnauthorizedCaller() throws Exception {
+        setUp(/* frpEnforcement */ true);
+
+        mPdbService.setAllowedUid(Binder.getCallingUid() + 1);  // unexpected uid
+        assertThrows(SecurityException.class,
+                () -> mInterface.setFactoryResetProtectionSecret(hashStringto32Bytes("secret")));
+    }
+
+    /**
+     * Verify that FRP always starts in active state (if flag-enabled), until something is done to
+     * deactivate it.
+     */
+    @Test
+    @Parameters({"false", "true"})
+    public void frpState_StartsActive(boolean frpEnabled) throws Exception {
+        setUp(frpEnabled);
+        // Create a service without calling formatPartition, which deactivates FRP.
+        PersistentDataBlockService pdbService = new FakePersistentDataBlockService(mContext,
+                mDataBlockFile.getPath(), DEFAULT_BLOCK_DEVICE_SIZE, frpEnabled,
+                mFrpSecretFile.getPath(), mFrpSecretTmpFile.getPath());
+        assertThat(pdbService.isFrpActive()).isEqualTo(frpEnabled);
+    }
+
+    @Test
+    public void frpState_AutomaticallyDeactivateWithDefault() throws Exception {
+        setUp(/* frpEnforcement */ true);
+
+        mPdbService.activateFrp();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+    }
+
+    @Test
+    public void frpState_AutomaticallyDeactivateWithPrimaryDataFile() throws Exception {
+        setUp(/* frpEnforcement */ true);
+        grantConfigureFrpPermission();
+
+        mInterface.setFactoryResetProtectionSecret(hashStringto32Bytes("secret"));
+
+        mPdbService.activateFrp();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+    }
+
+    @Test
+    public void frpState_AutomaticallyDeactivateWithBackupDataFile() throws Exception {
+        setUp(/* frpEnforcement */ true);
+        grantConfigureFrpPermission();
+
+        mInterface.setFactoryResetProtectionSecret(hashStringto32Bytes("secret"));
+        Files.move(mFrpSecretFile.toPath(), mFrpSecretTmpFile.toPath(), REPLACE_EXISTING);
+
+        mPdbService.activateFrp();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+    }
+
+    @Test
+    public void frpState_DeactivateWithSecret() throws Exception {
+        setUp(/* frpEnforcement */ true);
+        grantConfigureFrpPermission();
+
+        mInterface.setFactoryResetProtectionSecret(hashStringto32Bytes("secret"));
+        simulateDataWipe();
+
+        assertThat(mPdbService.isFrpActive()).isFalse();
+        mPdbService.activateFrp();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isFalse();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+
+        assertThat(mInterface.deactivateFactoryResetProtection(hashStringto32Bytes("wrongSecret")))
+                .isFalse();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+
+        assertThat(mInterface.deactivateFactoryResetProtection(hashStringto32Bytes("secret")))
+                .isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+
+        assertThat(mInterface.setFactoryResetProtectionSecret(new byte[FRP_SECRET_SIZE])).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+
+        mPdbService.activateFrp();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+    }
+
+    @Test
+    public void frpState_DeactivateOnUpgradeFromPreV() throws Exception {
+        setUp(/* frpEnforcement */ true);
+        grantConfigureFrpPermission();
+
+        mInterface.setFactoryResetProtectionSecret(hashStringto32Bytes("secret"));
+        // If the /data files are still present, deactivation will use them.  We want to verify
+        // that deactivation will succeed even if they are not present, so remove them.
+        simulateDataWipe();
+
+        // Verify that automatic deactivation fails without the /data files when we're not
+        // upgrading from pre-V.
+        mPdbService.activateFrp();
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isFalse();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+
+        // Verify that automatic deactivation succeeds when upgrading from pre-V.
+        mIsUpgradingFromPreV = true;
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+    }
+
+    /**
+     * There is code in PersistentDataBlockService to handle a specific corner case, that of a
+     * device that is upgraded from pre-V to V+, downgraded to pre-V and then upgraded to V+. In
+     * this scenario, the following happens:
+     *
+     * 1. When the device is upgraded to V+ and the user sets an LSKF and GAIA creds, FRP
+     *    enforcement is activated and three copies of the FRP secret are written to:
+     *     a.  The FRP secret field in PDB (plaintext).
+     *     b.  The GAIA challenge in PDB (encrypted).
+     *     c.  The FRP secret file in /data (plaintext).
+     * 2. When the device is downgraded to pre-V, /data is wiped, so copy (c) is destroyed. When the
+     *    user sets LSKF and GAIA creds, copy (b) is overwritten.  Copy (a) survives.
+     * 3. When the device is upgraded to V and boots the first time, FRP cannot be automatically
+     *    deactivated using copy (c), nor can the user deactivate FRP using copy (b), because both
+     *    are gone. Absent some special handling of this case, the device would be unusable.
+     *
+     *  To address this problem, if PersistentDataBlockService finds an FRP secret in (a) but none
+     *  in (b) or (c), and PackageManager reports that the device has just upgraded from pre-V to
+     *  V+, it zeros the FRP secret in (a).
+     *
+     * This test checks that the service handles this sequence of events correctly.
+     */
+    @Test
+    public void frpState_TestDowngradeUpgradeSequence() throws Exception {
+        // Simulate device in V+, with FRP configured.
+        setUp(/* frpEnforcement */ true);
+        grantConfigureFrpPermission();
+
+        assertThat(mInterface.setFactoryResetProtectionSecret(hashStringto32Bytes("secret")))
+                .isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+
+        // Simulate reboot, still in V+.
+        boolean frpEnabled = true;
+        mPdbService = new FakePersistentDataBlockService(mContext, mDataBlockFile.getPath(),
+                DEFAULT_BLOCK_DEVICE_SIZE, frpEnabled, mFrpSecretFile.getPath(),
+                mFrpSecretTmpFile.getPath());
+        assertThat(mPdbService.isFrpActive()).isTrue();
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+
+        // Simulate reboot after data wipe and downgrade to pre-V.
+        simulateDataWipe();
+        frpEnabled = false;
+        mPdbService = new FakePersistentDataBlockService(mContext, mDataBlockFile.getPath(),
+                DEFAULT_BLOCK_DEVICE_SIZE, frpEnabled, mFrpSecretFile.getPath(),
+                mFrpSecretTmpFile.getPath());
+        assertThat(mPdbService.isFrpActive()).isFalse();
+
+        // Simulate reboot after upgrade to V+, no data wipe.
+        frpEnabled = true;
+        mIsUpgradingFromPreV = true;
+        mPdbService = new FakePersistentDataBlockService(mContext, mDataBlockFile.getPath(),
+                DEFAULT_BLOCK_DEVICE_SIZE, frpEnabled, mFrpSecretTmpFile.getPath(),
+                mFrpSecretTmpFile.getPath());
+        mPdbService.setAllowedUid(Binder.getCallingUid()); // Needed for setFrpSecret().
+        assertThat(mPdbService.isFrpActive()).isTrue();
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+        assertThat(mPdbService.getInterfaceForTesting()
+                .setFactoryResetProtectionSecret(new byte[FRP_SECRET_SIZE])).isTrue();
+
+        // Simulate one more reboot.
+        mIsUpgradingFromPreV = false;
+        mPdbService = new FakePersistentDataBlockService(mContext, mDataBlockFile.getPath(),
+                DEFAULT_BLOCK_DEVICE_SIZE, frpEnabled, mFrpSecretTmpFile.getPath(),
+                mFrpSecretTmpFile.getPath());
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+    }
+
+    @Test
+    public void frpState_PrivilegedDeactivationByAuthorizedCaller() throws Exception {
+        setUp(/* frpEnforcement */ true);
+        grantConfigureFrpPermission();
+
+        assertThat(mPdbService.isFrpActive()).isFalse();
+        assertThat(mInterface.setFactoryResetProtectionSecret(hashStringto32Bytes("secret")))
+                .isTrue();
+
+        simulateDataWipe();
+        mPdbService.activateFrp();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isFalse();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+
+        assertThat(mInternalInterface.deactivateFactoryResetProtectionWithoutSecret()).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+    }
+
+    @Test
+    public void frpActive_WipeFails() throws Exception {
+        setUp(/* frpEnforcement */ true);
+
+        grantOemUnlockPermission();
+        mPdbService.activateFrp();
+        SecurityException e = assertThrows(SecurityException.class, () -> mInterface.wipe());
+        assertThat(e).hasMessageThat().contains("FRP is active");
+    }
+
+    @Test
+    public void frpActive_WriteFails() throws Exception {
+        setUp(/* frpEnforcement */ true);
+
+        mPdbService.activateFrp();
+        SecurityException e =
+                assertThrows(SecurityException.class, () -> mInterface.write("data".getBytes()));
+        assertThat(e).hasMessageThat().contains("FRP is active");
+    }
+
+    @Test
+    public void frpActive_SetSecretFails() throws Exception {
+        setUp(/* frpEnforcement */ true);
+        grantConfigureFrpPermission();
+
+        mPdbService.activateFrp();
+
+        byte[] hashedSecret = hashStringto32Bytes("secret");
+        SecurityException e = assertThrows(SecurityException.class, ()
+                -> mInterface.setFactoryResetProtectionSecret(hashedSecret));
+        assertThat(e).hasMessageThat().contains("FRP is active");
+        assertThat(mPdbService.isFrpActive()).isTrue();
+
+        // Verify that secret we failed to set isn't accepted.
+        assertThat(mInterface.deactivateFactoryResetProtection(hashedSecret)).isFalse();
+        assertThat(mPdbService.isFrpActive()).isTrue();
+
+        // Default should work, since it should never have been changed.
+        assertThat(mPdbService.automaticallyDeactivateFrpIfPossible()).isTrue();
+        assertThat(mPdbService.isFrpActive()).isFalse();
+    }
+
+    private void simulateDataWipe() throws IOException {
+        Files.deleteIfExists(mFrpSecretFile.toPath());
+        Files.deleteIfExists(mFrpSecretTmpFile.toPath());
+    }
+
+    private static byte[] hashStringto32Bytes(String secret) throws NoSuchAlgorithmException {
+        return MessageDigest.getInstance("SHA-256").digest(secret.getBytes());
+    }
+
     private void tamperWithDigest() throws Exception {
         try (var ch = FileChannel.open(mDataBlockFile.toPath(), StandardOpenOption.WRITE)) {
             ch.write(ByteBuffer.wrap("tampered-digest".getBytes()));
@@ -542,6 +972,14 @@
                 .checkCallingPermission(eq(Manifest.permission.ACCESS_PDB_STATE));
     }
 
+    private void grantConfigureFrpPermission() {
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+                eq(Manifest.permission.CONFIGURE_FACTORY_RESET_PROTECTION));
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(Manifest.permission.CONFIGURE_FACTORY_RESET_PROTECTION),
+                anyString());
+    }
+
     private ByteBuffer readBackingFile(long position, int size) throws Exception {
         try (var ch = FileChannel.open(mDataBlockFile.toPath(), StandardOpenOption.READ)) {
             var buffer = ByteBuffer.allocate(size);