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.MAX_PASSWORD_LENGTH; 20 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; 21 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW; 22 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM; 23 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; 24 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; 25 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; 26 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; 27 28 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE; 29 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; 30 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN; 31 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN; 32 import static com.android.internal.widget.LockPatternUtils.MIN_LOCK_PASSWORD_SIZE; 33 import static com.android.internal.widget.LockPatternUtils.MIN_LOCK_PATTERN_SIZE; 34 import static com.android.internal.widget.PasswordValidationError.CONTAINS_INVALID_CHARACTERS; 35 import static com.android.internal.widget.PasswordValidationError.CONTAINS_SEQUENCE; 36 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_DIGITS; 37 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LETTERS; 38 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LOWER_CASE; 39 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_DIGITS; 40 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_LETTER; 41 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_SYMBOLS; 42 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_UPPER_CASE; 43 import static com.android.internal.widget.PasswordValidationError.TOO_LONG; 44 import static com.android.internal.widget.PasswordValidationError.TOO_SHORT; 45 import static com.android.internal.widget.PasswordValidationError.TOO_SHORT_WHEN_ALL_NUMERIC; 46 import static com.android.internal.widget.PasswordValidationError.WEAK_CREDENTIAL_TYPE; 47 48 import android.annotation.IntDef; 49 import android.annotation.NonNull; 50 import android.annotation.Nullable; 51 import android.app.admin.DevicePolicyManager.PasswordComplexity; 52 import android.os.Parcel; 53 import android.os.Parcelable; 54 import android.util.Log; 55 56 import com.android.internal.widget.LockPatternUtils.CredentialType; 57 import com.android.internal.widget.LockscreenCredential; 58 import com.android.internal.widget.PasswordValidationError; 59 60 import java.lang.annotation.Retention; 61 import java.lang.annotation.RetentionPolicy; 62 import java.util.ArrayList; 63 import java.util.Collections; 64 import java.util.List; 65 import java.util.Objects; 66 67 /** 68 * A class that represents the metrics of a credential that are used to decide whether or not a 69 * credential meets the requirements. 70 * 71 * {@hide} 72 */ 73 public final class PasswordMetrics implements Parcelable { 74 private static final String TAG = "PasswordMetrics"; 75 76 // Maximum allowed number of repeated or ordered characters in a sequence before we'll 77 // consider it a complex PIN/password. 78 public static final int MAX_ALLOWED_SEQUENCE = 3; 79 80 // One of CREDENTIAL_TYPE_NONE, CREDENTIAL_TYPE_PATTERN, CREDENTIAL_TYPE_PIN or 81 // CREDENTIAL_TYPE_PASSWORD. 82 public @CredentialType int credType; 83 // Fields below only make sense when credType is PASSWORD. 84 public int length = 0; 85 public int letters = 0; 86 public int upperCase = 0; 87 public int lowerCase = 0; 88 public int numeric = 0; 89 public int symbols = 0; 90 public int nonLetter = 0; 91 public int nonNumeric = 0; 92 // MAX_VALUE is the most relaxed value, any sequence is ok, e.g. 123456789. 4 would forbid it. 93 public int seqLength = Integer.MAX_VALUE; 94 PasswordMetrics(int credType)95 public PasswordMetrics(int credType) { 96 this.credType = credType; 97 } 98 PasswordMetrics(int credType , int length, int letters, int upperCase, int lowerCase, int numeric, int symbols, int nonLetter, int nonNumeric, int seqLength)99 public PasswordMetrics(int credType , int length, int letters, int upperCase, int lowerCase, 100 int numeric, int symbols, int nonLetter, int nonNumeric, int seqLength) { 101 this.credType = credType; 102 this.length = length; 103 this.letters = letters; 104 this.upperCase = upperCase; 105 this.lowerCase = lowerCase; 106 this.numeric = numeric; 107 this.symbols = symbols; 108 this.nonLetter = nonLetter; 109 this.nonNumeric = nonNumeric; 110 this.seqLength = seqLength; 111 } 112 PasswordMetrics(PasswordMetrics other)113 private PasswordMetrics(PasswordMetrics other) { 114 this(other.credType, other.length, other.letters, other.upperCase, other.lowerCase, 115 other.numeric, other.symbols, other.nonLetter, other.nonNumeric, other.seqLength); 116 } 117 118 /** 119 * Returns {@code complexityLevel} or {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE} 120 * if {@code complexityLevel} is not valid. 121 * 122 * TODO: move to PasswordPolicy 123 */ 124 @PasswordComplexity sanitizeComplexityLevel(@asswordComplexity int complexityLevel)125 public static int sanitizeComplexityLevel(@PasswordComplexity int complexityLevel) { 126 switch (complexityLevel) { 127 case PASSWORD_COMPLEXITY_HIGH: 128 case PASSWORD_COMPLEXITY_MEDIUM: 129 case PASSWORD_COMPLEXITY_LOW: 130 case PASSWORD_COMPLEXITY_NONE: 131 return complexityLevel; 132 default: 133 Log.w(TAG, "Invalid password complexity used: " + complexityLevel); 134 return PASSWORD_COMPLEXITY_NONE; 135 } 136 } 137 138 @Override describeContents()139 public int describeContents() { 140 return 0; 141 } 142 143 @Override writeToParcel(Parcel dest, int flags)144 public void writeToParcel(Parcel dest, int flags) { 145 dest.writeInt(credType); 146 dest.writeInt(length); 147 dest.writeInt(letters); 148 dest.writeInt(upperCase); 149 dest.writeInt(lowerCase); 150 dest.writeInt(numeric); 151 dest.writeInt(symbols); 152 dest.writeInt(nonLetter); 153 dest.writeInt(nonNumeric); 154 dest.writeInt(seqLength); 155 } 156 157 public static final @NonNull Parcelable.Creator<PasswordMetrics> CREATOR 158 = new Parcelable.Creator<PasswordMetrics>() { 159 @Override 160 public PasswordMetrics createFromParcel(Parcel in) { 161 int credType = in.readInt(); 162 int length = in.readInt(); 163 int letters = in.readInt(); 164 int upperCase = in.readInt(); 165 int lowerCase = in.readInt(); 166 int numeric = in.readInt(); 167 int symbols = in.readInt(); 168 int nonLetter = in.readInt(); 169 int nonNumeric = in.readInt(); 170 int seqLength = in.readInt(); 171 return new PasswordMetrics(credType, length, letters, upperCase, lowerCase, 172 numeric, symbols, nonLetter, nonNumeric, seqLength); 173 } 174 175 @Override 176 public PasswordMetrics[] newArray(int size) { 177 return new PasswordMetrics[size]; 178 } 179 }; 180 181 /** 182 * Returns the {@code PasswordMetrics} for the given credential. 183 */ computeForCredential(LockscreenCredential credential)184 public static PasswordMetrics computeForCredential(LockscreenCredential credential) { 185 if (credential.isPassword() || credential.isPin()) { 186 return computeForPasswordOrPin(credential.getCredential(), credential.isPin()); 187 } else if (credential.isPattern()) { 188 PasswordMetrics metrics = new PasswordMetrics(CREDENTIAL_TYPE_PATTERN); 189 metrics.length = credential.size(); 190 return metrics; 191 } else if (credential.isNone()) { 192 return new PasswordMetrics(CREDENTIAL_TYPE_NONE); 193 } else { 194 throw new IllegalArgumentException("Unknown credential type " + credential.getType()); 195 } 196 } 197 198 /** 199 * Returns the {@code PasswordMetrics} for the given password or pin. 200 */ computeForPasswordOrPin(byte[] credential, boolean isPin)201 private static PasswordMetrics computeForPasswordOrPin(byte[] credential, boolean isPin) { 202 // Analyze the characters used. 203 int letters = 0; 204 int upperCase = 0; 205 int lowerCase = 0; 206 int numeric = 0; 207 int symbols = 0; 208 int nonLetter = 0; 209 int nonNumeric = 0; 210 final int length = credential.length; 211 for (byte b : credential) { 212 switch (categoryChar((char) b)) { 213 case CHAR_LOWER_CASE: 214 letters++; 215 lowerCase++; 216 nonNumeric++; 217 break; 218 case CHAR_UPPER_CASE: 219 letters++; 220 upperCase++; 221 nonNumeric++; 222 break; 223 case CHAR_DIGIT: 224 numeric++; 225 nonLetter++; 226 break; 227 case CHAR_SYMBOL: 228 symbols++; 229 nonLetter++; 230 nonNumeric++; 231 break; 232 } 233 } 234 235 final int credType = isPin ? CREDENTIAL_TYPE_PIN : CREDENTIAL_TYPE_PASSWORD; 236 final int seqLength = maxLengthSequence(credential); 237 return new PasswordMetrics(credType, length, letters, upperCase, lowerCase, 238 numeric, symbols, nonLetter, nonNumeric, seqLength); 239 } 240 241 /** 242 * Returns the maximum length of a sequential characters. A sequence is defined as 243 * monotonically increasing characters with a constant interval or the same character repeated. 244 * 245 * For example: 246 * maxLengthSequence("1234") == 4 247 * maxLengthSequence("13579") == 5 248 * maxLengthSequence("1234abc") == 4 249 * maxLengthSequence("aabc") == 3 250 * maxLengthSequence("qwertyuio") == 1 251 * maxLengthSequence("@ABC") == 3 252 * maxLengthSequence(";;;;") == 4 (anything that repeats) 253 * maxLengthSequence(":;<=>") == 1 (ordered, but not composed of alphas or digits) 254 * 255 * @param bytes the pass 256 * @return the number of sequential letters or digits 257 */ maxLengthSequence(@onNull byte[] bytes)258 public static int maxLengthSequence(@NonNull byte[] bytes) { 259 if (bytes.length == 0) return 0; 260 char previousChar = (char) bytes[0]; 261 @CharacterCatagory int category = categoryChar(previousChar); //current sequence category 262 int diff = 0; //difference between two consecutive characters 263 boolean hasDiff = false; //if we are currently targeting a sequence 264 int maxLength = 0; //maximum length of a sequence already found 265 int startSequence = 0; //where the current sequence started 266 for (int current = 1; current < bytes.length; current++) { 267 char currentChar = (char) bytes[current]; 268 @CharacterCatagory int categoryCurrent = categoryChar(currentChar); 269 int currentDiff = (int) currentChar - (int) previousChar; 270 if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) { 271 maxLength = Math.max(maxLength, current - startSequence); 272 startSequence = current; 273 hasDiff = false; 274 category = categoryCurrent; 275 } 276 else { 277 if(hasDiff && currentDiff != diff) { 278 maxLength = Math.max(maxLength, current - startSequence); 279 startSequence = current - 1; 280 } 281 diff = currentDiff; 282 hasDiff = true; 283 } 284 previousChar = currentChar; 285 } 286 maxLength = Math.max(maxLength, bytes.length - startSequence); 287 return maxLength; 288 } 289 290 @Retention(RetentionPolicy.SOURCE) 291 @IntDef(prefix = { "CHAR_" }, value = { 292 CHAR_UPPER_CASE, 293 CHAR_LOWER_CASE, 294 CHAR_DIGIT, 295 CHAR_SYMBOL 296 }) 297 private @interface CharacterCatagory {} 298 private static final int CHAR_LOWER_CASE = 0; 299 private static final int CHAR_UPPER_CASE = 1; 300 private static final int CHAR_DIGIT = 2; 301 private static final int CHAR_SYMBOL = 3; 302 303 @CharacterCatagory categoryChar(char c)304 private static int categoryChar(char c) { 305 if ('a' <= c && c <= 'z') return CHAR_LOWER_CASE; 306 if ('A' <= c && c <= 'Z') return CHAR_UPPER_CASE; 307 if ('0' <= c && c <= '9') return CHAR_DIGIT; 308 return CHAR_SYMBOL; 309 } 310 maxDiffCategory(@haracterCatagory int category)311 private static int maxDiffCategory(@CharacterCatagory int category) { 312 switch (category) { 313 case CHAR_LOWER_CASE: 314 case CHAR_UPPER_CASE: 315 return 1; 316 case CHAR_DIGIT: 317 return 10; 318 default: 319 return 0; 320 } 321 } 322 323 /** 324 * Returns the weakest metrics that is stricter or equal to all given metrics. 325 * 326 * TODO: move to PasswordPolicy 327 */ merge(List<PasswordMetrics> metrics)328 public static PasswordMetrics merge(List<PasswordMetrics> metrics) { 329 PasswordMetrics result = new PasswordMetrics(CREDENTIAL_TYPE_NONE); 330 for (PasswordMetrics m : metrics) { 331 result.maxWith(m); 332 } 333 334 return result; 335 } 336 337 /** 338 * Makes current metric at least as strong as {@code other} in every criterion. 339 * 340 * TODO: move to PasswordPolicy 341 */ maxWith(PasswordMetrics other)342 public void maxWith(PasswordMetrics other) { 343 credType = Math.max(credType, other.credType); 344 if (credType != CREDENTIAL_TYPE_PASSWORD && credType != CREDENTIAL_TYPE_PIN) { 345 return; 346 } 347 length = Math.max(length, other.length); 348 letters = Math.max(letters, other.letters); 349 upperCase = Math.max(upperCase, other.upperCase); 350 lowerCase = Math.max(lowerCase, other.lowerCase); 351 numeric = Math.max(numeric, other.numeric); 352 symbols = Math.max(symbols, other.symbols); 353 nonLetter = Math.max(nonLetter, other.nonLetter); 354 nonNumeric = Math.max(nonNumeric, other.nonNumeric); 355 seqLength = Math.min(seqLength, other.seqLength); 356 } 357 358 /** 359 * Returns minimum password quality for a given complexity level. 360 * 361 * TODO: this function is used for determining allowed credential types, so it should return 362 * credential type rather than 'quality'. 363 * 364 * TODO: move to PasswordPolicy 365 */ complexityLevelToMinQuality(int complexity)366 public static int complexityLevelToMinQuality(int complexity) { 367 switch (complexity) { 368 case PASSWORD_COMPLEXITY_HIGH: 369 case PASSWORD_COMPLEXITY_MEDIUM: 370 return PASSWORD_QUALITY_NUMERIC_COMPLEX; 371 case PASSWORD_COMPLEXITY_LOW: 372 return PASSWORD_QUALITY_SOMETHING; 373 case PASSWORD_COMPLEXITY_NONE: 374 default: 375 return PASSWORD_QUALITY_UNSPECIFIED; 376 } 377 } 378 379 /** 380 * Enum representing requirements for each complexity level. 381 * 382 * TODO: move to PasswordPolicy 383 */ 384 private enum ComplexityBucket { 385 // Keep ordered high -> low. BUCKET_HIGH(PASSWORD_COMPLEXITY_HIGH)386 BUCKET_HIGH(PASSWORD_COMPLEXITY_HIGH) { 387 @Override 388 boolean canHaveSequence() { 389 return false; 390 } 391 392 @Override 393 int getMinimumLength(boolean containsNonNumeric) { 394 return containsNonNumeric ? 6 : 8; 395 } 396 397 @Override 398 boolean allowsCredType(int credType) { 399 return credType == CREDENTIAL_TYPE_PASSWORD || credType == CREDENTIAL_TYPE_PIN; 400 } 401 }, BUCKET_MEDIUM(PASSWORD_COMPLEXITY_MEDIUM)402 BUCKET_MEDIUM(PASSWORD_COMPLEXITY_MEDIUM) { 403 @Override 404 boolean canHaveSequence() { 405 return false; 406 } 407 408 @Override 409 int getMinimumLength(boolean containsNonNumeric) { 410 return 4; 411 } 412 413 @Override 414 boolean allowsCredType(int credType) { 415 return credType == CREDENTIAL_TYPE_PASSWORD || credType == CREDENTIAL_TYPE_PIN; 416 } 417 }, BUCKET_LOW(PASSWORD_COMPLEXITY_LOW)418 BUCKET_LOW(PASSWORD_COMPLEXITY_LOW) { 419 @Override 420 boolean canHaveSequence() { 421 return true; 422 } 423 424 @Override 425 int getMinimumLength(boolean containsNonNumeric) { 426 return 0; 427 } 428 429 @Override 430 boolean allowsCredType(int credType) { 431 return credType != CREDENTIAL_TYPE_NONE; 432 } 433 }, BUCKET_NONE(PASSWORD_COMPLEXITY_NONE)434 BUCKET_NONE(PASSWORD_COMPLEXITY_NONE) { 435 @Override 436 boolean canHaveSequence() { 437 return true; 438 } 439 440 @Override 441 int getMinimumLength(boolean containsNonNumeric) { 442 return 0; 443 } 444 445 @Override 446 boolean allowsCredType(int credType) { 447 return true; 448 } 449 }; 450 451 int mComplexityLevel; 452 canHaveSequence()453 abstract boolean canHaveSequence(); getMinimumLength(boolean containsNonNumeric)454 abstract int getMinimumLength(boolean containsNonNumeric); allowsCredType(int credType)455 abstract boolean allowsCredType(int credType); 456 ComplexityBucket(int complexityLevel)457 ComplexityBucket(int complexityLevel) { 458 this.mComplexityLevel = complexityLevel; 459 } 460 forComplexity(int complexityLevel)461 static ComplexityBucket forComplexity(int complexityLevel) { 462 for (ComplexityBucket bucket : values()) { 463 if (bucket.mComplexityLevel == complexityLevel) { 464 return bucket; 465 } 466 } 467 throw new IllegalArgumentException("Invalid complexity level: " + complexityLevel); 468 } 469 } 470 471 /** 472 * Returns whether current metrics satisfies a given complexity bucket. 473 * 474 * TODO: move inside ComplexityBucket. 475 */ satisfiesBucket(ComplexityBucket bucket)476 private boolean satisfiesBucket(ComplexityBucket bucket) { 477 if (!bucket.allowsCredType(credType)) { 478 return false; 479 } 480 if (credType != CREDENTIAL_TYPE_PASSWORD && credType != CREDENTIAL_TYPE_PIN) { 481 return true; 482 } 483 return (bucket.canHaveSequence() || seqLength <= MAX_ALLOWED_SEQUENCE) 484 && length >= bucket.getMinimumLength(nonNumeric > 0 /* hasNonNumeric */); 485 } 486 487 /** 488 * Returns the maximum complexity level satisfied by password with this metrics. 489 * 490 * TODO: move inside ComplexityBucket. 491 */ determineComplexity()492 public int determineComplexity() { 493 for (ComplexityBucket bucket : ComplexityBucket.values()) { 494 if (satisfiesBucket(bucket)) { 495 return bucket.mComplexityLevel; 496 } 497 } 498 throw new IllegalStateException("Failed to figure out complexity for a given metrics"); 499 } 500 501 /** 502 * Validates a proposed lockscreen credential against minimum metrics and complexity. 503 * 504 * @param adminMetrics minimum metrics to satisfy admin requirements 505 * @param minComplexity minimum complexity imposed by the requester 506 * @param credential the proposed lockscreen credential 507 * 508 * @return a list of validation errors. An empty list means the credential is OK. 509 * 510 * TODO: move to PasswordPolicy 511 */ validateCredential( PasswordMetrics adminMetrics, int minComplexity, LockscreenCredential credential)512 public static List<PasswordValidationError> validateCredential( 513 PasswordMetrics adminMetrics, int minComplexity, LockscreenCredential credential) { 514 if (credential.hasInvalidChars()) { 515 return Collections.singletonList( 516 new PasswordValidationError(CONTAINS_INVALID_CHARACTERS, 0)); 517 } 518 PasswordMetrics actualMetrics = computeForCredential(credential); 519 return validatePasswordMetrics(adminMetrics, minComplexity, actualMetrics); 520 } 521 522 /** 523 * Validates password metrics against minimum metrics and complexity 524 * 525 * @param adminMetrics - minimum metrics to satisfy admin requirements. 526 * @param minComplexity - minimum complexity imposed by the requester. 527 * @param actualMetrics - metrics for password to validate. 528 * @return a list of password validation errors. An empty list means the password is OK. 529 * 530 * TODO: move to PasswordPolicy 531 */ validatePasswordMetrics( PasswordMetrics adminMetrics, int minComplexity, PasswordMetrics actualMetrics)532 public static List<PasswordValidationError> validatePasswordMetrics( 533 PasswordMetrics adminMetrics, int minComplexity, PasswordMetrics actualMetrics) { 534 final ComplexityBucket bucket = ComplexityBucket.forComplexity(minComplexity); 535 536 // Make sure credential type is satisfactory. 537 // TODO: stop relying on credential type ordering. 538 if (actualMetrics.credType < adminMetrics.credType 539 || !bucket.allowsCredType(actualMetrics.credType)) { 540 return Collections.singletonList(new PasswordValidationError(WEAK_CREDENTIAL_TYPE, 0)); 541 } 542 if (actualMetrics.credType == CREDENTIAL_TYPE_PATTERN) { 543 // For pattern, only need to check the length against the hardcoded minimum. If the 544 // pattern length is unavailable (e.g., PasswordMetrics that was stored on-disk before 545 // the pattern length started being included in it), assume it is okay. 546 if (actualMetrics.length != 0 && actualMetrics.length < MIN_LOCK_PATTERN_SIZE) { 547 return Collections.singletonList(new PasswordValidationError(TOO_SHORT, 548 MIN_LOCK_PATTERN_SIZE)); 549 } 550 return Collections.emptyList(); 551 } 552 if (actualMetrics.credType == CREDENTIAL_TYPE_NONE) { 553 return Collections.emptyList(); // Nothing to check for none. 554 } 555 556 if (actualMetrics.credType == CREDENTIAL_TYPE_PIN && actualMetrics.nonNumeric > 0) { 557 return Collections.singletonList( 558 new PasswordValidationError(CONTAINS_INVALID_CHARACTERS, 0)); 559 } 560 561 final ArrayList<PasswordValidationError> result = new ArrayList<>(); 562 if (actualMetrics.length > MAX_PASSWORD_LENGTH) { 563 result.add(new PasswordValidationError(TOO_LONG, MAX_PASSWORD_LENGTH)); 564 } 565 566 final PasswordMetrics minMetrics = applyComplexity(adminMetrics, 567 actualMetrics.credType == CREDENTIAL_TYPE_PIN, bucket); 568 569 // Clamp required length between maximum and minimum valid values. 570 minMetrics.length = Math.min(MAX_PASSWORD_LENGTH, 571 Math.max(minMetrics.length, MIN_LOCK_PASSWORD_SIZE)); 572 minMetrics.removeOverlapping(); 573 574 comparePasswordMetrics(minMetrics, bucket, actualMetrics, result); 575 576 return result; 577 } 578 579 /** 580 * TODO: move to PasswordPolicy 581 */ comparePasswordMetrics(PasswordMetrics minMetrics, ComplexityBucket bucket, PasswordMetrics actualMetrics, ArrayList<PasswordValidationError> result)582 private static void comparePasswordMetrics(PasswordMetrics minMetrics, ComplexityBucket bucket, 583 PasswordMetrics actualMetrics, ArrayList<PasswordValidationError> result) { 584 if (actualMetrics.length < minMetrics.length) { 585 result.add(new PasswordValidationError(TOO_SHORT, minMetrics.length)); 586 } 587 if (actualMetrics.nonNumeric == 0 && minMetrics.nonNumeric == 0 && minMetrics.letters == 0 588 && minMetrics.lowerCase == 0 && minMetrics.upperCase == 0 589 && minMetrics.symbols == 0) { 590 // When provided password is all numeric and all numeric password is allowed. 591 int allNumericMinimumLength = bucket.getMinimumLength(false); 592 if (allNumericMinimumLength > minMetrics.length 593 && allNumericMinimumLength > minMetrics.numeric 594 && actualMetrics.length < allNumericMinimumLength) { 595 result.add(new PasswordValidationError( 596 TOO_SHORT_WHEN_ALL_NUMERIC, allNumericMinimumLength)); 597 } 598 } 599 if (actualMetrics.letters < minMetrics.letters) { 600 result.add(new PasswordValidationError(NOT_ENOUGH_LETTERS, minMetrics.letters)); 601 } 602 if (actualMetrics.upperCase < minMetrics.upperCase) { 603 result.add(new PasswordValidationError(NOT_ENOUGH_UPPER_CASE, minMetrics.upperCase)); 604 } 605 if (actualMetrics.lowerCase < minMetrics.lowerCase) { 606 result.add(new PasswordValidationError(NOT_ENOUGH_LOWER_CASE, minMetrics.lowerCase)); 607 } 608 if (actualMetrics.numeric < minMetrics.numeric) { 609 result.add(new PasswordValidationError(NOT_ENOUGH_DIGITS, minMetrics.numeric)); 610 } 611 if (actualMetrics.symbols < minMetrics.symbols) { 612 result.add(new PasswordValidationError(NOT_ENOUGH_SYMBOLS, minMetrics.symbols)); 613 } 614 if (actualMetrics.nonLetter < minMetrics.nonLetter) { 615 result.add(new PasswordValidationError(NOT_ENOUGH_NON_LETTER, minMetrics.nonLetter)); 616 } 617 if (actualMetrics.nonNumeric < minMetrics.nonNumeric) { 618 result.add(new PasswordValidationError(NOT_ENOUGH_NON_DIGITS, minMetrics.nonNumeric)); 619 } 620 if (actualMetrics.seqLength > minMetrics.seqLength) { 621 result.add(new PasswordValidationError(CONTAINS_SEQUENCE, 0)); 622 } 623 } 624 625 /** 626 * Drop requirements that are superseded by others, e.g. if it is required to have 5 upper case 627 * letters and 5 lower case letters, there is no need to require minimum number of letters to 628 * be 10 since it will be fulfilled once upper and lower case requirements are fulfilled. 629 * 630 * TODO: move to PasswordPolicy 631 */ removeOverlapping()632 private void removeOverlapping() { 633 // upperCase + lowerCase can override letters 634 final int indirectLetters = upperCase + lowerCase; 635 636 // numeric + symbols can override nonLetter 637 final int indirectNonLetter = numeric + symbols; 638 639 // letters + symbols can override nonNumeric 640 final int effectiveLetters = Math.max(letters, indirectLetters); 641 final int indirectNonNumeric = effectiveLetters + symbols; 642 643 // letters + nonLetters can override length 644 // numeric + nonNumeric can also override length, so max it with previous. 645 final int effectiveNonLetter = Math.max(nonLetter, indirectNonLetter); 646 final int effectiveNonNumeric = Math.max(nonNumeric, indirectNonNumeric); 647 final int indirectLength = Math.max(effectiveLetters + effectiveNonLetter, 648 numeric + effectiveNonNumeric); 649 650 if (indirectLetters >= letters) { 651 letters = 0; 652 } 653 if (indirectNonLetter >= nonLetter) { 654 nonLetter = 0; 655 } 656 if (indirectNonNumeric >= nonNumeric) { 657 nonNumeric = 0; 658 } 659 if (indirectLength >= length) { 660 length = 0; 661 } 662 } 663 664 /** 665 * Combine minimum metrics, set by admin, complexity set by the requester and actual entered 666 * password metrics to get resulting minimum metrics that the password has to satisfy. Always 667 * returns a new PasswordMetrics object. 668 * 669 * TODO: move to PasswordPolicy 670 */ applyComplexity(PasswordMetrics adminMetrics, boolean isPin, int complexity)671 public static PasswordMetrics applyComplexity(PasswordMetrics adminMetrics, boolean isPin, 672 int complexity) { 673 return applyComplexity(adminMetrics, isPin, ComplexityBucket.forComplexity(complexity)); 674 } 675 applyComplexity(PasswordMetrics adminMetrics, boolean isPin, ComplexityBucket bucket)676 private static PasswordMetrics applyComplexity(PasswordMetrics adminMetrics, boolean isPin, 677 ComplexityBucket bucket) { 678 final PasswordMetrics minMetrics = new PasswordMetrics(adminMetrics); 679 680 if (!bucket.canHaveSequence()) { 681 minMetrics.seqLength = Math.min(minMetrics.seqLength, MAX_ALLOWED_SEQUENCE); 682 } 683 684 minMetrics.length = Math.max(minMetrics.length, bucket.getMinimumLength(!isPin)); 685 686 return minMetrics; 687 } 688 689 /** 690 * Returns true if password is non-empty and contains digits only. 691 * @param password 692 * @return 693 */ isNumericOnly(@onNull String password)694 public static boolean isNumericOnly(@NonNull String password) { 695 if (password.length() == 0) return false; 696 for (int i = 0; i < password.length(); i++) { 697 if (categoryChar(password.charAt(i)) != CHAR_DIGIT) return false; 698 } 699 return true; 700 } 701 702 @Override equals(@ullable Object o)703 public boolean equals(@Nullable Object o) { 704 if (this == o) return true; 705 if (o == null || getClass() != o.getClass()) return false; 706 final PasswordMetrics that = (PasswordMetrics) o; 707 return credType == that.credType 708 && length == that.length 709 && letters == that.letters 710 && upperCase == that.upperCase 711 && lowerCase == that.lowerCase 712 && numeric == that.numeric 713 && symbols == that.symbols 714 && nonLetter == that.nonLetter 715 && nonNumeric == that.nonNumeric 716 && seqLength == that.seqLength; 717 } 718 719 @Override hashCode()720 public int hashCode() { 721 return Objects.hash(credType, length, letters, upperCase, lowerCase, numeric, symbols, 722 nonLetter, nonNumeric, seqLength); 723 } 724 } 725