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