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