diff options
| author | 2017-12-19 18:01:56 +0000 | |
|---|---|---|
| committer | 2017-12-19 18:01:56 +0000 | |
| commit | f7ca81fbc99888076d56666475a33b7b1a40fbd7 (patch) | |
| tree | e00bdb587a926c94371db356e3ffd7b36a67cb09 | |
| parent | e51ffaa6aca7d8413bc51e9d472ca664d23869e5 (diff) | |
| parent | 7f31bb047820bd5bbf3baab461d24d49f1128052 (diff) | |
Merge "DPMS: password blacklist"
10 files changed, 620 insertions, 7 deletions
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 4bb4c50190b0..9ad990af3bb5 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2635,10 +2635,121 @@ public class DevicePolicyManager { } /** + * The maximum number of characters allowed in the password blacklist. + */ + private static final int PASSWORD_BLACKLIST_CHARACTER_LIMIT = 128 * 1000; + + /** + * Throws an exception if the password blacklist is too large. + * + * @hide + */ + public static void enforcePasswordBlacklistSize(List<String> blacklist) { + if (blacklist == null) { + return; + } + long characterCount = 0; + for (final String item : blacklist) { + characterCount += item.length(); + } + if (characterCount > PASSWORD_BLACKLIST_CHARACTER_LIMIT) { + throw new IllegalArgumentException("128 thousand blacklist character limit exceeded by " + + (characterCount - PASSWORD_BLACKLIST_CHARACTER_LIMIT) + " characters"); + } + } + + /** + * Called by an application that is administering the device to blacklist passwords. + * <p> + * Any blacklisted password or PIN is prevented from being enrolled by the user or the admin. + * Note that the match against the blacklist is case insensitive. The blacklist applies for all + * password qualities requested by {@link #setPasswordQuality} however it is not taken into + * consideration by {@link #isActivePasswordSufficient}. + * <p> + * The blacklist can be cleared by passing {@code null} or an empty list. The blacklist is + * given a name that is used to track which blacklist is currently set by calling {@link + * #getPasswordBlacklistName}. If the blacklist is being cleared, the name is ignored and {@link + * #getPasswordBlacklistName} will return {@code null}. The name can only be {@code null} when + * the blacklist is being cleared. + * <p> + * The blacklist is limited to a total of 128 thousand characters rather than limiting to a + * number of entries. + * <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. + * + * @param admin the {@link DeviceAdminReceiver} this request is associated with + * @param name name to associate with the blacklist + * @param blacklist list of passwords to blacklist or {@code null} to clear the blacklist + * @return whether the new blacklist was successfully installed + * @throws SecurityException if {@code admin} is not a device or profile owner + * @throws IllegalArgumentException if the blacklist surpasses the character limit + * @throws NullPointerException if {@code name} is {@code null} when setting a non-empty list + * + * @see #getPasswordBlacklistName + * @see #isActivePasswordSufficient + * @see #resetPasswordWithToken + * + * TODO(63578054): unhide for P + * @hide + */ + public boolean setPasswordBlacklist(@NonNull ComponentName admin, @Nullable String name, + @Nullable List<String> blacklist) { + enforcePasswordBlacklistSize(blacklist); + + try { + return mService.setPasswordBlacklist(admin, name, blacklist, mParentInstance); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Get the name of the password blacklist set by the given admin. + * + * @param admin the {@link DeviceAdminReceiver} this request is associated with + * @return the name of the blacklist or {@code null} if no blacklist is set + * + * @see #setPasswordBlacklist + * + * TODO(63578054): unhide for P + * @hide + */ + public @Nullable String getPasswordBlacklistName(@NonNull ComponentName admin) { + try { + return mService.getPasswordBlacklistName(admin, myUserId(), mParentInstance); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Test if a given password is blacklisted. + * + * @param userId the user to valiate for + * @param password the password to check against the blacklist + * @return whether the password is blacklisted + * + * @see #setPasswordBlacklist + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.TEST_BLACKLISTED_PASSWORD) + public boolean isPasswordBlacklisted(@UserIdInt int userId, @NonNull String password) { + try { + return mService.isPasswordBlacklisted(userId, password); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** * Determine whether the current password the user has set is sufficient to meet the policy * requirements (e.g. quality, minimum length) that have been requested by the admins of this * user and its participating profiles. Restrictions on profiles that have a separate challenge - * are not taken into account. The user must be unlocked in order to perform the check. + * are not taken into account. The user must be unlocked in order to perform the check. The + * password blacklist is not considered when checking sufficiency. * <p> * The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 912820818a78..f4cd797438ae 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -78,6 +78,10 @@ interface IDevicePolicyManager { long getPasswordExpiration(in ComponentName who, int userHandle, boolean parent); + boolean setPasswordBlacklist(in ComponentName who, String name, in List<String> blacklist, boolean parent); + String getPasswordBlacklistName(in ComponentName who, int userId, boolean parent); + boolean isPasswordBlacklisted(int userId, String password); + boolean isActivePasswordSufficient(int userHandle, boolean parent); boolean isProfileActivePasswordSufficientForParent(int userHandle); boolean isUsingUnifiedPassword(in ComponentName admin); diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 15e439ed3586..13fedfec6082 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1620,6 +1620,11 @@ <permission android:name="android.permission.ACCESS_PDB_STATE" android:protectionLevel="signature" /> + <!-- Allows testing if a passwords is forbidden by the admins. + @hide <p>Not for use by third-party applications. --> + <permission android:name="android.permission.TEST_BLACKLISTED_PASSWORD" + android:protectionLevel="signature" /> + <!-- @hide Allows system update service to notify device owner about pending updates. <p>Not for use by third-party applications. --> <permission android:name="android.permission.NOTIFY_PENDING_SYSTEM_UPDATE" diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java index 5b9e3a1e70eb..da42dc936132 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java @@ -15,6 +15,7 @@ */ package com.android.server.devicepolicy; +import android.annotation.UserIdInt; import android.app.admin.IDevicePolicyManager; import android.content.ComponentName; import android.os.PersistableBundle; @@ -24,6 +25,8 @@ import android.security.keystore.ParcelableKeyGenParameterSpec; import com.android.internal.R; import com.android.server.SystemService; +import java.util.List; + /** * Defines the required interface for IDevicePolicyManager implemenation. * @@ -68,6 +71,23 @@ abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub { return false; } + @Override + public boolean setPasswordBlacklist(ComponentName who, String name, List<String> blacklist, + boolean parent) { + return false; + } + + @Override + public String getPasswordBlacklistName(ComponentName who, @UserIdInt int userId, + boolean parent) { + return null; + } + + @Override + public boolean isPasswordBlacklisted(@UserIdInt int userId, String password) { + return false; + } + public boolean isUsingUnifiedPassword(ComponentName who) { return true; } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index bead31fc675e..d1a93d06b624 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -734,6 +734,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { private static final String TAG_PASSWORD_HISTORY_LENGTH = "password-history-length"; private static final String TAG_MIN_PASSWORD_LENGTH = "min-password-length"; private static final String ATTR_VALUE = "value"; + private static final String TAG_PASSWORD_BLACKLIST = "password-blacklist"; private static final String TAG_PASSWORD_QUALITY = "password-quality"; private static final String TAG_POLICIES = "policies"; private static final String TAG_CROSS_PROFILE_WIDGET_PROVIDERS = @@ -866,6 +867,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { // Default title of confirm credentials screen String organizationName = null; + // The blacklist data is stored in a file whose name is stored in the XML + String passwordBlacklistFile = null; + ActiveAdmin(DeviceAdminInfo _info, boolean parent) { info = _info; isParent = parent; @@ -947,6 +951,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { out.endTag(null, TAG_MIN_PASSWORD_NONLETTER); } } + if (passwordBlacklistFile != null) { + out.startTag(null, TAG_PASSWORD_BLACKLIST); + out.attribute(null, ATTR_VALUE, passwordBlacklistFile); + out.endTag(null, TAG_PASSWORD_BLACKLIST); + } if (maximumTimeToUnlock != DEF_MAXIMUM_TIME_TO_UNLOCK) { out.startTag(null, TAG_MAX_TIME_TO_UNLOCK); out.attribute(null, ATTR_VALUE, Long.toString(maximumTimeToUnlock)); @@ -1186,7 +1195,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } else if (TAG_MIN_PASSWORD_NONLETTER.equals(tag)) { minimumPasswordMetrics.nonLetter = Integer.parseInt( parser.getAttributeValue(null, ATTR_VALUE)); - } else if (TAG_MAX_TIME_TO_UNLOCK.equals(tag)) { + } else if (TAG_PASSWORD_BLACKLIST.equals(tag)) { + passwordBlacklistFile = parser.getAttributeValue(null, ATTR_VALUE); + }else if (TAG_MAX_TIME_TO_UNLOCK.equals(tag)) { maximumTimeToUnlock = Long.parseLong( parser.getAttributeValue(null, ATTR_VALUE)); } else if (TAG_STRONG_AUTH_UNLOCK_TIMEOUT.equals(tag)) { @@ -1441,6 +1452,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { pw.println(minimumPasswordMetrics.symbols); pw.print(prefix); pw.print("minimumPasswordNonLetter="); pw.println(minimumPasswordMetrics.nonLetter); + pw.print(prefix); pw.print("passwordBlacklist="); + pw.println(passwordBlacklistFile != null); pw.print(prefix); pw.print("maximumTimeToUnlock="); pw.println(maximumTimeToUnlock); pw.print(prefix); pw.print("strongAuthUnlockTimeout="); @@ -1693,6 +1706,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return new LockPatternUtils(mContext); } + PasswordBlacklist newPasswordBlacklist(File file) { + return new PasswordBlacklist(file); + } + boolean storageManagerIsFileBasedEncryptionEnabled() { return StorageManager.isFileEncryptedNativeOnly(); } @@ -2589,11 +2606,15 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } - private JournaledFile makeJournaledFile(int userHandle) { - final String base = userHandle == UserHandle.USER_SYSTEM - ? mInjector.getDevicePolicyFilePathForSystemUser() + DEVICE_POLICIES_XML - : new File(mInjector.environmentGetUserSystemDirectory(userHandle), - DEVICE_POLICIES_XML).getAbsolutePath(); + private File getPolicyFileDirectory(@UserIdInt int userId) { + return userId == UserHandle.USER_SYSTEM + ? new File(mInjector.getDevicePolicyFilePathForSystemUser()) + : mInjector.environmentGetUserSystemDirectory(userId); + } + + private JournaledFile makeJournaledFile(@UserIdInt int userId) { + final String base = new File(getPolicyFileDirectory(userId), DEVICE_POLICIES_XML) + .getAbsolutePath(); if (VERBOSE_LOG) { Log.v(LOG_TAG, "Opening " + base); } @@ -4064,6 +4085,136 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + /* @return the password blacklist set by the admin or {@code null} if none. */ + PasswordBlacklist getAdminPasswordBlacklistLocked(@NonNull ActiveAdmin admin) { + final int userId = UserHandle.getUserId(admin.getUid()); + return admin.passwordBlacklistFile == null ? null : new PasswordBlacklist( + new File(getPolicyFileDirectory(userId), admin.passwordBlacklistFile)); + } + + private static final String PASSWORD_BLACKLIST_FILE_PREFIX = "password-blacklist-"; + private static final String PASSWORD_BLACKLIST_FILE_SUFFIX = ""; + + @Override + public boolean setPasswordBlacklist(ComponentName who, String name, List<String> blacklist, + boolean parent) { + if (!mHasFeature) { + return false; + } + Preconditions.checkNotNull(who, "who is null"); + + synchronized (this) { + final ActiveAdmin admin = getActiveAdminForCallerLocked( + who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, parent); + final int userId = mInjector.userHandleGetCallingUserId(); + PasswordBlacklist adminBlacklist = getAdminPasswordBlacklistLocked(admin); + + if (blacklist == null || blacklist.isEmpty()) { + // Remove the adminBlacklist + admin.passwordBlacklistFile = null; + saveSettingsLocked(userId); + if (adminBlacklist != null) { + adminBlacklist.delete(); + } + return true; + } + + // Validate server side + Preconditions.checkNotNull(name, "name is null"); + DevicePolicyManager.enforcePasswordBlacklistSize(blacklist); + + // Blacklist is case insensitive so normalize to lower case + final int blacklistSize = blacklist.size(); + for (int i = 0; i < blacklistSize; ++i) { + blacklist.set(i, blacklist.get(i).toLowerCase()); + } + + final boolean isNewBlacklist = adminBlacklist == null; + if (isNewBlacklist) { + // Create a new file for the blacklist. There could be multiple admins, each setting + // different blacklists, to restrict a user's credential, for example a managed + // profile can impose restrictions on its parent while the parent is already + // restricted by its own admin. A deterministic naming scheme would be fragile if + // new types of admin are introduced so we generate and save the file name instead. + // This isn't a temporary file but it reuses the name generation logic + final File file; + try { + file = File.createTempFile(PASSWORD_BLACKLIST_FILE_PREFIX, + PASSWORD_BLACKLIST_FILE_SUFFIX, getPolicyFileDirectory(userId)); + } catch (IOException e) { + Slog.e(LOG_TAG, "Failed to make a file for the blacklist", e); + return false; + } + adminBlacklist = mInjector.newPasswordBlacklist(file); + } + + if (adminBlacklist.savePasswordBlacklist(name, blacklist)) { + if (isNewBlacklist) { + // The blacklist was saved so point the admin to the file + admin.passwordBlacklistFile = adminBlacklist.getFile().getName(); + saveSettingsLocked(userId); + } + return true; + } + } + + return false; + } + + @Override + public String getPasswordBlacklistName(ComponentName who, @UserIdInt int userId, + boolean parent) { + if (!mHasFeature) { + return null; + } + Preconditions.checkNotNull(who, "who is null"); + enforceFullCrossUsersPermission(userId); + synchronized (this) { + final ActiveAdmin admin = getActiveAdminForCallerLocked( + who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, parent); + final PasswordBlacklist blacklist = getAdminPasswordBlacklistLocked(admin); + if (blacklist == null) { + return null; + } + return blacklist.getName(); + } + } + + @Override + public boolean isPasswordBlacklisted(@UserIdInt int userId, String password) { + if (!mHasFeature) { + return false; + } + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.TEST_BLACKLISTED_PASSWORD, null); + return isPasswordBlacklistedInternal(userId, password); + } + + private boolean isPasswordBlacklistedInternal(@UserIdInt int userId, String password) { + Preconditions.checkNotNull(password, "Password is null"); + enforceFullCrossUsersPermission(userId); + + // Normalize to lower case for case insensitive blacklist match + final String lowerCasePassword = password.toLowerCase(); + + synchronized (this) { + final List<ActiveAdmin> admins = + getActiveAdminsForLockscreenPoliciesLocked(userId, /* parent */ false); + final int N = admins.size(); + for (int i = 0; i < N; i++) { + final PasswordBlacklist blacklist + = getAdminPasswordBlacklistLocked(admins.get(i)); + if (blacklist != null) { + if (blacklist.isPasswordBlacklisted(lowerCasePassword)) { + return true; + } + } + } + } + + return false; + } + @Override public boolean isActivePasswordSufficient(int userHandle, boolean parent) { if (!mHasFeature) { @@ -4420,6 +4571,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return false; } } + + if (isPasswordBlacklistedInternal(userHandle, password)) { + Slog.w(LOG_TAG, "resetPassword: the password is blacklisted"); + return false; + } } DevicePolicyData policy = getUserData(userHandle); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PasswordBlacklist.java b/services/devicepolicy/java/com/android/server/devicepolicy/PasswordBlacklist.java new file mode 100644 index 000000000000..6a9b53a0a2df --- /dev/null +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PasswordBlacklist.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.devicepolicy; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.AtomicFile; +import android.util.Slog; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +/** + * Manages the blacklisted passwords. + * + * This caller must ensure synchronized access. + */ +public class PasswordBlacklist { + private static final String TAG = "PasswordBlacklist"; + + private final AtomicFile mFile; + + /** + * Create an object to manage the password blacklist. + * + * This is a lightweight operation to prepare variables but not perform any IO. + */ + public PasswordBlacklist(File file) { + mFile = new AtomicFile(file); + } + + /** + * Atomically replace the blacklist. + * + * Pass {@code null} for an empty list. + */ + public boolean savePasswordBlacklist(@NonNull String name, @NonNull List<String> blacklist) { + FileOutputStream fos = null; + try { + fos = mFile.startWrite(); + final DataOutputStream out = buildStreamForWriting(fos); + final Header header = new Header(Header.VERSION_1, name, blacklist.size()); + header.write(out); + final int blacklistSize = blacklist.size(); + for (int i = 0; i < blacklistSize; ++i) { + out.writeUTF(blacklist.get(i)); + } + out.flush(); + mFile.finishWrite(fos); + return true; + } catch (IOException e) { + mFile.failWrite(fos); + return false; + } + } + + /** @return the name of the blacklist or {@code null} if none set. */ + public String getName() { + try (DataInputStream in = openForReading()) { + return Header.read(in).mName; + } catch (IOException e) { + Slog.wtf(TAG, "Failed to read blacklist file", e); + } + return null; + } + + /** @return the number of blacklisted passwords. */ + public int getSize() { + final int blacklistSize; + try (DataInputStream in = openForReading()) { + return Header.read(in).mSize; + } catch (IOException e) { + Slog.wtf(TAG, "Failed to read blacklist file", e); + } + return 0; + } + + /** @return whether the password matches an blacklisted item. */ + public boolean isPasswordBlacklisted(@NonNull String password) { + final int blacklistSize; + try (DataInputStream in = openForReading()) { + final Header header = Header.read(in); + for (int i = 0; i < header.mSize; ++i) { + if (in.readUTF().equals(password)) { + return true; + } + } + } catch (IOException e) { + Slog.wtf(TAG, "Failed to read blacklist file", e); + // Fail safe and block all passwords. Setting a new blacklist should resolve this + // problem which can be identified by examining the log. + return true; + } + return false; + } + + /** Delete the blacklist completely from disk. */ + public void delete() { + mFile.delete(); + } + + /** Get the file the blacklist is stored in. */ + public File getFile() { + return mFile.getBaseFile(); + } + + private DataOutputStream buildStreamForWriting(FileOutputStream fos) { + return new DataOutputStream(new BufferedOutputStream(fos)); + } + + private DataInputStream openForReading() throws IOException { + return new DataInputStream(new BufferedInputStream(mFile.openRead())); + } + + /** + * Helper to read and write the header of the blacklist file. + */ + private static class Header { + static final int VERSION_1 = 1; + + final int mVersion; // File format version + final String mName; + final int mSize; + + Header(int version, String name, int size) { + mVersion = version; + mName = name; + mSize = size; + } + + void write(DataOutputStream out) throws IOException { + out.writeInt(mVersion); + out.writeUTF(mName); + out.writeInt(mSize); + } + + static Header read(DataInputStream in) throws IOException { + final int version = in.readInt(); + final String name = in.readUTF(); + final int size = in.readInt(); + return new Header(version, name, size); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java index d168479a6b00..0650acb45c47 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java @@ -102,6 +102,11 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi this.context = injector.context; } + @Override + public boolean isPasswordBlacklisted(int userId, String password) { + return false; + } + public void notifyChangeToContentObserver(Uri uri, int userHandle) { ContentObserver co = mMockInjector.mContentObservers.get(new Pair<>(uri, userHandle)); @@ -205,6 +210,11 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi } @Override + PasswordBlacklist newPasswordBlacklist(File file) { + return services.passwordBlacklist; + } + + @Override boolean storageManagerIsFileBasedEncryptionEnabled() { return services.storageManager.isFileBasedEncryptionEnabled(); } 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 ca918c6fa81c..4779474faf9b 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -3765,6 +3765,36 @@ public class DevicePolicyManagerTest extends DpmTestBase { assertTrue(dpm.clearResetPasswordToken(admin1)); } + public void testSetPasswordBlacklistCannotBeCalledByNonAdmin() throws Exception { + assertExpectException(SecurityException.class, /* messageRegex= */ null, + () -> dpm.setPasswordBlacklist(admin1, null, null)); + verifyZeroInteractions(getServices().passwordBlacklist); + } + + public void testClearingPasswordBlacklistDoesNotCreateNewBlacklist() throws Exception { + setupProfileOwner(); + dpm.setPasswordBlacklist(admin1, null, null); + verifyZeroInteractions(getServices().passwordBlacklist); + } + + public void testSetPasswordBlacklistCreatesNewBlacklist() throws Exception { + final String name = "myblacklist"; + final List<String> explicit = Arrays.asList("password", "letmein"); + setupProfileOwner(); + dpm.setPasswordBlacklist(admin1, name, explicit); + verify(getServices().passwordBlacklist).savePasswordBlacklist(name, explicit); + } + + public void testSetPasswordBlacklistOnlyConvertsExplicitToLowerCase() throws Exception { + final List<String> mixedCase = Arrays.asList("password", "LETMEIN", "FooTBAll"); + final List<String> lowerCase = Arrays.asList("password", "letmein", "football"); + mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; + setupDeviceOwner(); + final String name = "Name of the Blacklist"; + dpm.setPasswordBlacklist(admin1, name, mixedCase); + verify(getServices().passwordBlacklist).savePasswordBlacklist(name, lowerCase); + } + public void testIsActivePasswordSufficient() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; mContext.packageName = admin1.getPackageName(); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java index 4ee5ba63a58e..8cb0459e042d 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java @@ -86,6 +86,7 @@ public class MockSystemServices { public final IBackupManager ibackupManager; public final IAudioService iaudioService; public final LockPatternUtils lockPatternUtils; + public final PasswordBlacklist passwordBlacklist; public final StorageManagerForMock storageManager; public final WifiManager wifiManager; public final SettingsForMock settings; @@ -120,6 +121,7 @@ public class MockSystemServices { ibackupManager = mock(IBackupManager.class); iaudioService = mock(IAudioService.class); lockPatternUtils = mock(LockPatternUtils.class); + passwordBlacklist = mock(PasswordBlacklist.class); storageManager = mock(StorageManagerForMock.class); wifiManager = mock(WifiManager.class); settings = mock(SettingsForMock.class); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/PasswordBlacklistTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/PasswordBlacklistTest.java new file mode 100644 index 000000000000..1b3fc2c1f207 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/PasswordBlacklistTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.devicepolicy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests for {@link PasswordBlacklist}. + * + * bit FrameworksServicesTests:com.android.server.devicepolicy.PasswordBlacklistTest + * runtest -x frameworks/base/services/tests/servicestests/src/com/android/server/devicepolicy/PasswordBlacklistTest.java + */ +@RunWith(AndroidJUnit4.class) +public final class PasswordBlacklistTest { + private File mBlacklistFile; + private PasswordBlacklist mBlacklist; + + @Before + public void setUp() throws IOException { + mBlacklistFile = File.createTempFile("pwdbl", null); + mBlacklist = new PasswordBlacklist(mBlacklistFile); + } + + @After + public void tearDown() { + mBlacklist.delete(); + } + + @Test + public void matchIsExact() { + // Note: Case sensitivity is handled by the user of PasswordBlacklist by normalizing the + // values stored in and tested against it. + mBlacklist.savePasswordBlacklist("matchIsExact", Arrays.asList("password", "qWERty")); + assertTrue(mBlacklist.isPasswordBlacklisted("password")); + assertTrue(mBlacklist.isPasswordBlacklisted("qWERty")); + assertFalse(mBlacklist.isPasswordBlacklisted("Password")); + assertFalse(mBlacklist.isPasswordBlacklisted("qwert")); + assertFalse(mBlacklist.isPasswordBlacklisted("letmein")); + } + + @Test + public void matchIsNotRegex() { + mBlacklist.savePasswordBlacklist("matchIsNotRegex", Arrays.asList("a+b*")); + assertTrue(mBlacklist.isPasswordBlacklisted("a+b*")); + assertFalse(mBlacklist.isPasswordBlacklisted("aaaa")); + assertFalse(mBlacklist.isPasswordBlacklisted("abbbb")); + assertFalse(mBlacklist.isPasswordBlacklisted("aaaa")); + } + + @Test + public void matchFailsSafe() throws IOException { + try (FileOutputStream fos = new FileOutputStream(mBlacklistFile)) { + // Write a malformed blacklist file + fos.write(17); + } + assertTrue(mBlacklist.isPasswordBlacklisted("anything")); + assertTrue(mBlacklist.isPasswordBlacklisted("at")); + assertTrue(mBlacklist.isPasswordBlacklisted("ALL")); + } + + @Test + public void blacklistCanBeNamed() { + final String name = "identifier"; + mBlacklist.savePasswordBlacklist(name, Arrays.asList("one", "two", "three")); + assertEquals(mBlacklist.getName(), name); + } + + @Test + public void reportsTheCorrectNumberOfEntries() { + mBlacklist.savePasswordBlacklist("Count Entries", Arrays.asList("1", "2", "3", "4")); + assertEquals(mBlacklist.getSize(), 4); + } + + @Test + public void reportsBlacklistFile() { + assertEquals(mBlacklistFile, mBlacklist.getFile()); + } +} |