1 /*
2  * Copyright (C) 2018 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 com.android.car.settings.security;
18 
19 import android.app.admin.DevicePolicyManager;
20 import android.app.admin.PasswordMetrics;
21 import android.content.Context;
22 
23 import com.android.car.settings.R;
24 import com.android.car.settings.common.Logger;
25 import com.android.car.setupwizardlib.InitialLockSetupConstants.ValidateLockFlags;
26 import com.android.internal.widget.LockscreenCredential;
27 
28 import java.util.LinkedList;
29 import java.util.List;
30 
31 /**
32  * Helper used by ChooseLockPinPasswordFragment
33  */
34 public class PasswordHelper {
35     public static final String EXTRA_CURRENT_SCREEN_LOCK = "extra_current_screen_lock";
36     public static final String EXTRA_CURRENT_PASSWORD_QUALITY = "extra_current_password_quality";
37     /**
38      * Required minimum length of PIN or password.
39      */
40     public static final int MIN_LENGTH = 4;
41     // Error code returned from validate(String).
42     static final int NO_ERROR = 0;
43     static final int CONTAINS_INVALID_CHARACTERS = 1;
44     static final int TOO_SHORT = 1 << 1;
45     static final int CONTAINS_NON_DIGITS = 1 << 2;
46     static final int CONTAINS_SEQUENTIAL_DIGITS = 1 << 3;
47     private static final Logger LOG = new Logger(PasswordHelper.class);
48     private final boolean mIsPin;
49 
PasswordHelper(boolean isPin)50     public PasswordHelper(boolean isPin) {
51         mIsPin = isPin;
52     }
53 
54     /**
55      * Returns one of the password quality values defined in {@link DevicePolicyManager}, such
56      * as NUMERIC, ALPHANUMERIC etc.
57      */
getPasswordQuality()58     public int getPasswordQuality() {
59         return mIsPin ? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC :
60                 DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
61     }
62 
63     /**
64      * Validates PIN/Password and returns the validation result.
65      *
66      * @param password the raw bytes for the password the user typed in
67      * @return the error code which should be non-zero where there is error. Otherwise
68      * {@link #NO_ERROR} should be returned.
69      */
validate(LockscreenCredential password)70     public int validate(LockscreenCredential password) {
71         return mIsPin ? validatePin(password.getCredential())
72                 : validatePassword(password.getCredential());
73     }
74 
75     /**
76      * Validates PIN/Password using the setup wizard {@link ValidateLockFlags}.
77      *
78      * @param password The password to validate.
79      * @return The error code where 0 is no error.
80      */
validateSetupWizard(byte[] password)81     public int validateSetupWizard(byte[] password) {
82         return mIsPin ? translateSettingsToSuwError(validatePin(password))
83                 : translateSettingsToSuwError(validatePassword(password));
84     }
85 
86     /**
87      * Converts error code from validatePassword to an array of messages describing the errors with
88      * important message comes first.  The messages are concatenated with a space in between.
89      * Please make sure each message ends with a period.
90      *
91      * @param errorCode the code returned by {@link #validatePassword(byte[]) validatePassword}
92      */
convertErrorCodeToMessages(Context context, int errorCode)93     public List<String> convertErrorCodeToMessages(Context context, int errorCode) {
94         return mIsPin ? convertPinErrorCodeToMessages(context, errorCode) :
95                 convertPasswordErrorCodeToMessages(context, errorCode);
96     }
97 
validatePassword(byte[] password)98     private int validatePassword(byte[] password) {
99         int errorCode = NO_ERROR;
100 
101         if (password.length < MIN_LENGTH) {
102             errorCode |= TOO_SHORT;
103         }
104 
105         // Allow non-control Latin-1 characters only.
106         for (int i = 0; i < password.length; i++) {
107             char c = (char) password[i];
108             if (c < 32 || c > 127) {
109                 errorCode |= CONTAINS_INVALID_CHARACTERS;
110                 break;
111             }
112         }
113 
114         return errorCode;
115     }
116 
translateSettingsToSuwError(int error)117     private int translateSettingsToSuwError(int error) {
118         int output = 0;
119         if ((error & CONTAINS_NON_DIGITS) > 0) {
120             LOG.v("CONTAINS_NON_DIGITS");
121             output |= ValidateLockFlags.INVALID_BAD_SYMBOLS;
122         }
123         if ((error & CONTAINS_INVALID_CHARACTERS) > 0) {
124             LOG.v("INVALID_CHAR");
125             output |= ValidateLockFlags.INVALID_BAD_SYMBOLS;
126         }
127         if ((error & TOO_SHORT) > 0) {
128             LOG.v("TOO_SHORT");
129             output |= ValidateLockFlags.INVALID_LENGTH;
130         }
131         if ((error & CONTAINS_SEQUENTIAL_DIGITS) > 0) {
132             LOG.v("SEQUENTIAL_DIGITS");
133             output |= ValidateLockFlags.INVALID_LACKS_COMPLEXITY;
134         }
135         return output;
136     }
137 
validatePin(byte[] pin)138     private int validatePin(byte[] pin) {
139         int errorCode = NO_ERROR;
140         PasswordMetrics metrics = PasswordMetrics.computeForPassword(pin);
141         int passwordQuality = getPasswordQuality();
142 
143         if (metrics.length < MIN_LENGTH) {
144             errorCode |= TOO_SHORT;
145         }
146 
147         if (metrics.letters > 0 || metrics.symbols > 0) {
148             errorCode |= CONTAINS_NON_DIGITS;
149         }
150 
151         if (passwordQuality == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX) {
152             // Check for repeated characters or sequences (e.g. '1234', '0000', '2468')
153             int sequence = PasswordMetrics.maxLengthSequence(pin);
154             if (sequence > PasswordMetrics.MAX_ALLOWED_SEQUENCE) {
155                 errorCode |= CONTAINS_SEQUENTIAL_DIGITS;
156             }
157         }
158 
159         return errorCode;
160     }
161 
convertPasswordErrorCodeToMessages(Context context, int errorCode)162     private List<String> convertPasswordErrorCodeToMessages(Context context, int errorCode) {
163         List<String> messages = new LinkedList<>();
164 
165         if ((errorCode & CONTAINS_INVALID_CHARACTERS) > 0) {
166             messages.add(context.getString(R.string.lockpassword_illegal_character));
167         }
168 
169         if ((errorCode & TOO_SHORT) > 0) {
170             messages.add(context.getString(R.string.lockpassword_password_too_short, MIN_LENGTH));
171         }
172 
173         return messages;
174     }
175 
convertPinErrorCodeToMessages(Context context, int errorCode)176     private List<String> convertPinErrorCodeToMessages(Context context, int errorCode) {
177         List<String> messages = new LinkedList<>();
178 
179         if ((errorCode & CONTAINS_NON_DIGITS) > 0) {
180             messages.add(context.getString(R.string.lockpassword_pin_contains_non_digits));
181         }
182 
183         if ((errorCode & CONTAINS_SEQUENTIAL_DIGITS) > 0) {
184             messages.add(context.getString(R.string.lockpassword_pin_no_sequential_digits));
185         }
186 
187         if ((errorCode & TOO_SHORT) > 0) {
188             messages.add(context.getString(R.string.lockpin_invalid_pin));
189         }
190 
191         return messages;
192     }
193 }
194