Harden Factory Reset Protection

This change modifies PersistentDataBlockService to track the factory
reset protection state, to block updating the data block when FRP is
active (meaning there may have been an untrusted reset), and to
require presentation of a secret to deactivate FRP.

Bug: 290312729
Test: atest PersistentDataBlockTest
Change-Id: Ibfd053dc07bdea947e3043fc13bb17a7f5a986e9
diff --git a/core/api/current.txt b/core/api/current.txt
index 8306f30..b5a65d5 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -40960,6 +40960,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 c46ca78..2226622 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -112,6 +112,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";
@@ -12672,13 +12673,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 c71a842..1e0cdd8 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);