1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app.admin;
18 
19 import static android.app.admin.DevicePolicyManager.MAX_PASSWORD_LENGTH;
20 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
21 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
22 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM;
23 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
24 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
25 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
26 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
27 
28 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
29 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
30 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
31 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN;
32 import static com.android.internal.widget.LockPatternUtils.MIN_LOCK_PASSWORD_SIZE;
33 import static com.android.internal.widget.LockPatternUtils.MIN_LOCK_PATTERN_SIZE;
34 import static com.android.internal.widget.PasswordValidationError.CONTAINS_INVALID_CHARACTERS;
35 import static com.android.internal.widget.PasswordValidationError.CONTAINS_SEQUENCE;
36 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_DIGITS;
37 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LETTERS;
38 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LOWER_CASE;
39 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_DIGITS;
40 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_LETTER;
41 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_SYMBOLS;
42 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_UPPER_CASE;
43 import static com.android.internal.widget.PasswordValidationError.TOO_LONG;
44 import static com.android.internal.widget.PasswordValidationError.TOO_SHORT;
45 import static com.android.internal.widget.PasswordValidationError.TOO_SHORT_WHEN_ALL_NUMERIC;
46 import static com.android.internal.widget.PasswordValidationError.WEAK_CREDENTIAL_TYPE;
47 
48 import android.annotation.IntDef;
49 import android.annotation.NonNull;
50 import android.annotation.Nullable;
51 import android.app.admin.DevicePolicyManager.PasswordComplexity;
52 import android.os.Parcel;
53 import android.os.Parcelable;
54 import android.util.Log;
55 
56 import com.android.internal.widget.LockPatternUtils.CredentialType;
57 import com.android.internal.widget.LockscreenCredential;
58 import com.android.internal.widget.PasswordValidationError;
59 
60 import java.lang.annotation.Retention;
61 import java.lang.annotation.RetentionPolicy;
62 import java.util.ArrayList;
63 import java.util.Collections;
64 import java.util.List;
65 import java.util.Objects;
66 
67 /**
68  * A class that represents the metrics of a credential that are used to decide whether or not a
69  * credential meets the requirements.
70  *
71  * {@hide}
72  */
73 public final class PasswordMetrics implements Parcelable {
74     private static final String TAG = "PasswordMetrics";
75 
76     // Maximum allowed number of repeated or ordered characters in a sequence before we'll
77     // consider it a complex PIN/password.
78     public static final int MAX_ALLOWED_SEQUENCE = 3;
79 
80     // One of CREDENTIAL_TYPE_NONE, CREDENTIAL_TYPE_PATTERN, CREDENTIAL_TYPE_PIN or
81     // CREDENTIAL_TYPE_PASSWORD.
82     public @CredentialType int credType;
83     // Fields below only make sense when credType is PASSWORD.
84     public int length = 0;
85     public int letters = 0;
86     public int upperCase = 0;
87     public int lowerCase = 0;
88     public int numeric = 0;
89     public int symbols = 0;
90     public int nonLetter = 0;
91     public int nonNumeric = 0;
92     // MAX_VALUE is the most relaxed value, any sequence is ok, e.g. 123456789. 4 would forbid it.
93     public int seqLength = Integer.MAX_VALUE;
94 
PasswordMetrics(int credType)95     public PasswordMetrics(int credType) {
96         this.credType = credType;
97     }
98 
PasswordMetrics(int credType , int length, int letters, int upperCase, int lowerCase, int numeric, int symbols, int nonLetter, int nonNumeric, int seqLength)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 
PasswordMetrics(PasswordMetrics other)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
sanitizeComplexityLevel(@asswordComplexity int complexityLevel)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
describeContents()139     public int describeContents() {
140         return 0;
141     }
142 
143     @Override
writeToParcel(Parcel dest, int flags)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      */
computeForCredential(LockscreenCredential credential)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      */
computeForPasswordOrPin(byte[] credential, boolean isPin)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      */
maxLengthSequence(@onNull byte[] bytes)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
categoryChar(char c)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 
maxDiffCategory(@haracterCatagory int category)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      */
merge(List<PasswordMetrics> metrics)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      */
maxWith(PasswordMetrics other)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      */
complexityLevelToMinQuality(int complexity)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.
BUCKET_HIGH(PASSWORD_COMPLEXITY_HIGH)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         },
BUCKET_MEDIUM(PASSWORD_COMPLEXITY_MEDIUM)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         },
BUCKET_LOW(PASSWORD_COMPLEXITY_LOW)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         },
BUCKET_NONE(PASSWORD_COMPLEXITY_NONE)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 
canHaveSequence()453         abstract boolean canHaveSequence();
getMinimumLength(boolean containsNonNumeric)454         abstract int getMinimumLength(boolean containsNonNumeric);
allowsCredType(int credType)455         abstract boolean allowsCredType(int credType);
456 
ComplexityBucket(int complexityLevel)457         ComplexityBucket(int complexityLevel) {
458             this.mComplexityLevel = complexityLevel;
459         }
460 
forComplexity(int complexityLevel)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      */
satisfiesBucket(ComplexityBucket bucket)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      */
determineComplexity()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      */
validateCredential( PasswordMetrics adminMetrics, int minComplexity, LockscreenCredential credential)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      */
validatePasswordMetrics( PasswordMetrics adminMetrics, int minComplexity, PasswordMetrics actualMetrics)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      */
comparePasswordMetrics(PasswordMetrics minMetrics, ComplexityBucket bucket, PasswordMetrics actualMetrics, ArrayList<PasswordValidationError> result)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      */
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 isPin, int complexity)671     public static PasswordMetrics applyComplexity(PasswordMetrics adminMetrics, boolean isPin,
672             int complexity) {
673         return applyComplexity(adminMetrics, isPin, ComplexityBucket.forComplexity(complexity));
674     }
675 
applyComplexity(PasswordMetrics adminMetrics, boolean isPin, ComplexityBucket bucket)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      */
isNumericOnly(@onNull String password)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
equals(@ullable Object o)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
hashCode()720     public int hashCode() {
721         return Objects.hash(credType, length, letters, upperCase, lowerCase, numeric, symbols,
722                 nonLetter, nonNumeric, seqLength);
723     }
724 }
725