From 1641c5c47e0a2a37d6b853e652a7661a2dd394ab Mon Sep 17 00:00:00 2001 From: Eran Messeri Date: Tue, 27 Oct 2020 08:36:11 +0000 Subject: [PATCH] Implement setRequiredPasswordComplexity Implement setRequiredPasswordComplexity and getRequiredPasswordComplexity, which enforce password requiremets according to one of four, platform-defined complexity levels. Start adjusting the platform to take the complexity into account: * isActivePasswordSufficient would return false if complexity is set but the current password does not meet the complexity criteria. * resetPasswordWithToken would not allow resetting the password to a value that does not meet the complexity criteria. Bug: 165573442 Test: atest FrameworksServicesTests:DevicePolicyManagerTest Test: atest com.android.cts.devicepolicy.MixedDeviceOwnerTest#testResetPasswordWithToken Change-Id: I4e66af0b03c521de70bba3192751164736d01c62 --- api/current.txt | 2 + core/api/current.txt | 2 + .../app/admin/DevicePolicyManager.java | 72 +++++++++- .../app/admin/IDevicePolicyManager.aidl | 2 + .../server/devicepolicy/ActiveAdmin.java | 14 ++ .../DevicePolicyManagerService.java | 106 ++++++++++++-- .../devicepolicy/DevicePolicyManagerTest.java | 135 ++++++++++++++++++ 7 files changed, 323 insertions(+), 10 deletions(-) diff --git a/api/current.txt b/api/current.txt index 344e93baedf03..7df44ab9adef9 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6950,6 +6950,7 @@ package android.app.admin { method @Nullable public java.util.List getPermittedCrossProfileNotificationListeners(@NonNull android.content.ComponentName); method @Nullable public java.util.List getPermittedInputMethods(@NonNull android.content.ComponentName); method public int getPersonalAppsSuspendedReasons(@NonNull android.content.ComponentName); + method public int getRequiredPasswordComplexity(); method public long getRequiredStrongAuthTimeout(@Nullable android.content.ComponentName); method public boolean getScreenCaptureDisabled(@Nullable android.content.ComponentName); method public java.util.List getSecondaryUsers(@NonNull android.content.ComponentName); @@ -7081,6 +7082,7 @@ package android.app.admin { method public void setProfileEnabled(@NonNull android.content.ComponentName); method public void setProfileName(@NonNull android.content.ComponentName, String); method public void setRecommendedGlobalProxy(@NonNull android.content.ComponentName, @Nullable android.net.ProxyInfo); + method public void setRequiredPasswordComplexity(int); method public void setRequiredStrongAuthTimeout(@NonNull android.content.ComponentName, long); method public boolean setResetPasswordToken(android.content.ComponentName, byte[]); method public void setRestrictionsProvider(@NonNull android.content.ComponentName, @Nullable android.content.ComponentName); diff --git a/core/api/current.txt b/core/api/current.txt index 9562715b8cfff..86de97204afcc 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -6950,6 +6950,7 @@ package android.app.admin { method @Nullable public java.util.List getPermittedCrossProfileNotificationListeners(@NonNull android.content.ComponentName); method @Nullable public java.util.List getPermittedInputMethods(@NonNull android.content.ComponentName); method public int getPersonalAppsSuspendedReasons(@NonNull android.content.ComponentName); + method public int getRequiredPasswordComplexity(); method public long getRequiredStrongAuthTimeout(@Nullable android.content.ComponentName); method public boolean getScreenCaptureDisabled(@Nullable android.content.ComponentName); method public java.util.List getSecondaryUsers(@NonNull android.content.ComponentName); @@ -7081,6 +7082,7 @@ package android.app.admin { method public void setProfileEnabled(@NonNull android.content.ComponentName); method public void setProfileName(@NonNull android.content.ComponentName, String); method public void setRecommendedGlobalProxy(@NonNull android.content.ComponentName, @Nullable android.net.ProxyInfo); + method public void setRequiredPasswordComplexity(int); method public void setRequiredStrongAuthTimeout(@NonNull android.content.ComponentName, long); method public boolean setResetPasswordToken(android.content.ComponentName, byte[]); method public void setRestrictionsProvider(@NonNull android.content.ComponentName, @Nullable android.content.ComponentName); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 1d644c43cd0d7..4752160da30af 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2729,6 +2729,9 @@ public class DevicePolicyManager { * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent * profile. * + *

Note: Specifying password requirements using this method clears the + * password complexity requirements set using {@link #setRequiredPasswordComplexity(int)}. + * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param quality The new desired quality. One of {@link #PASSWORD_QUALITY_UNSPECIFIED}, * {@link #PASSWORD_QUALITY_BIOMETRIC_WEAK}, @@ -3607,13 +3610,18 @@ public class DevicePolicyManager { *

Note that when called from a profile which uses an unified challenge with its parent, the * screen lock complexity of the parent will be returned. * + *

Apps need the {@link permission#REQUEST_PASSWORD_COMPLEXITY} permission to call this + * method. On Android {@link android.os.Build.VERSION_CODES#S} and above, the calling + * application does not need this permission if it is a device owner or a profile owner. + * *

This method can be called on the {@link DevicePolicyManager} instance * returned by {@link #getParentProfileInstance(ComponentName)} in order to retrieve * restrictions on the parent profile. * * @throws IllegalStateException if the user is not unlocked. * @throws SecurityException if the calling application does not have the permission - * {@link permission#REQUEST_PASSWORD_COMPLEXITY} + * {@link permission#REQUEST_PASSWORD_COMPLEXITY}, and is not a + * device owner or a profile owner. */ @PasswordComplexity @RequiresPermission(android.Manifest.permission.REQUEST_PASSWORD_COMPLEXITY) @@ -3629,6 +3637,66 @@ public class DevicePolicyManager { } } + /** + * Sets a password complexity requirement for the user's screen lock. + * The complexity level is one of the pre-defined levels. + * + *

Note that when called on a profile which uses an unified challenge with its parent, the + * complexity would apply to the unified challenge. + * + *

This method can be called on the {@link DevicePolicyManager} instance + * returned by {@link #getParentProfileInstance(ComponentName)} in order to set + * restrictions on the parent profile. + * + *

Note: Specifying password requirements using this method clears any + * password requirements set using the obsolete {@link #setPasswordQuality(ComponentName, int)} + * and any of its associated methods. + * + * @throws SecurityException if the calling application is not a device owner or a profile + * owner. + * @throws IllegalArgumentException if the complexity level is not one of the four above. + */ + public void setRequiredPasswordComplexity(@PasswordComplexity int passwordComplexity) { + if (mService == null) { + return; + } + + try { + mService.setRequiredPasswordComplexity(passwordComplexity, mParentInstance); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Gets the password complexity requirement set by {@link #setRequiredPasswordComplexity(int)}, + * for the current user. + * + *

The difference between this method and {@link #getPasswordComplexity()} is that this + * method simply returns the value set by {@link #setRequiredPasswordComplexity(int)} while + * {@link #getPasswordComplexity()} returns the complexity of the actual password. + * + *

This method can be called on the {@link DevicePolicyManager} instance + * returned by {@link #getParentProfileInstance(ComponentName)} in order to get + * restrictions on the parent profile. + * + * @throws SecurityException if the calling application is not a device owner or a profile + * owner. + */ + @PasswordComplexity + public int getRequiredPasswordComplexity() { + if (mService == null) { + return PASSWORD_COMPLEXITY_NONE; + } + + try { + return mService.getRequiredPasswordComplexity(mParentInstance); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * When called by a profile owner of a managed profile returns true if the profile uses unified * challenge with its parent user. @@ -9987,6 +10055,8 @@ public class DevicePolicyManager { *

  • {@link #getRequiredStrongAuthTimeout}
  • *
  • {@link #setRequiredStrongAuthTimeout}
  • *
  • {@link #getAccountTypesWithManagementDisabled}
  • + *
  • {@link #setRequiredPasswordComplexity(int)}
  • + *
  • {@link #getRequiredPasswordComplexity()}
  • * *

    * The following methods are supported for the parent instance but can only be called by the diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index f4105e9b0373c..58368bc3779ab 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -87,6 +87,8 @@ interface IDevicePolicyManager { boolean isProfileActivePasswordSufficientForParent(int userHandle); boolean isPasswordSufficientAfterProfileUnification(int userHandle, int profileUser); int getPasswordComplexity(boolean parent); + void setRequiredPasswordComplexity(int passwordComplexity, boolean parent); + int getRequiredPasswordComplexity(boolean parent); boolean isUsingUnifiedPassword(in ComponentName admin); int getCurrentFailedPasswordAttempts(int userHandle, boolean parent); int getProfileWithMinimumFailedPasswordsForWipe(int userHandle, boolean parent); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java index 412f5828828e0..d29534e8080e4 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java @@ -16,6 +16,7 @@ package com.android.server.devicepolicy; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; @@ -130,6 +131,7 @@ class ActiveAdmin { private static final String TAG_ALWAYS_ON_VPN_PACKAGE = "vpn-package"; private static final String TAG_ALWAYS_ON_VPN_LOCKDOWN = "vpn-lockdown"; private static final String TAG_COMMON_CRITERIA_MODE = "common-criteria-mode"; + private static final String TAG_PASSWORD_COMPLEXITY = "password-complexity"; private static final String ATTR_VALUE = "value"; private static final String ATTR_LAST_NETWORK_LOGGING_NOTIFICATION = "last-notification"; private static final String ATTR_NUM_NETWORK_LOGGING_NOTIFICATIONS = "num-notifications"; @@ -142,6 +144,9 @@ class ActiveAdmin { @NonNull PasswordPolicy mPasswordPolicy = new PasswordPolicy(); + @DevicePolicyManager.PasswordComplexity + int mPasswordComplexity = PASSWORD_COMPLEXITY_NONE; + @Nullable FactoryResetProtectionPolicy mFactoryResetProtectionPolicy = null; @@ -518,6 +523,10 @@ class ActiveAdmin { if (mCommonCriteriaMode) { writeAttributeValueToXml(out, TAG_COMMON_CRITERIA_MODE, mCommonCriteriaMode); } + + if (mPasswordComplexity != PASSWORD_COMPLEXITY_NONE) { + writeAttributeValueToXml(out, TAG_PASSWORD_COMPLEXITY, mPasswordComplexity); + } } void writeTextToXml(XmlSerializer out, String tag, String text) throws IOException { @@ -777,6 +786,8 @@ class ActiveAdmin { } else if (TAG_COMMON_CRITERIA_MODE.equals(tag)) { mCommonCriteriaMode = Boolean.parseBoolean( parser.getAttributeValue(null, ATTR_VALUE)); + } else if (TAG_PASSWORD_COMPLEXITY.equals(tag)) { + mPasswordComplexity = Integer.parseInt(parser.getAttributeValue(null, ATTR_VALUE)); } else { Slog.w(DevicePolicyManagerService.LOG_TAG, "Unknown admin tag: " + tag); XmlUtils.skipCurrentTag(parser); @@ -1112,5 +1123,8 @@ class ActiveAdmin { pw.print("mCommonCriteriaMode="); pw.println(mCommonCriteriaMode); + + pw.print("mPasswordComplexity="); + pw.println(mPasswordComplexity); } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index a1167e99201df..757f2486f4eae 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -60,6 +60,9 @@ import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_HOME; import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS; import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW; import static android.app.admin.DevicePolicyManager.NON_ORG_OWNED_PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER; +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_ALPHABETIC; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; @@ -3384,6 +3387,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final PasswordPolicy passwordPolicy = ap.mPasswordPolicy; if (passwordPolicy.quality != quality) { passwordPolicy.quality = quality; + ap.mPasswordComplexity = PASSWORD_COMPLEXITY_NONE; resetInactivePasswordRequirementsIfRPlus(userId, ap); updatePasswordValidityCheckpointLocked(userId, parent); updatePasswordQualityCacheForUserGroup(userId); @@ -3612,6 +3616,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { private void ensureMinimumQuality( int userId, ActiveAdmin admin, int minimumQuality, String operation) { mInjector.binderWithCleanCallingIdentity(() -> { + // This check will also take care of the case where the password requirements + // are specified as complexity rather than quality: When a password complexity + // is set, the quality is reset to "unspecified" which will be below any value + // of minimumQuality. if (admin.mPasswordPolicy.quality < minimumQuality && passwordQualityInvocationOrderCheckEnabled(admin.info.getPackageName(), userId)) { @@ -4209,6 +4217,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { for (ActiveAdmin admin : admins) { adminMetrics.add(admin.mPasswordPolicy.getMinMetrics()); } + //TODO: Take complexity into account, would need to take complexity from all admins + //in the admins list. return PasswordMetrics.validatePasswordMetrics(PasswordMetrics.merge(adminMetrics), PASSWORD_COMPLEXITY_NONE, false, metrics).isEmpty(); } @@ -4243,31 +4253,34 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { */ private boolean isPasswordSufficientForUserWithoutCheckpointLocked( @NonNull PasswordMetrics metrics, @UserIdInt int userId, boolean parent) { + final int complexity = getEffectivePasswordComplexityRequirementLocked(userId, parent); PasswordMetrics minMetrics = getPasswordMinimumMetrics(userId, parent); final List passwordValidationErrors = PasswordMetrics.validatePasswordMetrics( - minMetrics, PASSWORD_COMPLEXITY_NONE, false, metrics); + minMetrics, complexity, false, metrics); return passwordValidationErrors.isEmpty(); } @Override @PasswordComplexity public int getPasswordComplexity(boolean parent) { + final CallerIdentity caller = getNonPrivilegedOrAdminCallerIdentity(null); DevicePolicyEventLogger .createEvent(DevicePolicyEnums.GET_USER_PASSWORD_COMPLEXITY_LEVEL) .setStrings(parent ? CALLED_FROM_PARENT : NOT_CALLED_FROM_PARENT, mInjector.getPackageManager().getPackagesForUid( mInjector.binderGetCallingUid())) .write(); - final CallerIdentity caller = getCallerIdentity(); Preconditions.checkCallAuthorization(!parent || (isDeviceOwner(caller) || isProfileOwner(caller) || isSystemUid(caller)), "Only profile owner, device owner and system may call this method."); enforceUserUnlocked(caller.getUserId()); - mContext.enforceCallingOrSelfPermission( - REQUEST_PASSWORD_COMPLEXITY, - "Must have " + REQUEST_PASSWORD_COMPLEXITY + " permission."); + Preconditions.checkCallAuthorization( + hasCallingOrSelfPermission(REQUEST_PASSWORD_COMPLEXITY) + || isDeviceOwner(caller) || isProfileOwner(caller), + "Must have " + REQUEST_PASSWORD_COMPLEXITY + + " permission, or be a profile owner or device owner."); synchronized (getLockObject()) { final int credentialOwner = getCredentialOwner(caller.getUserId(), parent); @@ -4276,6 +4289,75 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + @Override + public void setRequiredPasswordComplexity(int passwordComplexity, boolean calledOnParent) { + if (!mHasFeature) { + return; + } + final Set allowedModes = Set.of(PASSWORD_COMPLEXITY_NONE, PASSWORD_COMPLEXITY_LOW, + PASSWORD_COMPLEXITY_MEDIUM, PASSWORD_COMPLEXITY_HIGH); + Preconditions.checkArgument(allowedModes.contains(passwordComplexity), + "Provided complexity is not one of the allowed values."); + + final CallerIdentity caller = getAdminCallerIdentity(null); + Preconditions.checkCallAuthorization(isDeviceOwner(caller) || isProfileOwner(caller)); + Preconditions.checkArgument(!calledOnParent || isProfileOwner(caller)); + + synchronized (getLockObject()) { + final ActiveAdmin admin = getParentOfAdminIfRequired( + getProfileOwnerOrDeviceOwnerLocked(caller), calledOnParent); + if (admin.mPasswordComplexity != passwordComplexity) { + mInjector.binderWithCleanCallingIdentity(() -> { + admin.mPasswordComplexity = passwordComplexity; + // Reset the password policy. + admin.mPasswordPolicy = new PasswordPolicy(); + updatePasswordValidityCheckpointLocked(caller.getUserId(), calledOnParent); + updatePasswordQualityCacheForUserGroup(caller.getUserId()); + saveSettingsLocked(caller.getUserId()); + //TODO: Log password complexity change if security logging is enabled. + }); + } + } + //TODO: Log metrics. + } + + private int getEffectivePasswordComplexityRequirementLocked(@UserIdInt int userHandle, + boolean parent) { + ensureLocked(); + List admins = getActiveAdminsForLockscreenPoliciesLocked( + getProfileParentUserIfRequested(userHandle, parent)); + int maxRequiredComplexity = PASSWORD_COMPLEXITY_NONE; + for (ActiveAdmin admin : admins) { + final ComponentName adminComponent = admin.info.getComponent(); + final int adminUser = admin.getUserHandle().getIdentifier(); + // Password complexity is only taken into account from DO/PO + if (isDeviceOwner(adminComponent, adminUser) + || isProfileOwner(adminComponent, adminUser)) { + maxRequiredComplexity = Math.max(maxRequiredComplexity, admin.mPasswordComplexity); + } + } + return maxRequiredComplexity; + } + + @Override + public int getRequiredPasswordComplexity(boolean calledOnParent) { + if (!mHasFeature) { + return PASSWORD_COMPLEXITY_NONE; + } + + final CallerIdentity caller = getAdminCallerIdentity(null); + Preconditions.checkCallAuthorization( + isDeviceOwner(caller) || isProfileOwner(caller)); + + Preconditions.checkArgument(!calledOnParent || hasProfileOwner(caller.getUserId())); + + synchronized (getLockObject()) { + final ActiveAdmin requiredAdmin = getParentOfAdminIfRequired( + getDeviceOrProfileOwnerAdminLocked(caller.getUserId()), calledOnParent); + return requiredAdmin.mPasswordComplexity; + } + } + @Override public int getCurrentFailedPasswordAttempts(int userHandle, boolean parent) { if (!mLockPatternUtils.hasSecureLockScreen()) { @@ -4466,15 +4548,17 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { synchronized (getLockObject()) { final PasswordMetrics minMetrics = getPasswordMinimumMetrics(userHandle); final List validationErrors; + final int complexity = + getEffectivePasswordComplexityRequirementLocked(userHandle, false); // TODO: Consider changing validation API to take LockscreenCredential. if (password.isEmpty()) { validationErrors = PasswordMetrics.validatePasswordMetrics( - minMetrics, PASSWORD_COMPLEXITY_NONE, false /* isPin */, + minMetrics, complexity, false /* isPin */, new PasswordMetrics(CREDENTIAL_TYPE_NONE)); } else { // TODO(b/120484642): remove getBytes() below validationErrors = PasswordMetrics.validatePassword( - minMetrics, PASSWORD_COMPLEXITY_NONE, false, password.getBytes()); + minMetrics, complexity, false, password.getBytes()); } if (!validationErrors.isEmpty()) { @@ -7461,6 +7545,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + private int getDeviceOwnerUserIdUncheckedLocked() { + return mOwners.hasDeviceOwner() ? mOwners.getDeviceOwnerUserId() : UserHandle.USER_NULL; + } + @Override public int getDeviceOwnerUserId() { if (!mHasFeature) { @@ -7469,7 +7557,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Preconditions.checkCallAuthorization(canManageUsers(getCallerIdentity())); synchronized (getLockObject()) { - return mOwners.hasDeviceOwner() ? mOwners.getDeviceOwnerUserId() : UserHandle.USER_NULL; + return getDeviceOwnerUserIdUncheckedLocked(); } } @@ -8010,7 +8098,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { */ private @Nullable ActiveAdmin getDeviceOrProfileOwnerAdminLocked(int userHandle) { ActiveAdmin admin = getProfileOwnerAdminLocked(userHandle); - if (admin == null && getDeviceOwnerUserId() == userHandle) { + if (admin == null && getDeviceOwnerUserIdUncheckedLocked() == userHandle) { admin = getDeviceOwnerAdminLocked(); } return admin; 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 77e16769d9059..c0c82d53b4f08 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -25,6 +25,7 @@ import static android.app.admin.DevicePolicyManager.ID_TYPE_IMEI; import static android.app.admin.DevicePolicyManager.ID_TYPE_MEID; import static android.app.admin.DevicePolicyManager.ID_TYPE_SERIAL; 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.WIPE_EUICC; @@ -6690,6 +6691,140 @@ public class DevicePolicyManagerTest extends DpmTestBase { .isEqualTo(DevicePolicyManager.PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT); } + @Test + public void testSetRequiredPasswordComplexity_UnauthorizedCallersOnDO() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; + setupDeviceOwner(); + // DO must be able to set it. + dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW); + // But not on the parent DPM. + assertExpectException(IllegalArgumentException.class, null, + () -> parentDpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW)); + // Another package must not be allowed to set password complexity. + mContext.binder.callingUid = DpmMockContext.ANOTHER_UID; + assertExpectException(SecurityException.class, null, + () -> dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW)); + } + + @Test + public void testSetRequiredPasswordComplexity_UnauthorizedCallersOnPO() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + setupProfileOwner(); + // PO must be able to set it. + dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW); + // And on the parent profile DPM instance. + parentDpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW); + // Another package must not be allowed to set password complexity. + mContext.binder.callingUid = DpmMockContext.ANOTHER_UID; + assertExpectException(SecurityException.class, null, + () -> dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW)); + } + + @Test + public void testSetRequiredPasswordComplexity_validValuesOnly() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + setupProfileOwner(); + + // Cannot set value other than password_complexity none/low/medium/high + assertExpectException(IllegalArgumentException.class, null, () -> + dpm.setRequiredPasswordComplexity(-1)); + assertExpectException(IllegalArgumentException.class, null, () -> + dpm.setRequiredPasswordComplexity(7)); + assertExpectException(IllegalArgumentException.class, null, () -> + dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH + 1)); + + final Set allowedModes = Set.of(PASSWORD_COMPLEXITY_NONE, PASSWORD_COMPLEXITY_LOW, + PASSWORD_COMPLEXITY_MEDIUM, PASSWORD_COMPLEXITY_HIGH); + for (int complexity : allowedModes) { + // Ensure exception is not thrown. + dpm.setRequiredPasswordComplexity(complexity); + } + } + + @Test + public void testSetRequiredPasswordComplexity_setAndGet() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + setupProfileOwner(); + + final Set allowedModes = Set.of(PASSWORD_COMPLEXITY_NONE, PASSWORD_COMPLEXITY_LOW, + PASSWORD_COMPLEXITY_MEDIUM, PASSWORD_COMPLEXITY_HIGH); + for (int complexity : allowedModes) { + dpm.setRequiredPasswordComplexity(complexity); + assertThat(dpm.getRequiredPasswordComplexity()).isEqualTo(complexity); + } + } + + @Test + public void testSetRequiredPasswordComplexityOnParent_setAndGet() throws Exception { + final int managedProfileUserId = 15; + final int managedProfileAdminUid = UserHandle.getUid(managedProfileUserId, 19436); + + addManagedProfile(admin1, managedProfileAdminUid, admin1); + mContext.binder.callingUid = managedProfileAdminUid; + + final Set allowedModes = Set.of(PASSWORD_COMPLEXITY_NONE, PASSWORD_COMPLEXITY_LOW, + PASSWORD_COMPLEXITY_MEDIUM, PASSWORD_COMPLEXITY_HIGH); + for (int complexity : allowedModes) { + dpm.getParentProfileInstance(admin1).setRequiredPasswordComplexity(complexity); + assertThat(dpm.getParentProfileInstance(admin1).getRequiredPasswordComplexity()) + .isEqualTo(complexity); + assertThat(dpm.getRequiredPasswordComplexity()).isEqualTo(PASSWORD_COMPLEXITY_NONE); + } + } + + @Test + public void testSetRequiredPasswordComplexity_isSufficient() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; + mContext.packageName = admin1.getPackageName(); + setupDeviceOwner(); + + dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH); + assertThat(dpm.getRequiredPasswordComplexity()).isEqualTo(PASSWORD_COMPLEXITY_HIGH); + when(getServices().packageManager.getPackagesForUid( + DpmMockContext.CALLER_SYSTEM_USER_UID)).thenReturn(new String[0]); + mServiceContext.permissions.add(permission.REQUEST_PASSWORD_COMPLEXITY); + assertThat(dpm.getPasswordComplexity()).isEqualTo(PASSWORD_COMPLEXITY_NONE); + + reset(mContext.spiedContext); + PasswordMetrics passwordMetricsNoSymbols = computeForPassword("1234".getBytes()); + setActivePasswordState(passwordMetricsNoSymbols); + assertThat(dpm.getPasswordComplexity()).isEqualTo(PASSWORD_COMPLEXITY_LOW); + assertThat(dpm.isActivePasswordSufficient()).isFalse(); + + reset(mContext.spiedContext); + passwordMetricsNoSymbols = computeForPassword("84125312943a".getBytes()); + setActivePasswordState(passwordMetricsNoSymbols); + assertThat(dpm.getPasswordComplexity()).isEqualTo(PASSWORD_COMPLEXITY_HIGH); + // using isActivePasswordSufficient + assertThat(dpm.isActivePasswordSufficient()).isTrue(); + } + + @Test + public void testSetRequiredPasswordComplexity_resetBySettingQuality() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + setupProfileOwner(); + + // Test that calling setPasswordQuality resets complexity to none. + dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH); + assertThat(dpm.getRequiredPasswordComplexity()).isEqualTo(PASSWORD_COMPLEXITY_HIGH); + dpm.setPasswordQuality(admin1, DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX); + assertThat(dpm.getRequiredPasswordComplexity()).isEqualTo(PASSWORD_COMPLEXITY_NONE); + } + + @Test + public void testSetRequiredPasswordComplexity_overridesQuality() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + setupProfileOwner(); + + // Test that calling setRequiredPasswordComplexity resets password quality. + dpm.setPasswordQuality(admin1, DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX); + assertThat(dpm.getPasswordQuality(admin1)).isEqualTo( + DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX); + dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH); + assertThat(dpm.getPasswordQuality(admin1)).isEqualTo( + DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED); + } + private void setUserUnlocked(int userHandle, boolean unlocked) { when(getServices().userManager.isUserUnlocked(eq(userHandle))).thenReturn(unlocked); }