/**
 * Copyright (c) 2016, 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 android.net.wifi.hotspot2.pps;

import static android.net.wifi.hotspot2.PasspointConfiguration.MAX_STRING_LENGTH;

import android.net.wifi.EAPConstants;
import android.net.wifi.ParcelUtil;
import android.net.wifi.WifiEnterpriseConfig;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
 * Class representing Credential subtree in the PerProviderSubscription (PPS)
 * Management Object (MO) tree.
 * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
 * Release 2 Technical Specification.
 *
 * In addition to the fields in the Credential subtree, this will also maintain necessary
 * information for the private key and certificates associated with this credential.
 */
public final class Credential implements Parcelable {
    private static final String TAG = "Credential";

    /**
     * Max string length for realm.  Refer to Credential/Realm node in Hotspot 2.0 Release 2
     * Technical Specification Section 9.1 for more info.
     */
    private static final int MAX_REALM_BYTES = 253;

    /**
     * The time this credential is created. It is in the format of number
     * of milliseconds since January 1, 1970, 00:00:00 GMT.
     * Using Long.MIN_VALUE to indicate unset value.
     */
    private long mCreationTimeInMillis = Long.MIN_VALUE;
    /**
     * @hide
     */
    public void setCreationTimeInMillis(long creationTimeInMillis) {
        mCreationTimeInMillis = creationTimeInMillis;
    }
    /**
     * @hide
     */
    public long getCreationTimeInMillis() {
        return mCreationTimeInMillis;
    }

    /**
     * The time this credential will expire. It is in the format of number
     * of milliseconds since January 1, 1970, 00:00:00 GMT.
    * Using Long.MIN_VALUE to indicate unset value.
     */
    private long mExpirationTimeInMillis = Long.MIN_VALUE;
    /**
     * @hide
     */
    public void setExpirationTimeInMillis(long expirationTimeInMillis) {
        mExpirationTimeInMillis = expirationTimeInMillis;
    }
    /**
     * @hide
     */
    public long getExpirationTimeInMillis() {
        return mExpirationTimeInMillis;
    }

    /**
     * The realm associated with this credential.  It will be used to determine
     * if this credential can be used to authenticate with a given hotspot by
     * comparing the realm specified in that hotspot's ANQP element.
     */
    private String mRealm = null;
    /**
     * Set the realm associated with this credential.
     *
     * @param realm The realm to set to
     */
    public void setRealm(String realm) {
        mRealm = realm;
    }
    /**
     * Get the realm associated with this credential.
     *
     * @return the realm associated with this credential
     */
    public String getRealm() {
        return mRealm;
    }

    /**
     * When set to true, the device should check AAA (Authentication, Authorization,
     * and Accounting) server's certificate during EAP (Extensible Authentication
     * Protocol) authentication.
     */
    private boolean mCheckAaaServerCertStatus = false;
    /**
     * @hide
     */
    public void setCheckAaaServerCertStatus(boolean checkAaaServerCertStatus) {
        mCheckAaaServerCertStatus = checkAaaServerCertStatus;
    }
    /**
     * @hide
     */
    public boolean getCheckAaaServerCertStatus() {
        return mCheckAaaServerCertStatus;
    }

    /**
     * Username-password based credential.
     * Contains the fields under PerProviderSubscription/Credential/UsernamePassword subtree.
     */
    public static final class UserCredential implements Parcelable {
        /**
         * Maximum string length for username.  Refer to Credential/UsernamePassword/Username
         * node in Hotspot 2.0 Release 2 Technical Specification Section 9.1 for more info.
         */
        private static final int MAX_USERNAME_BYTES = 253;

        /**
         * Maximum string length for password.  Refer to Credential/UsernamePassword/Password
         * in Hotspot 2.0 Release 2 Technical Specification Section 9.1 for more info.
         */
        private static final int MAX_PASSWORD_BYTES = 255;

        /**
         * Supported authentication methods.
         * @hide
         */
        public static final String AUTH_METHOD_PAP = "PAP";
        /** @hide */
        public static final String AUTH_METHOD_MSCHAP = "MS-CHAP";
        /** @hide */
        public static final String AUTH_METHOD_MSCHAPV2 = "MS-CHAP-V2";

        /**
         * Supported Non-EAP inner methods.  Refer to
         * Credential/UsernamePassword/EAPMethod/InnerEAPType in Hotspot 2.0 Release 2 Technical
         * Specification Section 9.1 for more info.
         */
        private static final Set<String> SUPPORTED_AUTH = new HashSet<String>(
                Arrays.asList(AUTH_METHOD_PAP, AUTH_METHOD_MSCHAP, AUTH_METHOD_MSCHAPV2));

        /**
         * Username of the credential.
         */
        private String mUsername = null;
        /**
         * Set the username associated with this user credential.
         *
         * @param username The username to set to
         */
        public void setUsername(String username) {
            mUsername = username;
        }
        /**
         * Get the username associated with this user credential.
         *
         * @return the username associated with this user credential
         */
        public String getUsername() {
            return mUsername;
        }

        /**
         * Base64-encoded password.
         */
        private String mPassword = null;
        /**
         * Set the Base64-encoded password associated with this user credential.
         *
         * @param password The password to set to
         */
        public void setPassword(String password) {
            mPassword = password;
        }
        /**
         * Get the Base64-encoded password associated with this user credential.
         *
         * @return the Base64-encoded password associated with this user credential
         */
        public String getPassword() {
            return mPassword;
        }

        /**
         * Flag indicating if the password is machine managed.
         */
        private boolean mMachineManaged = false;
        /**
         * @hide
         */
        public void setMachineManaged(boolean machineManaged) {
            mMachineManaged = machineManaged;
        }
        /**
         * @hide
         */
        public boolean getMachineManaged() {
            return mMachineManaged;
        }

        /**
         * The name of the application used to generate the password.
         */
        private String mSoftTokenApp = null;
        /**
         * @hide
         */
        public void setSoftTokenApp(String softTokenApp) {
            mSoftTokenApp = softTokenApp;
        }
        /**
         * @hide
         */
        public String getSoftTokenApp() {
            return mSoftTokenApp;
        }

        /**
         * Flag indicating if this credential is usable on other mobile devices as well.
         */
        private boolean mAbleToShare = false;
        /**
         * @hide
         */
        public void setAbleToShare(boolean ableToShare) {
            mAbleToShare = ableToShare;
        }
        /**
         * @hide
         */
        public boolean getAbleToShare() {
            return mAbleToShare;
        }

        /**
         * EAP (Extensible Authentication Protocol) method type.
         * Refer to
         * <a href="http://www.iana.org/assignments/eap-numbers/eap-numbers.xml#eap-numbers-4">
         * EAP Numbers</a> for valid values.
         * Using Integer.MIN_VALUE to indicate unset value.
         */
        private int mEapType = Integer.MIN_VALUE;
        /**
         * Set the EAP (Extensible Authentication Protocol) method type associated with this
         * user credential.
         * Refer to
         * <a href="http://www.iana.org/assignments/eap-numbers/eap-numbers.xml#eap-numbers-4">
         * EAP Numbers</a> for valid values.
         *
         * @param eapType The EAP method type associated with this user credential
         */
        public void setEapType(int eapType) {
            mEapType = eapType;
        }
        /**
         * Get the EAP (Extensible Authentication Protocol) method type associated with this
         * user credential.
         *
         * @return EAP method type
         */
        public int getEapType() {
            return mEapType;
        }

        /**
         * Non-EAP inner authentication method.
         */
        private String mNonEapInnerMethod = null;
        /**
         * Set the inner non-EAP method associated with this user credential.
         *
         * @param nonEapInnerMethod The non-EAP inner method to set to
         */
        public void setNonEapInnerMethod(String nonEapInnerMethod) {
            mNonEapInnerMethod = nonEapInnerMethod;
        }
        /**
         * Get the inner non-EAP method associated with this user credential.
         *
         * @return Non-EAP inner method associated with this user credential
         */
        public String getNonEapInnerMethod() {
            return mNonEapInnerMethod;
        }

        /**
         * Constructor for creating UserCredential with default values.
         */
        public UserCredential() {}

        /**
         * Copy constructor.
         *
         * @param source The source to copy from
         */
        public UserCredential(UserCredential source) {
            if (source != null) {
                mUsername = source.mUsername;
                mPassword = source.mPassword;
                mMachineManaged = source.mMachineManaged;
                mSoftTokenApp = source.mSoftTokenApp;
                mAbleToShare = source.mAbleToShare;
                mEapType = source.mEapType;
                mNonEapInnerMethod = source.mNonEapInnerMethod;
            }
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(mUsername);
            dest.writeString(mPassword);
            dest.writeInt(mMachineManaged ? 1 : 0);
            dest.writeString(mSoftTokenApp);
            dest.writeInt(mAbleToShare ? 1 : 0);
            dest.writeInt(mEapType);
            dest.writeString(mNonEapInnerMethod);
        }

        @Override
        public boolean equals(Object thatObject) {
            if (this == thatObject) {
                return true;
            }
            if (!(thatObject instanceof UserCredential)) {
                return false;
            }

            UserCredential that = (UserCredential) thatObject;
            return TextUtils.equals(mUsername, that.mUsername)
                    && TextUtils.equals(mPassword, that.mPassword)
                    && mMachineManaged == that.mMachineManaged
                    && TextUtils.equals(mSoftTokenApp, that.mSoftTokenApp)
                    && mAbleToShare == that.mAbleToShare
                    && mEapType == that.mEapType
                    && TextUtils.equals(mNonEapInnerMethod, that.mNonEapInnerMethod);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mUsername, mPassword, mMachineManaged, mSoftTokenApp,
                    mAbleToShare, mEapType, mNonEapInnerMethod);
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("Username=").append(mUsername).append(", ");
            builder.append("MachineManaged=").append(mMachineManaged).append(", ");
            builder.append("SoftTokenApp=").append(mSoftTokenApp).append(", ");
            builder.append("AbleToShare=").append(mAbleToShare).append(", ");
            builder.append("EAPType=").append(mEapType).append(", ");
            builder.append("AuthMethod=").append(mNonEapInnerMethod);
            return builder.toString();
        }

        /**
         * Validate the configuration data.
         *
         * @return true on success or false on failure
         * @hide
         */
        public boolean validate() {
            if (TextUtils.isEmpty(mUsername)) {
                Log.d(TAG, "Missing username");
                return false;
            }
            if (mUsername.getBytes(StandardCharsets.UTF_8).length > MAX_USERNAME_BYTES) {
                Log.d(TAG, "username exceeding maximum length: "
                        + mUsername.getBytes(StandardCharsets.UTF_8).length);
                return false;
            }

            if (TextUtils.isEmpty(mPassword)) {
                Log.d(TAG, "Missing password");
                return false;
            }
            if (mPassword.getBytes(StandardCharsets.UTF_8).length > MAX_PASSWORD_BYTES) {
                Log.d(TAG, "password exceeding maximum length: "
                        + mPassword.getBytes(StandardCharsets.UTF_8).length);
                return false;
            }
            if (mSoftTokenApp != null) {
                if (mSoftTokenApp.getBytes(StandardCharsets.UTF_8).length > MAX_STRING_LENGTH) {
                    Log.d(TAG, "app name exceeding maximum length: "
                            + mSoftTokenApp.getBytes(StandardCharsets.UTF_8).length);
                    return false;
                }
            }

            // Only supports EAP-TTLS for user credential.
            if (mEapType != EAPConstants.EAP_TTLS) {
                Log.d(TAG, "Invalid EAP Type for user credential: " + mEapType);
                return false;
            }

            // Verify Non-EAP inner method for EAP-TTLS.
            if (!SUPPORTED_AUTH.contains(mNonEapInnerMethod)) {
                Log.d(TAG, "Invalid non-EAP inner method for EAP-TTLS: " + mNonEapInnerMethod);
                return false;
            }
            return true;
        }

        public static final @android.annotation.NonNull Creator<UserCredential> CREATOR =
            new Creator<UserCredential>() {
                @Override
                public UserCredential createFromParcel(Parcel in) {
                    UserCredential userCredential = new UserCredential();
                    userCredential.setUsername(in.readString());
                    userCredential.setPassword(in.readString());
                    userCredential.setMachineManaged(in.readInt() != 0);
                    userCredential.setSoftTokenApp(in.readString());
                    userCredential.setAbleToShare(in.readInt() != 0);
                    userCredential.setEapType(in.readInt());
                    userCredential.setNonEapInnerMethod(in.readString());
                    return userCredential;
                }

                @Override
                public UserCredential[] newArray(int size) {
                    return new UserCredential[size];
                }
            };

        /**
         * Get a unique identifier for UserCredential.
         *
         * @hide
         * @return a Unique identifier for a UserCredential object
         */
        public int getUniqueId() {
            return Objects.hash(mUsername);
        }
    }
    private UserCredential mUserCredential = null;
    /**
     * Set the user credential information.
     *
     * @param userCredential The user credential to set to
     */
    public void setUserCredential(UserCredential userCredential) {
        mUserCredential = userCredential;
    }
    /**
     * Get the user credential information.
     *
     * @return user credential information
     */
    public UserCredential getUserCredential() {
        return mUserCredential;
    }

    /**
     * Certificate based credential.  This is used for EAP-TLS.
     * Contains fields under PerProviderSubscription/Credential/DigitalCertificate subtree.
     */
    public static final class CertificateCredential implements Parcelable {
        /**
         * Supported certificate types.
         * @hide
         */
        public static final String CERT_TYPE_X509V3 = "x509v3";

        /**
         * Certificate SHA-256 fingerprint length.
         */
        private static final int CERT_SHA256_FINGER_PRINT_LENGTH = 32;

        /**
         * Certificate type.
         */
        private String mCertType = null;
        /**
         * Set the certificate type associated with this certificate credential.
         *
         * @param certType The certificate type to set to
         */
        public void setCertType(String certType) {
            mCertType = certType;
        }
        /**
         * Get the certificate type associated with this certificate credential.
         *
         * @return certificate type
         */
        public String getCertType() {
            return mCertType;
        }

        /**
         * The SHA-256 fingerprint of the certificate.
         */
        private byte[] mCertSha256Fingerprint = null;
        /**
         * Set the certificate SHA-256 fingerprint associated with this certificate credential.
         *
         * @param certSha256Fingerprint The certificate fingerprint to set to
         */
        public void setCertSha256Fingerprint(byte[] certSha256Fingerprint) {
            mCertSha256Fingerprint = certSha256Fingerprint;
        }
        /**
         * Get the certificate SHA-256 fingerprint associated with this certificate credential.
         *
         * @return certificate SHA-256 fingerprint
         */
        public byte[] getCertSha256Fingerprint() {
            return mCertSha256Fingerprint;
        }

        /**
         * Constructor for creating CertificateCredential with default values.
         */
        public CertificateCredential() {}

        /**
         * Copy constructor.
         *
         * @param source The source to copy from
         */
        public CertificateCredential(CertificateCredential source) {
            if (source != null) {
                mCertType = source.mCertType;
                if (source.mCertSha256Fingerprint != null) {
                    mCertSha256Fingerprint = Arrays.copyOf(source.mCertSha256Fingerprint,
                                                          source.mCertSha256Fingerprint.length);
                }
            }
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(mCertType);
            dest.writeByteArray(mCertSha256Fingerprint);
        }

        @Override
        public boolean equals(Object thatObject) {
            if (this == thatObject) {
                return true;
            }
            if (!(thatObject instanceof CertificateCredential)) {
                return false;
            }

            CertificateCredential that = (CertificateCredential) thatObject;
            return TextUtils.equals(mCertType, that.mCertType)
                    && Arrays.equals(mCertSha256Fingerprint, that.mCertSha256Fingerprint);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mCertType, Arrays.hashCode(mCertSha256Fingerprint));
        }

        @Override
        public String toString() {
            return "CertificateType=" + mCertType;
        }

        /**
         * Validate the configuration data.
         *
         * @return true on success or false on failure
         * @hide
         */
        public boolean validate() {
            if (!TextUtils.equals(CERT_TYPE_X509V3, mCertType)) {
                Log.d(TAG, "Unsupported certificate type: " + mCertType);
                return false;
            }
            if (mCertSha256Fingerprint == null
                    || mCertSha256Fingerprint.length != CERT_SHA256_FINGER_PRINT_LENGTH) {
                Log.d(TAG, "Invalid SHA-256 fingerprint");
                return false;
            }
            return true;
        }

        public static final @android.annotation.NonNull Creator<CertificateCredential> CREATOR =
            new Creator<CertificateCredential>() {
                @Override
                public CertificateCredential createFromParcel(Parcel in) {
                    CertificateCredential certCredential = new CertificateCredential();
                    certCredential.setCertType(in.readString());
                    certCredential.setCertSha256Fingerprint(in.createByteArray());
                    return certCredential;
                }

                @Override
                public CertificateCredential[] newArray(int size) {
                    return new CertificateCredential[size];
                }
            };
    }
    private CertificateCredential mCertCredential = null;
    /**
     * Set the certificate credential information.
     *
     * @param certCredential The certificate credential to set to
     */
    public void setCertCredential(CertificateCredential certCredential) {
        mCertCredential = certCredential;
    }
    /**
     * Get the certificate credential information.
     *
     * @return certificate credential information
     */
    public CertificateCredential getCertCredential() {
        return mCertCredential;
    }

    /**
     * SIM (Subscriber Identify Module) based credential.
     * Contains fields under PerProviderSubscription/Credential/SIM subtree.
     */
    public static final class SimCredential implements Parcelable {
        /**
         * Maximum string length for IMSI.
         */
        private static final int MAX_IMSI_LENGTH = 15;

        /**
         * International Mobile Subscriber Identity, is used to identify the user
         * of a cellular network and is a unique identification associated with all
         * cellular networks
         */
        private String mImsi = null;
        /**
         * Set the IMSI (International Mobile Subscriber Identity) associated with this SIM
         * credential.
         *
         * @param imsi The IMSI to set to
         */
        public void setImsi(String imsi) {
            mImsi = imsi;
        }
        /**
         * Get the IMSI (International Mobile Subscriber Identity) associated with this SIM
         * credential.
         *
         * @return IMSI associated with this SIM credential
         */
        public String getImsi() {
            return mImsi;
        }

        /**
         * EAP (Extensible Authentication Protocol) method type for using SIM credential.
         * Refer to http://www.iana.org/assignments/eap-numbers/eap-numbers.xml#eap-numbers-4
         * for valid values.
         * Using Integer.MIN_VALUE to indicate unset value.
         */
        private int mEapType = Integer.MIN_VALUE;
        /**
         * Set the EAP (Extensible Authentication Protocol) method type associated with this
         * SIM credential.
         *
         * @param eapType The EAP method type to set to
         */
        public void setEapType(int eapType) {
            mEapType = eapType;
        }
        /**
         * Get the EAP (Extensible Authentication Protocol) method type associated with this
         * SIM credential.
         *
         * @return EAP method type associated with this SIM credential
         */
        public int getEapType() {
            return mEapType;
        }

        /**
         * Constructor for creating SimCredential with default values.
         */
        public SimCredential() {}

        /**
         * Copy constructor
         *
         * @param source The source to copy from
         */
        public SimCredential(SimCredential source) {
            if (source != null) {
                mImsi = source.mImsi;
                mEapType = source.mEapType;
            }
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public boolean equals(Object thatObject) {
            if (this == thatObject) {
                return true;
            }
            if (!(thatObject instanceof SimCredential)) {
                return false;
            }

            SimCredential that = (SimCredential) thatObject;
            return TextUtils.equals(mImsi, that.mImsi)
                    && mEapType == that.mEapType;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mImsi, mEapType);
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            String imsi;
            if (mImsi != null) {
                if (mImsi.length() > 6 && mImsi.charAt(6) != '*') {
                    // Truncate the full IMSI from the log
                    imsi = mImsi.substring(0, 6) + "****";
                } else {
                    imsi = mImsi;
                }
                builder.append("IMSI=").append(imsi).append(", ");
            }
            builder.append("EAPType=").append(mEapType);
            return builder.toString();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(mImsi);
            dest.writeInt(mEapType);
        }

        /**
         * Validate the configuration data.
         *
         * @return true on success or false on failure
         * @hide
         */
        public boolean validate() {
            // Note: this only validate the format of IMSI string itself.  Additional verification
            // will be done by WifiService at the time of provisioning to verify against the IMSI
            // of the SIM card installed in the device.
            if (!verifyImsi()) {
                return false;
            }
            if (mEapType != EAPConstants.EAP_SIM && mEapType != EAPConstants.EAP_AKA
                    && mEapType != EAPConstants.EAP_AKA_PRIME) {
                Log.d(TAG, "Invalid EAP Type for SIM credential: " + mEapType);
                return false;
            }
            return true;
        }

        public static final @android.annotation.NonNull Creator<SimCredential> CREATOR =
            new Creator<SimCredential>() {
                @Override
                public SimCredential createFromParcel(Parcel in) {
                    SimCredential simCredential = new SimCredential();
                    simCredential.setImsi(in.readString());
                    simCredential.setEapType(in.readInt());
                    return simCredential;
                }

                @Override
                public SimCredential[] newArray(int size) {
                    return new SimCredential[size];
                }
            };

        /**
         * Verify the IMSI (International Mobile Subscriber Identity) string.  The string
         * should contain zero or more numeric digits, and might ends with a "*" for prefix
         * matching.
         *
         * @return true if IMSI is valid, false otherwise.
         */
        private boolean verifyImsi() {
            if (TextUtils.isEmpty(mImsi)) {
                Log.d(TAG, "Missing IMSI");
                return false;
            }
            if (mImsi.length() > MAX_IMSI_LENGTH) {
                Log.d(TAG, "IMSI exceeding maximum length: " + mImsi.length());
                return false;
            }

            // Locate the first non-digit character.
            int nonDigit;
            char stopChar = '\0';
            for (nonDigit = 0; nonDigit < mImsi.length(); nonDigit++) {
                stopChar = mImsi.charAt(nonDigit);
                if (stopChar < '0' || stopChar > '9') {
                    break;
                }
            }

            if (nonDigit == mImsi.length()) {
                return true;
            }
            else if (nonDigit == mImsi.length()-1 && stopChar == '*') {
                // Prefix matching.
                return true;
            }
            return false;
        }
    }
    private SimCredential mSimCredential = null;
    /**
     * Set the SIM credential information.
     *
     * @param simCredential The SIM credential to set to
     */
    public void setSimCredential(SimCredential simCredential) {
        mSimCredential = simCredential;
    }
    /**
     * Get the SIM credential information.
     *
     * @return SIM credential information
     */
    public SimCredential getSimCredential() {
        return mSimCredential;
    }

    /**
     * CA (Certificate Authority) X509 certificates.
     */
    private X509Certificate[] mCaCertificates = null;

    /**
     * Set the CA (Certification Authority) certificate associated with this credential.
     *
     * @param caCertificate The CA certificate to set to
     */
    public void setCaCertificate(X509Certificate caCertificate) {
        mCaCertificates = null;
        if (caCertificate != null) {
            mCaCertificates = new X509Certificate[] {caCertificate};
        }
    }

    /**
     * Set the CA (Certification Authority) certificates associated with this credential.
     *
     * @param caCertificates The list of CA certificates to set to
     * @hide
     */
    public void setCaCertificates(X509Certificate[] caCertificates) {
        mCaCertificates = caCertificates;
    }

    /**
     * Get the CA (Certification Authority) certificate associated with this credential.
     *
     * @return CA certificate associated with this credential, {@code null} if certificate is not
     * set or certificate is more than one.
     */
    public X509Certificate getCaCertificate() {
        return mCaCertificates == null || mCaCertificates.length > 1 ? null : mCaCertificates[0];
    }

    /**
     * Get the CA (Certification Authority) certificates associated with this credential.
     *
     * @return The list of CA certificates associated with this credential
     * @hide
     */
    public X509Certificate[] getCaCertificates() {
        return mCaCertificates;
    }

    /**
     * Client side X509 certificate chain.
     */
    private X509Certificate[] mClientCertificateChain = null;
    /**
     * Set the client certificate chain associated with this credential.
     *
     * @param certificateChain The client certificate chain to set to
     */
    public void setClientCertificateChain(X509Certificate[] certificateChain) {
        mClientCertificateChain = certificateChain;
    }
    /**
     * Get the client certificate chain associated with this credential.
     *
     * @return client certificate chain associated with this credential
     */
    public X509Certificate[] getClientCertificateChain() {
        return mClientCertificateChain;
    }

    /**
     * Client side private key.
     */
    private PrivateKey mClientPrivateKey = null;
    /**
     * Set the client private key associated with this credential.
     *
     * @param clientPrivateKey the client private key to set to
     */
    public void setClientPrivateKey(PrivateKey clientPrivateKey) {
        mClientPrivateKey = clientPrivateKey;
    }
    /**
     * Get the client private key associated with this credential.
     *
     * @return client private key associated with this credential.
     */
    public PrivateKey getClientPrivateKey() {
        return mClientPrivateKey;
    }

    /**
     * The required minimum TLS version.
     */
    private @WifiEnterpriseConfig.TlsVersion int mMinimumTlsVersion = WifiEnterpriseConfig.TLS_V1_0;
    /**
     * Set the minimum TLS version for TLS-based EAP methods.
     *
     * {@link android.net.wifi.WifiManager#isTlsMinimumVersionSupported()} indicates whether
     * or not a minimum TLS version can be set. If not supported, the minimum TLS version
     * is always TLS v1.0.
     * <p>
     * {@link android.net.wifi.WifiManager#isTlsV13Supported()} indicates whether or not
     * TLS v1.3 is supported. If requested minimum is not supported, it will default to
     * the maximum supported version.
     *
     * @param tlsVersion the TLS version
     * @throws IllegalArgumentException if the TLS version is invalid.
     */
    public void setMinimumTlsVersion(@WifiEnterpriseConfig.TlsVersion int tlsVersion)
            throws IllegalArgumentException {
        if (tlsVersion < WifiEnterpriseConfig.TLS_VERSION_MIN
                || tlsVersion > WifiEnterpriseConfig.TLS_VERSION_MAX) {
            throw new IllegalArgumentException(
                    "Invalid TLS version: " + tlsVersion);
        }
        mMinimumTlsVersion = tlsVersion;
    }

    /**
     * Get the minimum TLS version for TLS-based EAP methods.
     *
     * @return the TLS version
     */
    public @WifiEnterpriseConfig.TlsVersion int getMinimumTlsVersion() {
        return mMinimumTlsVersion;
    }

    /**
     * Constructor for creating Credential with default values.
     */
    public Credential() {}

    /**
     * Copy constructor.
     *
     * @param source The source to copy from
     */
    public Credential(Credential source) {
        if (source != null) {
            mCreationTimeInMillis = source.mCreationTimeInMillis;
            mExpirationTimeInMillis = source.mExpirationTimeInMillis;
            mRealm = source.mRealm;
            mCheckAaaServerCertStatus = source.mCheckAaaServerCertStatus;
            if (source.mUserCredential != null) {
                mUserCredential = new UserCredential(source.mUserCredential);
            }
            if (source.mCertCredential != null) {
                mCertCredential = new CertificateCredential(source.mCertCredential);
            }
            if (source.mSimCredential != null) {
                mSimCredential = new SimCredential(source.mSimCredential);
            }
            if (source.mClientCertificateChain != null) {
                mClientCertificateChain = Arrays.copyOf(source.mClientCertificateChain,
                                                        source.mClientCertificateChain.length);
            }
            if (source.mCaCertificates != null) {
                mCaCertificates = Arrays.copyOf(source.mCaCertificates,
                        source.mCaCertificates.length);
            }

            mClientPrivateKey = source.mClientPrivateKey;
            mMinimumTlsVersion = source.mMinimumTlsVersion;
        }
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(mCreationTimeInMillis);
        dest.writeLong(mExpirationTimeInMillis);
        dest.writeString(mRealm);
        dest.writeInt(mCheckAaaServerCertStatus ? 1 : 0);
        dest.writeParcelable(mUserCredential, flags);
        dest.writeParcelable(mCertCredential, flags);
        dest.writeParcelable(mSimCredential, flags);
        ParcelUtil.writeCertificates(dest, mCaCertificates);
        ParcelUtil.writeCertificates(dest, mClientCertificateChain);
        ParcelUtil.writePrivateKey(dest, mClientPrivateKey);
        dest.writeInt(mMinimumTlsVersion);
    }

    @Override
    public boolean equals(Object thatObject) {
        if (this == thatObject) {
            return true;
        }
        if (!(thatObject instanceof Credential)) {
            return false;
        }

        Credential that = (Credential) thatObject;
        return TextUtils.equals(mRealm, that.mRealm)
                && mCreationTimeInMillis == that.mCreationTimeInMillis
                && mExpirationTimeInMillis == that.mExpirationTimeInMillis
                && mCheckAaaServerCertStatus == that.mCheckAaaServerCertStatus
                && (mUserCredential == null ? that.mUserCredential == null
                    : mUserCredential.equals(that.mUserCredential))
                && (mCertCredential == null ? that.mCertCredential == null
                    : mCertCredential.equals(that.mCertCredential))
                && (mSimCredential == null ? that.mSimCredential == null
                    : mSimCredential.equals(that.mSimCredential))
                && isX509CertificatesEquals(mCaCertificates, that.mCaCertificates)
                && isX509CertificatesEquals(mClientCertificateChain, that.mClientCertificateChain)
                && isPrivateKeyEquals(mClientPrivateKey, that.mClientPrivateKey)
                && mMinimumTlsVersion == that.mMinimumTlsVersion;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mCreationTimeInMillis, mExpirationTimeInMillis, mRealm,
                mCheckAaaServerCertStatus, mUserCredential, mCertCredential, mSimCredential,
                mClientPrivateKey, Arrays.hashCode(mCaCertificates),
                Arrays.hashCode(mClientCertificateChain), mMinimumTlsVersion);
    }

    /**
     * Get a unique identifier for Credential. This identifier depends only on items that remain
     * constant throughout the lifetime of a subscription's credentials.
     *
     * @hide
     * @return a Unique identifier for a Credential object
     */
    public int getUniqueId() {
        return Objects.hash(mUserCredential != null ? mUserCredential.getUniqueId() : 0,
                mCertCredential, mSimCredential, mRealm);
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Realm: ").append(mRealm).append("\n");
        builder.append("CreationTime: ").append(mCreationTimeInMillis != Long.MIN_VALUE
                ? new Date(mCreationTimeInMillis) : "Not specified").append("\n");
        builder.append("ExpirationTime: ").append(mExpirationTimeInMillis != Long.MIN_VALUE
                ? new Date(mExpirationTimeInMillis) : "Not specified").append("\n");
        builder.append("CheckAAAServerStatus: ").append(mCheckAaaServerCertStatus).append("\n");
        if (mUserCredential != null) {
            builder.append("UserCredential Begin ---\n");
            builder.append(mUserCredential);
            builder.append("UserCredential End ---\n");
        }
        if (mCertCredential != null) {
            builder.append("CertificateCredential Begin ---\n");
            builder.append(mCertCredential);
            builder.append("CertificateCredential End ---\n");
        }
        builder.append("MinimumTlsVersion: ").append(mMinimumTlsVersion).append("\n");
        if (mSimCredential != null) {
            builder.append("SIMCredential Begin ---\n");
            builder.append(mSimCredential);
            builder.append("SIMCredential End ---\n");
        }
        return builder.toString();
    }

    /**
     * Validate the configuration data.
     *
     * @return true on success or false on failure
     * @hide
     */
    public boolean validate() {
        if (TextUtils.isEmpty(mRealm)) {
            Log.d(TAG, "Missing realm");
            return false;
        }
        if (mRealm.getBytes(StandardCharsets.UTF_8).length > MAX_REALM_BYTES) {
            Log.d(TAG, "realm exceeding maximum length: "
                    + mRealm.getBytes(StandardCharsets.UTF_8).length);
            return false;
        }

        // Verify the credential.
        if (mUserCredential != null) {
            if (!verifyUserCredential()) {
                return false;
            }
        } else if (mCertCredential != null) {
            if (!verifyCertCredential()) {
                return false;
            }
        } else if (mSimCredential != null) {
            if (!verifySimCredential()) {
                return false;
            }
        } else {
            Log.d(TAG, "Missing required credential");
            return false;
        }

        return true;
    }

    public static final @android.annotation.NonNull Creator<Credential> CREATOR =
        new Creator<Credential>() {
            @Override
            public Credential createFromParcel(Parcel in) {
                Credential credential = new Credential();
                credential.setCreationTimeInMillis(in.readLong());
                credential.setExpirationTimeInMillis(in.readLong());
                credential.setRealm(in.readString());
                credential.setCheckAaaServerCertStatus(in.readInt() != 0);
                credential.setUserCredential(in.readParcelable(null));
                credential.setCertCredential(in.readParcelable(null));
                credential.setSimCredential(in.readParcelable(null));
                credential.setCaCertificates(ParcelUtil.readCertificates(in));
                credential.setClientCertificateChain(ParcelUtil.readCertificates(in));
                credential.setClientPrivateKey(ParcelUtil.readPrivateKey(in));
                credential.setMinimumTlsVersion(in.readInt());
                return credential;
            }

            @Override
            public Credential[] newArray(int size) {
                return new Credential[size];
            }
        };

    /**
     * Verify user credential.
     * If no CA certificate is provided, then the system uses the CAs in the trust store.
     *
     * @return true if user credential is valid, false otherwise.
     */
    private boolean verifyUserCredential() {
        if (mUserCredential == null) {
            Log.d(TAG, "Missing user credential");
            return false;
        }
        if (mCertCredential != null || mSimCredential != null) {
            Log.d(TAG, "Contained more than one type of credential");
            return false;
        }
        if (!mUserCredential.validate()) {
            return false;
        }

        return true;
    }

    /**
     * Verify certificate credential, which is used for EAP-TLS.  This will verify
     * that the necessary client key and certificates are provided.
     * If no CA certificate is provided, then the system uses the CAs in the trust store.
     *
     * @return true if certificate credential is valid, false otherwise.
     */
    private boolean verifyCertCredential() {
        if (mCertCredential == null) {
            Log.d(TAG, "Missing certificate credential");
            return false;
        }
        if (mUserCredential != null || mSimCredential != null) {
            Log.d(TAG, "Contained more than one type of credential");
            return false;
        }

        if (!mCertCredential.validate()) {
            return false;
        }

        if (mClientPrivateKey == null) {
            Log.d(TAG, "Missing client private key for certificate credential");
            return false;
        }
        try {
            // Verify SHA-256 fingerprint for client certificate.
            if (!verifySha256Fingerprint(mClientCertificateChain,
                    mCertCredential.getCertSha256Fingerprint())) {
                Log.d(TAG, "SHA-256 fingerprint mismatch");
                return false;
            }
        } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
            Log.d(TAG, "Failed to verify SHA-256 fingerprint: " + e.getMessage());
            return false;
        }

        return true;
    }

    /**
     * Verify SIM credential.
     *
     * @return true if SIM credential is valid, false otherwise.
     */
    private boolean verifySimCredential() {
        if (mSimCredential == null) {
            Log.d(TAG, "Missing SIM credential");
            return false;
        }
        if (mUserCredential != null || mCertCredential != null) {
            Log.d(TAG, "Contained more than one type of credential");
            return false;
        }
        return mSimCredential.validate();
    }

    private static boolean isPrivateKeyEquals(PrivateKey key1, PrivateKey key2) {
        if (key1 == null && key2 == null) {
            return true;
        }

        /* Return false if only one of them is null */
        if (key1 == null || key2 == null) {
            return false;
        }

        return TextUtils.equals(key1.getAlgorithm(), key2.getAlgorithm()) &&
                Arrays.equals(key1.getEncoded(), key2.getEncoded());
    }

    /**
     * Verify two X.509 certificates are identical.
     *
     * @param cert1 a certificate to compare
     * @param cert2 a certificate to compare
     * @return {@code true} if given certificates are the same each other, {@code false} otherwise.
     * @hide
     */
    public static boolean isX509CertificateEquals(X509Certificate cert1, X509Certificate cert2) {
        if (cert1 == null && cert2 == null) {
            return true;
        }

        /* Return false if only one of them is null */
        if (cert1 == null || cert2 == null) {
            return false;
        }

        boolean result = false;
        try {
            result = Arrays.equals(cert1.getEncoded(), cert2.getEncoded());
        } catch (CertificateEncodingException e) {
            /* empty, return false. */
        }
        return result;
    }

    private static boolean isX509CertificatesEquals(X509Certificate[] certs1,
                                                    X509Certificate[] certs2) {
        if (certs1 == null && certs2 == null) {
            return true;
        }

        /* Return false if only one of them is null */
        if (certs1 == null || certs2 == null) {
            return false;
        }

        if (certs1.length != certs2.length) {
            return false;
        }

        for (int i = 0; i < certs1.length; i++) {
            if (!isX509CertificateEquals(certs1[i], certs2[i])) {
                return false;
            }
        }

        return true;
    }

    /**
     * Verify that the digest for a certificate in the certificate chain matches expected
     * fingerprint.  The certificate that matches the fingerprint is the client certificate.
     *
     * @param certChain Chain of certificates
     * @param expectedFingerprint The expected SHA-256 digest of the client certificate
     * @return true if the certificate chain contains a matching certificate, false otherwise
     * @throws NoSuchAlgorithmException
     * @throws CertificateEncodingException
     */
    private static boolean verifySha256Fingerprint(X509Certificate[] certChain,
                                                   byte[] expectedFingerprint)
            throws NoSuchAlgorithmException, CertificateEncodingException {
        if (certChain == null) {
            return false;
        }
        MessageDigest digester = MessageDigest.getInstance("SHA-256");
        for (X509Certificate certificate : certChain) {
            digester.reset();
            byte[] fingerprint = digester.digest(certificate.getEncoded());
            if (Arrays.equals(expectedFingerprint, fingerprint)) {
                return true;
            }
        }
        return false;
    }
}
