From d8e49b45b11802fd5a6f4a77301873aaf69885cb Mon Sep 17 00:00:00 2001 From: Eran Messeri Date: Fri, 13 Nov 2020 15:35:46 +0000 Subject: [PATCH] Enforce password complexity in lockscreen setting Enforce a lock screen that adheres with the required complexity set by the admin. This is done by querying the DevicePolicyManager for the complexity set for the given user, and merging it with the complexity from the "change lock screen" intent (if any). If the admin sets a higher complexity requirement than the app triggering the lock screen change request, then the admin-set complexity is enforced and the user is not shown information about the requesting app. Bug: 165573442 Test: Manually, set complexity using TestDPC and see it applies. Test: m RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.password.ChooseLockGenericTest Test: m RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.password.ChooseLockPasswordTest Change-Id: If3f24f7430bdcbcd34265339f7d2a1ff82a44fc1 --- .../settings/password/ChooseLockGeneric.java | 19 ++++-- .../settings/password/ChooseLockPassword.java | 2 + .../password/ChooseLockGenericTest.java | 60 +++++++++++++++++++ .../password/ChooseLockPasswordTest.java | 49 +++++++++++++-- .../shadow/ShadowLockPatternUtils.java | 26 ++++++++ 5 files changed, 147 insertions(+), 9 deletions(-) diff --git a/src/com/android/settings/password/ChooseLockGeneric.java b/src/com/android/settings/password/ChooseLockGeneric.java index 35e369e9422..8b0c2c92ec5 100644 --- a/src/com/android/settings/password/ChooseLockGeneric.java +++ b/src/com/android/settings/password/ChooseLockGeneric.java @@ -221,9 +221,19 @@ public class ChooseLockGeneric extends SettingsActivity { mForBiometrics = intent.getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_FOR_BIOMETRICS, false); - mRequestedMinComplexity = intent + final int complexityFromIntent = intent .getIntExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE); - mCallerAppName = + final int complexityFromAdmin = mLockPatternUtils.getRequestedPasswordComplexity( + mUserId); + mRequestedMinComplexity = Math.max(complexityFromIntent, complexityFromAdmin); + final boolean isComplexityProvidedByAdmin = (complexityFromAdmin > complexityFromIntent) + && mRequestedMinComplexity > PASSWORD_COMPLEXITY_NONE; + + // If the complexity is provided by the admin, do not get the caller app's name. + // If the app requires, for example, low complexity, and the admin requires high + // complexity, it does not make sense to show a footer telling the user it's the app + // requesting a particular complexity because the admin-set complexity will override it. + mCallerAppName = isComplexityProvidedByAdmin ? null : intent.getStringExtra(EXTRA_KEY_CALLER_APP_NAME); mIsCallingAppAdmin = intent .getBooleanExtra(EXTRA_KEY_IS_CALLING_APP_ADMIN, /* defValue= */ false); @@ -669,8 +679,9 @@ public class ChooseLockGeneric extends SettingsActivity { final PreferenceScreen entries = getPreferenceScreen(); int adminEnforcedQuality = mDpm.getPasswordQuality(null, mUserId); - EnforcedAdmin enforcedAdmin = RestrictedLockUtilsInternal.checkIfPasswordQualityIsSet( - getActivity(), mUserId); + EnforcedAdmin enforcedAdmin = + RestrictedLockUtilsInternal.checkIfPasswordQualityIsSet(getActivity(), + mUserId); // If we are to unify a work challenge at the end of the credential enrollment, manually // merge any password policy from that profile here, so we are enrolling a compliant // password. This is because once unified, the profile's password policy will diff --git a/src/com/android/settings/password/ChooseLockPassword.java b/src/com/android/settings/password/ChooseLockPassword.java index a73b73a8e3b..0c84ba97628 100644 --- a/src/com/android/settings/password/ChooseLockPassword.java +++ b/src/com/android/settings/password/ChooseLockPassword.java @@ -421,6 +421,8 @@ public class ChooseLockPassword extends SettingsActivity { if (mUnificationProfileId != UserHandle.USER_NULL) { mMinMetrics.maxWith( mLockPatternUtils.getRequestedPasswordMetrics(mUnificationProfileId)); + mMinComplexity = Math.max(mMinComplexity, + mLockPatternUtils.getRequestedPasswordComplexity(mUnificationProfileId)); } if (intent.getBooleanExtra( diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java index 036df2cea8b..60979805c25 100644 --- a/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java +++ b/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java @@ -21,6 +21,7 @@ import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW; import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM; import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; import static com.android.settings.password.ChooseLockGeneric.ChooseLockGenericFragment.KEY_LOCK_SETTINGS_FOOTER; import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CALLER_APP_NAME; @@ -90,6 +91,7 @@ public class ChooseLockGenericTest { Global.putInt(application.getContentResolver(), Global.DEVICE_PROVISIONED, 1); ShadowStorageManager.reset(); ShadowPersistentDataBlockManager.reset(); + ShadowLockPatternUtils.reset(); } @Test @@ -377,6 +379,64 @@ public class ChooseLockGenericTest { ChooseLockSettingsHelper.EXTRA_KEY_UNIFICATION_PROFILE_CREDENTIAL)).isNotNull(); } + @Test + public void updatePreferencesOrFinish_ComplexityIsReadFromDPM() { + ShadowStorageManager.setIsFileEncryptedNativeOrEmulated(false); + ShadowLockPatternUtils.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH); + + initActivity(null); + mFragment.updatePreferencesOrFinish(false /* isRecreatingActivity */); + + FooterPreference footer = mFragment.findPreference(KEY_LOCK_SETTINGS_FOOTER); + assertThat(footer.getTitle()).isEqualTo(null); + + Intent intent = mFragment.getLockPasswordIntent(PASSWORD_QUALITY_COMPLEX); + assertThat(intent.getIntExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, + PASSWORD_COMPLEXITY_NONE)).isEqualTo(PASSWORD_COMPLEXITY_HIGH); + } + + @Test + public void updatePreferencesOrFinish_ComplexityIsMergedWithDPM() { + ShadowStorageManager.setIsFileEncryptedNativeOrEmulated(false); + ShadowLockPatternUtils.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH); + Intent intent = new Intent() + .putExtra(EXTRA_KEY_CALLER_APP_NAME, "app name") + .putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_LOW); + initActivity(intent); + + mFragment.updatePreferencesOrFinish(false /* isRecreatingActivity */); + + // Footer should be null because admin complexity wins. + FooterPreference footer = mFragment.findPreference(KEY_LOCK_SETTINGS_FOOTER); + assertThat(footer.getTitle()).isEqualTo(null); + + Intent passwordIntent = mFragment.getLockPasswordIntent(PASSWORD_QUALITY_COMPLEX); + assertThat(passwordIntent.getIntExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, + PASSWORD_COMPLEXITY_NONE)).isEqualTo(PASSWORD_COMPLEXITY_HIGH); + } + + @Test + public void updatePreferencesOrFinish_ComplexityIsMergedWithDPM_AppIsHigher() { + ShadowStorageManager.setIsFileEncryptedNativeOrEmulated(false); + ShadowLockPatternUtils.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW); + Intent intent = new Intent() + .putExtra(EXTRA_KEY_CALLER_APP_NAME, "app name") + .putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_HIGH); + initActivity(intent); + + mFragment.updatePreferencesOrFinish(false /* isRecreatingActivity */); + + // Footer should include app name because app requirement is higher. + CharSequence expectedTitle = + mActivity.getString(R.string.unlock_footer_high_complexity_requested, "app name"); + FooterPreference footer = mFragment.findPreference(KEY_LOCK_SETTINGS_FOOTER); + assertThat(footer.getTitle()).isEqualTo(expectedTitle); + + Intent passwordIntent = mFragment.getLockPasswordIntent(PASSWORD_QUALITY_COMPLEX); + assertThat(passwordIntent.getIntExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, + PASSWORD_COMPLEXITY_NONE)).isEqualTo(PASSWORD_COMPLEXITY_HIGH); + } + private void initActivity(@Nullable Intent intent) { if (intent == null) { intent = new Intent(); diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java index 030bb80d5ba..0ca6a3ee114 100644 --- a/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java +++ b/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java @@ -31,6 +31,7 @@ import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED import static com.android.internal.widget.LockPatternUtils.PASSWORD_TYPE_KEY; import static com.android.settings.password.ChooseLockGeneric.CONFIRM_CREDENTIALS; import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_UNIFICATION_PROFILE_ID; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -67,6 +68,7 @@ import org.robolectric.shadows.ShadowDrawable; @RunWith(RobolectricTestRunner.class) @Config(shadows = { SettingsShadowResources.class, + ShadowLockPatternUtils.class, ShadowUtils.class, ShadowDevicePolicyManager.class, }) @@ -84,6 +86,7 @@ public class ChooseLockPasswordTest { @After public void tearDown() { SettingsShadowResources.reset(); + ShadowLockPatternUtils.reset(); } @Test @@ -378,6 +381,29 @@ public class ChooseLockPasswordTest { assertThat(drawable.getCreatedFromResId()).isNotEqualTo(R.drawable.ic_fingerprint_header); } + @Test + public void validateComplexityMergedFromDpmOnCreate() { + ShadowLockPatternUtils.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_HIGH, + /* passwordType= */ PASSWORD_QUALITY_NUMERIC, + /* userEnteredPassword= */ LockscreenCredential.createNone(), + "PIN must be at least 8 digits"); + } + + @Test + public void validateComplexityMergedFromUnificationUserOnCreate() { + ShadowLockPatternUtils.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW); + ShadowLockPatternUtils.setRequiredPasswordComplexity(123, PASSWORD_COMPLEXITY_HIGH); + + Intent intent = createIntentForPasswordValidation(PASSWORD_COMPLEXITY_NONE, + PASSWORD_QUALITY_NUMERIC); + intent.putExtra(EXTRA_KEY_UNIFICATION_PROFILE_ID, 123); + assertPasswordValidationResultForIntent(LockscreenCredential.createNone(), intent, + "PIN must be at least 8 digits"); + } + private ChooseLockPassword buildChooseLockPasswordActivity(Intent intent) { return Robolectric.buildActivity(ChooseLockPassword.class, intent).setup().get(); } @@ -400,14 +426,27 @@ public class ChooseLockPasswordTest { private void assertPasswordValidationResult(@PasswordComplexity int minComplexity, int passwordType, LockscreenCredential userEnteredPassword, String... expectedValidationResult) { - Intent intent = new Intent(); - intent.putExtra(CONFIRM_CREDENTIALS, false); - intent.putExtra(PASSWORD_TYPE_KEY, passwordType); - intent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, minComplexity); + Intent intent = createIntentForPasswordValidation(minComplexity, passwordType); + assertPasswordValidationResultForIntent(userEnteredPassword, intent, + expectedValidationResult); + } + + private void assertPasswordValidationResultForIntent(LockscreenCredential userEnteredPassword, + Intent intent, String... expectedValidationResult) { ChooseLockPassword activity = buildChooseLockPasswordActivity(intent); ChooseLockPasswordFragment fragment = getChooseLockPasswordFragment(activity); fragment.validatePassword(userEnteredPassword); String[] messages = fragment.convertErrorCodeToMessages(); - assertThat(messages).asList().containsExactly((Object[]) expectedValidationResult); + assertThat(messages).asList().containsExactly(expectedValidationResult); + } + + private Intent createIntentForPasswordValidation( + @PasswordComplexity int minComplexity, + int passwordType) { + Intent intent = new Intent(); + intent.putExtra(CONFIRM_CREDENTIALS, false); + intent.putExtra(PASSWORD_TYPE_KEY, passwordType); + intent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, minComplexity); + return intent; } } diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java index 38756ac7102..3a159b209ba 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java @@ -18,19 +18,31 @@ package com.android.settings.testutils.shadow; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; +import android.os.UserHandle; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockscreenCredential; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Implements(LockPatternUtils.class) public class ShadowLockPatternUtils { private static boolean sDeviceEncryptionEnabled; + private static Map sUserToComplexityMap = new HashMap<>(); + + + @Resetter + public static void reset() { + sUserToComplexityMap.clear(); + sDeviceEncryptionEnabled = false; + } @Implementation protected boolean hasSecureLockScreen() { @@ -76,4 +88,18 @@ public class ShadowLockPatternUtils { protected boolean checkPasswordHistory(byte[] passwordToCheck, byte[] hashFactor, int userId) { return false; } + + @Implementation + public @DevicePolicyManager.PasswordComplexity int getRequestedPasswordComplexity(int userId) { + return sUserToComplexityMap.getOrDefault(userId, + DevicePolicyManager.PASSWORD_COMPLEXITY_NONE); + } + + public static void setRequiredPasswordComplexity(int userId, int complexity) { + sUserToComplexityMap.put(userId, complexity); + } + + public static void setRequiredPasswordComplexity(int complexity) { + setRequiredPasswordComplexity(UserHandle.myUserId(), complexity); + } }