| /* |
| * Copyright (C) 2016 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_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_SOMETHING; |
| import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; |
| |
| import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; |
| import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN; |
| |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.app.admin.DevicePolicyManager.PasswordComplexity; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.Preconditions; |
| import com.android.internal.widget.LockPatternUtils.CredentialType; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| |
| /** |
| * 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. |
| * |
| * {@hide} |
| */ |
| public class PasswordMetrics implements Parcelable { |
| // 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 int length = 0; |
| public int letters = 0; |
| public int upperCase = 0; |
| public int lowerCase = 0; |
| public int numeric = 0; |
| public int symbols = 0; |
| public int nonLetter = 0; |
| |
| public PasswordMetrics() {} |
| |
| public PasswordMetrics(int quality) { |
| this.quality = quality; |
| } |
| |
| public PasswordMetrics(int quality, int length) { |
| this.quality = quality; |
| 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; |
| } |
| |
| 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; |
| } |
| |
| /** |
| * Returns {@code complexityLevel} or {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE} |
| * if {@code complexityLevel} is not valid. |
| */ |
| @PasswordComplexity |
| public static int sanitizeComplexityLevel(@PasswordComplexity int complexityLevel) { |
| return PasswordComplexityBucket.complexityLevelToBucket(complexityLevel).mComplexityLevel; |
| } |
| |
| public boolean isDefault() { |
| return quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED |
| && length == 0 && letters == 0 && upperCase == 0 && lowerCase == 0 |
| && numeric == 0 && symbols == 0 && nonLetter == 0; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeInt(quality); |
| dest.writeInt(length); |
| dest.writeInt(letters); |
| dest.writeInt(upperCase); |
| dest.writeInt(lowerCase); |
| dest.writeInt(numeric); |
| dest.writeInt(symbols); |
| dest.writeInt(nonLetter); |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<PasswordMetrics> CREATOR |
| = new Parcelable.Creator<PasswordMetrics>() { |
| public PasswordMetrics createFromParcel(Parcel in) { |
| return new PasswordMetrics(in); |
| } |
| |
| public PasswordMetrics[] newArray(int size) { |
| return new PasswordMetrics[size]; |
| } |
| }; |
| |
| /** |
| * Returnsthe {@code PasswordMetrics} for a given credential. |
| * |
| * If the credential is a pin or a password, equivalent to {@link #computeForPassword(byte[])}. |
| * {@code credential} cannot be null when {@code type} is |
| * {@link com.android.internal.widget.LockPatternUtils#CREDENTIAL_TYPE_PASSWORD}. |
| */ |
| public static PasswordMetrics computeForCredential( |
| @CredentialType int type, byte[] credential) { |
| if (type == CREDENTIAL_TYPE_PASSWORD) { |
| Preconditions.checkNotNull(credential, "credential cannot be null"); |
| return PasswordMetrics.computeForPassword(credential); |
| } else if (type == CREDENTIAL_TYPE_PATTERN) { |
| return new PasswordMetrics(PASSWORD_QUALITY_SOMETHING); |
| } else /* if (type == CREDENTIAL_TYPE_NONE) */ { |
| return new PasswordMetrics(PASSWORD_QUALITY_UNSPECIFIED); |
| } |
| } |
| |
| /** |
| * Returns the {@code PasswordMetrics} for a given password |
| */ |
| public static PasswordMetrics computeForPassword(@NonNull byte[] password) { |
| // Analyse the characters used |
| int letters = 0; |
| int upperCase = 0; |
| int lowerCase = 0; |
| int numeric = 0; |
| int symbols = 0; |
| int nonLetter = 0; |
| final int length = password.length; |
| for (byte b : password) { |
| switch (categoryChar((char) b)) { |
| case CHAR_LOWER_CASE: |
| letters++; |
| lowerCase++; |
| break; |
| case CHAR_UPPER_CASE: |
| letters++; |
| upperCase++; |
| break; |
| case CHAR_DIGIT: |
| numeric++; |
| nonLetter++; |
| break; |
| case CHAR_SYMBOL: |
| symbols++; |
| nonLetter++; |
| 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; |
| } |
| |
| /** |
| * Returns the maximum length of a sequential characters. A sequence is defined as |
| * monotonically increasing characters with a constant interval or the same character repeated. |
| * |
| * For example: |
| * maxLengthSequence("1234") == 4 |
| * maxLengthSequence("13579") == 5 |
| * maxLengthSequence("1234abc") == 4 |
| * maxLengthSequence("aabc") == 3 |
| * maxLengthSequence("qwertyuio") == 1 |
| * maxLengthSequence("@ABC") == 3 |
| * maxLengthSequence(";;;;") == 4 (anything that repeats) |
| * maxLengthSequence(":;<=>") == 1 (ordered, but not composed of alphas or digits) |
| * |
| * @param bytes the pass |
| * @return the number of sequential letters or digits |
| */ |
| public static int maxLengthSequence(@NonNull byte[] bytes) { |
| if (bytes.length == 0) return 0; |
| char previousChar = (char) bytes[0]; |
| @CharacterCatagory int category = categoryChar(previousChar); //current sequence category |
| int diff = 0; //difference between two consecutive characters |
| boolean hasDiff = false; //if we are currently targeting a sequence |
| int maxLength = 0; //maximum length of a sequence already found |
| int startSequence = 0; //where the current sequence started |
| for (int current = 1; current < bytes.length; current++) { |
| char currentChar = (char) bytes[current]; |
| @CharacterCatagory int categoryCurrent = categoryChar(currentChar); |
| int currentDiff = (int) currentChar - (int) previousChar; |
| if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) { |
| maxLength = Math.max(maxLength, current - startSequence); |
| startSequence = current; |
| hasDiff = false; |
| category = categoryCurrent; |
| } |
| else { |
| if(hasDiff && currentDiff != diff) { |
| maxLength = Math.max(maxLength, current - startSequence); |
| startSequence = current - 1; |
| } |
| diff = currentDiff; |
| hasDiff = true; |
| } |
| previousChar = currentChar; |
| } |
| maxLength = Math.max(maxLength, bytes.length - startSequence); |
| return maxLength; |
| } |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef(prefix = { "CHAR_" }, value = { |
| CHAR_UPPER_CASE, |
| CHAR_LOWER_CASE, |
| CHAR_DIGIT, |
| CHAR_SYMBOL |
| }) |
| private @interface CharacterCatagory {} |
| private static final int CHAR_LOWER_CASE = 0; |
| private static final int CHAR_UPPER_CASE = 1; |
| private static final int CHAR_DIGIT = 2; |
| private static final int CHAR_SYMBOL = 3; |
| |
| @CharacterCatagory |
| private static int categoryChar(char c) { |
| if ('a' <= c && c <= 'z') return CHAR_LOWER_CASE; |
| if ('A' <= c && c <= 'Z') return CHAR_UPPER_CASE; |
| if ('0' <= c && c <= '9') return CHAR_DIGIT; |
| return CHAR_SYMBOL; |
| } |
| |
| private static int maxDiffCategory(@CharacterCatagory int category) { |
| switch (category) { |
| case CHAR_LOWER_CASE: |
| case CHAR_UPPER_CASE: |
| return 1; |
| case CHAR_DIGIT: |
| return 10; |
| default: |
| return 0; |
| } |
| } |
| |
| /** Determines the {@link PasswordComplexity} of this {@link PasswordMetrics}. */ |
| @PasswordComplexity |
| public int determineComplexity() { |
| for (PasswordComplexityBucket bucket : PasswordComplexityBucket.BUCKETS) { |
| if (satisfiesBucket(bucket.mMetrics)) { |
| return bucket.mComplexityLevel; |
| } |
| } |
| return PASSWORD_COMPLEXITY_NONE; |
| } |
| |
| /** |
| * Requirements in terms of {@link PasswordMetrics} for each {@link PasswordComplexity}. |
| */ |
| 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; |
| } |
| |
| this.mMetrics = metricsArray; |
| this.mComplexityLevel = complexityLevel; |
| |
| } |
| |
| /** Returns the bucket that {@code complexityLevel} represents. */ |
| private static PasswordComplexityBucket complexityLevelToBucket( |
| @PasswordComplexity int complexityLevel) { |
| for (PasswordComplexityBucket bucket : BUCKETS) { |
| if (bucket.mComplexityLevel == complexityLevel) { |
| return bucket; |
| } |
| } |
| return NONE; |
| } |
| } |
| } |