/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; import libcore.util.HexEncoding; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.List; import java.util.Objects; /** * A class representing a lockscreen credential, also called a Lock Screen Knowledge Factor (LSKF). * It can be a PIN, pattern, password, or none (a.k.a. empty). * *

As required by some security certification, the framework tries its best to * remove copies of the lockscreen credential bytes from memory. In this regard, this class * abuses the {@link AutoCloseable} interface for sanitizing memory. This * presents a nice syntax to auto-zeroize memory with the try-with-resource statement: *

 * try {LockscreenCredential credential = LockscreenCredential.createPassword(...) {
 *     // Process the credential in some way
 * }
 * 
* With this construct, we can guarantee that there will be no copies of the credential left in * memory when the object goes out of scope. This should help mitigate certain class of attacks * where the attacker gains read-only access to full device memory (cold boot attack, unsecured * software/hardware memory dumping interfaces such as JTAG). */ public class LockscreenCredential implements Parcelable, AutoCloseable { private final int mType; // Stores raw credential bytes, or null if credential has been zeroized. A none credential // is represented as a byte array of length 0. private byte[] mCredential; // This indicates that the credential used characters outside ASCII 32–127. // // Such credentials were never intended to be allowed. However, Android 10–14 had a bug where // conversion from the chars the user entered to the credential bytes used a simple truncation. // Thus, any 'char' whose remainder mod 256 was in the range 32–127 was accepted and was // equivalent to some ASCII character. For example, ™, which is U+2122, was truncated to ASCII // 0x22 which is the double-quote character ". // // We have to continue to allow a LockscreenCredential to be constructed with this bug, so that // existing devices can be unlocked if their password used this bug. However, we prevent new // passwords that use this bug from being set. The boolean below keeps track of the information // needed to do that check, since the conversion to mCredential may have been lossy. private final boolean mHasInvalidChars; /** * Private constructor, use static builder methods instead. * *

Builder methods should create a private copy of the credential bytes and pass in here. * LockscreenCredential will only store the reference internally without copying. This is to * minimize the number of extra copies introduced. */ private LockscreenCredential(int type, byte[] credential, boolean hasInvalidChars) { Objects.requireNonNull(credential); if (type == CREDENTIAL_TYPE_NONE) { Preconditions.checkArgument(credential.length == 0); } else { // Do not allow constructing a CREDENTIAL_TYPE_PASSWORD_OR_PIN object. Preconditions.checkArgument(type == CREDENTIAL_TYPE_PIN || type == CREDENTIAL_TYPE_PASSWORD || type == CREDENTIAL_TYPE_PATTERN); // Do not validate credential.length yet. All non-none credentials have a minimum // length requirement; however, one of the uses of LockscreenCredential is to represent // a proposed credential that might be too short. For example, a LockscreenCredential // with type CREDENTIAL_TYPE_PIN and length 0 represents an attempt to set an empty PIN. // This differs from an actual attempt to set a none credential. We have to allow the // LockscreenCredential object to be constructed so that the validation logic can run, // even though the validation logic will ultimately reject the credential as too short. } mType = type; mCredential = credential; mHasInvalidChars = hasInvalidChars; } private LockscreenCredential(int type, CharSequence credential) { this(type, charsToBytesTruncating(credential), hasInvalidChars(credential)); } /** * Creates a LockscreenCredential object representing a none credential. */ public static LockscreenCredential createNone() { return new LockscreenCredential(CREDENTIAL_TYPE_NONE, new byte[0], false); } /** * Creates a LockscreenCredential object representing the given pattern. */ public static LockscreenCredential createPattern(@NonNull List pattern) { return new LockscreenCredential(CREDENTIAL_TYPE_PATTERN, LockPatternUtils.patternToByteArray(pattern), /* hasInvalidChars= */ false); } /** * Creates a LockscreenCredential object representing the given alphabetic password. */ public static LockscreenCredential createPassword(@NonNull CharSequence password) { return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD, password); } /** * Creates a LockscreenCredential object representing the system-generated, system-managed * password for a profile with unified challenge. This credential has type {@code * CREDENTIAL_TYPE_PASSWORD} for now. TODO: consider add a new credential type for this. This * can then supersede the isLockTiedToParent argument in various places in LSS. */ public static LockscreenCredential createUnifiedProfilePassword(@NonNull byte[] password) { return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD, Arrays.copyOf(password, password.length), /* hasInvalidChars= */ false); } /** * Creates a LockscreenCredential object representing the given numeric PIN. */ public static LockscreenCredential createPin(@NonNull CharSequence pin) { return new LockscreenCredential(CREDENTIAL_TYPE_PIN, pin); } /** * Creates a LockscreenCredential object representing the given alphabetic password. * If the supplied password is empty, create a none credential object. */ public static LockscreenCredential createPasswordOrNone(@Nullable CharSequence password) { if (TextUtils.isEmpty(password)) { return createNone(); } else { return createPassword(password); } } /** * Creates a LockscreenCredential object representing the given numeric PIN. * If the supplied password is empty, create a none credential object. */ public static LockscreenCredential createPinOrNone(@Nullable CharSequence pin) { if (TextUtils.isEmpty(pin)) { return createNone(); } else { return createPin(pin); } } private void ensureNotZeroized() { Preconditions.checkState(mCredential != null, "Credential is already zeroized"); } /** * Returns the type of this credential. Can be one of {@link #CREDENTIAL_TYPE_NONE}, * {@link #CREDENTIAL_TYPE_PATTERN}, {@link #CREDENTIAL_TYPE_PIN} or * {@link #CREDENTIAL_TYPE_PASSWORD}. */ public int getType() { ensureNotZeroized(); return mType; } /** * Returns the credential bytes. This is a direct reference of the internal field so * callers should not modify it. * */ public byte[] getCredential() { ensureNotZeroized(); return mCredential; } /** Returns whether this is a none credential */ public boolean isNone() { ensureNotZeroized(); return mType == CREDENTIAL_TYPE_NONE; } /** Returns whether this is a pattern credential */ public boolean isPattern() { ensureNotZeroized(); return mType == CREDENTIAL_TYPE_PATTERN; } /** Returns whether this is a numeric pin credential */ public boolean isPin() { ensureNotZeroized(); return mType == CREDENTIAL_TYPE_PIN; } /** Returns whether this is an alphabetic password credential */ public boolean isPassword() { ensureNotZeroized(); return mType == CREDENTIAL_TYPE_PASSWORD; } /** Returns the length of the credential */ public int size() { ensureNotZeroized(); return mCredential.length; } /** Returns true if this credential was constructed with any chars outside the allowed range */ public boolean hasInvalidChars() { ensureNotZeroized(); return mHasInvalidChars; } /** Create a copy of the credential */ public LockscreenCredential duplicate() { return new LockscreenCredential(mType, mCredential != null ? Arrays.copyOf(mCredential, mCredential.length) : null, mHasInvalidChars); } /** * Zeroize the credential bytes. */ public void zeroize() { if (mCredential != null) { Arrays.fill(mCredential, (byte) 0); mCredential = null; } } /** * Checks whether the credential meets basic requirements for setting it as a new credential. * * This is redundant if {@link android.app.admin.PasswordMetrics#validateCredential()}, which * does more comprehensive checks, is correctly called first (which it should be). * * @throws IllegalArgumentException if the credential contains invalid characters or is too * short */ public void validateBasicRequirements() { if (mHasInvalidChars) { throw new IllegalArgumentException("credential contains invalid characters"); } switch (getType()) { case CREDENTIAL_TYPE_PATTERN: if (size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { throw new IllegalArgumentException("pattern must be at least " + LockPatternUtils.MIN_LOCK_PATTERN_SIZE + " dots long."); } break; case CREDENTIAL_TYPE_PIN: if (size() < LockPatternUtils.MIN_LOCK_PASSWORD_SIZE) { throw new IllegalArgumentException("PIN must be at least " + LockPatternUtils.MIN_LOCK_PASSWORD_SIZE + " digits long."); } break; case CREDENTIAL_TYPE_PASSWORD: if (size() < LockPatternUtils.MIN_LOCK_PASSWORD_SIZE) { throw new IllegalArgumentException("password must be at least " + LockPatternUtils.MIN_LOCK_PASSWORD_SIZE + " characters long."); } break; } } /** * Check if this credential's type matches one that's retrieved from disk. The nuance here is * that the framework used to not distinguish between PIN and password, so this method will * allow a PIN/Password LockscreenCredential to match against the legacy * {@link #CREDENTIAL_TYPE_PASSWORD_OR_PIN} stored on disk. */ public boolean checkAgainstStoredType(int storedCredentialType) { if (storedCredentialType == CREDENTIAL_TYPE_PASSWORD_OR_PIN) { return getType() == CREDENTIAL_TYPE_PASSWORD || getType() == CREDENTIAL_TYPE_PIN; } return getType() == storedCredentialType; } /** * Hash the password for password history check purpose. */ public String passwordToHistoryHash(byte[] salt, byte[] hashFactor) { return passwordToHistoryHash(mCredential, salt, hashFactor); } /** * Hash the password for password history check purpose. */ public static String passwordToHistoryHash( byte[] passwordToHash, byte[] salt, byte[] hashFactor) { if (passwordToHash == null || passwordToHash.length == 0 || hashFactor == null || salt == null) { return null; } try { MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); sha256.update(hashFactor); sha256.update(passwordToHash); sha256.update(salt); return HexEncoding.encodeToString(sha256.digest()); } catch (NoSuchAlgorithmException e) { throw new AssertionError("Missing digest algorithm: ", e); } } /** * Hash the given password for the password history, using the legacy algorithm. * * @deprecated This algorithm is insecure because the password can be easily bruteforced, given * the hash and salt. Use {@link #passwordToHistoryHash(byte[], byte[], byte[])} * instead, which incorporates an SP-derived secret into the hash. * * @return the legacy password hash */ @Deprecated public static String legacyPasswordToHash(byte[] password, byte[] salt) { if (password == null || password.length == 0 || salt == null) { return null; } try { byte[] saltedPassword = ArrayUtils.concat(password, salt); byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword); byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword); Arrays.fill(saltedPassword, (byte) 0); return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5)); } catch (NoSuchAlgorithmException e) { throw new AssertionError("Missing digest algorithm: ", e); } } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mType); dest.writeByteArray(mCredential); dest.writeBoolean(mHasInvalidChars); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public LockscreenCredential createFromParcel(Parcel source) { return new LockscreenCredential(source.readInt(), source.createByteArray(), source.readBoolean()); } @Override public LockscreenCredential[] newArray(int size) { return new LockscreenCredential[size]; } }; @Override public int describeContents() { return 0; } @Override public void close() { zeroize(); } @Override public void finalize() { zeroize(); } @Override public int hashCode() { // Effective Java — Always override hashCode when you override equals return Objects.hash(mType, Arrays.hashCode(mCredential), mHasInvalidChars); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof LockscreenCredential)) return false; final LockscreenCredential other = (LockscreenCredential) o; return mType == other.mType && Arrays.equals(mCredential, other.mCredential) && mHasInvalidChars == other.mHasInvalidChars; } private static boolean hasInvalidChars(CharSequence chars) { // // Consider the password to have invalid characters if it contains any non-ASCII characters // or control characters. There are multiple reasons for this restriction: // // - Non-ASCII characters might only be possible to enter on a third-party keyboard app // (IME) that is available when setting the password but not when verifying it after a // reboot. This can happen if the keyboard is not direct boot aware or gets uninstalled. // // - Unicode strings that look identical to the user can map to different byte[]. Yet, only // one byte[] can be accepted. Unicode normalization can solve this problem to some // extent, but still many Unicode characters look similar and could cause confusion. // // - For backwards compatibility reasons, the upper 8 bits of the 16-bit 'chars' are // discarded by charsToBytesTruncating(). Thus, as-is passwords with characters above // U+00FF (255) are not as secure as they should be. IMPORTANT: Do not change the below // code to allow characters above U+00FF (255) without fixing this issue! // for (int i = 0; i < chars.length(); i++) { char c = chars.charAt(i); if (c < 32 || c > 127) { return true; } } return false; } /** * Converts a CharSequence to a byte array, intentionally truncating chars greater than 255 for * backwards compatibility reasons. See {@link #mHasInvalidChars}. * * @param chars The CharSequence to convert * @return A byte array representing the input */ private static byte[] charsToBytesTruncating(CharSequence chars) { byte[] bytes = new byte[chars.length()]; for (int i = 0; i < chars.length(); i++) { bytes[i] = (byte) chars.charAt(i); } return bytes; } }