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. 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 /** Returns whether this is an empty credential */ isNone()179 public boolean isNone() { 180 ensureNotZeroized(); 181 return mType == CREDENTIAL_TYPE_NONE; 182 } 183 184 /** Returns whether this is a pattern credential */ isPattern()185 public boolean isPattern() { 186 ensureNotZeroized(); 187 return mType == CREDENTIAL_TYPE_PATTERN; 188 } 189 190 /** Returns whether this is a numeric pin credential */ isPin()191 public boolean isPin() { 192 ensureNotZeroized(); 193 return mType == CREDENTIAL_TYPE_PIN; 194 } 195 196 /** Returns whether this is an alphabetic password credential */ isPassword()197 public boolean isPassword() { 198 ensureNotZeroized(); 199 return mType == CREDENTIAL_TYPE_PASSWORD; 200 } 201 202 /** Returns the length of the credential */ size()203 public int size() { 204 ensureNotZeroized(); 205 return mCredential.length; 206 } 207 208 /** Create a copy of the credential */ duplicate()209 public LockscreenCredential duplicate() { 210 return new LockscreenCredential(mType, 211 mCredential != null ? Arrays.copyOf(mCredential, mCredential.length) : null); 212 } 213 214 /** 215 * Zeroize the credential bytes. 216 */ zeroize()217 public void zeroize() { 218 if (mCredential != null) { 219 Arrays.fill(mCredential, (byte) 0); 220 mCredential = null; 221 } 222 } 223 224 /** 225 * Check if the credential meets minimal length requirement. 226 * 227 * @throws IllegalArgumentException if the credential is too short. 228 */ checkLength()229 public void checkLength() { 230 if (isNone()) { 231 return; 232 } 233 if (isPattern()) { 234 if (size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { 235 throw new IllegalArgumentException("pattern must not be null and at least " 236 + LockPatternUtils.MIN_LOCK_PATTERN_SIZE + " dots long."); 237 } 238 return; 239 } 240 if (isPassword() || isPin()) { 241 if (size() < LockPatternUtils.MIN_LOCK_PASSWORD_SIZE) { 242 throw new IllegalArgumentException("password must not be null and at least " 243 + "of length " + LockPatternUtils.MIN_LOCK_PASSWORD_SIZE); 244 } 245 return; 246 } 247 } 248 249 /** 250 * Check if this credential's type matches one that's retrieved from disk. The nuance here is 251 * that the framework used to not distinguish between PIN and password, so this method will 252 * allow a PIN/Password LockscreenCredential to match against the legacy 253 * {@link #CREDENTIAL_TYPE_PASSWORD_OR_PIN} stored on disk. 254 */ checkAgainstStoredType(int storedCredentialType)255 public boolean checkAgainstStoredType(int storedCredentialType) { 256 if (storedCredentialType == CREDENTIAL_TYPE_PASSWORD_OR_PIN) { 257 return getType() == CREDENTIAL_TYPE_PASSWORD || getType() == CREDENTIAL_TYPE_PIN; 258 } 259 return getType() == storedCredentialType; 260 } 261 262 /** 263 * Hash the password for password history check purpose. 264 */ passwordToHistoryHash(byte[] salt, byte[] hashFactor)265 public String passwordToHistoryHash(byte[] salt, byte[] hashFactor) { 266 return passwordToHistoryHash(mCredential, salt, hashFactor); 267 } 268 269 /** 270 * Hash the password for password history check purpose. 271 */ passwordToHistoryHash( byte[] passwordToHash, byte[] salt, byte[] hashFactor)272 public static String passwordToHistoryHash( 273 byte[] passwordToHash, byte[] salt, byte[] hashFactor) { 274 if (passwordToHash == null || passwordToHash.length == 0 275 || hashFactor == null || salt == null) { 276 return null; 277 } 278 try { 279 MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); 280 sha256.update(hashFactor); 281 sha256.update(passwordToHash); 282 sha256.update(salt); 283 return HexEncoding.encodeToString(sha256.digest()); 284 } catch (NoSuchAlgorithmException e) { 285 throw new AssertionError("Missing digest algorithm: ", e); 286 } 287 } 288 289 /** 290 * Hash the given password for the password history, using the legacy algorithm. 291 * 292 * @deprecated This algorithm is insecure because the password can be easily bruteforced, given 293 * the hash and salt. Use {@link #passwordToHistoryHash(byte[], byte[], byte[])} 294 * instead, which incorporates an SP-derived secret into the hash. 295 * 296 * @return the legacy password hash 297 */ 298 @Deprecated legacyPasswordToHash(byte[] password, byte[] salt)299 public static String legacyPasswordToHash(byte[] password, byte[] salt) { 300 if (password == null || password.length == 0 || salt == null) { 301 return null; 302 } 303 304 try { 305 byte[] saltedPassword = ArrayUtils.concat(password, salt); 306 byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword); 307 byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword); 308 309 Arrays.fill(saltedPassword, (byte) 0); 310 return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5)); 311 } catch (NoSuchAlgorithmException e) { 312 throw new AssertionError("Missing digest algorithm: ", e); 313 } 314 } 315 316 @Override writeToParcel(Parcel dest, int flags)317 public void writeToParcel(Parcel dest, int flags) { 318 dest.writeInt(mType); 319 dest.writeByteArray(mCredential); 320 } 321 322 public static final Parcelable.Creator<LockscreenCredential> CREATOR = 323 new Parcelable.Creator<LockscreenCredential>() { 324 325 @Override 326 public LockscreenCredential createFromParcel(Parcel source) { 327 return new LockscreenCredential(source.readInt(), source.createByteArray()); 328 } 329 330 @Override 331 public LockscreenCredential[] newArray(int size) { 332 return new LockscreenCredential[size]; 333 } 334 }; 335 336 @Override describeContents()337 public int describeContents() { 338 return 0; 339 } 340 341 @Override close()342 public void close() { 343 zeroize(); 344 } 345 346 @Override hashCode()347 public int hashCode() { 348 // Effective Java — Always override hashCode when you override equals 349 return Objects.hash(mType, Arrays.hashCode(mCredential)); 350 } 351 352 @Override equals(Object o)353 public boolean equals(Object o) { 354 if (o == this) return true; 355 if (!(o instanceof LockscreenCredential)) return false; 356 final LockscreenCredential other = (LockscreenCredential) o; 357 return mType == other.mType && Arrays.equals(mCredential, other.mCredential); 358 } 359 360 /** 361 * Converts a CharSequence to a byte array without requiring a toString(), which creates an 362 * additional copy. 363 * 364 * @param chars The CharSequence to convert 365 * @return A byte array representing the input 366 */ charSequenceToByteArray(CharSequence chars)367 private static byte[] charSequenceToByteArray(CharSequence chars) { 368 if (chars == null) { 369 return new byte[0]; 370 } 371 byte[] bytes = new byte[chars.length()]; 372 for (int i = 0; i < chars.length(); i++) { 373 bytes[i] = (byte) chars.charAt(i); 374 } 375 return bytes; 376 } 377 } 378