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 static com.android.internal.widget.PasswordValidationError.CONTAINS_INVALID_CHARACTERS; 20 import static com.android.internal.widget.PasswordValidationError.CONTAINS_SEQUENCE; 21 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_DIGITS; 22 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LETTERS; 23 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LOWER_CASE; 24 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_DIGITS; 25 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_LETTER; 26 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_SYMBOLS; 27 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_UPPER_CASE; 28 import static com.android.internal.widget.PasswordValidationError.RECENTLY_USED; 29 import static com.android.internal.widget.PasswordValidationError.TOO_LONG; 30 import static com.android.internal.widget.PasswordValidationError.TOO_SHORT; 31 32 import android.annotation.UserIdInt; 33 import android.app.admin.DevicePolicyManager; 34 import android.app.admin.PasswordMetrics; 35 import android.content.Context; 36 37 import androidx.annotation.VisibleForTesting; 38 39 import com.android.car.settings.R; 40 import com.android.car.settings.common.Logger; 41 import com.android.car.setupwizardlib.InitialLockSetupConstants.LockTypes; 42 import com.android.internal.widget.LockPatternUtils; 43 import com.android.internal.widget.LockscreenCredential; 44 import com.android.internal.widget.PasswordValidationError; 45 import com.android.settingslib.utils.StringUtil; 46 47 import java.util.ArrayList; 48 import java.util.Collections; 49 import java.util.List; 50 51 /** 52 * Helper used by ChooseLockPinPasswordFragment 53 * Much of the logic is taken from {@link com.android.settings.password.ChooseLockPassword} 54 */ 55 public class PasswordHelper { 56 public static final String EXTRA_CURRENT_SCREEN_LOCK = "extra_current_screen_lock"; 57 public static final String EXTRA_CURRENT_PASSWORD_QUALITY = "extra_current_password_quality"; 58 59 // String to be returned to when there is no password complexity validation error. 60 @VisibleForTesting 61 static final String NO_ERROR_MESSAGE = ""; 62 63 // Error code returned from validateSetupWizard(byte[] password). 64 static final int NO_ERROR = 0; 65 static final int ERROR_CODE = 1; 66 private static final Logger LOG = new Logger(PasswordHelper.class); 67 68 private final Context mContext; 69 private final boolean mIsPin; 70 private final LockPatternUtils mLockPatternUtils; 71 private final PasswordMetrics mMinMetrics; 72 private List<PasswordValidationError> mValidationErrors; 73 private byte[] mPasswordHistoryHashFactor; 74 75 @UserIdInt 76 private final int mUserId; 77 78 @DevicePolicyManager.PasswordComplexity 79 private final int mMinComplexity; 80 PasswordHelper(Context context, boolean isPin, @UserIdInt int userId)81 public PasswordHelper(Context context, boolean isPin, @UserIdInt int userId) { 82 mContext = context; 83 mIsPin = isPin; 84 mUserId = userId; 85 mLockPatternUtils = new LockPatternUtils(context); 86 mMinMetrics = mLockPatternUtils.getRequestedPasswordMetrics( 87 mUserId, /* deviceWideOnly= */ false); 88 mMinComplexity = mLockPatternUtils.getRequestedPasswordComplexity( 89 mUserId, /* deviceWideOnly= */ false); 90 } 91 92 @VisibleForTesting PasswordHelper(Context context, boolean isPin, @UserIdInt int userId, LockPatternUtils lockPatternUtils, PasswordMetrics minMetrics, @DevicePolicyManager.PasswordComplexity int minComplexity)93 PasswordHelper(Context context, boolean isPin, @UserIdInt int userId, 94 LockPatternUtils lockPatternUtils, PasswordMetrics minMetrics, 95 @DevicePolicyManager.PasswordComplexity int minComplexity) { 96 mContext = context; 97 mIsPin = isPin; 98 mUserId = userId; 99 mLockPatternUtils = lockPatternUtils; 100 mMinMetrics = minMetrics; 101 mMinComplexity = minComplexity; 102 } 103 104 /** 105 * Validates PIN/Password, updates mValidationErrors, and then returns the validation result. 106 * 107 * @param password password or PIN entered by the user in bytes. 108 * @return The ERROR_CODE if there is any validation error, or NO_ERROR otherwise. 109 */ validateSetupWizard(byte[] password)110 public int validateSetupWizard(byte[] password) { 111 mValidationErrors = PasswordMetrics.validatePassword(/* adminMetrics */ mMinMetrics, 112 /* minComplexity */ mMinComplexity, /* isPin */ mIsPin, /* password */ password); 113 return mValidationErrors.isEmpty() ? NO_ERROR : ERROR_CODE; 114 } 115 116 /** 117 * Validates the PIN/Password/Pattern and return the combined error message associated with the 118 * user input if exists, or return {@code NO_ERROR_MESSAGE} otherwise. 119 * 120 * If the lock type is PASSWORD or PIN, update mValidationErrors. 121 * 122 * @param credentialBytes password/PIN/pattern entered by the user in bytes. 123 * @return The error message to display to users describing all credential validation errors, 124 * where an empty String NO_ERROR_MSG when there is no error. 125 */ validateSetupWizardAndReturnError(@ockTypes int lockType, byte[] credentialBytes)126 public String validateSetupWizardAndReturnError(@LockTypes int lockType, 127 byte[] credentialBytes) { 128 if (lockType == LockTypes.PATTERN) { 129 return validatePatternAndReturnError(credentialBytes); 130 } else if (lockType == LockTypes.PASSWORD || lockType == LockTypes.PIN) { 131 mValidationErrors = PasswordMetrics.validatePassword(/* adminMetrics */ mMinMetrics, 132 /* minComplexity */ mMinComplexity, /* isPin */ lockType == LockTypes.PIN, 133 /* password */ credentialBytes); 134 if (!mValidationErrors.isEmpty()) { 135 List<String> messages = convertErrorCodeToMessages(); 136 if (!messages.isEmpty()) { 137 return getCombinedErrorMessage(messages); 138 } 139 LOG.wtf("A validation error was returned, but no matching error message was found"); 140 return mContext.getString(R.string.lockpassword_invalid_password); 141 } 142 return NO_ERROR_MESSAGE; 143 } else { 144 // the only accepted lock types are PATTERN, PASSWORD, and PIN for validation. 145 LOG.wtf("An unknown lock type was pass in"); 146 return mContext.getString(R.string.locktype_unavailable); 147 } 148 } 149 validatePatternAndReturnError(byte[] pattern)150 private String validatePatternAndReturnError(byte[] pattern) { 151 if (pattern.length < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { 152 return mContext.getString(R.string.lockpattern_recording_incorrect_too_short); 153 } 154 return NO_ERROR_MESSAGE; 155 } 156 157 /** 158 * Combines all error messages into a String displayable to the user. 159 * 160 * @param messages the return value of convertErrorCodeToMessages 161 * @return new line separated String with each new line describing the error 162 */ getCombinedErrorMessage(List<String> messages)163 String getCombinedErrorMessage(List<String> messages) { 164 return String.join("\n", messages); 165 } 166 167 /** 168 * Validates PIN/Password and returns the validation result and updates mValidationErrors 169 * and checks whether the password has been reused. 170 * 171 * @param enteredCredential credential the user typed in. 172 * @param existingCredential existing credential the user previously set. 173 * @return whether password satisfies all the requirements. 174 */ validate(LockscreenCredential enteredCredential, LockscreenCredential existingCredential)175 public boolean validate(LockscreenCredential enteredCredential, 176 LockscreenCredential existingCredential) { 177 byte[] password = enteredCredential.getCredential(); 178 mValidationErrors = 179 PasswordMetrics.validatePassword(mMinMetrics, mMinComplexity, mIsPin, password); 180 if (mValidationErrors.isEmpty() && mLockPatternUtils.checkPasswordHistory( 181 password, getPasswordHistoryHashFactor(existingCredential), mUserId)) { 182 mValidationErrors = 183 Collections.singletonList(new PasswordValidationError(RECENTLY_USED)); 184 } 185 186 return mValidationErrors.isEmpty(); 187 } 188 189 /** 190 * Lazily computes and returns the history hash factor of the user id of the current process 191 * {@code mUserId}, used for password history check. 192 */ getPasswordHistoryHashFactor(LockscreenCredential credential)193 private byte[] getPasswordHistoryHashFactor(LockscreenCredential credential) { 194 if (mPasswordHistoryHashFactor == null) { 195 mPasswordHistoryHashFactor = mLockPatternUtils.getPasswordHistoryHashFactor( 196 credential != null ? credential : LockscreenCredential.createNone(), mUserId); 197 } 198 return mPasswordHistoryHashFactor; 199 } 200 201 /** 202 * Returns an array of messages describing any errors of the last 203 * {@link #validate(LockscreenCredential)} call, important messages come first. 204 */ convertErrorCodeToMessages()205 public List<String> convertErrorCodeToMessages() { 206 List<String> messages = new ArrayList<>(); 207 for (PasswordValidationError error : mValidationErrors) { 208 switch (error.errorCode) { 209 case CONTAINS_INVALID_CHARACTERS: 210 messages.add(mContext.getString(R.string.lockpassword_illegal_character)); 211 break; 212 case NOT_ENOUGH_UPPER_CASE: 213 messages.add(StringUtil.getIcuPluralsString(mContext, error.requirement, 214 R.string.lockpassword_password_requires_uppercase)); 215 break; 216 case NOT_ENOUGH_LOWER_CASE: 217 messages.add(StringUtil.getIcuPluralsString(mContext, error.requirement, 218 R.string.lockpassword_password_requires_lowercase)); 219 break; 220 case NOT_ENOUGH_LETTERS: 221 messages.add(StringUtil.getIcuPluralsString(mContext, error.requirement, 222 R.string.lockpassword_password_requires_letters)); 223 break; 224 case NOT_ENOUGH_DIGITS: 225 messages.add(StringUtil.getIcuPluralsString(mContext, error.requirement, 226 R.string.lockpassword_password_requires_numeric)); 227 break; 228 case NOT_ENOUGH_SYMBOLS: 229 messages.add(StringUtil.getIcuPluralsString(mContext, error.requirement, 230 R.string.lockpassword_password_requires_symbols)); 231 break; 232 case NOT_ENOUGH_NON_LETTER: 233 messages.add(StringUtil.getIcuPluralsString(mContext, error.requirement, 234 R.string.lockpassword_password_requires_nonletter)); 235 break; 236 case NOT_ENOUGH_NON_DIGITS: 237 messages.add(StringUtil.getIcuPluralsString(mContext, error.requirement, 238 R.string.lockpassword_password_requires_nonnumerical)); 239 break; 240 case TOO_SHORT: 241 messages.add(StringUtil.getIcuPluralsString(mContext, error.requirement, 242 mIsPin 243 ? R.string.lockpassword_pin_too_short 244 : R.string.lockpassword_password_too_short)); 245 break; 246 case TOO_LONG: 247 messages.add(StringUtil.getIcuPluralsString(mContext, error.requirement + 1, 248 mIsPin 249 ? R.string.lockpassword_pin_too_long 250 : R.string.lockpassword_password_too_long)); 251 break; 252 case CONTAINS_SEQUENCE: 253 messages.add(mContext.getString( 254 R.string.lockpassword_pin_no_sequential_digits)); 255 break; 256 case RECENTLY_USED: 257 messages.add(mContext.getString(mIsPin 258 ? R.string.lockpassword_pin_recently_used 259 : R.string.lockpassword_password_recently_used)); 260 break; 261 default: 262 LOG.wtf("unknown error validating password: " + error); 263 } 264 } 265 return messages; 266 } 267 268 /** 269 * Zero out credentials and force garbage collection to remove any remnants of user password 270 * shards from memory. Should be used in onDestroy for any LockscreenCredential fields. 271 * 272 * @param credentials the credentials to zero out, can be null 273 **/ zeroizeCredentials(LockscreenCredential... credentials)274 public static void zeroizeCredentials(LockscreenCredential... credentials) { 275 for (LockscreenCredential credential : credentials) { 276 if (credential != null) { 277 credential.zeroize(); 278 } 279 } 280 281 System.gc(); 282 System.runFinalization(); 283 System.gc(); 284 } 285 } 286