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.PASSWORD_COMPLEXITY_HIGH;
20 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
21 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM;
22 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
23 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
24 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
25 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
26 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
27 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
28 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
29 
30 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
31 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
32 
33 import android.annotation.IntDef;
34 import android.annotation.NonNull;
35 import android.app.admin.DevicePolicyManager.PasswordComplexity;
36 import android.os.Parcel;
37 import android.os.Parcelable;
38 
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.internal.util.Preconditions;
41 import com.android.internal.widget.LockPatternUtils.CredentialType;
42 
43 import java.lang.annotation.Retention;
44 import java.lang.annotation.RetentionPolicy;
45 
46 /**
47  * A class that represents the metrics of a credential that are used to decide whether or not a
48  * credential meets the requirements. If the credential is a pattern, only quality matters.
49  *
50  * {@hide}
51  */
52 public class PasswordMetrics implements Parcelable {
53     // Maximum allowed number of repeated or ordered characters in a sequence before we'll
54     // consider it a complex PIN/password.
55     public static final int MAX_ALLOWED_SEQUENCE = 3;
56 
57     public int quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
58     public int length = 0;
59     public int letters = 0;
60     public int upperCase = 0;
61     public int lowerCase = 0;
62     public int numeric = 0;
63     public int symbols = 0;
64     public int nonLetter = 0;
65 
PasswordMetrics()66     public PasswordMetrics() {}
67 
PasswordMetrics(int quality)68     public PasswordMetrics(int quality) {
69         this.quality = quality;
70     }
71 
PasswordMetrics(int quality, int length)72     public PasswordMetrics(int quality, int length) {
73         this.quality = quality;
74         this.length = length;
75     }
76 
PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase, int numeric, int symbols, int nonLetter)77     public PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase,
78             int numeric, int symbols, int nonLetter) {
79         this(quality, length);
80         this.letters = letters;
81         this.upperCase = upperCase;
82         this.lowerCase = lowerCase;
83         this.numeric = numeric;
84         this.symbols = symbols;
85         this.nonLetter = nonLetter;
86     }
87 
PasswordMetrics(Parcel in)88     private PasswordMetrics(Parcel in) {
89         quality = in.readInt();
90         length = in.readInt();
91         letters = in.readInt();
92         upperCase = in.readInt();
93         lowerCase = in.readInt();
94         numeric = in.readInt();
95         symbols = in.readInt();
96         nonLetter = in.readInt();
97     }
98 
99     /** Returns the min quality allowed by {@code complexityLevel}. */
complexityLevelToMinQuality(@asswordComplexity int complexityLevel)100     public static int complexityLevelToMinQuality(@PasswordComplexity int complexityLevel) {
101         // this would be the quality of the first metrics since mMetrics is sorted in ascending
102         // order of quality
103         return PasswordComplexityBucket
104                 .complexityLevelToBucket(complexityLevel).mMetrics[0].quality;
105     }
106 
107     /**
108      * Returns a merged minimum {@link PasswordMetrics} requirements that a new password must meet
109      * to fulfil {@code requestedQuality}, {@code requiresNumeric} and {@code
110      * requiresLettersOrSymbols}, which are derived from {@link DevicePolicyManager} requirements,
111      * and {@code complexityLevel}.
112      *
113      * <p>Note that we are taking {@code userEnteredPasswordQuality} into account because there are
114      * more than one set of metrics to meet the minimum complexity requirement and inspecting what
115      * the user has entered can help determine whether the alphabetic or alphanumeric set of metrics
116      * should be used. For example, suppose minimum complexity requires either ALPHABETIC(8+), or
117      * ALPHANUMERIC(6+). If the user has entered "a", the length requirement displayed on the UI
118      * would be 8. Then the user appends "1" to make it "a1". We now know the user is entering
119      * an alphanumeric password so we would update the min complexity required min length to 6.
120      */
getMinimumMetrics(@asswordComplexity int complexityLevel, int userEnteredPasswordQuality, int requestedQuality, boolean requiresNumeric, boolean requiresLettersOrSymbols)121     public static PasswordMetrics getMinimumMetrics(@PasswordComplexity int complexityLevel,
122             int userEnteredPasswordQuality, int requestedQuality, boolean requiresNumeric,
123             boolean requiresLettersOrSymbols) {
124         int targetQuality = Math.max(
125                 userEnteredPasswordQuality,
126                 getActualRequiredQuality(
127                         requestedQuality, requiresNumeric, requiresLettersOrSymbols));
128         return getTargetQualityMetrics(complexityLevel, targetQuality);
129     }
130 
131     /**
132      * Returns the {@link PasswordMetrics} at {@code complexityLevel} which the metrics quality
133      * is the same as {@code targetQuality}.
134      *
135      * <p>If {@code complexityLevel} does not allow {@code targetQuality}, returns the metrics
136      * with the min quality at {@code complexityLevel}.
137      */
138     // TODO(bernardchau): update tests to test getMinimumMetrics and change this to be private
139     @VisibleForTesting
getTargetQualityMetrics( @asswordComplexity int complexityLevel, int targetQuality)140     public static PasswordMetrics getTargetQualityMetrics(
141             @PasswordComplexity int complexityLevel, int targetQuality) {
142         PasswordComplexityBucket targetBucket =
143                 PasswordComplexityBucket.complexityLevelToBucket(complexityLevel);
144         for (PasswordMetrics metrics : targetBucket.mMetrics) {
145             if (targetQuality == metrics.quality) {
146                 return metrics;
147             }
148         }
149         // none of the metrics at complexityLevel has targetQuality, return metrics with min quality
150         // see test case testGetMinimumMetrics_actualRequiredQualityStricter for an example, where
151         // min complexity allows at least NUMERIC_COMPLEX, user has not entered anything yet, and
152         // requested quality is NUMERIC
153         return targetBucket.mMetrics[0];
154     }
155 
156     /**
157      * Finds out the actual quality requirement based on whether quality is {@link
158      * DevicePolicyManager#PASSWORD_QUALITY_COMPLEX} and whether digits, letters or symbols are
159      * required.
160      */
161     @VisibleForTesting
162     // TODO(bernardchau): update tests to test getMinimumMetrics and change this to be private
getActualRequiredQuality( int requestedQuality, boolean requiresNumeric, boolean requiresLettersOrSymbols)163     public static int getActualRequiredQuality(
164             int requestedQuality, boolean requiresNumeric, boolean requiresLettersOrSymbols) {
165         if (requestedQuality != PASSWORD_QUALITY_COMPLEX) {
166             return requestedQuality;
167         }
168 
169         // find out actual password quality from complex requirements
170         if (requiresNumeric && requiresLettersOrSymbols) {
171             return PASSWORD_QUALITY_ALPHANUMERIC;
172         }
173         if (requiresLettersOrSymbols) {
174             return PASSWORD_QUALITY_ALPHABETIC;
175         }
176         if (requiresNumeric) {
177             // cannot specify numeric complex using complex quality so this must be numeric
178             return PASSWORD_QUALITY_NUMERIC;
179         }
180 
181         // reaching here means dpm sets quality to complex without specifying any requirements
182         return PASSWORD_QUALITY_UNSPECIFIED;
183     }
184 
185     /**
186      * Returns {@code complexityLevel} or {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE}
187      * if {@code complexityLevel} is not valid.
188      */
189     @PasswordComplexity
sanitizeComplexityLevel(@asswordComplexity int complexityLevel)190     public static int sanitizeComplexityLevel(@PasswordComplexity int complexityLevel) {
191         return PasswordComplexityBucket.complexityLevelToBucket(complexityLevel).mComplexityLevel;
192     }
193 
isDefault()194     public boolean isDefault() {
195         return quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
196                 && length == 0 && letters == 0 && upperCase == 0 && lowerCase == 0
197                 && numeric == 0 && symbols == 0 && nonLetter == 0;
198     }
199 
200     @Override
describeContents()201     public int describeContents() {
202         return 0;
203     }
204 
205     @Override
writeToParcel(Parcel dest, int flags)206     public void writeToParcel(Parcel dest, int flags) {
207         dest.writeInt(quality);
208         dest.writeInt(length);
209         dest.writeInt(letters);
210         dest.writeInt(upperCase);
211         dest.writeInt(lowerCase);
212         dest.writeInt(numeric);
213         dest.writeInt(symbols);
214         dest.writeInt(nonLetter);
215     }
216 
217     public static final @android.annotation.NonNull Parcelable.Creator<PasswordMetrics> CREATOR
218             = new Parcelable.Creator<PasswordMetrics>() {
219         public PasswordMetrics createFromParcel(Parcel in) {
220             return new PasswordMetrics(in);
221         }
222 
223         public PasswordMetrics[] newArray(int size) {
224             return new PasswordMetrics[size];
225         }
226     };
227 
228     /**
229      * Returnsthe {@code PasswordMetrics} for a given credential.
230      *
231      * If the credential is a pin or a password, equivalent to {@link #computeForPassword(byte[])}.
232      * {@code credential} cannot be null when {@code type} is
233      * {@link com.android.internal.widget.LockPatternUtils#CREDENTIAL_TYPE_PASSWORD}.
234      */
computeForCredential( @redentialType int type, byte[] credential)235     public static PasswordMetrics computeForCredential(
236             @CredentialType int type, byte[] credential) {
237         if (type == CREDENTIAL_TYPE_PASSWORD) {
238             Preconditions.checkNotNull(credential, "credential cannot be null");
239             return PasswordMetrics.computeForPassword(credential);
240         } else if (type == CREDENTIAL_TYPE_PATTERN)  {
241             return new PasswordMetrics(PASSWORD_QUALITY_SOMETHING);
242         } else /* if (type == CREDENTIAL_TYPE_NONE) */ {
243             return new PasswordMetrics(PASSWORD_QUALITY_UNSPECIFIED);
244         }
245     }
246 
247     /**
248      * Returns the {@code PasswordMetrics} for a given password
249      */
computeForPassword(@onNull byte[] password)250     public static PasswordMetrics computeForPassword(@NonNull byte[] password) {
251         // Analyse the characters used
252         int letters = 0;
253         int upperCase = 0;
254         int lowerCase = 0;
255         int numeric = 0;
256         int symbols = 0;
257         int nonLetter = 0;
258         final int length = password.length;
259         for (byte b : password) {
260             switch (categoryChar((char) b)) {
261                 case CHAR_LOWER_CASE:
262                     letters++;
263                     lowerCase++;
264                     break;
265                 case CHAR_UPPER_CASE:
266                     letters++;
267                     upperCase++;
268                     break;
269                 case CHAR_DIGIT:
270                     numeric++;
271                     nonLetter++;
272                     break;
273                 case CHAR_SYMBOL:
274                     symbols++;
275                     nonLetter++;
276                     break;
277             }
278         }
279 
280         // Determine the quality of the password
281         final boolean hasNumeric = numeric > 0;
282         final boolean hasNonNumeric = (letters + symbols) > 0;
283         final int quality;
284         if (hasNonNumeric && hasNumeric) {
285             quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
286         } else if (hasNonNumeric) {
287             quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
288         } else if (hasNumeric) {
289             quality = maxLengthSequence(password) > MAX_ALLOWED_SEQUENCE
290                     ? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
291                     : DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
292         } else {
293             quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
294         }
295 
296         return new PasswordMetrics(
297                 quality, length, letters, upperCase, lowerCase, numeric, symbols, nonLetter);
298     }
299 
300     @Override
equals(Object other)301     public boolean equals(Object other) {
302         if (!(other instanceof PasswordMetrics)) {
303             return false;
304         }
305         PasswordMetrics o = (PasswordMetrics) other;
306         return this.quality == o.quality
307                 && this.length == o.length
308                 && this.letters == o.letters
309                 && this.upperCase == o.upperCase
310                 && this.lowerCase == o.lowerCase
311                 && this.numeric == o.numeric
312                 && this.symbols == o.symbols
313                 && this.nonLetter == o.nonLetter;
314     }
315 
satisfiesBucket(PasswordMetrics... bucket)316     private boolean satisfiesBucket(PasswordMetrics... bucket) {
317         for (PasswordMetrics metrics : bucket) {
318             if (this.quality == metrics.quality) {
319                 return this.length >= metrics.length;
320             }
321         }
322         return false;
323     }
324 
325     /**
326      * Returns the maximum length of a sequential characters. A sequence is defined as
327      * monotonically increasing characters with a constant interval or the same character repeated.
328      *
329      * For example:
330      * maxLengthSequence("1234") == 4
331      * maxLengthSequence("13579") == 5
332      * maxLengthSequence("1234abc") == 4
333      * maxLengthSequence("aabc") == 3
334      * maxLengthSequence("qwertyuio") == 1
335      * maxLengthSequence("@ABC") == 3
336      * maxLengthSequence(";;;;") == 4 (anything that repeats)
337      * maxLengthSequence(":;<=>") == 1  (ordered, but not composed of alphas or digits)
338      *
339      * @param bytes the pass
340      * @return the number of sequential letters or digits
341      */
maxLengthSequence(@onNull byte[] bytes)342     public static int maxLengthSequence(@NonNull byte[] bytes) {
343         if (bytes.length == 0) return 0;
344         char previousChar = (char) bytes[0];
345         @CharacterCatagory int category = categoryChar(previousChar); //current sequence category
346         int diff = 0; //difference between two consecutive characters
347         boolean hasDiff = false; //if we are currently targeting a sequence
348         int maxLength = 0; //maximum length of a sequence already found
349         int startSequence = 0; //where the current sequence started
350         for (int current = 1; current < bytes.length; current++) {
351             char currentChar = (char) bytes[current];
352             @CharacterCatagory int categoryCurrent = categoryChar(currentChar);
353             int currentDiff = (int) currentChar - (int) previousChar;
354             if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) {
355                 maxLength = Math.max(maxLength, current - startSequence);
356                 startSequence = current;
357                 hasDiff = false;
358                 category = categoryCurrent;
359             }
360             else {
361                 if(hasDiff && currentDiff != diff) {
362                     maxLength = Math.max(maxLength, current - startSequence);
363                     startSequence = current - 1;
364                 }
365                 diff = currentDiff;
366                 hasDiff = true;
367             }
368             previousChar = currentChar;
369         }
370         maxLength = Math.max(maxLength, bytes.length - startSequence);
371         return maxLength;
372     }
373 
374     @Retention(RetentionPolicy.SOURCE)
375     @IntDef(prefix = { "CHAR_" }, value = {
376             CHAR_UPPER_CASE,
377             CHAR_LOWER_CASE,
378             CHAR_DIGIT,
379             CHAR_SYMBOL
380     })
381     private @interface CharacterCatagory {}
382     private static final int CHAR_LOWER_CASE = 0;
383     private static final int CHAR_UPPER_CASE = 1;
384     private static final int CHAR_DIGIT = 2;
385     private static final int CHAR_SYMBOL = 3;
386 
387     @CharacterCatagory
categoryChar(char c)388     private static int categoryChar(char c) {
389         if ('a' <= c && c <= 'z') return CHAR_LOWER_CASE;
390         if ('A' <= c && c <= 'Z') return CHAR_UPPER_CASE;
391         if ('0' <= c && c <= '9') return CHAR_DIGIT;
392         return CHAR_SYMBOL;
393     }
394 
maxDiffCategory(@haracterCatagory int category)395     private static int maxDiffCategory(@CharacterCatagory int category) {
396         switch (category) {
397             case CHAR_LOWER_CASE:
398             case CHAR_UPPER_CASE:
399                 return 1;
400             case CHAR_DIGIT:
401                 return 10;
402             default:
403                 return 0;
404         }
405     }
406 
407     /** Determines the {@link PasswordComplexity} of this {@link PasswordMetrics}. */
408     @PasswordComplexity
determineComplexity()409     public int determineComplexity() {
410         for (PasswordComplexityBucket bucket : PasswordComplexityBucket.BUCKETS) {
411             if (satisfiesBucket(bucket.mMetrics)) {
412                 return bucket.mComplexityLevel;
413             }
414         }
415         return PASSWORD_COMPLEXITY_NONE;
416     }
417 
418     /**
419      * Requirements in terms of {@link PasswordMetrics} for each {@link PasswordComplexity}.
420      */
421     private static class PasswordComplexityBucket {
422         /**
423          * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_HIGH} in terms of
424          * {@link PasswordMetrics}.
425          */
426         private static final PasswordComplexityBucket HIGH =
427                 new PasswordComplexityBucket(
428                         PASSWORD_COMPLEXITY_HIGH,
429                         new PasswordMetrics(
430                                 DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */
431                                 8),
432                         new PasswordMetrics(
433                                 DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 6),
434                         new PasswordMetrics(
435                                 DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */
436                                 6));
437 
438         /**
439          * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_MEDIUM} in terms of
440          * {@link PasswordMetrics}.
441          */
442         private static final PasswordComplexityBucket MEDIUM =
443                 new PasswordComplexityBucket(
444                         PASSWORD_COMPLEXITY_MEDIUM,
445                         new PasswordMetrics(
446                                 DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */
447                                 4),
448                         new PasswordMetrics(
449                                 DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 4),
450                         new PasswordMetrics(
451                                 DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */
452                                 4));
453 
454         /**
455          * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_LOW} in terms of
456          * {@link PasswordMetrics}.
457          */
458         private static final PasswordComplexityBucket LOW =
459                 new PasswordComplexityBucket(
460                         PASSWORD_COMPLEXITY_LOW,
461                         new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING),
462                         new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC),
463                         new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX),
464                         new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC),
465                         new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC));
466 
467         /**
468          * A special bucket to represent {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE}.
469          */
470         private static final PasswordComplexityBucket NONE =
471                 new PasswordComplexityBucket(PASSWORD_COMPLEXITY_NONE, new PasswordMetrics());
472 
473         /** Array containing all buckets from high to low. */
474         private static final PasswordComplexityBucket[] BUCKETS =
475                 new PasswordComplexityBucket[] {HIGH, MEDIUM, LOW};
476 
477         @PasswordComplexity
478         private final int mComplexityLevel;
479         private final PasswordMetrics[] mMetrics;
480 
481         /**
482          * @param metricsArray must be sorted in ascending order of {@link #quality}.
483          */
PasswordComplexityBucket(@asswordComplexity int complexityLevel, PasswordMetrics... metricsArray)484         private PasswordComplexityBucket(@PasswordComplexity int complexityLevel,
485                 PasswordMetrics... metricsArray) {
486             int previousQuality = PASSWORD_QUALITY_UNSPECIFIED;
487             for (PasswordMetrics metrics : metricsArray) {
488                 if (metrics.quality < previousQuality) {
489                     throw new IllegalArgumentException("metricsArray must be sorted in ascending"
490                             + " order of quality");
491                 }
492                 previousQuality = metrics.quality;
493             }
494 
495             this.mMetrics = metricsArray;
496             this.mComplexityLevel = complexityLevel;
497 
498         }
499 
500         /** Returns the bucket that {@code complexityLevel} represents. */
complexityLevelToBucket( @asswordComplexity int complexityLevel)501         private static PasswordComplexityBucket complexityLevelToBucket(
502                 @PasswordComplexity int complexityLevel) {
503             for (PasswordComplexityBucket bucket : BUCKETS) {
504                 if (bucket.mComplexityLevel == complexityLevel) {
505                     return bucket;
506                 }
507             }
508             return NONE;
509         }
510     }
511 }
512