• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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