FRP: Explicitly throw UnsupportedOperation when FRP is not supported

Changes KeyguardManager.createConfirmFactoryResetCredentialIntent such that
it throws UnsupportedOperationException when credential-based FRP is not
available on the device (either because storage was corrupted, or the device
simply does not support it).

This allows GMScore to distinguish this case from when the previous owner
simply didn't set up security.

Also fixes a few places where we previously unintentionally crashed with an
NPE.

Bug: 72368658
Test: atest LockSettingsStorageTests
Change-Id: I92fe1d4b06834e76a4bfffad41276ca28c68ce70
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index 024dbcb..553099f 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -167,10 +167,11 @@
      *                             clicking this button, the activity returns
      *                             {@link #RESULT_ALTERNATE}
      *
-     * @return  the intent for launching the activity or null if the credential of the previous
-     * owner can not be verified (e.g. because there was none, or the device does not support
-     * verifying credentials after a factory reset, or device setup has already been completed).
-     *
+     * @return the intent for launching the activity or null if the previous owner of the device
+     *         did not set a credential.
+     * @throws UnsupportedOperationException if the device does not support factory reset
+     *                                       credentials
+     * @throws IllegalStateException if the device has already been provisioned
      * @hide
      */
     @SystemApi
@@ -178,14 +179,14 @@
             CharSequence title, CharSequence description, CharSequence alternateButtonLabel) {
         if (!LockPatternUtils.frpCredentialEnabled(mContext)) {
             Log.w(TAG, "Factory reset credentials not supported.");
-            return null;
+            throw new UnsupportedOperationException("not supported on this device");
         }
 
         // Cannot verify credential if the device is provisioned
         if (Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
             Log.e(TAG, "Factory reset credential cannot be verified after provisioning.");
-            return null;
+            throw new IllegalStateException("must not be provisioned yet");
         }
 
         // Make sure we have a credential
@@ -194,8 +195,10 @@
                     ServiceManager.getService(Context.PERSISTENT_DATA_BLOCK_SERVICE));
             if (pdb == null) {
                 Log.e(TAG, "No persistent data block service");
-                return null;
+                throw new UnsupportedOperationException("not supported on this device");
             }
+            // The following will throw an UnsupportedOperationException if the device does not
+            // support factory reset credentials (or something went wrong retrieving it).
             if (!pdb.hasFrpCredentialHandle()) {
                 Log.i(TAG, "The persistent data block does not have a factory reset credential.");
                 return null;
diff --git a/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java b/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java
index 833def3..1e9a007 100644
--- a/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java
+++ b/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java
@@ -24,7 +24,11 @@
     /** Stores the handle to a lockscreen credential to be used for Factory Reset Protection. */
     void setFrpCredentialHandle(byte[] handle);
 
-    /** Retrieves handle to a lockscreen credential to be used for Factory Reset Protection. */
+    /**
+     * Retrieves handle to a lockscreen credential to be used for Factory Reset Protection.
+     *
+     * @throws IllegalStateException if the underlying storage is corrupt or inaccessible.
+     */
     byte[] getFrpCredentialHandle();
 
     /** Update the OEM unlock enabled bit, bypassing user restriction checks. */
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index 4298140..21093b9 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -28,6 +28,7 @@
 import android.os.UserManager;
 import android.service.persistentdata.IPersistentDataBlockService;
 import android.service.persistentdata.PersistentDataBlockManager;
+import android.util.Log;
 import android.util.Slog;
 
 import com.android.internal.R;
@@ -582,7 +583,12 @@
         @Override
         public boolean hasFrpCredentialHandle() {
             enforcePersistentDataBlockAccess();
-            return mInternalService.getFrpCredentialHandle() != null;
+            try {
+                return mInternalService.getFrpCredentialHandle() != null;
+            } catch (IllegalStateException e) {
+                Slog.e(TAG, "error reading frp handle", e);
+                throw new UnsupportedOperationException("cannot read frp credential");
+            }
         }
     };
 
@@ -638,7 +644,7 @@
         @Override
         public byte[] getFrpCredentialHandle() {
             if (!enforceChecksumValidity()) {
-                return null;
+                throw new IllegalStateException("invalid checksum");
             }
 
             DataInputStream inputStream;
@@ -646,8 +652,7 @@
                 inputStream = new DataInputStream(
                         new FileInputStream(new File(mDataBlockFile)));
             } catch (FileNotFoundException e) {
-                Slog.e(TAG, "partition not available");
-                return null;
+                throw new IllegalStateException("frp partition not available");
             }
 
             try {
@@ -662,8 +667,7 @@
                     return bytes;
                 }
             } catch (IOException e) {
-                Slog.e(TAG, "unable to access persistent partition", e);
-                return null;
+                throw new IllegalStateException("frp handle not readable", e);
             } finally {
                 IoUtils.closeQuietly(inputStream);
             }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index 70d6072..f62e8a9 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -627,7 +627,12 @@
         if (persistentDataBlock == null) {
             return PersistentData.NONE;
         }
-        return PersistentData.fromBytes(persistentDataBlock.getFrpCredentialHandle());
+        try {
+            return PersistentData.fromBytes(persistentDataBlock.getFrpCredentialHandle());
+        } catch (IllegalStateException e) {
+            Slog.e(TAG, "Error reading persistent data block", e);
+            return PersistentData.NONE;
+        }
     }
 
     public static class PersistentData {
@@ -670,11 +675,11 @@
                     return new PersistentData(type, userId, qualityForUi, payload);
                 } else {
                     Slog.wtf(TAG, "Unknown PersistentData version code: " + version);
-                    return null;
+                    return NONE;
                 }
             } catch (IOException e) {
                 Slog.wtf(TAG, "Could not parse PersistentData", e);
-                return null;
+                return NONE;
             }
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
index 4bdd1c5..bc61c58 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
@@ -18,11 +18,14 @@
 
 import android.content.Context;
 
+import com.android.server.PersistentDataBlockManagerInternal;
+
 import java.io.File;
 
 public class LockSettingsStorageTestable extends LockSettingsStorage {
 
     public File mStorageDir;
+    public PersistentDataBlockManagerInternal mPersistentDataBlock;
 
     public LockSettingsStorageTestable(Context context, File storageDir) {
         super(context);
@@ -53,6 +56,11 @@
                 userId).getAbsolutePath());
     }
 
+    @Override
+    public PersistentDataBlockManagerInternal getPersistentDataBlock() {
+        return mPersistentDataBlock;
+    }
+
     private File makeDirs(File baseDir, String filePath) {
         File path = new File(filePath);
         if (path.getParent() == null) {
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
index b0325cb..237091d 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
@@ -29,8 +29,12 @@
 import android.os.UserManager;
 import android.os.storage.StorageManager;
 import android.test.AndroidTestCase;
+import android.util.Log;
+import android.util.Log.TerribleFailure;
+import android.util.Log.TerribleFailureHandler;
 
 import com.android.internal.widget.LockPatternUtils;
+import com.android.server.PersistentDataBlockManagerInternal;
 import com.android.server.locksettings.LockSettingsStorage.CredentialHash;
 import com.android.server.locksettings.LockSettingsStorage.PersistentData;
 
@@ -52,7 +56,7 @@
 
     public static final byte[] PAYLOAD = new byte[] {1, 2, -1, -2, 33};
 
-    LockSettingsStorage mStorage;
+    LockSettingsStorageTestable mStorage;
     File mStorageDir;
 
     private File mDb;
@@ -346,6 +350,39 @@
         assertEquals(null, mStorage.readSyntheticPasswordState(10, 1234L, "state"));
     }
 
+    public void testPersistentDataBlock_unavailable() {
+        mStorage.mPersistentDataBlock = null;
+
+        assertSame(PersistentData.NONE, mStorage.readPersistentDataBlock());
+    }
+
+    public void testPersistentDataBlock_empty() {
+        mStorage.mPersistentDataBlock = mock(PersistentDataBlockManagerInternal.class);
+
+        assertSame(PersistentData.NONE, mStorage.readPersistentDataBlock());
+    }
+
+    public void testPersistentDataBlock_withData() {
+        mStorage.mPersistentDataBlock = mock(PersistentDataBlockManagerInternal.class);
+        when(mStorage.mPersistentDataBlock.getFrpCredentialHandle())
+                .thenReturn(PersistentData.toBytes(PersistentData.TYPE_SP_WEAVER, SOME_USER_ID,
+                        DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, PAYLOAD));
+
+        PersistentData data = mStorage.readPersistentDataBlock();
+
+        assertEquals(PersistentData.TYPE_SP_WEAVER, data.type);
+        assertEquals(SOME_USER_ID, data.userId);
+        assertEquals(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, data.qualityForUi);
+        assertArrayEquals(PAYLOAD, data.payload);
+    }
+
+    public void testPersistentDataBlock_exception() {
+        mStorage.mPersistentDataBlock = mock(PersistentDataBlockManagerInternal.class);
+        when(mStorage.mPersistentDataBlock.getFrpCredentialHandle())
+                .thenThrow(new IllegalStateException("oops"));
+        assertSame(PersistentData.NONE, mStorage.readPersistentDataBlock());
+    }
+
     public void testPersistentData_serializeUnserialize() {
         byte[] serialized = PersistentData.toBytes(PersistentData.TYPE_SP, SOME_USER_ID,
                 DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, PAYLOAD);
@@ -366,6 +403,13 @@
         assertSame(PersistentData.NONE, deserialized);
     }
 
+    public void testPersistentData_unserializeInvalid() {
+        assertNotNull(suppressAndReturnWtf(() -> {
+            PersistentData deserialized = PersistentData.fromBytes(new byte[]{5});
+            assertSame(PersistentData.NONE, deserialized);
+        }));
+    }
+
     public void testPersistentData_unserialize_version1() {
         // This test ensures that we can read serialized VERSION_1 PersistentData even if we change
         // the wire format in the future.
@@ -450,4 +494,19 @@
         assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PATTERN, cred.type);
         assertArrayEquals(pattern, cred.hash);
     }
+
+    /**
+     * Suppresses reporting of the WTF to system_server, so we don't pollute the dropbox with
+     * intentionally caused WTFs.
+     */
+    private TerribleFailure suppressAndReturnWtf(Runnable r) {
+        TerribleFailure[] captured = new TerribleFailure[1];
+        TerribleFailureHandler prevWtfHandler = Log.setWtfHandler((t, w, s) -> captured[0] = w);
+        try {
+            r.run();
+        } finally {
+            Log.setWtfHandler(prevWtfHandler);
+        }
+        return captured[0];
+    }
 }