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 static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; 20 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW; 21 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM; 22 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; 23 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC; 24 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; 25 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; 26 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; 27 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; 28 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; 29 30 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; 31 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN; 32 33 import android.annotation.IntDef; 34 import android.annotation.NonNull; 35 import android.app.admin.DevicePolicyManager.PasswordComplexity; 36 import android.os.Parcel; 37 import android.os.Parcelable; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.util.Preconditions; 41 import com.android.internal.widget.LockPatternUtils.CredentialType; 42 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 46 /** 47 * A class that represents the metrics of a credential that are used to decide whether or not a 48 * credential meets the requirements. If the credential is a pattern, only quality matters. 49 * 50 * {@hide} 51 */ 52 public class PasswordMetrics implements Parcelable { 53 // Maximum allowed number of repeated or ordered characters in a sequence before we'll 54 // consider it a complex PIN/password. 55 public static final int MAX_ALLOWED_SEQUENCE = 3; 56 57 public int quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; 58 public int length = 0; 59 public int letters = 0; 60 public int upperCase = 0; 61 public int lowerCase = 0; 62 public int numeric = 0; 63 public int symbols = 0; 64 public int nonLetter = 0; 65 PasswordMetrics()66 public PasswordMetrics() {} 67 PasswordMetrics(int quality)68 public PasswordMetrics(int quality) { 69 this.quality = quality; 70 } 71 PasswordMetrics(int quality, int length)72 public PasswordMetrics(int quality, int length) { 73 this.quality = quality; 74 this.length = length; 75 } 76 PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase, int numeric, int symbols, int nonLetter)77 public PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase, 78 int numeric, int symbols, int nonLetter) { 79 this(quality, length); 80 this.letters = letters; 81 this.upperCase = upperCase; 82 this.lowerCase = lowerCase; 83 this.numeric = numeric; 84 this.symbols = symbols; 85 this.nonLetter = nonLetter; 86 } 87 PasswordMetrics(Parcel in)88 private PasswordMetrics(Parcel in) { 89 quality = in.readInt(); 90 length = in.readInt(); 91 letters = in.readInt(); 92 upperCase = in.readInt(); 93 lowerCase = in.readInt(); 94 numeric = in.readInt(); 95 symbols = in.readInt(); 96 nonLetter = in.readInt(); 97 } 98 99 /** Returns the min quality allowed by {@code complexityLevel}. */ complexityLevelToMinQuality(@asswordComplexity int complexityLevel)100 public static int complexityLevelToMinQuality(@PasswordComplexity int complexityLevel) { 101 // this would be the quality of the first metrics since mMetrics is sorted in ascending 102 // order of quality 103 return PasswordComplexityBucket 104 .complexityLevelToBucket(complexityLevel).mMetrics[0].quality; 105 } 106 107 /** 108 * Returns a merged minimum {@link PasswordMetrics} requirements that a new password must meet 109 * to fulfil {@code requestedQuality}, {@code requiresNumeric} and {@code 110 * requiresLettersOrSymbols}, which are derived from {@link DevicePolicyManager} requirements, 111 * and {@code complexityLevel}. 112 * 113 * <p>Note that we are taking {@code userEnteredPasswordQuality} into account because there are 114 * more than one set of metrics to meet the minimum complexity requirement and inspecting what 115 * the user has entered can help determine whether the alphabetic or alphanumeric set of metrics 116 * should be used. For example, suppose minimum complexity requires either ALPHABETIC(8+), or 117 * ALPHANUMERIC(6+). If the user has entered "a", the length requirement displayed on the UI 118 * would be 8. Then the user appends "1" to make it "a1". We now know the user is entering 119 * an alphanumeric password so we would update the min complexity required min length to 6. 120 */ getMinimumMetrics(@asswordComplexity int complexityLevel, int userEnteredPasswordQuality, int requestedQuality, boolean requiresNumeric, boolean requiresLettersOrSymbols)121 public static PasswordMetrics getMinimumMetrics(@PasswordComplexity int complexityLevel, 122 int userEnteredPasswordQuality, int requestedQuality, boolean requiresNumeric, 123 boolean requiresLettersOrSymbols) { 124 int targetQuality = Math.max( 125 userEnteredPasswordQuality, 126 getActualRequiredQuality( 127 requestedQuality, requiresNumeric, requiresLettersOrSymbols)); 128 return getTargetQualityMetrics(complexityLevel, targetQuality); 129 } 130 131 /** 132 * Returns the {@link PasswordMetrics} at {@code complexityLevel} which the metrics quality 133 * is the same as {@code targetQuality}. 134 * 135 * <p>If {@code complexityLevel} does not allow {@code targetQuality}, returns the metrics 136 * with the min quality at {@code complexityLevel}. 137 */ 138 // TODO(bernardchau): update tests to test getMinimumMetrics and change this to be private 139 @VisibleForTesting getTargetQualityMetrics( @asswordComplexity int complexityLevel, int targetQuality)140 public static PasswordMetrics getTargetQualityMetrics( 141 @PasswordComplexity int complexityLevel, int targetQuality) { 142 PasswordComplexityBucket targetBucket = 143 PasswordComplexityBucket.complexityLevelToBucket(complexityLevel); 144 for (PasswordMetrics metrics : targetBucket.mMetrics) { 145 if (targetQuality == metrics.quality) { 146 return metrics; 147 } 148 } 149 // none of the metrics at complexityLevel has targetQuality, return metrics with min quality 150 // see test case testGetMinimumMetrics_actualRequiredQualityStricter for an example, where 151 // min complexity allows at least NUMERIC_COMPLEX, user has not entered anything yet, and 152 // requested quality is NUMERIC 153 return targetBucket.mMetrics[0]; 154 } 155 156 /** 157 * Finds out the actual quality requirement based on whether quality is {@link 158 * DevicePolicyManager#PASSWORD_QUALITY_COMPLEX} and whether digits, letters or symbols are 159 * required. 160 */ 161 @VisibleForTesting 162 // TODO(bernardchau): update tests to test getMinimumMetrics and change this to be private getActualRequiredQuality( int requestedQuality, boolean requiresNumeric, boolean requiresLettersOrSymbols)163 public static int getActualRequiredQuality( 164 int requestedQuality, boolean requiresNumeric, boolean requiresLettersOrSymbols) { 165 if (requestedQuality != PASSWORD_QUALITY_COMPLEX) { 166 return requestedQuality; 167 } 168 169 // find out actual password quality from complex requirements 170 if (requiresNumeric && requiresLettersOrSymbols) { 171 return PASSWORD_QUALITY_ALPHANUMERIC; 172 } 173 if (requiresLettersOrSymbols) { 174 return PASSWORD_QUALITY_ALPHABETIC; 175 } 176 if (requiresNumeric) { 177 // cannot specify numeric complex using complex quality so this must be numeric 178 return PASSWORD_QUALITY_NUMERIC; 179 } 180 181 // reaching here means dpm sets quality to complex without specifying any requirements 182 return PASSWORD_QUALITY_UNSPECIFIED; 183 } 184 185 /** 186 * Returns {@code complexityLevel} or {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE} 187 * if {@code complexityLevel} is not valid. 188 */ 189 @PasswordComplexity sanitizeComplexityLevel(@asswordComplexity int complexityLevel)190 public static int sanitizeComplexityLevel(@PasswordComplexity int complexityLevel) { 191 return PasswordComplexityBucket.complexityLevelToBucket(complexityLevel).mComplexityLevel; 192 } 193 isDefault()194 public boolean isDefault() { 195 return quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED 196 && length == 0 && letters == 0 && upperCase == 0 && lowerCase == 0 197 && numeric == 0 && symbols == 0 && nonLetter == 0; 198 } 199 200 @Override describeContents()201 public int describeContents() { 202 return 0; 203 } 204 205 @Override writeToParcel(Parcel dest, int flags)206 public void writeToParcel(Parcel dest, int flags) { 207 dest.writeInt(quality); 208 dest.writeInt(length); 209 dest.writeInt(letters); 210 dest.writeInt(upperCase); 211 dest.writeInt(lowerCase); 212 dest.writeInt(numeric); 213 dest.writeInt(symbols); 214 dest.writeInt(nonLetter); 215 } 216 217 public static final @android.annotation.NonNull Parcelable.Creator<PasswordMetrics> CREATOR 218 = new Parcelable.Creator<PasswordMetrics>() { 219 public PasswordMetrics createFromParcel(Parcel in) { 220 return new PasswordMetrics(in); 221 } 222 223 public PasswordMetrics[] newArray(int size) { 224 return new PasswordMetrics[size]; 225 } 226 }; 227 228 /** 229 * Returnsthe {@code PasswordMetrics} for a given credential. 230 * 231 * If the credential is a pin or a password, equivalent to {@link #computeForPassword(byte[])}. 232 * {@code credential} cannot be null when {@code type} is 233 * {@link com.android.internal.widget.LockPatternUtils#CREDENTIAL_TYPE_PASSWORD}. 234 */ computeForCredential( @redentialType int type, byte[] credential)235 public static PasswordMetrics computeForCredential( 236 @CredentialType int type, byte[] credential) { 237 if (type == CREDENTIAL_TYPE_PASSWORD) { 238 Preconditions.checkNotNull(credential, "credential cannot be null"); 239 return PasswordMetrics.computeForPassword(credential); 240 } else if (type == CREDENTIAL_TYPE_PATTERN) { 241 return new PasswordMetrics(PASSWORD_QUALITY_SOMETHING); 242 } else /* if (type == CREDENTIAL_TYPE_NONE) */ { 243 return new PasswordMetrics(PASSWORD_QUALITY_UNSPECIFIED); 244 } 245 } 246 247 /** 248 * Returns the {@code PasswordMetrics} for a given password 249 */ computeForPassword(@onNull byte[] password)250 public static PasswordMetrics computeForPassword(@NonNull byte[] password) { 251 // Analyse the characters used 252 int letters = 0; 253 int upperCase = 0; 254 int lowerCase = 0; 255 int numeric = 0; 256 int symbols = 0; 257 int nonLetter = 0; 258 final int length = password.length; 259 for (byte b : password) { 260 switch (categoryChar((char) b)) { 261 case CHAR_LOWER_CASE: 262 letters++; 263 lowerCase++; 264 break; 265 case CHAR_UPPER_CASE: 266 letters++; 267 upperCase++; 268 break; 269 case CHAR_DIGIT: 270 numeric++; 271 nonLetter++; 272 break; 273 case CHAR_SYMBOL: 274 symbols++; 275 nonLetter++; 276 break; 277 } 278 } 279 280 // Determine the quality of the password 281 final boolean hasNumeric = numeric > 0; 282 final boolean hasNonNumeric = (letters + symbols) > 0; 283 final int quality; 284 if (hasNonNumeric && hasNumeric) { 285 quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; 286 } else if (hasNonNumeric) { 287 quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC; 288 } else if (hasNumeric) { 289 quality = maxLengthSequence(password) > MAX_ALLOWED_SEQUENCE 290 ? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC 291 : DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; 292 } else { 293 quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; 294 } 295 296 return new PasswordMetrics( 297 quality, length, letters, upperCase, lowerCase, numeric, symbols, nonLetter); 298 } 299 300 @Override equals(Object other)301 public boolean equals(Object other) { 302 if (!(other instanceof PasswordMetrics)) { 303 return false; 304 } 305 PasswordMetrics o = (PasswordMetrics) other; 306 return this.quality == o.quality 307 && this.length == o.length 308 && this.letters == o.letters 309 && this.upperCase == o.upperCase 310 && this.lowerCase == o.lowerCase 311 && this.numeric == o.numeric 312 && this.symbols == o.symbols 313 && this.nonLetter == o.nonLetter; 314 } 315 satisfiesBucket(PasswordMetrics... bucket)316 private boolean satisfiesBucket(PasswordMetrics... bucket) { 317 for (PasswordMetrics metrics : bucket) { 318 if (this.quality == metrics.quality) { 319 return this.length >= metrics.length; 320 } 321 } 322 return false; 323 } 324 325 /** 326 * Returns the maximum length of a sequential characters. A sequence is defined as 327 * monotonically increasing characters with a constant interval or the same character repeated. 328 * 329 * For example: 330 * maxLengthSequence("1234") == 4 331 * maxLengthSequence("13579") == 5 332 * maxLengthSequence("1234abc") == 4 333 * maxLengthSequence("aabc") == 3 334 * maxLengthSequence("qwertyuio") == 1 335 * maxLengthSequence("@ABC") == 3 336 * maxLengthSequence(";;;;") == 4 (anything that repeats) 337 * maxLengthSequence(":;<=>") == 1 (ordered, but not composed of alphas or digits) 338 * 339 * @param bytes the pass 340 * @return the number of sequential letters or digits 341 */ maxLengthSequence(@onNull byte[] bytes)342 public static int maxLengthSequence(@NonNull byte[] bytes) { 343 if (bytes.length == 0) return 0; 344 char previousChar = (char) bytes[0]; 345 @CharacterCatagory int category = categoryChar(previousChar); //current sequence category 346 int diff = 0; //difference between two consecutive characters 347 boolean hasDiff = false; //if we are currently targeting a sequence 348 int maxLength = 0; //maximum length of a sequence already found 349 int startSequence = 0; //where the current sequence started 350 for (int current = 1; current < bytes.length; current++) { 351 char currentChar = (char) bytes[current]; 352 @CharacterCatagory int categoryCurrent = categoryChar(currentChar); 353 int currentDiff = (int) currentChar - (int) previousChar; 354 if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) { 355 maxLength = Math.max(maxLength, current - startSequence); 356 startSequence = current; 357 hasDiff = false; 358 category = categoryCurrent; 359 } 360 else { 361 if(hasDiff && currentDiff != diff) { 362 maxLength = Math.max(maxLength, current - startSequence); 363 startSequence = current - 1; 364 } 365 diff = currentDiff; 366 hasDiff = true; 367 } 368 previousChar = currentChar; 369 } 370 maxLength = Math.max(maxLength, bytes.length - startSequence); 371 return maxLength; 372 } 373 374 @Retention(RetentionPolicy.SOURCE) 375 @IntDef(prefix = { "CHAR_" }, value = { 376 CHAR_UPPER_CASE, 377 CHAR_LOWER_CASE, 378 CHAR_DIGIT, 379 CHAR_SYMBOL 380 }) 381 private @interface CharacterCatagory {} 382 private static final int CHAR_LOWER_CASE = 0; 383 private static final int CHAR_UPPER_CASE = 1; 384 private static final int CHAR_DIGIT = 2; 385 private static final int CHAR_SYMBOL = 3; 386 387 @CharacterCatagory categoryChar(char c)388 private static int categoryChar(char c) { 389 if ('a' <= c && c <= 'z') return CHAR_LOWER_CASE; 390 if ('A' <= c && c <= 'Z') return CHAR_UPPER_CASE; 391 if ('0' <= c && c <= '9') return CHAR_DIGIT; 392 return CHAR_SYMBOL; 393 } 394 maxDiffCategory(@haracterCatagory int category)395 private static int maxDiffCategory(@CharacterCatagory int category) { 396 switch (category) { 397 case CHAR_LOWER_CASE: 398 case CHAR_UPPER_CASE: 399 return 1; 400 case CHAR_DIGIT: 401 return 10; 402 default: 403 return 0; 404 } 405 } 406 407 /** Determines the {@link PasswordComplexity} of this {@link PasswordMetrics}. */ 408 @PasswordComplexity determineComplexity()409 public int determineComplexity() { 410 for (PasswordComplexityBucket bucket : PasswordComplexityBucket.BUCKETS) { 411 if (satisfiesBucket(bucket.mMetrics)) { 412 return bucket.mComplexityLevel; 413 } 414 } 415 return PASSWORD_COMPLEXITY_NONE; 416 } 417 418 /** 419 * Requirements in terms of {@link PasswordMetrics} for each {@link PasswordComplexity}. 420 */ 421 private static class PasswordComplexityBucket { 422 /** 423 * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_HIGH} in terms of 424 * {@link PasswordMetrics}. 425 */ 426 private static final PasswordComplexityBucket HIGH = 427 new PasswordComplexityBucket( 428 PASSWORD_COMPLEXITY_HIGH, 429 new PasswordMetrics( 430 DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */ 431 8), 432 new PasswordMetrics( 433 DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 6), 434 new PasswordMetrics( 435 DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */ 436 6)); 437 438 /** 439 * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_MEDIUM} in terms of 440 * {@link PasswordMetrics}. 441 */ 442 private static final PasswordComplexityBucket MEDIUM = 443 new PasswordComplexityBucket( 444 PASSWORD_COMPLEXITY_MEDIUM, 445 new PasswordMetrics( 446 DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */ 447 4), 448 new PasswordMetrics( 449 DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 4), 450 new PasswordMetrics( 451 DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */ 452 4)); 453 454 /** 455 * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_LOW} in terms of 456 * {@link PasswordMetrics}. 457 */ 458 private static final PasswordComplexityBucket LOW = 459 new PasswordComplexityBucket( 460 PASSWORD_COMPLEXITY_LOW, 461 new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING), 462 new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC), 463 new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX), 464 new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC), 465 new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC)); 466 467 /** 468 * A special bucket to represent {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE}. 469 */ 470 private static final PasswordComplexityBucket NONE = 471 new PasswordComplexityBucket(PASSWORD_COMPLEXITY_NONE, new PasswordMetrics()); 472 473 /** Array containing all buckets from high to low. */ 474 private static final PasswordComplexityBucket[] BUCKETS = 475 new PasswordComplexityBucket[] {HIGH, MEDIUM, LOW}; 476 477 @PasswordComplexity 478 private final int mComplexityLevel; 479 private final PasswordMetrics[] mMetrics; 480 481 /** 482 * @param metricsArray must be sorted in ascending order of {@link #quality}. 483 */ PasswordComplexityBucket(@asswordComplexity int complexityLevel, PasswordMetrics... metricsArray)484 private PasswordComplexityBucket(@PasswordComplexity int complexityLevel, 485 PasswordMetrics... metricsArray) { 486 int previousQuality = PASSWORD_QUALITY_UNSPECIFIED; 487 for (PasswordMetrics metrics : metricsArray) { 488 if (metrics.quality < previousQuality) { 489 throw new IllegalArgumentException("metricsArray must be sorted in ascending" 490 + " order of quality"); 491 } 492 previousQuality = metrics.quality; 493 } 494 495 this.mMetrics = metricsArray; 496 this.mComplexityLevel = complexityLevel; 497 498 } 499 500 /** Returns the bucket that {@code complexityLevel} represents. */ complexityLevelToBucket( @asswordComplexity int complexityLevel)501 private static PasswordComplexityBucket complexityLevelToBucket( 502 @PasswordComplexity int complexityLevel) { 503 for (PasswordComplexityBucket bucket : BUCKETS) { 504 if (bucket.mComplexityLevel == complexityLevel) { 505 return bucket; 506 } 507 } 508 return NONE; 509 } 510 } 511 } 512