Stop saving password metrics to disk

Previously, DevicePolicyManager saved password stats (number of letters,
number of symbols, etc) to disk for FDE devices. This made it possible
for the isActivePasswordSufficient() API to return a result before the
password was entered for the first time after a reboot. Access to these
stats would significantly narrow the space of possible passwords an
attacker would need to explore.

Going forward, every time either the password or the password
requirements change, a flag will be persisted indicating whether the
current password meets the requirements. Before the password is entered
for the first time after a reboot, isActivePasswordSufficient() simply
returns the value of this flag. (After the password is entered for the
first time, isActivePasswordSufficient() uses password stats saved in
memory, as is the case today.)

This creates a window where isActivePasswordSufficient() may return an
incorrect value before the password is entered for the first time, if
the requirements are changed after startup so that the current password
no longer meets the requirements. This has been deemed an acceptable
compromise in order to avoid storing potentially sensitive data.

Test: runtest -c com.android.server.devicepolicy.DevicePolicyManagerTest
frameworks-services

Bug: 34218769
Change-Id: I5d3cd008a9ee2787bcb10ed5455bb61c6014ae00
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index c2b0ea5..fb74d05 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -26,6 +26,7 @@
 import android.app.admin.DeviceAdminReceiver;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.DevicePolicyManagerInternal;
+import android.app.admin.PasswordMetrics;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -3836,6 +3837,80 @@
         assertTrue(dpm.clearResetPasswordToken(admin1));
     }
 
+    public void testIsActivePasswordSufficient() throws Exception {
+        mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
+        mContext.packageName = admin1.getPackageName();
+        setupDeviceOwner();
+
+        dpm.setPasswordQuality(admin1, DevicePolicyManager.PASSWORD_QUALITY_COMPLEX);
+        dpm.setPasswordMinimumLength(admin1, 8);
+        dpm.setPasswordMinimumLetters(admin1, 6);
+        dpm.setPasswordMinimumLowerCase(admin1, 3);
+        dpm.setPasswordMinimumUpperCase(admin1, 1);
+        dpm.setPasswordMinimumNonLetter(admin1, 1);
+        dpm.setPasswordMinimumNumeric(admin1, 1);
+        dpm.setPasswordMinimumSymbols(admin1, 0);
+
+        PasswordMetrics passwordMetricsNoSymbols = new PasswordMetrics(
+                DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, 9,
+                8, 2,
+                6, 1,
+                0, 1);
+
+        setActivePasswordState(passwordMetricsNoSymbols);
+        assertTrue(dpm.isActivePasswordSufficient());
+
+        initializeDpms();
+        reset(mContext.spiedContext);
+        assertTrue(dpm.isActivePasswordSufficient());
+
+        // This call simulates the user entering the password for the first time after a reboot.
+        // This causes password metrics to be reloaded into memory.  Until this happens,
+        // dpm.isActivePasswordSufficient() will continue to return its last checkpointed value,
+        // even if the DPC changes password requirements so that the password no longer meets the
+        // requirements.  This is a known limitation of the current implementation of
+        // isActivePasswordSufficient() - see b/34218769.
+        setActivePasswordState(passwordMetricsNoSymbols);
+        assertTrue(dpm.isActivePasswordSufficient());
+
+        dpm.setPasswordMinimumSymbols(admin1, 1);
+        // This assertion would fail if we had not called setActivePasswordState() again after
+        // initializeDpms() - see previous comment.
+        assertFalse(dpm.isActivePasswordSufficient());
+
+        initializeDpms();
+        reset(mContext.spiedContext);
+        assertFalse(dpm.isActivePasswordSufficient());
+
+        PasswordMetrics passwordMetricsWithSymbols = new PasswordMetrics(
+                DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, 9,
+                7, 2,
+                5, 1,
+                1, 2);
+
+        setActivePasswordState(passwordMetricsWithSymbols);
+        assertTrue(dpm.isActivePasswordSufficient());
+    }
+
+    private void setActivePasswordState(PasswordMetrics passwordMetrics) {
+        int userHandle = UserHandle.getUserId(mContext.binder.callingUid);
+        final long ident = mContext.binder.clearCallingIdentity();
+        try {
+            dpm.setActivePasswordState(passwordMetrics, userHandle);
+            dpm.reportPasswordChanged(userHandle);
+
+            final Intent intent = new Intent(DeviceAdminReceiver.ACTION_PASSWORD_CHANGED);
+            intent.setComponent(admin1);
+            intent.putExtra(Intent.EXTRA_USER, UserHandle.of(mContext.binder.callingUid));
+
+            verify(mContext.spiedContext, times(1)).sendBroadcastAsUser(
+                    MockUtils.checkIntent(intent),
+                    MockUtils.checkUserHandle(userHandle));
+        } finally {
+            mContext.binder.restoreCallingIdentity(ident);
+        }
+    }
+
     public void testIsCurrentInputMethodSetByOwnerForDeviceOwner() throws Exception {
         final String currentIme = Settings.Secure.DEFAULT_INPUT_METHOD;
         final Uri currentImeUri = Settings.Secure.getUriFor(currentIme);