Aurimas Liutikas | dc3f885 | 2024-07-11 10:07:48 -0700 | [diff] [blame^] | 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 | |
| 95 | public PasswordMetrics(int credType) { |
| 96 | this.credType = credType; |
| 97 | } |
| 98 | |
| 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 | |
| 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 |
| 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 |
| 139 | public int describeContents() { |
| 140 | return 0; |
| 141 | } |
| 142 | |
| 143 | @Override |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 |
| 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 | |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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. |
| 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 | }, |
| 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 | }, |
| 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 | }, |
| 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 | |
| 453 | abstract boolean canHaveSequence(); |
| 454 | abstract int getMinimumLength(boolean containsNonNumeric); |
| 455 | abstract boolean allowsCredType(int credType); |
| 456 | |
| 457 | ComplexityBucket(int complexityLevel) { |
| 458 | this.mComplexityLevel = complexityLevel; |
| 459 | } |
| 460 | |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 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 | */ |
| 671 | public static PasswordMetrics applyComplexity(PasswordMetrics adminMetrics, boolean isPin, |
| 672 | int complexity) { |
| 673 | return applyComplexity(adminMetrics, isPin, ComplexityBucket.forComplexity(complexity)); |
| 674 | } |
| 675 | |
| 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 | */ |
| 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 |
| 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 |
| 720 | public int hashCode() { |
| 721 | return Objects.hash(credType, length, letters, upperCase, lowerCase, numeric, symbols, |
| 722 | nonLetter, nonNumeric, seqLength); |
| 723 | } |
| 724 | } |