script-astra/Android/Sdk/sources/android-35/com/android/internal/widget/LockscreenCredential.java
localadmin 4380f00a78 init
2025-01-20 18:15:20 +03:00

450 lines
18 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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).
*
* <p> 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:
* <pre>
* try {LockscreenCredential credential = LockscreenCredential.createPassword(...) {
* // Process the credential in some way
* }
* </pre>
* 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 32127.
//
// Such credentials were never intended to be allowed. However, Android 1014 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 32127 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.
*
* <p> 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<LockPatternView.Cell> 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<LockscreenCredential> CREATOR =
new Parcelable.Creator<LockscreenCredential>() {
@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;
}
}