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