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);