diff options
Diffstat (limited to 'core/java')
6 files changed, 659 insertions, 264 deletions
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index c3c383ce5e55..8ea1ff539c9f 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2330,6 +2330,12 @@ public class DevicePolicyManager { "android.app.action.ADMIN_POLICY_COMPLIANCE"; /** + * Maximum supported password length. Kind-of arbitrary. + * @hide + */ + public static final int MAX_PASSWORD_LENGTH = 16; + + /** * Return true if the given administrator component is currently active (enabled) in the system. * * @param admin The administrator component to check for. @@ -3233,6 +3239,22 @@ public class DevicePolicyManager { } /** + * Returns minimum PasswordMetrics that satisfies all admin policies. + * + * @hide + */ + public PasswordMetrics getPasswordMinimumMetrics(@UserIdInt int userHandle) { + if (mService != null) { + try { + return mService.getPasswordMinimumMetrics(userHandle); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return null; + } + + /** * Called by an application that is administering the device to set the length of the password * history. After setting this, the user will not be able to enter a new password that is the * same as any password in the history. Note that the current password will remain until the @@ -3415,8 +3437,7 @@ public class DevicePolicyManager { if (!pm.hasSystemFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN)) { return 0; } - // Kind-of arbitrary. - return 16; + return MAX_PASSWORD_LENGTH; } /** diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 0da5b7a1cf62..7d2c54ea1436 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -72,6 +72,8 @@ interface IDevicePolicyManager { void setPasswordMinimumNonLetter(in ComponentName who, int length, boolean parent); int getPasswordMinimumNonLetter(in ComponentName who, int userHandle, boolean parent); + PasswordMetrics getPasswordMinimumMetrics(int userHandle); + void setPasswordHistoryLength(in ComponentName who, int length, boolean parent); int getPasswordHistoryLength(in ComponentName who, int userHandle, boolean parent); diff --git a/core/java/android/app/admin/PasswordMetrics.java b/core/java/android/app/admin/PasswordMetrics.java index 464f75c3d0b7..d9bfde5af61a 100644 --- a/core/java/android/app/admin/PasswordMetrics.java +++ b/core/java/android/app/admin/PasswordMetrics.java @@ -16,41 +16,65 @@ package android.app.admin; +import static android.app.admin.DevicePolicyManager.MAX_PASSWORD_LENGTH; 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; -import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; -import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; +import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE; +import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; +import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN; +import static com.android.internal.widget.LockPatternUtils.MIN_LOCK_PASSWORD_SIZE; +import static com.android.internal.widget.PasswordValidationError.CONTAINS_INVALID_CHARACTERS; +import static com.android.internal.widget.PasswordValidationError.CONTAINS_SEQUENCE; +import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_DIGITS; +import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LETTERS; +import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LOWER_CASE; +import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_DIGITS; +import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_LETTER; +import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_SYMBOLS; +import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_UPPER_CASE; +import static com.android.internal.widget.PasswordValidationError.TOO_LONG; +import static com.android.internal.widget.PasswordValidationError.TOO_SHORT; +import static com.android.internal.widget.PasswordValidationError.WEAK_CREDENTIAL_TYPE; + import android.annotation.IntDef; import android.annotation.NonNull; import android.app.admin.DevicePolicyManager.PasswordComplexity; import android.os.Parcel; import android.os.Parcelable; +import android.util.Log; -import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.widget.LockPatternUtils.CredentialType; import com.android.internal.widget.LockscreenCredential; +import com.android.internal.widget.PasswordValidationError; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; /** * A class that represents the metrics of a credential that are used to decide whether or not a - * credential meets the requirements. If the credential is a pattern, only quality matters. + * credential meets the requirements. * * {@hide} */ -public class PasswordMetrics implements Parcelable { +public final class PasswordMetrics implements Parcelable { + private static final String TAG = "PasswordMetrics"; + // Maximum allowed number of repeated or ordered characters in a sequence before we'll // consider it a complex PIN/password. public static final int MAX_ALLOWED_SEQUENCE = 3; - public int quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; + public @CredentialType int credType; + // Fields below only make sense when credType is PASSWORD. public int length = 0; public int letters = 0; public int upperCase = 0; @@ -58,139 +82,62 @@ public class PasswordMetrics implements Parcelable { public int numeric = 0; public int symbols = 0; public int nonLetter = 0; + public int nonNumeric = 0; + // MAX_VALUE is the most relaxed value, any sequence is ok, e.g. 123456789. 4 would forbid it. + public int seqLength = Integer.MAX_VALUE; - public PasswordMetrics() {} - - public PasswordMetrics(int quality) { - this.quality = quality; + public PasswordMetrics(int credType) { + this.credType = credType; } - public PasswordMetrics(int quality, int length) { - this.quality = quality; + public PasswordMetrics(int credType , int length, int letters, int upperCase, int lowerCase, + int numeric, int symbols, int nonLetter, int nonNumeric, int seqLength) { + this.credType = credType; this.length = length; - } - - public PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase, - int numeric, int symbols, int nonLetter) { - this(quality, length); this.letters = letters; this.upperCase = upperCase; this.lowerCase = lowerCase; this.numeric = numeric; this.symbols = symbols; this.nonLetter = nonLetter; + this.nonNumeric = nonNumeric; + this.seqLength = seqLength; } - private PasswordMetrics(Parcel in) { - quality = in.readInt(); - length = in.readInt(); - letters = in.readInt(); - upperCase = in.readInt(); - lowerCase = in.readInt(); - numeric = in.readInt(); - symbols = in.readInt(); - nonLetter = in.readInt(); - } - - /** Returns the min quality allowed by {@code complexityLevel}. */ - public static int complexityLevelToMinQuality(@PasswordComplexity int complexityLevel) { - // this would be the quality of the first metrics since mMetrics is sorted in ascending - // order of quality - return PasswordComplexityBucket - .complexityLevelToBucket(complexityLevel).mMetrics[0].quality; - } - - /** - * Returns a merged minimum {@link PasswordMetrics} requirements that a new password must meet - * to fulfil {@code requestedQuality}, {@code requiresNumeric} and {@code - * requiresLettersOrSymbols}, which are derived from {@link DevicePolicyManager} requirements, - * and {@code complexityLevel}. - * - * <p>Note that we are taking {@code userEnteredPasswordQuality} into account because there are - * more than one set of metrics to meet the minimum complexity requirement and inspecting what - * the user has entered can help determine whether the alphabetic or alphanumeric set of metrics - * should be used. For example, suppose minimum complexity requires either ALPHABETIC(8+), or - * ALPHANUMERIC(6+). If the user has entered "a", the length requirement displayed on the UI - * would be 8. Then the user appends "1" to make it "a1". We now know the user is entering - * an alphanumeric password so we would update the min complexity required min length to 6. - */ - public static PasswordMetrics getMinimumMetrics(@PasswordComplexity int complexityLevel, - int userEnteredPasswordQuality, int requestedQuality, boolean requiresNumeric, - boolean requiresLettersOrSymbols) { - int targetQuality = Math.max( - userEnteredPasswordQuality, - getActualRequiredQuality( - requestedQuality, requiresNumeric, requiresLettersOrSymbols)); - return getTargetQualityMetrics(complexityLevel, targetQuality); - } - - /** - * Returns the {@link PasswordMetrics} at {@code complexityLevel} which the metrics quality - * is the same as {@code targetQuality}. - * - * <p>If {@code complexityLevel} does not allow {@code targetQuality}, returns the metrics - * with the min quality at {@code complexityLevel}. - */ - // TODO(bernardchau): update tests to test getMinimumMetrics and change this to be private - @VisibleForTesting - public static PasswordMetrics getTargetQualityMetrics( - @PasswordComplexity int complexityLevel, int targetQuality) { - PasswordComplexityBucket targetBucket = - PasswordComplexityBucket.complexityLevelToBucket(complexityLevel); - for (PasswordMetrics metrics : targetBucket.mMetrics) { - if (targetQuality == metrics.quality) { - return metrics; - } - } - // none of the metrics at complexityLevel has targetQuality, return metrics with min quality - // see test case testGetMinimumMetrics_actualRequiredQualityStricter for an example, where - // min complexity allows at least NUMERIC_COMPLEX, user has not entered anything yet, and - // requested quality is NUMERIC - return targetBucket.mMetrics[0]; - } - - /** - * Finds out the actual quality requirement based on whether quality is {@link - * DevicePolicyManager#PASSWORD_QUALITY_COMPLEX} and whether digits, letters or symbols are - * required. - */ - @VisibleForTesting - // TODO(bernardchau): update tests to test getMinimumMetrics and change this to be private - public static int getActualRequiredQuality( - int requestedQuality, boolean requiresNumeric, boolean requiresLettersOrSymbols) { - if (requestedQuality != PASSWORD_QUALITY_COMPLEX) { - return requestedQuality; - } - - // find out actual password quality from complex requirements - if (requiresNumeric && requiresLettersOrSymbols) { - return PASSWORD_QUALITY_ALPHANUMERIC; - } - if (requiresLettersOrSymbols) { - return PASSWORD_QUALITY_ALPHABETIC; - } - if (requiresNumeric) { - // cannot specify numeric complex using complex quality so this must be numeric - return PASSWORD_QUALITY_NUMERIC; - } - - // reaching here means dpm sets quality to complex without specifying any requirements - return PASSWORD_QUALITY_UNSPECIFIED; + private PasswordMetrics(PasswordMetrics other) { + this(other.credType, other.length, other.letters, other.upperCase, other.lowerCase, + other.numeric, other.symbols, other.nonLetter, other.nonNumeric, other.seqLength); } /** * Returns {@code complexityLevel} or {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE} * if {@code complexityLevel} is not valid. + * + * TODO: move to PasswordPolicy */ @PasswordComplexity public static int sanitizeComplexityLevel(@PasswordComplexity int complexityLevel) { - return PasswordComplexityBucket.complexityLevelToBucket(complexityLevel).mComplexityLevel; + switch (complexityLevel) { + case PASSWORD_COMPLEXITY_HIGH: + case PASSWORD_COMPLEXITY_MEDIUM: + case PASSWORD_COMPLEXITY_LOW: + case PASSWORD_COMPLEXITY_NONE: + return complexityLevel; + default: + Log.w(TAG, "Invalid password complexity used: " + complexityLevel); + return PASSWORD_COMPLEXITY_NONE; + } } - public boolean isDefault() { - return quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED - && length == 0 && letters == 0 && upperCase == 0 && lowerCase == 0 - && numeric == 0 && symbols == 0 && nonLetter == 0; + private static boolean hasInvalidCharacters(byte[] password) { + // Allow non-control Latin-1 characters only. + for (byte b : password) { + char c = (char) b; + if (c < 32 || c > 127) { + return true; + } + } + return false; } @Override @@ -200,7 +147,7 @@ public class PasswordMetrics implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(quality); + dest.writeInt(credType); dest.writeInt(length); dest.writeInt(letters); dest.writeInt(upperCase); @@ -208,12 +155,25 @@ public class PasswordMetrics implements Parcelable { dest.writeInt(numeric); dest.writeInt(symbols); dest.writeInt(nonLetter); + dest.writeInt(nonNumeric); + dest.writeInt(seqLength); } - public static final @android.annotation.NonNull Parcelable.Creator<PasswordMetrics> CREATOR + public static final @NonNull Parcelable.Creator<PasswordMetrics> CREATOR = new Parcelable.Creator<PasswordMetrics>() { public PasswordMetrics createFromParcel(Parcel in) { - return new PasswordMetrics(in); + int credType = in.readInt(); + int length = in.readInt(); + int letters = in.readInt(); + int upperCase = in.readInt(); + int lowerCase = in.readInt(); + int numeric = in.readInt(); + int symbols = in.readInt(); + int nonLetter = in.readInt(); + int nonNumeric = in.readInt(); + int seqLength = in.readInt(); + return new PasswordMetrics(credType, length, letters, upperCase, lowerCase, numeric, + symbols, nonLetter, nonNumeric, seqLength); } public PasswordMetrics[] newArray(int size) { @@ -232,9 +192,9 @@ public class PasswordMetrics implements Parcelable { if (credential.isPassword()) { return PasswordMetrics.computeForPassword(credential.getCredential()); } else if (credential.isPattern()) { - return new PasswordMetrics(PASSWORD_QUALITY_SOMETHING); + return new PasswordMetrics(CREDENTIAL_TYPE_PATTERN); } else if (credential.isNone()) { - return new PasswordMetrics(PASSWORD_QUALITY_UNSPECIFIED); + return new PasswordMetrics(CREDENTIAL_TYPE_NONE); } else { throw new IllegalArgumentException("Unknown credential type " + credential.getType()); } @@ -251,16 +211,19 @@ public class PasswordMetrics implements Parcelable { int numeric = 0; int symbols = 0; int nonLetter = 0; + int nonNumeric = 0; final int length = password.length; for (byte b : password) { switch (categoryChar((char) b)) { case CHAR_LOWER_CASE: letters++; lowerCase++; + nonNumeric++; break; case CHAR_UPPER_CASE: letters++; upperCase++; + nonNumeric++; break; case CHAR_DIGIT: numeric++; @@ -269,53 +232,14 @@ public class PasswordMetrics implements Parcelable { case CHAR_SYMBOL: symbols++; nonLetter++; + nonNumeric++; break; } } - // Determine the quality of the password - final boolean hasNumeric = numeric > 0; - final boolean hasNonNumeric = (letters + symbols) > 0; - final int quality; - if (hasNonNumeric && hasNumeric) { - quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; - } else if (hasNonNumeric) { - quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC; - } else if (hasNumeric) { - quality = maxLengthSequence(password) > MAX_ALLOWED_SEQUENCE - ? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC - : DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; - } else { - quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; - } - - return new PasswordMetrics( - quality, length, letters, upperCase, lowerCase, numeric, symbols, nonLetter); - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof PasswordMetrics)) { - return false; - } - PasswordMetrics o = (PasswordMetrics) other; - return this.quality == o.quality - && this.length == o.length - && this.letters == o.letters - && this.upperCase == o.upperCase - && this.lowerCase == o.lowerCase - && this.numeric == o.numeric - && this.symbols == o.symbols - && this.nonLetter == o.nonLetter; - } - - private boolean satisfiesBucket(PasswordMetrics... bucket) { - for (PasswordMetrics metrics : bucket) { - if (this.quality == metrics.quality) { - return this.length >= metrics.length; - } - } - return false; + final int seqLength = maxLengthSequence(password); + return new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD, length, letters, upperCase, lowerCase, + numeric, symbols, nonLetter, nonNumeric, seqLength); } /** @@ -400,108 +324,394 @@ public class PasswordMetrics implements Parcelable { } } - /** Determines the {@link PasswordComplexity} of this {@link PasswordMetrics}. */ - @PasswordComplexity - public int determineComplexity() { - for (PasswordComplexityBucket bucket : PasswordComplexityBucket.BUCKETS) { - if (satisfiesBucket(bucket.mMetrics)) { - return bucket.mComplexityLevel; - } + /** + * Returns the weakest metrics that is stricter or equal to all given metrics. + * + * TODO: move to PasswordPolicy + */ + public static PasswordMetrics merge(List<PasswordMetrics> metrics) { + PasswordMetrics result = new PasswordMetrics(CREDENTIAL_TYPE_NONE); + for (PasswordMetrics m : metrics) { + result.maxWith(m); } - return PASSWORD_COMPLEXITY_NONE; + + return result; } /** - * Requirements in terms of {@link PasswordMetrics} for each {@link PasswordComplexity}. + * Makes current metric at least as strong as {@code other} in every criterion. + * + * TODO: move to PasswordPolicy */ - private static class PasswordComplexityBucket { - /** - * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_HIGH} in terms of - * {@link PasswordMetrics}. - */ - private static final PasswordComplexityBucket HIGH = - new PasswordComplexityBucket( - PASSWORD_COMPLEXITY_HIGH, - new PasswordMetrics( - DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */ - 8), - new PasswordMetrics( - DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 6), - new PasswordMetrics( - DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */ - 6)); - - /** - * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_MEDIUM} in terms of - * {@link PasswordMetrics}. - */ - private static final PasswordComplexityBucket MEDIUM = - new PasswordComplexityBucket( - PASSWORD_COMPLEXITY_MEDIUM, - new PasswordMetrics( - DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */ - 4), - new PasswordMetrics( - DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 4), - new PasswordMetrics( - DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */ - 4)); - - /** - * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_LOW} in terms of - * {@link PasswordMetrics}. - */ - private static final PasswordComplexityBucket LOW = - new PasswordComplexityBucket( - PASSWORD_COMPLEXITY_LOW, - new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING), - new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC), - new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX), - new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC), - new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC)); - - /** - * A special bucket to represent {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE}. - */ - private static final PasswordComplexityBucket NONE = - new PasswordComplexityBucket(PASSWORD_COMPLEXITY_NONE, new PasswordMetrics()); - - /** Array containing all buckets from high to low. */ - private static final PasswordComplexityBucket[] BUCKETS = - new PasswordComplexityBucket[] {HIGH, MEDIUM, LOW}; - - @PasswordComplexity - private final int mComplexityLevel; - private final PasswordMetrics[] mMetrics; - - /** - * @param metricsArray must be sorted in ascending order of {@link #quality}. - */ - private PasswordComplexityBucket(@PasswordComplexity int complexityLevel, - PasswordMetrics... metricsArray) { - int previousQuality = PASSWORD_QUALITY_UNSPECIFIED; - for (PasswordMetrics metrics : metricsArray) { - if (metrics.quality < previousQuality) { - throw new IllegalArgumentException("metricsArray must be sorted in ascending" - + " order of quality"); - } - previousQuality = metrics.quality; + private void maxWith(PasswordMetrics other) { + credType = Math.max(credType, other.credType); + if (credType != CREDENTIAL_TYPE_PASSWORD) { + return; + } + length = Math.max(length, other.length); + letters = Math.max(letters, other.letters); + upperCase = Math.max(upperCase, other.upperCase); + lowerCase = Math.max(lowerCase, other.lowerCase); + numeric = Math.max(numeric, other.numeric); + symbols = Math.max(symbols, other.symbols); + nonLetter = Math.max(nonLetter, other.nonLetter); + nonNumeric = Math.max(nonNumeric, other.nonNumeric); + seqLength = Math.min(seqLength, other.seqLength); + } + + /** + * Returns minimum password quality for a given complexity level. + * + * TODO: this function is used for determining allowed credential types, so it should return + * credential type rather than 'quality'. + * + * TODO: move to PasswordPolicy + */ + public static int complexityLevelToMinQuality(int complexity) { + switch (complexity) { + case PASSWORD_COMPLEXITY_HIGH: + case PASSWORD_COMPLEXITY_MEDIUM: + return PASSWORD_QUALITY_NUMERIC_COMPLEX; + case PASSWORD_COMPLEXITY_LOW: + return PASSWORD_QUALITY_SOMETHING; + case PASSWORD_COMPLEXITY_NONE: + default: + return PASSWORD_QUALITY_UNSPECIFIED; + } + } + + /** + * Enum representing requirements for each complexity level. + * + * TODO: move to PasswordPolicy + */ + private enum ComplexityBucket { + // Keep ordered high -> low. + BUCKET_HIGH(PASSWORD_COMPLEXITY_HIGH) { + @Override + boolean canHaveSequence() { + return false; } - this.mMetrics = metricsArray; - this.mComplexityLevel = complexityLevel; + @Override + int getMinimumLength(boolean containsNonNumeric) { + return containsNonNumeric ? 6 : 8; + } + + @Override + boolean allowsNumericPassword() { + return false; + } + + @Override + boolean allowsCredType(int credType) { + return credType == CREDENTIAL_TYPE_PASSWORD; + } + }, + BUCKET_MEDIUM(PASSWORD_COMPLEXITY_MEDIUM) { + @Override + boolean canHaveSequence() { + return false; + } + + @Override + int getMinimumLength(boolean containsNonNumeric) { + return 4; + } + + @Override + boolean allowsNumericPassword() { + return false; + } + + @Override + boolean allowsCredType(int credType) { + return credType == CREDENTIAL_TYPE_PASSWORD; + } + }, + BUCKET_LOW(PASSWORD_COMPLEXITY_LOW) { + @Override + boolean canHaveSequence() { + return true; + } + + @Override + int getMinimumLength(boolean containsNonNumeric) { + return 0; + } + + @Override + boolean allowsNumericPassword() { + return true; + } + + @Override + boolean allowsCredType(int credType) { + return credType != CREDENTIAL_TYPE_NONE; + } + }, + BUCKET_NONE(PASSWORD_COMPLEXITY_NONE) { + @Override + boolean canHaveSequence() { + return true; + } + + @Override + int getMinimumLength(boolean containsNonNumeric) { + return 0; + } + + @Override + boolean allowsNumericPassword() { + return true; + } + + @Override + boolean allowsCredType(int credType) { + return true; + } + }; + int mComplexityLevel; + + abstract boolean canHaveSequence(); + abstract int getMinimumLength(boolean containsNonNumeric); + abstract boolean allowsNumericPassword(); + abstract boolean allowsCredType(int credType); + + ComplexityBucket(int complexityLevel) { + this.mComplexityLevel = complexityLevel; } - /** Returns the bucket that {@code complexityLevel} represents. */ - private static PasswordComplexityBucket complexityLevelToBucket( - @PasswordComplexity int complexityLevel) { - for (PasswordComplexityBucket bucket : BUCKETS) { + static ComplexityBucket forComplexity(int complexityLevel) { + for (ComplexityBucket bucket : values()) { if (bucket.mComplexityLevel == complexityLevel) { return bucket; } } - return NONE; + throw new IllegalArgumentException("Invalid complexity level: " + complexityLevel); } } + + /** + * Returns whether current metrics satisfies a given complexity bucket. + * + * TODO: move inside ComplexityBucket. + */ + private boolean satisfiesBucket(ComplexityBucket bucket) { + if (!bucket.allowsCredType(credType)) { + return false; + } + if (credType != CREDENTIAL_TYPE_PASSWORD) { + return true; + } + return (bucket.canHaveSequence() || seqLength <= MAX_ALLOWED_SEQUENCE) + && length >= bucket.getMinimumLength(nonNumeric > 0 /* hasNonNumeric */); + } + + /** + * Returns the maximum complexity level satisfied by password with this metrics. + * + * TODO: move inside ComplexityBucket. + */ + public int determineComplexity() { + for (ComplexityBucket bucket : ComplexityBucket.values()) { + if (satisfiesBucket(bucket)) { + return bucket.mComplexityLevel; + } + } + throw new IllegalStateException("Failed to figure out complexity for a given metrics"); + } + + /** + * Validates password against minimum metrics and complexity. + * + * @param adminMetrics - minimum metrics to satisfy admin requirements. + * @param minComplexity - minimum complexity imposed by the requester. + * @param isPin - whether it is PIN that should be only digits + * @param password - password to validate. + * @return a list of password validation errors. An empty list means the password is OK. + * + * TODO: move to PasswordPolicy + */ + public static List<PasswordValidationError> validatePassword( + PasswordMetrics adminMetrics, int minComplexity, boolean isPin, byte[] password) { + + if (hasInvalidCharacters(password)) { + return Collections.singletonList( + new PasswordValidationError(CONTAINS_INVALID_CHARACTERS, 0)); + } + + final PasswordMetrics enteredMetrics = computeForPassword(password); + return validatePasswordMetrics(adminMetrics, minComplexity, isPin, enteredMetrics); + } + + /** + * Validates password metrics against minimum metrics and complexity + * + * @param adminMetrics - minimum metrics to satisfy admin requirements. + * @param minComplexity - minimum complexity imposed by the requester. + * @param isPin - whether it is PIN that should be only digits + * @param actualMetrics - metrics for password to validate. + * @return a list of password validation errors. An empty list means the password is OK. + * + * TODO: move to PasswordPolicy + */ + public static List<PasswordValidationError> validatePasswordMetrics( + PasswordMetrics adminMetrics, int minComplexity, boolean isPin, + PasswordMetrics actualMetrics) { + final ComplexityBucket bucket = ComplexityBucket.forComplexity(minComplexity); + + // Make sure credential type is satisfactory. + // TODO: stop relying on credential type ordering. + if (actualMetrics.credType < adminMetrics.credType + || !bucket.allowsCredType(actualMetrics.credType)) { + return Collections.singletonList(new PasswordValidationError(WEAK_CREDENTIAL_TYPE, 0)); + } + // TODO: this needs to be modified if CREDENTIAL_TYPE_PIN is added. + if (actualMetrics.credType != CREDENTIAL_TYPE_PASSWORD) { + return Collections.emptyList(); // Nothing to check for pattern or none. + } + + if (isPin && actualMetrics.nonNumeric > 0) { + return Collections.singletonList( + new PasswordValidationError(CONTAINS_INVALID_CHARACTERS, 0)); + } + + final ArrayList<PasswordValidationError> result = new ArrayList<>(); + if (actualMetrics.length > MAX_PASSWORD_LENGTH) { + result.add(new PasswordValidationError(TOO_LONG, MAX_PASSWORD_LENGTH)); + } + + final PasswordMetrics minMetrics = applyComplexity(adminMetrics, isPin, bucket); + + // Clamp required length between maximum and minimum valid values. + minMetrics.length = Math.min(MAX_PASSWORD_LENGTH, + Math.max(minMetrics.length, MIN_LOCK_PASSWORD_SIZE)); + minMetrics.removeOverlapping(); + + comparePasswordMetrics(minMetrics, actualMetrics, result); + + return result; + } + + /** + * TODO: move to PasswordPolicy + */ + private static void comparePasswordMetrics(PasswordMetrics minMetrics, + PasswordMetrics actualMetrics, ArrayList<PasswordValidationError> result) { + if (actualMetrics.length < minMetrics.length) { + result.add(new PasswordValidationError(TOO_SHORT, minMetrics.length)); + } + if (actualMetrics.letters < minMetrics.letters) { + result.add(new PasswordValidationError(NOT_ENOUGH_LETTERS, minMetrics.letters)); + } + if (actualMetrics.upperCase < minMetrics.upperCase) { + result.add(new PasswordValidationError(NOT_ENOUGH_UPPER_CASE, minMetrics.upperCase)); + } + if (actualMetrics.lowerCase < minMetrics.lowerCase) { + result.add(new PasswordValidationError(NOT_ENOUGH_LOWER_CASE, minMetrics.lowerCase)); + } + if (actualMetrics.numeric < minMetrics.numeric) { + result.add(new PasswordValidationError(NOT_ENOUGH_DIGITS, minMetrics.numeric)); + } + if (actualMetrics.symbols < minMetrics.symbols) { + result.add(new PasswordValidationError(NOT_ENOUGH_SYMBOLS, minMetrics.symbols)); + } + if (actualMetrics.nonLetter < minMetrics.nonLetter) { + result.add(new PasswordValidationError(NOT_ENOUGH_NON_LETTER, minMetrics.nonLetter)); + } + if (actualMetrics.nonNumeric < minMetrics.nonNumeric) { + result.add(new PasswordValidationError(NOT_ENOUGH_NON_DIGITS, minMetrics.nonNumeric)); + } + if (actualMetrics.seqLength > minMetrics.seqLength) { + result.add(new PasswordValidationError(CONTAINS_SEQUENCE, 0)); + } + } + + /** + * Drop requirements that are superseded by others, e.g. if it is required to have 5 upper case + * letters and 5 lower case letters, there is no need to require minimum number of letters to + * be 10 since it will be fulfilled once upper and lower case requirements are fulfilled. + * + * TODO: move to PasswordPolicy + */ + private void removeOverlapping() { + // upperCase + lowerCase can override letters + final int indirectLetters = upperCase + lowerCase; + + // numeric + symbols can override nonLetter + final int indirectNonLetter = numeric + symbols; + + // letters + symbols can override nonNumeric + final int effectiveLetters = Math.max(letters, indirectLetters); + final int indirectNonNumeric = effectiveLetters + symbols; + + // letters + nonLetters can override length + // numeric + nonNumeric can also override length, so max it with previous. + final int effectiveNonLetter = Math.max(nonLetter, indirectNonLetter); + final int effectiveNonNumeric = Math.max(nonNumeric, indirectNonNumeric); + final int indirectLength = Math.max(effectiveLetters + effectiveNonLetter, + numeric + effectiveNonNumeric); + + if (indirectLetters >= letters) { + letters = 0; + } + if (indirectNonLetter >= nonLetter) { + nonLetter = 0; + } + if (indirectNonNumeric >= nonNumeric) { + nonNumeric = 0; + } + if (indirectLength >= length) { + length = 0; + } + } + + /** + * Combine minimum metrics, set by admin, complexity set by the requester and actual entered + * password metrics to get resulting minimum metrics that the password has to satisfy. Always + * returns a new PasswordMetrics object. + * + * TODO: move to PasswordPolicy + */ + private static PasswordMetrics applyComplexity( + PasswordMetrics adminMetrics, boolean isPin, ComplexityBucket bucket) { + final PasswordMetrics minMetrics = new PasswordMetrics(adminMetrics); + + if (!bucket.canHaveSequence()) { + minMetrics.seqLength = Math.min(minMetrics.seqLength, MAX_ALLOWED_SEQUENCE); + } + + minMetrics.length = Math.max(minMetrics.length, bucket.getMinimumLength(!isPin)); + + if (!isPin && !bucket.allowsNumericPassword()) { + minMetrics.nonNumeric = Math.max(minMetrics.nonNumeric, 1); + } + + return minMetrics; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final PasswordMetrics that = (PasswordMetrics) o; + return credType == that.credType + && length == that.length + && letters == that.letters + && upperCase == that.upperCase + && lowerCase == that.lowerCase + && numeric == that.numeric + && symbols == that.symbols + && nonLetter == that.nonLetter + && nonNumeric == that.nonNumeric + && seqLength == that.seqLength; + } + + @Override + public int hashCode() { + return Objects.hash(credType, length, letters, upperCase, lowerCase, numeric, symbols, + nonLetter, nonNumeric, seqLength); + } } diff --git a/core/java/android/app/admin/PasswordPolicy.java b/core/java/android/app/admin/PasswordPolicy.java new file mode 100644 index 000000000000..13f11ad74d12 --- /dev/null +++ b/core/java/android/app/admin/PasswordPolicy.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 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 android.app.admin; + +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; + +import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE; +import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; +import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN; + +/** + * {@hide} + */ +public class PasswordPolicy { + public static final int DEF_MINIMUM_LENGTH = 0; + public static final int DEF_MINIMUM_LETTERS = 1; + public static final int DEF_MINIMUM_UPPER_CASE = 0; + public static final int DEF_MINIMUM_LOWER_CASE = 0; + public static final int DEF_MINIMUM_NUMERIC = 1; + public static final int DEF_MINIMUM_SYMBOLS = 1; + public static final int DEF_MINIMUM_NON_LETTER = 0; + + public int quality = PASSWORD_QUALITY_UNSPECIFIED; + public int length = DEF_MINIMUM_LENGTH; + public int letters = DEF_MINIMUM_LETTERS; + public int upperCase = DEF_MINIMUM_UPPER_CASE; + public int lowerCase = DEF_MINIMUM_LOWER_CASE; + public int numeric = DEF_MINIMUM_NUMERIC; + public int symbols = DEF_MINIMUM_SYMBOLS; + public int nonLetter = DEF_MINIMUM_NON_LETTER; + + /** + * Returns a minimum password metrics that the password should have to satisfy current policy. + */ + public PasswordMetrics getMinMetrics() { + if (quality == PASSWORD_QUALITY_UNSPECIFIED) { + return new PasswordMetrics(CREDENTIAL_TYPE_NONE); + } else if (quality == PASSWORD_QUALITY_BIOMETRIC_WEAK + || quality == PASSWORD_QUALITY_SOMETHING) { + return new PasswordMetrics(CREDENTIAL_TYPE_PATTERN); + } // quality is NUMERIC or stronger. + + PasswordMetrics result = new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD); + result.length = length; + + if (quality == PASSWORD_QUALITY_NUMERIC_COMPLEX) { + result.seqLength = PasswordMetrics.MAX_ALLOWED_SEQUENCE; + } else if (quality == PASSWORD_QUALITY_ALPHABETIC) { + result.nonNumeric = 1; + } else if (quality == PASSWORD_QUALITY_ALPHANUMERIC) { + result.numeric = 1; + result.nonNumeric = 1; + } else if (quality == PASSWORD_QUALITY_COMPLEX) { + result.numeric = numeric; + result.letters = letters; + result.upperCase = upperCase; + result.lowerCase = lowerCase; + result.nonLetter = nonLetter; + result.symbols = symbols; + } + return result; + } +} diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 1daa25af11fc..8fea703e7e42 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -30,6 +30,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; import android.app.admin.DevicePolicyManager; +import android.app.admin.PasswordMetrics; import android.app.trust.IStrongAuthTracker; import android.app.trust.TrustManager; import android.content.ComponentName; @@ -58,10 +59,10 @@ import android.util.SparseLongArray; import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; -import com.google.android.collect.Lists; - import libcore.util.HexEncoding; +import com.google.android.collect.Lists; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.security.MessageDigest; @@ -77,7 +78,6 @@ import java.util.StringJoiner; * Utilities for the lock pattern and its settings. */ public class LockPatternUtils { - private static final String TAG = "LockPatternUtils"; private static final boolean FRP_CREDENTIAL_ENABLED = true; @@ -114,6 +114,7 @@ public class LockPatternUtils { */ public static final int MIN_PATTERN_REGISTER_FAIL = MIN_LOCK_PATTERN_SIZE; + // NOTE: When modifying this, make sure credential sufficiency validation logic is intact. public static final int CREDENTIAL_TYPE_NONE = -1; public static final int CREDENTIAL_TYPE_PATTERN = 1; public static final int CREDENTIAL_TYPE_PASSWORD = 2; @@ -289,10 +290,10 @@ public class LockPatternUtils { return getDevicePolicyManager().getPasswordMaximumLength(quality); } - /** - * Gets the device policy password mode. If the mode is non-specific, returns - * MODE_PATTERN which allows the user to choose anything. - */ + public PasswordMetrics getRequestedPasswordMetrics(int userId) { + return getDevicePolicyManager().getPasswordMinimumMetrics(userId); + } + public int getRequestedPasswordQuality(int userId) { return getDevicePolicyManager().getPasswordQuality(null, userId); } diff --git a/core/java/com/android/internal/widget/PasswordValidationError.java b/core/java/com/android/internal/widget/PasswordValidationError.java new file mode 100644 index 000000000000..41b234ef024e --- /dev/null +++ b/core/java/com/android/internal/widget/PasswordValidationError.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 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.internal.widget; + +/** + * Password validation error containing an error code and optional requirement. + */ +public class PasswordValidationError { + // Password validation error codes + public static final int WEAK_CREDENTIAL_TYPE = 1; + public static final int CONTAINS_INVALID_CHARACTERS = 2; + public static final int TOO_SHORT = 3; + public static final int TOO_LONG = 4; + public static final int CONTAINS_SEQUENCE = 5; + public static final int NOT_ENOUGH_LETTERS = 6; + public static final int NOT_ENOUGH_UPPER_CASE = 7; + public static final int NOT_ENOUGH_LOWER_CASE = 8; + public static final int NOT_ENOUGH_DIGITS = 9; + public static final int NOT_ENOUGH_SYMBOLS = 10; + public static final int NOT_ENOUGH_NON_LETTER = 11; + public static final int NOT_ENOUGH_NON_DIGITS = 12; + public static final int RECENTLY_USED = 13; + // WARNING: if you add a new error, make sure it is presented to the user correctly in Settings. + + public final int errorCode; + public final int requirement; + + public PasswordValidationError(int errorCode) { + this(errorCode, 0); + } + + public PasswordValidationError(int errorCode, int requirement) { + this.errorCode = errorCode; + this.requirement = requirement; + } + + @Override + public String toString() { + return errorCodeToString(errorCode) + (requirement > 0 ? "; required: " + requirement : ""); + } + + /** + * Returns textual representation of the error for logging purposes. + */ + private static String errorCodeToString(int error) { + switch (error) { + case WEAK_CREDENTIAL_TYPE: return "Weak credential type"; + case CONTAINS_INVALID_CHARACTERS: return "Contains an invalid character"; + case TOO_SHORT: return "Password too short"; + case TOO_LONG: return "Password too long"; + case CONTAINS_SEQUENCE: return "Sequence too long"; + case NOT_ENOUGH_LETTERS: return "Too few letters"; + case NOT_ENOUGH_UPPER_CASE: return "Too few upper case letters"; + case NOT_ENOUGH_LOWER_CASE: return "Too few lower case letters"; + case NOT_ENOUGH_DIGITS: return "Too few numeric characters"; + case NOT_ENOUGH_SYMBOLS: return "Too few symbols"; + case NOT_ENOUGH_NON_LETTER: return "Too few non-letter characters"; + case NOT_ENOUGH_NON_DIGITS: return "Too few non-numeric characters"; + case RECENTLY_USED: return "Pin or password was recently used"; + default: return "Unknown error " + error; + } + } + +} |
