blob: dd311755bf3f5edb5ec330d6e3281fcaadbf4e58 [file] [log] [blame]
Aurimas Liutikasdc3f8852024-07-11 10:07:48 -07001/*
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
17package android.app.admin;
18
19import static android.app.admin.DevicePolicyManager.MAX_PASSWORD_LENGTH;
20import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
21import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
22import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM;
23import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
24import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
25import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
26import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
27
28import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
29import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
30import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
31import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN;
32import static com.android.internal.widget.LockPatternUtils.MIN_LOCK_PASSWORD_SIZE;
33import static com.android.internal.widget.LockPatternUtils.MIN_LOCK_PATTERN_SIZE;
34import static com.android.internal.widget.PasswordValidationError.CONTAINS_INVALID_CHARACTERS;
35import static com.android.internal.widget.PasswordValidationError.CONTAINS_SEQUENCE;
36import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_DIGITS;
37import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LETTERS;
38import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LOWER_CASE;
39import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_DIGITS;
40import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_LETTER;
41import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_SYMBOLS;
42import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_UPPER_CASE;
43import static com.android.internal.widget.PasswordValidationError.TOO_LONG;
44import static com.android.internal.widget.PasswordValidationError.TOO_SHORT;
45import static com.android.internal.widget.PasswordValidationError.TOO_SHORT_WHEN_ALL_NUMERIC;
46import static com.android.internal.widget.PasswordValidationError.WEAK_CREDENTIAL_TYPE;
47
48import android.annotation.IntDef;
49import android.annotation.NonNull;
50import android.annotation.Nullable;
51import android.app.admin.DevicePolicyManager.PasswordComplexity;
52import android.os.Parcel;
53import android.os.Parcelable;
54import android.util.Log;
55
56import com.android.internal.widget.LockPatternUtils.CredentialType;
57import com.android.internal.widget.LockscreenCredential;
58import com.android.internal.widget.PasswordValidationError;
59
60import java.lang.annotation.Retention;
61import java.lang.annotation.RetentionPolicy;
62import java.util.ArrayList;
63import java.util.Collections;
64import java.util.List;
65import 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 */
73public 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}