diff options
7 files changed, 323 insertions, 10 deletions
diff --git a/api/current.txt b/api/current.txt index 344e93baedf0..7df44ab9adef 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6950,6 +6950,7 @@ package android.app.admin { method @Nullable public java.util.List<java.lang.String> getPermittedCrossProfileNotificationListeners(@NonNull android.content.ComponentName); method @Nullable public java.util.List<java.lang.String> 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<android.os.UserHandle> 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 9562715b8cff..86de97204afc 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<java.lang.String> getPermittedCrossProfileNotificationListeners(@NonNull android.content.ComponentName); method @Nullable public java.util.List<java.lang.String> 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<android.os.UserHandle> 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 1d644c43cd0d..4752160da30a 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. * + * <p><strong>Note:</strong> 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 { * <p>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. * + * <p>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. + * * <p>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) @@ -3630,6 +3638,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. + * + * <p>Note that when called on a profile which uses an unified challenge with its parent, the + * complexity would apply to the unified challenge. + * + * <p>This method can be called on the {@link DevicePolicyManager} instance + * returned by {@link #getParentProfileInstance(ComponentName)} in order to set + * restrictions on the parent profile. + * + * <p><strong>Note:</strong> 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. + * + * <p>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. + * + * <p>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 { * <li>{@link #getRequiredStrongAuthTimeout}</li> * <li>{@link #setRequiredStrongAuthTimeout}</li> * <li>{@link #getAccountTypesWithManagementDisabled}</li> + * <li>{@link #setRequiredPasswordComplexity(int)} </li> + * <li>{@link #getRequiredPasswordComplexity()}</li> * </ul> * <p> * 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 f4105e9b0373..58368bc3779a 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 412f5828828e..d29534e8080e 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 a1167e99201d..757f2486f4ea 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<PasswordValidationError> 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); @@ -4277,6 +4290,75 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @Override + public void setRequiredPasswordComplexity(int passwordComplexity, boolean calledOnParent) { + if (!mHasFeature) { + return; + } + final Set<Integer> 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<ActiveAdmin> 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()) { return 0; @@ -4466,15 +4548,17 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { synchronized (getLockObject()) { final PasswordMetrics minMetrics = getPasswordMinimumMetrics(userHandle); final List<PasswordValidationError> 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 77e16769d905..c0c82d53b4f0 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<Integer> 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<Integer> 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<Integer> 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); } |