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