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.os.storage.StorageManager; 30 import android.text.TextUtils; 31 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. It can be either an empty password, a pattern 44 * or a password (or PIN). 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 password left in 56 * memory when the credential goes out of scope. This should help mitigate certain class of 57 * attacks where the attcker gains read-only access to full device memory (cold boot attack, 58 * unsecured 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. An empty password 64 // is represented as a byte array of length 0. 65 private byte[] mCredential; 66 67 /** 68 * Private constructor, use static builder methods instead. 69 * 70 * <p> Builder methods should create a private copy of the credential bytes and pass in here. 71 * LockscreenCredential will only store the reference internally without copying. This is to 72 * minimize the number of extra copies introduced. 73 */ LockscreenCredential(int type, byte[] credential)74 private LockscreenCredential(int type, byte[] credential) { 75 Objects.requireNonNull(credential); 76 if (type == CREDENTIAL_TYPE_NONE) { 77 Preconditions.checkArgument(credential.length == 0); 78 } else { 79 // Do not allow constructing a CREDENTIAL_TYPE_PASSWORD_OR_PIN object. 80 Preconditions.checkArgument(type == CREDENTIAL_TYPE_PIN 81 || type == CREDENTIAL_TYPE_PASSWORD 82 || type == CREDENTIAL_TYPE_PATTERN); 83 Preconditions.checkArgument(credential.length > 0); 84 } 85 mType = type; 86 mCredential = credential; 87 } 88 89 /** 90 * Creates a LockscreenCredential object representing empty password. 91 */ createNone()92 public static LockscreenCredential createNone() { 93 return new LockscreenCredential(CREDENTIAL_TYPE_NONE, new byte[0]); 94 } 95 96 /** 97 * Creates a LockscreenCredential object representing the given pattern. 98 */ createPattern(@onNull List<LockPatternView.Cell> pattern)99 public static LockscreenCredential createPattern(@NonNull List<LockPatternView.Cell> pattern) { 100 return new LockscreenCredential(CREDENTIAL_TYPE_PATTERN, 101 LockPatternUtils.patternToByteArray(pattern)); 102 } 103 104 /** 105 * Creates a LockscreenCredential object representing the given alphabetic password. 106 */ createPassword(@onNull CharSequence password)107 public static LockscreenCredential createPassword(@NonNull CharSequence password) { 108 return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD, 109 charSequenceToByteArray(password)); 110 } 111 112 /** 113 * Creates a LockscreenCredential object representing a managed password for profile with 114 * unified challenge. This credentiall will have type {@code CREDENTIAL_TYPE_PASSWORD} for now. 115 * TODO: consider add a new credential type for this. This can then supersede the 116 * isLockTiedToParent argument in various places in LSS. 117 */ createManagedPassword(@onNull byte[] password)118 public static LockscreenCredential createManagedPassword(@NonNull byte[] password) { 119 return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD, 120 Arrays.copyOf(password, password.length)); 121 } 122 123 /** 124 * Creates a LockscreenCredential object representing the given numeric PIN. 125 */ createPin(@onNull CharSequence pin)126 public static LockscreenCredential createPin(@NonNull CharSequence pin) { 127 return new LockscreenCredential(CREDENTIAL_TYPE_PIN, 128 charSequenceToByteArray(pin)); 129 } 130 131 /** 132 * Creates a LockscreenCredential object representing the given alphabetic password. 133 * If the supplied password is empty, create an empty credential object. 134 */ createPasswordOrNone(@ullable CharSequence password)135 public static LockscreenCredential createPasswordOrNone(@Nullable CharSequence password) { 136 if (TextUtils.isEmpty(password)) { 137 return createNone(); 138 } else { 139 return createPassword(password); 140 } 141 } 142 143 /** 144 * Creates a LockscreenCredential object representing the given numeric PIN. 145 * If the supplied password is empty, create an empty credential object. 146 */ createPinOrNone(@ullable CharSequence pin)147 public static LockscreenCredential createPinOrNone(@Nullable CharSequence pin) { 148 if (TextUtils.isEmpty(pin)) { 149 return createNone(); 150 } else { 151 return createPin(pin); 152 } 153 } 154 ensureNotZeroized()155 private void ensureNotZeroized() { 156 Preconditions.checkState(mCredential != null, "Credential is already zeroized"); 157 } 158 /** 159 * Returns the type of this credential. Can be one of {@link #CREDENTIAL_TYPE_NONE}, 160 * {@link #CREDENTIAL_TYPE_PATTERN}, {@link #CREDENTIAL_TYPE_PIN} or 161 * {@link #CREDENTIAL_TYPE_PASSWORD}. 162 */ getType()163 public int getType() { 164 ensureNotZeroized(); 165 return mType; 166 } 167 168 /** 169 * Returns the credential bytes. This is a direct reference of the internal field so 170 * callers should not modify it. 171 * 172 */ getCredential()173 public byte[] getCredential() { 174 ensureNotZeroized(); 175 return mCredential; 176 } 177 178 /** 179 * Returns the credential type recognized by {@link StorageManager}. Can be one of 180 * {@link StorageManager#CRYPT_TYPE_DEFAULT}, {@link StorageManager#CRYPT_TYPE_PATTERN}, 181 * {@link StorageManager#CRYPT_TYPE_PIN} or {@link StorageManager#CRYPT_TYPE_PASSWORD}. 182 */ getStorageCryptType()183 public int getStorageCryptType() { 184 if (isNone()) { 185 return StorageManager.CRYPT_TYPE_DEFAULT; 186 } 187 if (isPattern()) { 188 return StorageManager.CRYPT_TYPE_PATTERN; 189 } 190 if (isPin()) { 191 return StorageManager.CRYPT_TYPE_PIN; 192 } 193 if (isPassword()) { 194 return StorageManager.CRYPT_TYPE_PASSWORD; 195 } 196 throw new IllegalStateException("Unhandled credential type"); 197 } 198 199 /** Returns whether this is an empty credential */ isNone()200 public boolean isNone() { 201 ensureNotZeroized(); 202 return mType == CREDENTIAL_TYPE_NONE; 203 } 204 205 /** Returns whether this is a pattern credential */ isPattern()206 public boolean isPattern() { 207 ensureNotZeroized(); 208 return mType == CREDENTIAL_TYPE_PATTERN; 209 } 210 211 /** Returns whether this is a numeric pin credential */ isPin()212 public boolean isPin() { 213 ensureNotZeroized(); 214 return mType == CREDENTIAL_TYPE_PIN; 215 } 216 217 /** Returns whether this is an alphabetic password credential */ isPassword()218 public boolean isPassword() { 219 ensureNotZeroized(); 220 return mType == CREDENTIAL_TYPE_PASSWORD; 221 } 222 223 /** Returns the length of the credential */ size()224 public int size() { 225 ensureNotZeroized(); 226 return mCredential.length; 227 } 228 229 /** Create a copy of the credential */ duplicate()230 public LockscreenCredential duplicate() { 231 return new LockscreenCredential(mType, 232 mCredential != null ? Arrays.copyOf(mCredential, mCredential.length) : null); 233 } 234 235 /** 236 * Zeroize the credential bytes. 237 */ zeroize()238 public void zeroize() { 239 if (mCredential != null) { 240 Arrays.fill(mCredential, (byte) 0); 241 mCredential = null; 242 } 243 } 244 245 /** 246 * Check if the credential meets minimal length requirement. 247 * 248 * @throws IllegalArgumentException if the credential is too short. 249 */ checkLength()250 public void checkLength() { 251 if (isNone()) { 252 return; 253 } 254 if (isPattern()) { 255 if (size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { 256 throw new IllegalArgumentException("pattern must not be null and at least " 257 + LockPatternUtils.MIN_LOCK_PATTERN_SIZE + " dots long."); 258 } 259 return; 260 } 261 if (isPassword() || isPin()) { 262 if (size() < LockPatternUtils.MIN_LOCK_PASSWORD_SIZE) { 263 throw new IllegalArgumentException("password must not be null and at least " 264 + "of length " + LockPatternUtils.MIN_LOCK_PASSWORD_SIZE); 265 } 266 return; 267 } 268 } 269 270 /** 271 * Check if this credential's type matches one that's retrieved from disk. The nuance here is 272 * that the framework used to not distinguish between PIN and password, so this method will 273 * allow a PIN/Password LockscreenCredential to match against the legacy 274 * {@link #CREDENTIAL_TYPE_PASSWORD_OR_PIN} stored on disk. 275 */ checkAgainstStoredType(int storedCredentialType)276 public boolean checkAgainstStoredType(int storedCredentialType) { 277 if (storedCredentialType == CREDENTIAL_TYPE_PASSWORD_OR_PIN) { 278 return getType() == CREDENTIAL_TYPE_PASSWORD || getType() == CREDENTIAL_TYPE_PIN; 279 } 280 return getType() == storedCredentialType; 281 } 282 283 /** 284 * Hash the password for password history check purpose. 285 */ passwordToHistoryHash(byte[] salt, byte[] hashFactor)286 public String passwordToHistoryHash(byte[] salt, byte[] hashFactor) { 287 return passwordToHistoryHash(mCredential, salt, hashFactor); 288 } 289 290 /** 291 * Hash the password for password history check purpose. 292 */ passwordToHistoryHash( byte[] passwordToHash, byte[] salt, byte[] hashFactor)293 public static String passwordToHistoryHash( 294 byte[] passwordToHash, byte[] salt, byte[] hashFactor) { 295 if (passwordToHash == null || passwordToHash.length == 0 296 || hashFactor == null || salt == null) { 297 return null; 298 } 299 try { 300 MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); 301 sha256.update(hashFactor); 302 byte[] saltedPassword = Arrays.copyOf(passwordToHash, passwordToHash.length 303 + salt.length); 304 System.arraycopy(salt, 0, saltedPassword, passwordToHash.length, salt.length); 305 sha256.update(saltedPassword); 306 Arrays.fill(saltedPassword, (byte) 0); 307 return new String(HexEncoding.encode(sha256.digest())); 308 } catch (NoSuchAlgorithmException e) { 309 throw new AssertionError("Missing digest algorithm: ", e); 310 } 311 } 312 313 /** 314 * Generate a hash for the given password. To avoid brute force attacks, we use a salted hash. 315 * Not the most secure, but it is at least a second level of protection. First level is that 316 * the file is in a location only readable by the system process. 317 * 318 * @return the hash of the pattern in a byte array. 319 */ legacyPasswordToHash(byte[] salt)320 public String legacyPasswordToHash(byte[] salt) { 321 return legacyPasswordToHash(mCredential, salt); 322 } 323 324 /** 325 * Generate a hash for the given password. To avoid brute force attacks, we use a salted hash. 326 * Not the most secure, but it is at least a second level of protection. First level is that 327 * the file is in a location only readable by the system process. 328 * 329 * @param password the gesture pattern. 330 * 331 * @return the hash of the pattern in a byte array. 332 */ legacyPasswordToHash(byte[] password, byte[] salt)333 public static String legacyPasswordToHash(byte[] password, byte[] salt) { 334 if (password == null || password.length == 0 || salt == null) { 335 return null; 336 } 337 338 try { 339 // Previously the password was passed as a String with the following code: 340 // byte[] saltedPassword = (password + salt).getBytes(); 341 // The code below creates the identical digest preimage using byte arrays: 342 byte[] saltedPassword = Arrays.copyOf(password, password.length + salt.length); 343 System.arraycopy(salt, 0, saltedPassword, password.length, salt.length); 344 byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword); 345 byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword); 346 347 byte[] combined = new byte[sha1.length + md5.length]; 348 System.arraycopy(sha1, 0, combined, 0, sha1.length); 349 System.arraycopy(md5, 0, combined, sha1.length, md5.length); 350 351 final char[] hexEncoded = HexEncoding.encode(combined); 352 Arrays.fill(saltedPassword, (byte) 0); 353 return new String(hexEncoded); 354 } catch (NoSuchAlgorithmException e) { 355 throw new AssertionError("Missing digest algorithm: ", e); 356 } 357 } 358 359 @Override writeToParcel(Parcel dest, int flags)360 public void writeToParcel(Parcel dest, int flags) { 361 dest.writeInt(mType); 362 dest.writeByteArray(mCredential); 363 } 364 365 public static final Parcelable.Creator<LockscreenCredential> CREATOR = 366 new Parcelable.Creator<LockscreenCredential>() { 367 368 @Override 369 public LockscreenCredential createFromParcel(Parcel source) { 370 return new LockscreenCredential(source.readInt(), source.createByteArray()); 371 } 372 373 @Override 374 public LockscreenCredential[] newArray(int size) { 375 return new LockscreenCredential[size]; 376 } 377 }; 378 379 @Override describeContents()380 public int describeContents() { 381 return 0; 382 } 383 384 @Override close()385 public void close() { 386 zeroize(); 387 } 388 389 @Override hashCode()390 public int hashCode() { 391 // Effective Java — Always override hashCode when you override equals 392 return (17 + mType) * 31 + mCredential.hashCode(); 393 } 394 395 @Override equals(Object o)396 public boolean equals(Object o) { 397 if (o == this) return true; 398 if (!(o instanceof LockscreenCredential)) return false; 399 final LockscreenCredential other = (LockscreenCredential) o; 400 return mType == other.mType && Arrays.equals(mCredential, other.mCredential); 401 } 402 403 /** 404 * Converts a CharSequence to a byte array without requiring a toString(), which creates an 405 * additional copy. 406 * 407 * @param chars The CharSequence to convert 408 * @return A byte array representing the input 409 */ charSequenceToByteArray(CharSequence chars)410 private static byte[] charSequenceToByteArray(CharSequence chars) { 411 if (chars == null) { 412 return new byte[0]; 413 } 414 byte[] bytes = new byte[chars.length()]; 415 for (int i = 0; i < chars.length(); i++) { 416 bytes[i] = (byte) chars.charAt(i); 417 } 418 return bytes; 419 } 420 } 421