/*
 * 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 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.
     *
     * <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;
    }
}
