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
17package android.app.admin;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.app.admin.DevicePolicyManager;
22import android.os.Parcelable;
23import android.os.Parcel;
24
25import java.lang.annotation.Retention;
26import java.lang.annotation.RetentionPolicy;
27import 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 */
35public 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
49    public PasswordMetrics() {}
50
51    public PasswordMetrics(int quality, int length) {
52        this.quality = quality;
53        this.length = length;
54    }
55
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
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
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
85    public int describeContents() {
86        return 0;
87    }
88
89    @Override
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
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     */
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
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
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