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