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 android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.app.admin.DevicePolicyManager;
22 import android.os.Parcelable;
23 import android.os.Parcel;
24 
25 import java.lang.annotation.Retention;
26 import java.lang.annotation.RetentionPolicy;
27 import java.io.IOException;
28 
29 /**
30  * A class that represents the metrics of a password that are used to decide whether or not a
31  * password meets the requirements.
32  *
33  * {@hide}
34  */
35 public class PasswordMetrics implements Parcelable {
36     // Maximum allowed number of repeated or ordered characters in a sequence before we'll
37     // consider it a complex PIN/password.
38     public static final int MAX_ALLOWED_SEQUENCE = 3;
39 
40     public int quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
41     public int length = 0;
42     public int letters = 0;
43     public int upperCase = 0;
44     public int lowerCase = 0;
45     public int numeric = 0;
46     public int symbols = 0;
47     public int nonLetter = 0;
48 
PasswordMetrics()49     public PasswordMetrics() {}
50 
PasswordMetrics(int quality, int length)51     public PasswordMetrics(int quality, int length) {
52         this.quality = quality;
53         this.length = length;
54     }
55 
PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase, int numeric, int symbols, int nonLetter)56     public PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase,
57             int numeric, int symbols, int nonLetter) {
58         this(quality, length);
59         this.letters = letters;
60         this.upperCase = upperCase;
61         this.lowerCase = lowerCase;
62         this.numeric = numeric;
63         this.symbols = symbols;
64         this.nonLetter = nonLetter;
65     }
66 
PasswordMetrics(Parcel in)67     private PasswordMetrics(Parcel in) {
68         quality = in.readInt();
69         length = in.readInt();
70         letters = in.readInt();
71         upperCase = in.readInt();
72         lowerCase = in.readInt();
73         numeric = in.readInt();
74         symbols = in.readInt();
75         nonLetter = in.readInt();
76     }
77 
isDefault()78     public boolean isDefault() {
79         return quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
80                 && length == 0 && letters == 0 && upperCase == 0 && lowerCase == 0
81                 && numeric == 0 && symbols == 0 && nonLetter == 0;
82     }
83 
84     @Override
describeContents()85     public int describeContents() {
86         return 0;
87     }
88 
89     @Override
writeToParcel(Parcel dest, int flags)90     public void writeToParcel(Parcel dest, int flags) {
91         dest.writeInt(quality);
92         dest.writeInt(length);
93         dest.writeInt(letters);
94         dest.writeInt(upperCase);
95         dest.writeInt(lowerCase);
96         dest.writeInt(numeric);
97         dest.writeInt(symbols);
98         dest.writeInt(nonLetter);
99     }
100 
101     public static final Parcelable.Creator<PasswordMetrics> CREATOR
102             = new Parcelable.Creator<PasswordMetrics>() {
103         public PasswordMetrics createFromParcel(Parcel in) {
104             return new PasswordMetrics(in);
105         }
106 
107         public PasswordMetrics[] newArray(int size) {
108             return new PasswordMetrics[size];
109         }
110     };
111 
computeForPassword(@onNull String password)112     public static PasswordMetrics computeForPassword(@NonNull String password) {
113         // Analyse the characters used
114         int letters = 0;
115         int upperCase = 0;
116         int lowerCase = 0;
117         int numeric = 0;
118         int symbols = 0;
119         int nonLetter = 0;
120         final int length = password.length();
121         for (int i = 0; i < length; i++) {
122             switch (categoryChar(password.charAt(i))) {
123                 case CHAR_LOWER_CASE:
124                     letters++;
125                     lowerCase++;
126                     break;
127                 case CHAR_UPPER_CASE:
128                     letters++;
129                     upperCase++;
130                     break;
131                 case CHAR_DIGIT:
132                     numeric++;
133                     nonLetter++;
134                     break;
135                 case CHAR_SYMBOL:
136                     symbols++;
137                     nonLetter++;
138                     break;
139             }
140         }
141 
142         // Determine the quality of the password
143         final boolean hasNumeric = numeric > 0;
144         final boolean hasNonNumeric = (letters + symbols) > 0;
145         final int quality;
146         if (hasNonNumeric && hasNumeric) {
147             quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
148         } else if (hasNonNumeric) {
149             quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
150         } else if (hasNumeric) {
151             quality = maxLengthSequence(password) > MAX_ALLOWED_SEQUENCE
152                     ? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
153                     : DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
154         } else {
155             quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
156         }
157 
158         return new PasswordMetrics(
159                 quality, length, letters, upperCase, lowerCase, numeric, symbols, nonLetter);
160     }
161 
162     /*
163      * Returns the maximum length of a sequential characters. A sequence is defined as
164      * monotonically increasing characters with a constant interval or the same character repeated.
165      *
166      * For example:
167      * maxLengthSequence("1234") == 4
168      * maxLengthSequence("13579") == 5
169      * maxLengthSequence("1234abc") == 4
170      * maxLengthSequence("aabc") == 3
171      * maxLengthSequence("qwertyuio") == 1
172      * maxLengthSequence("@ABC") == 3
173      * maxLengthSequence(";;;;") == 4 (anything that repeats)
174      * maxLengthSequence(":;<=>") == 1  (ordered, but not composed of alphas or digits)
175      *
176      * @param string the pass
177      * @return the number of sequential letters or digits
178      */
maxLengthSequence(@onNull String string)179     public static int maxLengthSequence(@NonNull String string) {
180         if (string.length() == 0) return 0;
181         char previousChar = string.charAt(0);
182         @CharacterCatagory int category = categoryChar(previousChar); //current sequence category
183         int diff = 0; //difference between two consecutive characters
184         boolean hasDiff = false; //if we are currently targeting a sequence
185         int maxLength = 0; //maximum length of a sequence already found
186         int startSequence = 0; //where the current sequence started
187         for (int current = 1; current < string.length(); current++) {
188             char currentChar = string.charAt(current);
189             @CharacterCatagory int categoryCurrent = categoryChar(currentChar);
190             int currentDiff = (int) currentChar - (int) previousChar;
191             if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) {
192                 maxLength = Math.max(maxLength, current - startSequence);
193                 startSequence = current;
194                 hasDiff = false;
195                 category = categoryCurrent;
196             }
197             else {
198                 if(hasDiff && currentDiff != diff) {
199                     maxLength = Math.max(maxLength, current - startSequence);
200                     startSequence = current - 1;
201                 }
202                 diff = currentDiff;
203                 hasDiff = true;
204             }
205             previousChar = currentChar;
206         }
207         maxLength = Math.max(maxLength, string.length() - startSequence);
208         return maxLength;
209     }
210 
211     @Retention(RetentionPolicy.SOURCE)
212     @IntDef({CHAR_UPPER_CASE, CHAR_LOWER_CASE, CHAR_DIGIT, CHAR_SYMBOL})
213     private @interface CharacterCatagory {}
214     private static final int CHAR_LOWER_CASE = 0;
215     private static final int CHAR_UPPER_CASE = 1;
216     private static final int CHAR_DIGIT = 2;
217     private static final int CHAR_SYMBOL = 3;
218 
219     @CharacterCatagory
categoryChar(char c)220     private static int categoryChar(char c) {
221         if ('a' <= c && c <= 'z') return CHAR_LOWER_CASE;
222         if ('A' <= c && c <= 'Z') return CHAR_UPPER_CASE;
223         if ('0' <= c && c <= '9') return CHAR_DIGIT;
224         return CHAR_SYMBOL;
225     }
226 
maxDiffCategory(@haracterCatagory int category)227     private static int maxDiffCategory(@CharacterCatagory int category) {
228         switch (category) {
229             case CHAR_LOWER_CASE:
230             case CHAR_UPPER_CASE:
231                 return 1;
232             case CHAR_DIGIT:
233                 return 10;
234             default:
235                 return 0;
236         }
237     }
238 }
239