1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.app.admin; 18 19import android.annotation.IntDef; 20import android.annotation.NonNull; 21import android.app.admin.DevicePolicyManager; 22import android.os.Parcelable; 23import android.os.Parcel; 24 25import java.lang.annotation.Retention; 26import java.lang.annotation.RetentionPolicy; 27import java.io.IOException; 28 29/** 30 * A class that represents the metrics of a password that are used to decide whether or not a 31 * password meets the requirements. 32 * 33 * {@hide} 34 */ 35public class PasswordMetrics implements Parcelable { 36 // Maximum allowed number of repeated or ordered characters in a sequence before we'll 37 // consider it a complex PIN/password. 38 public static final int MAX_ALLOWED_SEQUENCE = 3; 39 40 public int quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; 41 public int length = 0; 42 public int letters = 0; 43 public int upperCase = 0; 44 public int lowerCase = 0; 45 public int numeric = 0; 46 public int symbols = 0; 47 public int nonLetter = 0; 48 49 public PasswordMetrics() {} 50 51 public PasswordMetrics(int quality, int length) { 52 this.quality = quality; 53 this.length = length; 54 } 55 56 public PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase, 57 int numeric, int symbols, int nonLetter) { 58 this(quality, length); 59 this.letters = letters; 60 this.upperCase = upperCase; 61 this.lowerCase = lowerCase; 62 this.numeric = numeric; 63 this.symbols = symbols; 64 this.nonLetter = nonLetter; 65 } 66 67 private PasswordMetrics(Parcel in) { 68 quality = in.readInt(); 69 length = in.readInt(); 70 letters = in.readInt(); 71 upperCase = in.readInt(); 72 lowerCase = in.readInt(); 73 numeric = in.readInt(); 74 symbols = in.readInt(); 75 nonLetter = in.readInt(); 76 } 77 78 public boolean isDefault() { 79 return quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED 80 && length == 0 && letters == 0 && upperCase == 0 && lowerCase == 0 81 && numeric == 0 && symbols == 0 && nonLetter == 0; 82 } 83 84 @Override 85 public int describeContents() { 86 return 0; 87 } 88 89 @Override 90 public void writeToParcel(Parcel dest, int flags) { 91 dest.writeInt(quality); 92 dest.writeInt(length); 93 dest.writeInt(letters); 94 dest.writeInt(upperCase); 95 dest.writeInt(lowerCase); 96 dest.writeInt(numeric); 97 dest.writeInt(symbols); 98 dest.writeInt(nonLetter); 99 } 100 101 public static final Parcelable.Creator<PasswordMetrics> CREATOR 102 = new Parcelable.Creator<PasswordMetrics>() { 103 public PasswordMetrics createFromParcel(Parcel in) { 104 return new PasswordMetrics(in); 105 } 106 107 public PasswordMetrics[] newArray(int size) { 108 return new PasswordMetrics[size]; 109 } 110 }; 111 112 public static PasswordMetrics computeForPassword(@NonNull String password) { 113 // Analyse the characters used 114 int letters = 0; 115 int upperCase = 0; 116 int lowerCase = 0; 117 int numeric = 0; 118 int symbols = 0; 119 int nonLetter = 0; 120 final int length = password.length(); 121 for (int i = 0; i < length; i++) { 122 switch (categoryChar(password.charAt(i))) { 123 case CHAR_LOWER_CASE: 124 letters++; 125 lowerCase++; 126 break; 127 case CHAR_UPPER_CASE: 128 letters++; 129 upperCase++; 130 break; 131 case CHAR_DIGIT: 132 numeric++; 133 nonLetter++; 134 break; 135 case CHAR_SYMBOL: 136 symbols++; 137 nonLetter++; 138 break; 139 } 140 } 141 142 // Determine the quality of the password 143 final boolean hasNumeric = numeric > 0; 144 final boolean hasNonNumeric = (letters + symbols) > 0; 145 final int quality; 146 if (hasNonNumeric && hasNumeric) { 147 quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; 148 } else if (hasNonNumeric) { 149 quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC; 150 } else if (hasNumeric) { 151 quality = maxLengthSequence(password) > MAX_ALLOWED_SEQUENCE 152 ? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC 153 : DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; 154 } else { 155 quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; 156 } 157 158 return new PasswordMetrics( 159 quality, length, letters, upperCase, lowerCase, numeric, symbols, nonLetter); 160 } 161 162 /* 163 * Returns the maximum length of a sequential characters. A sequence is defined as 164 * monotonically increasing characters with a constant interval or the same character repeated. 165 * 166 * For example: 167 * maxLengthSequence("1234") == 4 168 * maxLengthSequence("13579") == 5 169 * maxLengthSequence("1234abc") == 4 170 * maxLengthSequence("aabc") == 3 171 * maxLengthSequence("qwertyuio") == 1 172 * maxLengthSequence("@ABC") == 3 173 * maxLengthSequence(";;;;") == 4 (anything that repeats) 174 * maxLengthSequence(":;<=>") == 1 (ordered, but not composed of alphas or digits) 175 * 176 * @param string the pass 177 * @return the number of sequential letters or digits 178 */ 179 public static int maxLengthSequence(@NonNull String string) { 180 if (string.length() == 0) return 0; 181 char previousChar = string.charAt(0); 182 @CharacterCatagory int category = categoryChar(previousChar); //current sequence category 183 int diff = 0; //difference between two consecutive characters 184 boolean hasDiff = false; //if we are currently targeting a sequence 185 int maxLength = 0; //maximum length of a sequence already found 186 int startSequence = 0; //where the current sequence started 187 for (int current = 1; current < string.length(); current++) { 188 char currentChar = string.charAt(current); 189 @CharacterCatagory int categoryCurrent = categoryChar(currentChar); 190 int currentDiff = (int) currentChar - (int) previousChar; 191 if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) { 192 maxLength = Math.max(maxLength, current - startSequence); 193 startSequence = current; 194 hasDiff = false; 195 category = categoryCurrent; 196 } 197 else { 198 if(hasDiff && currentDiff != diff) { 199 maxLength = Math.max(maxLength, current - startSequence); 200 startSequence = current - 1; 201 } 202 diff = currentDiff; 203 hasDiff = true; 204 } 205 previousChar = currentChar; 206 } 207 maxLength = Math.max(maxLength, string.length() - startSequence); 208 return maxLength; 209 } 210 211 @Retention(RetentionPolicy.SOURCE) 212 @IntDef({CHAR_UPPER_CASE, CHAR_LOWER_CASE, CHAR_DIGIT, CHAR_SYMBOL}) 213 private @interface CharacterCatagory {} 214 private static final int CHAR_LOWER_CASE = 0; 215 private static final int CHAR_UPPER_CASE = 1; 216 private static final int CHAR_DIGIT = 2; 217 private static final int CHAR_SYMBOL = 3; 218 219 @CharacterCatagory 220 private static int categoryChar(char c) { 221 if ('a' <= c && c <= 'z') return CHAR_LOWER_CASE; 222 if ('A' <= c && c <= 'Z') return CHAR_UPPER_CASE; 223 if ('0' <= c && c <= '9') return CHAR_DIGIT; 224 return CHAR_SYMBOL; 225 } 226 227 private static int maxDiffCategory(@CharacterCatagory int category) { 228 switch (category) { 229 case CHAR_LOWER_CASE: 230 case CHAR_UPPER_CASE: 231 return 1; 232 case CHAR_DIGIT: 233 return 10; 234 default: 235 return 0; 236 } 237 } 238} 239