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