blob: d71d3553db7e3c195e420efaefe2f3df248f6e40 [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
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -0700213 public static final @android.annotation.NonNull Parcelable.Creator<PasswordMetrics> CREATOR
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100214 = 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
Rich Canningsf64ec632019-02-21 12:40:36 -0800224 /**
225 * Returns the {@code PasswordMetrics} for a given password
226 */
227 public static PasswordMetrics computeForPassword(@NonNull byte[] password) {
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100228 // Analyse the characters used
229 int letters = 0;
230 int upperCase = 0;
231 int lowerCase = 0;
232 int numeric = 0;
233 int symbols = 0;
234 int nonLetter = 0;
Rich Canningsf64ec632019-02-21 12:40:36 -0800235 final int length = password.length;
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100236 for (int i = 0; i < length; i++) {
Rich Canningsf64ec632019-02-21 12:40:36 -0800237 switch (categoryChar((char) password[i])) {
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100238 case CHAR_LOWER_CASE:
239 letters++;
240 lowerCase++;
241 break;
242 case CHAR_UPPER_CASE:
243 letters++;
244 upperCase++;
245 break;
246 case CHAR_DIGIT:
247 numeric++;
248 nonLetter++;
249 break;
250 case CHAR_SYMBOL:
251 symbols++;
252 nonLetter++;
253 break;
254 }
255 }
256
257 // Determine the quality of the password
258 final boolean hasNumeric = numeric > 0;
259 final boolean hasNonNumeric = (letters + symbols) > 0;
260 final int quality;
261 if (hasNonNumeric && hasNumeric) {
262 quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
263 } else if (hasNonNumeric) {
264 quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
265 } else if (hasNumeric) {
266 quality = maxLengthSequence(password) > MAX_ALLOWED_SEQUENCE
267 ? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
268 : DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
269 } else {
270 quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
271 }
272
273 return new PasswordMetrics(
274 quality, length, letters, upperCase, lowerCase, numeric, symbols, nonLetter);
275 }
276
Rubin Xu7cf45092017-08-28 11:47:35 +0100277 @Override
278 public boolean equals(Object other) {
279 if (!(other instanceof PasswordMetrics)) {
280 return false;
281 }
282 PasswordMetrics o = (PasswordMetrics) other;
283 return this.quality == o.quality
284 && this.length == o.length
285 && this.letters == o.letters
286 && this.upperCase == o.upperCase
287 && this.lowerCase == o.lowerCase
288 && this.numeric == o.numeric
289 && this.symbols == o.symbols
290 && this.nonLetter == o.nonLetter;
291 }
292
Bernard Chaue9586552018-11-29 10:59:31 +0000293 private boolean satisfiesBucket(PasswordMetrics... bucket) {
294 for (PasswordMetrics metrics : bucket) {
295 if (this.quality == metrics.quality) {
296 return this.length >= metrics.length;
297 }
298 }
299 return false;
300 }
301
Rich Canningsf64ec632019-02-21 12:40:36 -0800302 /**
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100303 * Returns the maximum length of a sequential characters. A sequence is defined as
304 * monotonically increasing characters with a constant interval or the same character repeated.
305 *
306 * For example:
307 * maxLengthSequence("1234") == 4
308 * maxLengthSequence("13579") == 5
309 * maxLengthSequence("1234abc") == 4
310 * maxLengthSequence("aabc") == 3
311 * maxLengthSequence("qwertyuio") == 1
312 * maxLengthSequence("@ABC") == 3
313 * maxLengthSequence(";;;;") == 4 (anything that repeats)
314 * maxLengthSequence(":;<=>") == 1 (ordered, but not composed of alphas or digits)
315 *
Rich Canningsf64ec632019-02-21 12:40:36 -0800316 * @param bytes the pass
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100317 * @return the number of sequential letters or digits
318 */
Rich Canningsf64ec632019-02-21 12:40:36 -0800319 public static int maxLengthSequence(@NonNull byte[] bytes) {
320 if (bytes.length == 0) return 0;
321 char previousChar = (char) bytes[0];
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100322 @CharacterCatagory int category = categoryChar(previousChar); //current sequence category
323 int diff = 0; //difference between two consecutive characters
324 boolean hasDiff = false; //if we are currently targeting a sequence
325 int maxLength = 0; //maximum length of a sequence already found
326 int startSequence = 0; //where the current sequence started
Rich Canningsf64ec632019-02-21 12:40:36 -0800327 for (int current = 1; current < bytes.length; current++) {
328 char currentChar = (char) bytes[current];
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100329 @CharacterCatagory int categoryCurrent = categoryChar(currentChar);
330 int currentDiff = (int) currentChar - (int) previousChar;
331 if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) {
332 maxLength = Math.max(maxLength, current - startSequence);
333 startSequence = current;
334 hasDiff = false;
335 category = categoryCurrent;
336 }
337 else {
338 if(hasDiff && currentDiff != diff) {
339 maxLength = Math.max(maxLength, current - startSequence);
340 startSequence = current - 1;
341 }
342 diff = currentDiff;
343 hasDiff = true;
344 }
345 previousChar = currentChar;
346 }
Rich Canningsf64ec632019-02-21 12:40:36 -0800347 maxLength = Math.max(maxLength, bytes.length - startSequence);
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100348 return maxLength;
349 }
350
351 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -0700352 @IntDef(prefix = { "CHAR_" }, value = {
353 CHAR_UPPER_CASE,
354 CHAR_LOWER_CASE,
355 CHAR_DIGIT,
356 CHAR_SYMBOL
357 })
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100358 private @interface CharacterCatagory {}
359 private static final int CHAR_LOWER_CASE = 0;
360 private static final int CHAR_UPPER_CASE = 1;
361 private static final int CHAR_DIGIT = 2;
362 private static final int CHAR_SYMBOL = 3;
363
364 @CharacterCatagory
365 private static int categoryChar(char c) {
366 if ('a' <= c && c <= 'z') return CHAR_LOWER_CASE;
367 if ('A' <= c && c <= 'Z') return CHAR_UPPER_CASE;
368 if ('0' <= c && c <= '9') return CHAR_DIGIT;
369 return CHAR_SYMBOL;
370 }
371
372 private static int maxDiffCategory(@CharacterCatagory int category) {
373 switch (category) {
374 case CHAR_LOWER_CASE:
375 case CHAR_UPPER_CASE:
376 return 1;
377 case CHAR_DIGIT:
378 return 10;
379 default:
380 return 0;
381 }
382 }
Bernard Chaue9586552018-11-29 10:59:31 +0000383
384 /** Determines the {@link PasswordComplexity} of this {@link PasswordMetrics}. */
385 @PasswordComplexity
386 public int determineComplexity() {
387 for (PasswordComplexityBucket bucket : PasswordComplexityBucket.BUCKETS) {
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000388 if (satisfiesBucket(bucket.mMetrics)) {
Bernard Chaue9586552018-11-29 10:59:31 +0000389 return bucket.mComplexityLevel;
390 }
391 }
392 return PASSWORD_COMPLEXITY_NONE;
393 }
394
395 /**
396 * Requirements in terms of {@link PasswordMetrics} for each {@link PasswordComplexity}.
397 */
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000398 private static class PasswordComplexityBucket {
Bernard Chaue9586552018-11-29 10:59:31 +0000399 /**
400 * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_HIGH} in terms of
401 * {@link PasswordMetrics}.
402 */
403 private static final PasswordComplexityBucket HIGH =
404 new PasswordComplexityBucket(
405 PASSWORD_COMPLEXITY_HIGH,
406 new PasswordMetrics(
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000407 DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */
408 8),
Bernard Chaue9586552018-11-29 10:59:31 +0000409 new PasswordMetrics(
410 DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 6),
411 new PasswordMetrics(
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000412 DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */
413 6));
Bernard Chaue9586552018-11-29 10:59:31 +0000414
415 /**
416 * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_MEDIUM} in terms of
417 * {@link PasswordMetrics}.
418 */
419 private static final PasswordComplexityBucket MEDIUM =
420 new PasswordComplexityBucket(
421 PASSWORD_COMPLEXITY_MEDIUM,
422 new PasswordMetrics(
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000423 DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */
424 4),
Bernard Chaue9586552018-11-29 10:59:31 +0000425 new PasswordMetrics(
426 DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 4),
427 new PasswordMetrics(
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000428 DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */
Bernard Chaue9586552018-11-29 10:59:31 +0000429 4));
430
431 /**
432 * Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_LOW} in terms of
433 * {@link PasswordMetrics}.
434 */
435 private static final PasswordComplexityBucket LOW =
436 new PasswordComplexityBucket(
437 PASSWORD_COMPLEXITY_LOW,
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000438 new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING),
Bernard Chaue9586552018-11-29 10:59:31 +0000439 new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC),
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000440 new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX),
441 new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC),
442 new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC));
Bernard Chaue9586552018-11-29 10:59:31 +0000443
444 /**
445 * A special bucket to represent {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE}.
446 */
447 private static final PasswordComplexityBucket NONE =
448 new PasswordComplexityBucket(PASSWORD_COMPLEXITY_NONE, new PasswordMetrics());
449
450 /** Array containing all buckets from high to low. */
451 private static final PasswordComplexityBucket[] BUCKETS =
452 new PasswordComplexityBucket[] {HIGH, MEDIUM, LOW};
453
454 @PasswordComplexity
455 private final int mComplexityLevel;
456 private final PasswordMetrics[] mMetrics;
457
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000458 /**
459 * @param metricsArray must be sorted in ascending order of {@link #quality}.
460 */
Bernard Chaue9586552018-11-29 10:59:31 +0000461 private PasswordComplexityBucket(@PasswordComplexity int complexityLevel,
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000462 PasswordMetrics... metricsArray) {
463 int previousQuality = PASSWORD_QUALITY_UNSPECIFIED;
464 for (PasswordMetrics metrics : metricsArray) {
465 if (metrics.quality < previousQuality) {
466 throw new IllegalArgumentException("metricsArray must be sorted in ascending"
467 + " order of quality");
468 }
469 previousQuality = metrics.quality;
470 }
Bernard Chaue9586552018-11-29 10:59:31 +0000471
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000472 this.mMetrics = metricsArray;
473 this.mComplexityLevel = complexityLevel;
474
Bernard Chaue9586552018-11-29 10:59:31 +0000475 }
476
477 /** Returns the bucket that {@code complexityLevel} represents. */
Bernard Chau60e0f7f2018-11-29 16:36:39 +0000478 private static PasswordComplexityBucket complexityLevelToBucket(
Bernard Chaue9586552018-11-29 10:59:31 +0000479 @PasswordComplexity int complexityLevel) {
480 for (PasswordComplexityBucket bucket : BUCKETS) {
481 if (bucket.mComplexityLevel == complexityLevel) {
482 return bucket;
483 }
484 }
485 return NONE;
486 }
487 }
Andrew Scull5f9e6f32016-08-02 14:22:17 +0100488}