| /* |
| * 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 android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.app.admin.DevicePolicyManager.PasswordComplexity; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| |
| /** |
| * A class that represents the metrics of a password that are used to decide whether or not a |
| * password meets the requirements. |
| * |
| * {@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; |
| |
| // TODO(b/120536847): refactor isActivePasswordSufficient logic so that the actual password |
| // quality is not overwritten |
| 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(); |
| } |
| |
| 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 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]; |
| } |
| }; |
| |
| public static PasswordMetrics computeForPassword(@NonNull String 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 (int i = 0; i < length; i++) { |
| switch (categoryChar(password.charAt(i))) { |
| 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 string the pass |
| * @return the number of sequential letters or digits |
| */ |
| public static int maxLengthSequence(@NonNull String string) { |
| if (string.length() == 0) return 0; |
| char previousChar = string.charAt(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 < string.length(); current++) { |
| char currentChar = string.charAt(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, string.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.getMetrics())) { |
| return bucket.mComplexityLevel; |
| } |
| } |
| return PASSWORD_COMPLEXITY_NONE; |
| } |
| |
| /** |
| * Requirements in terms of {@link PasswordMetrics} for each {@link PasswordComplexity}. |
| */ |
| public 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_ALPHANUMERIC, /* length= */ 6), |
| new PasswordMetrics( |
| DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 6), |
| new PasswordMetrics( |
| DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */ |
| 8)); |
| |
| /** |
| * 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_ALPHANUMERIC, /* length= */ 4), |
| new PasswordMetrics( |
| DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 4), |
| new PasswordMetrics( |
| DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* 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_ALPHANUMERIC), |
| new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC), |
| new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX), |
| new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC), |
| new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING)); |
| |
| /** |
| * 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; |
| |
| private PasswordComplexityBucket(@PasswordComplexity int complexityLevel, |
| PasswordMetrics... metrics) { |
| this.mComplexityLevel = complexityLevel; |
| this.mMetrics = metrics; |
| } |
| |
| /** Returns the {@link PasswordMetrics} that meet the min requirements of this bucket. */ |
| public PasswordMetrics[] getMetrics() { |
| return mMetrics; |
| } |
| |
| /** Returns the bucket that {@code complexityLevel} represents. */ |
| public static PasswordComplexityBucket complexityLevelToBucket( |
| @PasswordComplexity int complexityLevel) { |
| for (PasswordComplexityBucket bucket : BUCKETS) { |
| if (bucket.mComplexityLevel == complexityLevel) { |
| return bucket; |
| } |
| } |
| return NONE; |
| } |
| } |
| } |