/* * 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 android.annotation.IntDef; import android.annotation.NonNull; import android.app.admin.DevicePolicyManager; import android.os.Parcelable; import android.os.Parcel; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.io.IOException; /** * 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; 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, 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 CREATOR = new Parcelable.Creator() { 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); } /* * 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({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; } } }