1 /* 2 * Copyright (C) 2019 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.internal.widget; 18 19 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE; 20 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; 21 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN; 22 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN; 23 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN; 24 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.os.Parcel; 28 import android.os.Parcelable; 29 import android.text.TextUtils; 30 31 import com.android.internal.util.ArrayUtils; 32 import com.android.internal.util.Preconditions; 33 34 import libcore.util.HexEncoding; 35 36 import java.security.MessageDigest; 37 import java.security.NoSuchAlgorithmException; 38 import java.util.Arrays; 39 import java.util.List; 40 import java.util.Objects; 41 42 /** 43 * A class representing a lockscreen credential, also called a Lock Screen Knowledge Factor (LSKF). 44 * It can be a PIN, pattern, password, or none (a.k.a. empty). 45 * 46 * <p> As required by some security certification, the framework tries its best to 47 * remove copies of the lockscreen credential bytes from memory. In this regard, this class 48 * abuses the {@link AutoCloseable} interface for sanitizing memory. This 49 * presents a nice syntax to auto-zeroize memory with the try-with-resource statement: 50 * <pre> 51 * try {LockscreenCredential credential = LockscreenCredential.createPassword(...) { 52 * // Process the credential in some way 53 * } 54 * </pre> 55 * With this construct, we can guarantee that there will be no copies of the credential left in 56 * memory when the object goes out of scope. This should help mitigate certain class of attacks 57 * where the attacker gains read-only access to full device memory (cold boot attack, unsecured 58 * software/hardware memory dumping interfaces such as JTAG). 59 */ 60 public class LockscreenCredential implements Parcelable, AutoCloseable { 61 62 private final int mType; 63 // Stores raw credential bytes, or null if credential has been zeroized. A none credential 64 // is represented as a byte array of length 0. 65 private byte[] mCredential; 66 67 // This indicates that the credential used characters outside ASCII 32–127. 68 // 69 // Such credentials were never intended to be allowed. However, Android 10–14 had a bug where 70 // conversion from the chars the user entered to the credential bytes used a simple truncation. 71 // Thus, any 'char' whose remainder mod 256 was in the range 32–127 was accepted and was 72 // equivalent to some ASCII character. For example, ™, which is U+2122, was truncated to ASCII 73 // 0x22 which is the double-quote character ". 74 // 75 // We have to continue to allow a LockscreenCredential to be constructed with this bug, so that 76 // existing devices can be unlocked if their password used this bug. However, we prevent new 77 // passwords that use this bug from being set. The boolean below keeps track of the information 78 // needed to do that check, since the conversion to mCredential may have been lossy. 79 private final boolean mHasInvalidChars; 80 81 /** 82 * Private constructor, use static builder methods instead. 83 * 84 * <p> Builder methods should create a private copy of the credential bytes using a non-movable 85 * array and pass it in here. LockscreenCredential will only store the reference internally 86 * without copying. This is to minimize the number of extra copies introduced. 87 */ LockscreenCredential(int type, byte[] credential, boolean hasInvalidChars)88 private LockscreenCredential(int type, byte[] credential, boolean hasInvalidChars) { 89 Objects.requireNonNull(credential); 90 if (type == CREDENTIAL_TYPE_NONE) { 91 Preconditions.checkArgument(credential.length == 0); 92 } else { 93 // Do not allow constructing a CREDENTIAL_TYPE_PASSWORD_OR_PIN object. 94 Preconditions.checkArgument(type == CREDENTIAL_TYPE_PIN 95 || type == CREDENTIAL_TYPE_PASSWORD 96 || type == CREDENTIAL_TYPE_PATTERN); 97 // Do not validate credential.length yet. All non-none credentials have a minimum 98 // length requirement; however, one of the uses of LockscreenCredential is to represent 99 // a proposed credential that might be too short. For example, a LockscreenCredential 100 // with type CREDENTIAL_TYPE_PIN and length 0 represents an attempt to set an empty PIN. 101 // This differs from an actual attempt to set a none credential. We have to allow the 102 // LockscreenCredential object to be constructed so that the validation logic can run, 103 // even though the validation logic will ultimately reject the credential as too short. 104 } 105 mType = type; 106 mCredential = credential; 107 mHasInvalidChars = hasInvalidChars; 108 } 109 LockscreenCredential(int type, CharSequence credential)110 private LockscreenCredential(int type, CharSequence credential) { 111 this(type, charsToBytesTruncating(credential), hasInvalidChars(credential)); 112 } 113 114 /** 115 * Creates a LockscreenCredential object representing a none credential. 116 */ createNone()117 public static LockscreenCredential createNone() { 118 return new LockscreenCredential(CREDENTIAL_TYPE_NONE, new byte[0], false); 119 } 120 121 /** 122 * Creates a LockscreenCredential object representing the given pattern. 123 */ createPattern(@onNull List<LockPatternView.Cell> pattern)124 public static LockscreenCredential createPattern(@NonNull List<LockPatternView.Cell> pattern) { 125 return new LockscreenCredential(CREDENTIAL_TYPE_PATTERN, 126 LockPatternUtils.patternToByteArray(pattern), /* hasInvalidChars= */ false); 127 } 128 129 /** 130 * Creates a LockscreenCredential object representing the given alphabetic password. 131 */ createPassword(@onNull CharSequence password)132 public static LockscreenCredential createPassword(@NonNull CharSequence password) { 133 return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD, password); 134 } 135 136 /** 137 * Creates a LockscreenCredential object representing the system-generated, system-managed 138 * password for a profile with unified challenge. This credential has type {@code 139 * CREDENTIAL_TYPE_PASSWORD} for now. TODO: consider add a new credential type for this. This 140 * can then supersede the isLockTiedToParent argument in various places in LSS. 141 */ createUnifiedProfilePassword(@onNull byte[] password)142 public static LockscreenCredential createUnifiedProfilePassword(@NonNull byte[] password) { 143 return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD, 144 copyOfArrayNonMovable(password), /* hasInvalidChars= */ false); 145 } 146 147 /** 148 * Creates a LockscreenCredential object representing the given numeric PIN. 149 */ createPin(@onNull CharSequence pin)150 public static LockscreenCredential createPin(@NonNull CharSequence pin) { 151 return new LockscreenCredential(CREDENTIAL_TYPE_PIN, pin); 152 } 153 154 /** 155 * Creates a LockscreenCredential object representing the given alphabetic password. 156 * If the supplied password is empty, create a none credential object. 157 */ createPasswordOrNone(@ullable CharSequence password)158 public static LockscreenCredential createPasswordOrNone(@Nullable CharSequence password) { 159 if (TextUtils.isEmpty(password)) { 160 return createNone(); 161 } else { 162 return createPassword(password); 163 } 164 } 165 166 /** 167 * Creates a LockscreenCredential object representing the given numeric PIN. 168 * If the supplied password is empty, create a none credential object. 169 */ createPinOrNone(@ullable CharSequence pin)170 public static LockscreenCredential createPinOrNone(@Nullable CharSequence pin) { 171 if (TextUtils.isEmpty(pin)) { 172 return createNone(); 173 } else { 174 return createPin(pin); 175 } 176 } 177 ensureNotZeroized()178 private void ensureNotZeroized() { 179 Preconditions.checkState(mCredential != null, "Credential is already zeroized"); 180 } 181 /** 182 * Returns the type of this credential. Can be one of {@link #CREDENTIAL_TYPE_NONE}, 183 * {@link #CREDENTIAL_TYPE_PATTERN}, {@link #CREDENTIAL_TYPE_PIN} or 184 * {@link #CREDENTIAL_TYPE_PASSWORD}. 185 */ getType()186 public int getType() { 187 ensureNotZeroized(); 188 return mType; 189 } 190 191 /** 192 * Returns the credential bytes. This is a direct reference of the internal field so 193 * callers should not modify it. 194 * 195 */ getCredential()196 public byte[] getCredential() { 197 ensureNotZeroized(); 198 return mCredential; 199 } 200 201 /** Returns whether this is a none credential */ isNone()202 public boolean isNone() { 203 ensureNotZeroized(); 204 return mType == CREDENTIAL_TYPE_NONE; 205 } 206 207 /** Returns whether this is a pattern credential */ isPattern()208 public boolean isPattern() { 209 ensureNotZeroized(); 210 return mType == CREDENTIAL_TYPE_PATTERN; 211 } 212 213 /** Returns whether this is a numeric pin credential */ isPin()214 public boolean isPin() { 215 ensureNotZeroized(); 216 return mType == CREDENTIAL_TYPE_PIN; 217 } 218 219 /** Returns whether this is an alphabetic password credential */ isPassword()220 public boolean isPassword() { 221 ensureNotZeroized(); 222 return mType == CREDENTIAL_TYPE_PASSWORD; 223 } 224 225 /** Returns the length of the credential */ size()226 public int size() { 227 ensureNotZeroized(); 228 return mCredential.length; 229 } 230 231 /** Returns true if this credential was constructed with any chars outside the allowed range */ hasInvalidChars()232 public boolean hasInvalidChars() { 233 ensureNotZeroized(); 234 return mHasInvalidChars; 235 } 236 237 /** Create a copy of the credential */ duplicate()238 public LockscreenCredential duplicate() { 239 return new LockscreenCredential(mType, 240 mCredential != null ? copyOfArrayNonMovable(mCredential) : null, 241 mHasInvalidChars); 242 } 243 244 /** 245 * Zeroize the credential bytes. 246 */ zeroize()247 public void zeroize() { 248 if (mCredential != null) { 249 LockPatternUtils.zeroize(mCredential); 250 mCredential = null; 251 } 252 } 253 254 /** 255 * Copies the given array into a new non-movable array. 256 */ copyOfArrayNonMovable(byte[] array)257 private static byte[] copyOfArrayNonMovable(byte[] array) { 258 byte[] copy = LockPatternUtils.newNonMovableByteArray(array.length); 259 System.arraycopy(array, 0, copy, 0, array.length); 260 return copy; 261 } 262 263 /** 264 * Checks whether the credential meets basic requirements for setting it as a new credential. 265 * 266 * This is redundant if {@link android.app.admin.PasswordMetrics#validateCredential()}, which 267 * does more comprehensive checks, is correctly called first (which it should be). 268 * 269 * @throws IllegalArgumentException if the credential contains invalid characters or is too 270 * short 271 */ validateBasicRequirements()272 public void validateBasicRequirements() { 273 if (mHasInvalidChars) { 274 throw new IllegalArgumentException("credential contains invalid characters"); 275 } 276 switch (getType()) { 277 case CREDENTIAL_TYPE_PATTERN: 278 if (size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { 279 throw new IllegalArgumentException("pattern must be at least " 280 + LockPatternUtils.MIN_LOCK_PATTERN_SIZE + " dots long."); 281 } 282 break; 283 case CREDENTIAL_TYPE_PIN: 284 if (size() < LockPatternUtils.MIN_LOCK_PASSWORD_SIZE) { 285 throw new IllegalArgumentException("PIN must be at least " 286 + LockPatternUtils.MIN_LOCK_PASSWORD_SIZE + " digits long."); 287 } 288 break; 289 case CREDENTIAL_TYPE_PASSWORD: 290 if (size() < LockPatternUtils.MIN_LOCK_PASSWORD_SIZE) { 291 throw new IllegalArgumentException("password must be at least " 292 + LockPatternUtils.MIN_LOCK_PASSWORD_SIZE + " characters long."); 293 } 294 break; 295 } 296 } 297 298 /** 299 * Check if this credential's type matches one that's retrieved from disk. The nuance here is 300 * that the framework used to not distinguish between PIN and password, so this method will 301 * allow a PIN/Password LockscreenCredential to match against the legacy 302 * {@link #CREDENTIAL_TYPE_PASSWORD_OR_PIN} stored on disk. 303 */ checkAgainstStoredType(int storedCredentialType)304 public boolean checkAgainstStoredType(int storedCredentialType) { 305 if (storedCredentialType == CREDENTIAL_TYPE_PASSWORD_OR_PIN) { 306 return getType() == CREDENTIAL_TYPE_PASSWORD || getType() == CREDENTIAL_TYPE_PIN; 307 } 308 return getType() == storedCredentialType; 309 } 310 311 /** 312 * Hash the password for password history check purpose. 313 */ passwordToHistoryHash(byte[] salt, byte[] hashFactor)314 public String passwordToHistoryHash(byte[] salt, byte[] hashFactor) { 315 return passwordToHistoryHash(mCredential, salt, hashFactor); 316 } 317 318 /** 319 * Hash the password for password history check purpose. 320 */ passwordToHistoryHash( byte[] passwordToHash, byte[] salt, byte[] hashFactor)321 public static String passwordToHistoryHash( 322 byte[] passwordToHash, byte[] salt, byte[] hashFactor) { 323 if (passwordToHash == null || passwordToHash.length == 0 324 || hashFactor == null || salt == null) { 325 return null; 326 } 327 try { 328 MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); 329 sha256.update(hashFactor); 330 sha256.update(passwordToHash); 331 sha256.update(salt); 332 return HexEncoding.encodeToString(sha256.digest()); 333 } catch (NoSuchAlgorithmException e) { 334 throw new AssertionError("Missing digest algorithm: ", e); 335 } 336 } 337 338 /** 339 * Hash the given password for the password history, using the legacy algorithm. 340 * 341 * @deprecated This algorithm is insecure because the password can be easily bruteforced, given 342 * the hash and salt. Use {@link #passwordToHistoryHash(byte[], byte[], byte[])} 343 * instead, which incorporates an SP-derived secret into the hash. 344 * 345 * @return the legacy password hash 346 */ 347 @Deprecated legacyPasswordToHash(byte[] password, byte[] salt)348 public static String legacyPasswordToHash(byte[] password, byte[] salt) { 349 if (password == null || password.length == 0 || salt == null) { 350 return null; 351 } 352 353 try { 354 byte[] saltedPassword = ArrayUtils.concat(password, salt); 355 byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword); 356 byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword); 357 358 LockPatternUtils.zeroize(saltedPassword); 359 return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5)); 360 } catch (NoSuchAlgorithmException e) { 361 throw new AssertionError("Missing digest algorithm: ", e); 362 } 363 } 364 365 @Override writeToParcel(Parcel dest, int flags)366 public void writeToParcel(Parcel dest, int flags) { 367 dest.writeInt(mType); 368 dest.writeByteArray(mCredential); 369 dest.writeBoolean(mHasInvalidChars); 370 } 371 372 public static final Parcelable.Creator<LockscreenCredential> CREATOR = 373 new Parcelable.Creator<LockscreenCredential>() { 374 375 @Override 376 public LockscreenCredential createFromParcel(Parcel source) { 377 return new LockscreenCredential(source.readInt(), source.createByteArray(), 378 source.readBoolean()); 379 } 380 381 @Override 382 public LockscreenCredential[] newArray(int size) { 383 return new LockscreenCredential[size]; 384 } 385 }; 386 387 @Override describeContents()388 public int describeContents() { 389 return 0; 390 } 391 392 @Override close()393 public void close() { 394 zeroize(); 395 } 396 397 @Override finalize()398 public void finalize() { 399 zeroize(); 400 } 401 402 @Override hashCode()403 public int hashCode() { 404 // Effective Java — Always override hashCode when you override equals 405 return Objects.hash(mType, Arrays.hashCode(mCredential), mHasInvalidChars); 406 } 407 408 @Override equals(Object o)409 public boolean equals(Object o) { 410 if (o == this) return true; 411 if (!(o instanceof LockscreenCredential)) return false; 412 final LockscreenCredential other = (LockscreenCredential) o; 413 return mType == other.mType && Arrays.equals(mCredential, other.mCredential) 414 && mHasInvalidChars == other.mHasInvalidChars; 415 } 416 hasInvalidChars(CharSequence chars)417 private static boolean hasInvalidChars(CharSequence chars) { 418 // 419 // Consider the password to have invalid characters if it contains any non-ASCII characters 420 // or control characters. There are multiple reasons for this restriction: 421 // 422 // - Non-ASCII characters might only be possible to enter on a third-party keyboard app 423 // (IME) that is available when setting the password but not when verifying it after a 424 // reboot. This can happen if the keyboard is not direct boot aware or gets uninstalled. 425 // 426 // - Unicode strings that look identical to the user can map to different byte[]. Yet, only 427 // one byte[] can be accepted. Unicode normalization can solve this problem to some 428 // extent, but still many Unicode characters look similar and could cause confusion. 429 // 430 // - For backwards compatibility reasons, the upper 8 bits of the 16-bit 'chars' are 431 // discarded by charsToBytesTruncating(). Thus, as-is passwords with characters above 432 // U+00FF (255) are not as secure as they should be. IMPORTANT: Do not change the below 433 // code to allow characters above U+00FF (255) without fixing this issue! 434 // 435 for (int i = 0; i < chars.length(); i++) { 436 char c = chars.charAt(i); 437 if (c < 32 || c > 127) { 438 return true; 439 } 440 } 441 return false; 442 } 443 444 /** 445 * Converts a CharSequence to a byte array, intentionally truncating chars greater than 255 for 446 * backwards compatibility reasons. See {@link #mHasInvalidChars}. 447 * 448 * @param chars The CharSequence to convert 449 * @return A byte array representing the input 450 */ charsToBytesTruncating(CharSequence chars)451 private static byte[] charsToBytesTruncating(CharSequence chars) { 452 byte[] bytes = LockPatternUtils.newNonMovableByteArray(chars.length()); 453 for (int i = 0; i < chars.length(); i++) { 454 bytes[i] = (byte) chars.charAt(i); 455 } 456 return bytes; 457 } 458 } 459