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 static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
20 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
21 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM;
22 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
23 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
24 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
25 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
26 import static android.app.admin.PasswordMetrics.complexityLevelToMinQuality;
27 import static android.app.admin.PasswordMetrics.sanitizeComplexityLevel;
28 import static android.app.admin.PasswordMetrics.validateCredential;
29 import static android.app.admin.PasswordMetrics.validatePasswordMetrics;
30 
31 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
32 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
33 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
34 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN;
35 
36 import static org.junit.Assert.assertEquals;
37 import static org.junit.Assert.assertTrue;
38 
39 import android.os.Parcel;
40 import android.platform.test.annotations.Presubmit;
41 
42 import androidx.test.ext.junit.runners.AndroidJUnit4;
43 import androidx.test.filters.SmallTest;
44 
45 import com.android.internal.widget.LockPatternUtils;
46 import com.android.internal.widget.LockscreenCredential;
47 import com.android.internal.widget.PasswordValidationError;
48 
49 import org.junit.Test;
50 import org.junit.runner.RunWith;
51 
52 import java.util.Arrays;
53 import java.util.Collections;
54 import java.util.HashMap;
55 import java.util.List;
56 
57 /** Unit tests for {@link PasswordMetrics}. */
58 @RunWith(AndroidJUnit4.class)
59 @SmallTest
60 @Presubmit
61 public class PasswordMetricsTest {
62     @Test
testParceling()63     public void testParceling() {
64         final int credType = CREDENTIAL_TYPE_PASSWORD;
65         final int length = 1;
66         final int letters = 2;
67         final int upperCase = 3;
68         final int lowerCase = 4;
69         final int numeric = 5;
70         final int symbols = 6;
71         final int nonLetter = 7;
72         final int nonNumeric = 8;
73         final int seqLength = 9;
74 
75         final Parcel parcel = Parcel.obtain();
76         PasswordMetrics metrics = new PasswordMetrics(credType, length, letters, upperCase,
77                 lowerCase, numeric, symbols, nonLetter, nonNumeric, seqLength);
78         try {
79             metrics.writeToParcel(parcel, 0);
80             parcel.setDataPosition(0);
81             metrics = PasswordMetrics.CREATOR.createFromParcel(parcel);
82         } finally {
83             parcel.recycle();
84         }
85 
86         assertEquals(credType, metrics.credType);
87         assertEquals(length, metrics.length);
88         assertEquals(letters, metrics.letters);
89         assertEquals(upperCase, metrics.upperCase);
90         assertEquals(lowerCase, metrics.lowerCase);
91         assertEquals(numeric, metrics.numeric);
92         assertEquals(symbols, metrics.symbols);
93         assertEquals(nonLetter, metrics.nonLetter);
94         assertEquals(nonNumeric, metrics.nonNumeric);
95         assertEquals(seqLength, metrics.seqLength);
96     }
97 
98     @Test
testComputeForPassword_metrics()99     public void testComputeForPassword_metrics() {
100         final PasswordMetrics metrics = metricsForPassword("6B~0z1Z3*8A");
101         assertEquals(11, metrics.length);
102         assertEquals(4, metrics.letters);
103         assertEquals(3, metrics.upperCase);
104         assertEquals(1, metrics.lowerCase);
105         assertEquals(5, metrics.numeric);
106         assertEquals(2, metrics.symbols);
107         assertEquals(7, metrics.nonLetter);
108     }
109 
110     @Test
testMaxLengthSequence()111     public void testMaxLengthSequence() {
112         assertEquals(4, PasswordMetrics.maxLengthSequence("1234".getBytes()));
113         assertEquals(5, PasswordMetrics.maxLengthSequence("13579".getBytes()));
114         assertEquals(4, PasswordMetrics.maxLengthSequence("1234abd".getBytes()));
115         assertEquals(3, PasswordMetrics.maxLengthSequence("aabc".getBytes()));
116         assertEquals(1, PasswordMetrics.maxLengthSequence("qwertyuio".getBytes()));
117         assertEquals(3, PasswordMetrics.maxLengthSequence("@ABC".getBytes()));
118         // anything that repeats
119         assertEquals(4, PasswordMetrics.maxLengthSequence(";;;;".getBytes()));
120         // ordered, but not composed of alphas or digits
121         assertEquals(1, PasswordMetrics.maxLengthSequence(":;<=>".getBytes()));
122     }
123 
124     @Test
testDetermineComplexity_none()125     public void testDetermineComplexity_none() {
126         assertEquals(PASSWORD_COMPLEXITY_NONE,
127                 new PasswordMetrics(CREDENTIAL_TYPE_NONE).determineComplexity());
128     }
129 
130     @Test
testDetermineComplexity_lowSomething()131     public void testDetermineComplexity_lowSomething() {
132         assertEquals(PASSWORD_COMPLEXITY_LOW,
133                 new PasswordMetrics(CREDENTIAL_TYPE_PATTERN).determineComplexity());
134     }
135 
136     @Test
testDetermineComplexity_lowNumeric()137     public void testDetermineComplexity_lowNumeric() {
138         assertEquals(PASSWORD_COMPLEXITY_LOW, metricsForPin("1234").determineComplexity());
139     }
140 
141     @Test
testDetermineComplexity_lowNumericComplex()142     public void testDetermineComplexity_lowNumericComplex() {
143         assertEquals(PASSWORD_COMPLEXITY_LOW, metricsForPin("124").determineComplexity());
144     }
145 
146     @Test
testDetermineComplexity_lowAlphabetic()147     public void testDetermineComplexity_lowAlphabetic() {
148         assertEquals(PASSWORD_COMPLEXITY_LOW, metricsForPassword("a!").determineComplexity());
149     }
150 
151     @Test
testDetermineComplexity_lowAlphanumeric()152     public void testDetermineComplexity_lowAlphanumeric() {
153         assertEquals(PASSWORD_COMPLEXITY_LOW, metricsForPassword("a!1").determineComplexity());
154     }
155 
156     @Test
testDetermineComplexity_mediumNumericComplex()157     public void testDetermineComplexity_mediumNumericComplex() {
158         assertEquals(PASSWORD_COMPLEXITY_MEDIUM, metricsForPin("1238").determineComplexity());
159     }
160 
161     @Test
testDetermineComplexity_mediumAlphabetic()162     public void testDetermineComplexity_mediumAlphabetic() {
163         assertEquals(PASSWORD_COMPLEXITY_MEDIUM, metricsForPassword("ab!c").determineComplexity());
164     }
165 
166     @Test
testDetermineComplexity_mediumAlphanumeric()167     public void testDetermineComplexity_mediumAlphanumeric() {
168         assertEquals(PASSWORD_COMPLEXITY_MEDIUM, metricsForPassword("ab!1").determineComplexity());
169     }
170 
171     @Test
testDetermineComplexity_highNumericComplex()172     public void testDetermineComplexity_highNumericComplex() {
173         assertEquals(PASSWORD_COMPLEXITY_HIGH, metricsForPin("12389647!").determineComplexity());
174     }
175 
176     @Test
testDetermineComplexity_highAlphabetic()177     public void testDetermineComplexity_highAlphabetic() {
178         assertEquals(PASSWORD_COMPLEXITY_HIGH,
179                 metricsForPassword("alphabetic!").determineComplexity());
180     }
181 
182     @Test
testDetermineComplexity_highAlphanumeric()183     public void testDetermineComplexity_highAlphanumeric() {
184         assertEquals(PASSWORD_COMPLEXITY_HIGH,
185                 metricsForPassword("alphanumeric123!").determineComplexity());
186     }
187 
188     @Test
testSanitizeComplexityLevel_none()189     public void testSanitizeComplexityLevel_none() {
190         assertEquals(PASSWORD_COMPLEXITY_NONE, sanitizeComplexityLevel(PASSWORD_COMPLEXITY_NONE));
191 
192     }
193 
194     @Test
testSanitizeComplexityLevel_low()195     public void testSanitizeComplexityLevel_low() {
196         assertEquals(PASSWORD_COMPLEXITY_LOW, sanitizeComplexityLevel(PASSWORD_COMPLEXITY_LOW));
197     }
198 
199     @Test
testSanitizeComplexityLevel_medium()200     public void testSanitizeComplexityLevel_medium() {
201         assertEquals(
202                 PASSWORD_COMPLEXITY_MEDIUM, sanitizeComplexityLevel(PASSWORD_COMPLEXITY_MEDIUM));
203     }
204 
205     @Test
testSanitizeComplexityLevel_high()206     public void testSanitizeComplexityLevel_high() {
207         assertEquals(PASSWORD_COMPLEXITY_HIGH, sanitizeComplexityLevel(PASSWORD_COMPLEXITY_HIGH));
208     }
209 
210     @Test
testSanitizeComplexityLevel_invalid()211     public void testSanitizeComplexityLevel_invalid() {
212         assertEquals(PASSWORD_COMPLEXITY_NONE, sanitizeComplexityLevel(-1));
213     }
214 
215     @Test
testComplexityLevelToMinQuality_none()216     public void testComplexityLevelToMinQuality_none() {
217         assertEquals(PASSWORD_QUALITY_UNSPECIFIED,
218                 complexityLevelToMinQuality(PASSWORD_COMPLEXITY_NONE));
219     }
220 
221     @Test
testComplexityLevelToMinQuality_low()222     public void testComplexityLevelToMinQuality_low() {
223         assertEquals(PASSWORD_QUALITY_SOMETHING,
224                 complexityLevelToMinQuality(PASSWORD_COMPLEXITY_LOW));
225     }
226 
227     @Test
testComplexityLevelToMinQuality_medium()228     public void testComplexityLevelToMinQuality_medium() {
229         assertEquals(PASSWORD_QUALITY_NUMERIC_COMPLEX,
230                 complexityLevelToMinQuality(PASSWORD_COMPLEXITY_MEDIUM));
231     }
232 
233     @Test
testComplexityLevelToMinQuality_high()234     public void testComplexityLevelToMinQuality_high() {
235         assertEquals(PASSWORD_QUALITY_NUMERIC_COMPLEX,
236                 complexityLevelToMinQuality(PASSWORD_COMPLEXITY_HIGH));
237     }
238 
239     @Test
testComplexityLevelToMinQuality_invalid()240     public void testComplexityLevelToMinQuality_invalid() {
241         assertEquals(PASSWORD_QUALITY_UNSPECIFIED, complexityLevelToMinQuality(-1));
242     }
243 
244     @Test
testMerge_single()245     public void testMerge_single() {
246         PasswordMetrics metrics = new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD);
247         assertEquals(CREDENTIAL_TYPE_PASSWORD,
248                 PasswordMetrics.merge(Collections.singletonList(metrics)).credType);
249     }
250 
251     @Test
testMerge_credentialTypes()252     public void testMerge_credentialTypes() {
253         PasswordMetrics none = new PasswordMetrics(CREDENTIAL_TYPE_NONE);
254         PasswordMetrics pattern = new PasswordMetrics(CREDENTIAL_TYPE_PATTERN);
255         PasswordMetrics password = new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD);
256         assertEquals(CREDENTIAL_TYPE_PATTERN,
257                 PasswordMetrics.merge(Arrays.asList(new PasswordMetrics[]{none, pattern}))
258                         .credType);
259         assertEquals(CREDENTIAL_TYPE_PASSWORD,
260                 PasswordMetrics.merge(Arrays.asList(new PasswordMetrics[]{none, password}))
261                         .credType);
262         assertEquals(CREDENTIAL_TYPE_PASSWORD,
263                 PasswordMetrics.merge(Arrays.asList(new PasswordMetrics[]{password, pattern}))
264                         .credType);
265     }
266 
267     @Test
testValidatePasswordMetrics_credentialTypes()268     public void testValidatePasswordMetrics_credentialTypes() {
269         PasswordMetrics none = new PasswordMetrics(CREDENTIAL_TYPE_NONE);
270         PasswordMetrics pattern = new PasswordMetrics(CREDENTIAL_TYPE_PATTERN);
271         PasswordMetrics password = new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD);
272         PasswordMetrics pin = new PasswordMetrics(CREDENTIAL_TYPE_PIN);
273 
274         // To pass minimal length check.
275         password.length = 4;
276         pin.length = 4;
277 
278         // No errors expected, credential is of stronger or equal type.
279         assertValidationErrors(
280                 validatePasswordMetrics(none, PASSWORD_COMPLEXITY_NONE, none));
281         assertValidationErrors(
282                 validatePasswordMetrics(none, PASSWORD_COMPLEXITY_NONE, pattern));
283         assertValidationErrors(
284                 validatePasswordMetrics(none, PASSWORD_COMPLEXITY_NONE, password));
285         assertValidationErrors(
286                 validatePasswordMetrics(none, PASSWORD_COMPLEXITY_NONE, pin));
287         assertValidationErrors(
288                 validatePasswordMetrics(pattern, PASSWORD_COMPLEXITY_NONE, pattern));
289         assertValidationErrors(
290                 validatePasswordMetrics(pattern, PASSWORD_COMPLEXITY_NONE, password));
291         assertValidationErrors(
292                 validatePasswordMetrics(password, PASSWORD_COMPLEXITY_NONE, password));
293         assertValidationErrors(
294                 validatePasswordMetrics(pin, PASSWORD_COMPLEXITY_NONE, pin));
295 
296         // Now actual credential type is weaker than required:
297         assertValidationErrors(
298                 validatePasswordMetrics(pattern, PASSWORD_COMPLEXITY_NONE, none),
299                 PasswordValidationError.WEAK_CREDENTIAL_TYPE, 0);
300         assertValidationErrors(
301                 validatePasswordMetrics(password, PASSWORD_COMPLEXITY_NONE, none),
302                 PasswordValidationError.WEAK_CREDENTIAL_TYPE, 0);
303         assertValidationErrors(
304                 validatePasswordMetrics(password, PASSWORD_COMPLEXITY_NONE, pattern),
305                 PasswordValidationError.WEAK_CREDENTIAL_TYPE, 0);
306         assertValidationErrors(
307                 validatePasswordMetrics(password, PASSWORD_COMPLEXITY_NONE, pin),
308                 PasswordValidationError.WEAK_CREDENTIAL_TYPE, 0);
309     }
310 
311     @Test
testValidatePasswordMetrics_pinAndComplexityHigh()312     public void testValidatePasswordMetrics_pinAndComplexityHigh() {
313         PasswordMetrics adminMetrics = new PasswordMetrics(CREDENTIAL_TYPE_PIN);
314         PasswordMetrics actualMetrics = new PasswordMetrics(CREDENTIAL_TYPE_PIN);
315         actualMetrics.length = 6;
316         actualMetrics.seqLength = 1;
317 
318         assertValidationErrors(
319                 validatePasswordMetrics(adminMetrics, PASSWORD_COMPLEXITY_HIGH, actualMetrics),
320                 PasswordValidationError.TOO_SHORT, 8);
321     }
322 
323     @Test
testValidatePasswordMetrics_nonAllNumberPasswordAndComplexityHigh()324     public void testValidatePasswordMetrics_nonAllNumberPasswordAndComplexityHigh() {
325         PasswordMetrics adminMetrics = new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD);
326         PasswordMetrics actualMetrics = new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD);
327         actualMetrics.length = 5;
328         actualMetrics.nonNumeric = 1;
329         actualMetrics.seqLength = 1;
330 
331         assertValidationErrors(
332                 validatePasswordMetrics(adminMetrics, PASSWORD_COMPLEXITY_HIGH, actualMetrics),
333                 PasswordValidationError.TOO_SHORT, 6);
334     }
335 
336     @Test
testValidatePasswordMetrics_allNumberPasswordAndComplexityHigh()337     public void testValidatePasswordMetrics_allNumberPasswordAndComplexityHigh() {
338         PasswordMetrics adminMetrics = new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD);
339         PasswordMetrics actualMetrics = new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD);
340         actualMetrics.length = 6;
341         actualMetrics.seqLength = 1;
342 
343         assertValidationErrors(
344                 validatePasswordMetrics(adminMetrics, PASSWORD_COMPLEXITY_HIGH, actualMetrics),
345                 PasswordValidationError.TOO_SHORT_WHEN_ALL_NUMERIC, 8);
346     }
347 
348     @Test
testValidatePasswordMetrics_allNumberPasswordAndRequireNonNumeric()349     public void testValidatePasswordMetrics_allNumberPasswordAndRequireNonNumeric() {
350         PasswordMetrics adminMetrics = new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD);
351         adminMetrics.nonNumeric = 1;
352         PasswordMetrics actualMetrics = new PasswordMetrics(CREDENTIAL_TYPE_PASSWORD);
353         actualMetrics.length = 6;
354         actualMetrics.seqLength = 1;
355 
356         assertValidationErrors(
357                 validatePasswordMetrics(adminMetrics, PASSWORD_COMPLEXITY_HIGH, actualMetrics),
358                 PasswordValidationError.NOT_ENOUGH_NON_DIGITS, 1);
359     }
360 
361     @Test
testValidateCredential_none()362     public void testValidateCredential_none() {
363         PasswordMetrics adminMetrics;
364         LockscreenCredential none = LockscreenCredential.createNone();
365 
366         adminMetrics = new PasswordMetrics(CREDENTIAL_TYPE_NONE);
367         assertValidationErrors(
368                 validateCredential(adminMetrics, PASSWORD_COMPLEXITY_NONE, none));
369 
370         adminMetrics = new PasswordMetrics(CREDENTIAL_TYPE_PIN);
371         assertValidationErrors(
372                 validateCredential(adminMetrics, PASSWORD_COMPLEXITY_NONE, none),
373                 PasswordValidationError.WEAK_CREDENTIAL_TYPE, 0);
374     }
375 
376     @Test
testValidateCredential_password()377     public void testValidateCredential_password() {
378         PasswordMetrics adminMetrics;
379         LockscreenCredential password;
380 
381         adminMetrics = new PasswordMetrics(CREDENTIAL_TYPE_NONE);
382         password = LockscreenCredential.createPassword("password");
383         assertValidationErrors(
384                 validateCredential(adminMetrics, PASSWORD_COMPLEXITY_LOW, password));
385 
386         // Test that validateCredential() checks LockscreenCredential#hasInvalidChars().
387         adminMetrics = new PasswordMetrics(CREDENTIAL_TYPE_NONE);
388         password = LockscreenCredential.createPassword("™™™™");
389         assertTrue(password.hasInvalidChars());
390         assertValidationErrors(
391                 validateCredential(adminMetrics, PASSWORD_COMPLEXITY_LOW, password),
392                 PasswordValidationError.CONTAINS_INVALID_CHARACTERS, 0);
393 
394         // Test one more case where validateCredential() should reject the password.  Beyond this,
395         // the unit tests for the lower-level method validatePasswordMetrics() should be sufficient.
396         adminMetrics = new PasswordMetrics(CREDENTIAL_TYPE_NONE);
397         adminMetrics.length = 6;
398         password = LockscreenCredential.createPassword("pass");
399         assertValidationErrors(
400                 validateCredential(adminMetrics, PASSWORD_COMPLEXITY_LOW, password),
401                 PasswordValidationError.TOO_SHORT, 6);
402     }
403 
createPattern(String patternString)404     private LockscreenCredential createPattern(String patternString) {
405         return LockscreenCredential.createPattern(LockPatternUtils.byteArrayToPattern(
406                 patternString.getBytes()));
407     }
408 
metricsForPassword(String password)409     private static PasswordMetrics metricsForPassword(String password) {
410         return PasswordMetrics.computeForCredential(LockscreenCredential.createPassword(password));
411     }
412 
metricsForPin(String pin)413     private static PasswordMetrics metricsForPin(String pin) {
414         return PasswordMetrics.computeForCredential(LockscreenCredential.createPin(pin));
415     }
416 
417     @Test
testValidateCredential_pattern()418     public void testValidateCredential_pattern() {
419         PasswordMetrics adminMetrics = new PasswordMetrics(CREDENTIAL_TYPE_NONE);
420         assertValidationErrors(
421                 validateCredential(adminMetrics, PASSWORD_COMPLEXITY_NONE, createPattern("123")),
422                 PasswordValidationError.TOO_SHORT, 4);
423         assertValidationErrors(
424                 validateCredential(adminMetrics, PASSWORD_COMPLEXITY_NONE, createPattern("1234")));
425     }
426 
427     /**
428      * @param expected sequence of validation error codes followed by requirement values, must have
429      *                 even number of elements. Empty means no errors.
430      */
assertValidationErrors( List<PasswordValidationError> actualErrors, int... expected)431     private void assertValidationErrors(
432             List<PasswordValidationError> actualErrors, int... expected) {
433         assertEquals("Test programming error: content shoud have even number of elements",
434                 0, expected.length % 2);
435         assertEquals("wrong number of validation errors", expected.length / 2, actualErrors.size());
436         HashMap<Integer, Integer> errorMap = new HashMap<>();
437         for (PasswordValidationError error : actualErrors) {
438             errorMap.put(error.errorCode, error.requirement);
439         }
440 
441         for (int i = 0; i < expected.length / 2; i++) {
442             final int expectedError = expected[i * 2];
443             final int expectedRequirement = expected[i * 2 + 1];
444             assertTrue("error expected but not reported: " + expectedError,
445                     errorMap.containsKey(expectedError));
446             assertEquals("unexpected requirement for error: " + expectedError,
447                     Integer.valueOf(expectedRequirement), errorMap.get(expectedError));
448         }
449     }
450 }
451