| /** |
| * 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 android.net.wifi.EAPConstants; |
| import android.net.wifi.ParcelUtil; |
| 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 = 63; |
| |
| /** |
| * 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("\n"); |
| builder.append("MachineManaged: ").append(mMachineManaged).append("\n"); |
| builder.append("SoftTokenApp: ").append(mSoftTokenApp).append("\n"); |
| builder.append("AbleToShare: ").append(mAbleToShare).append("\n"); |
| builder.append("EAPType: ").append(mEapType).append("\n"); |
| builder.append("AuthMethod: ").append(mNonEapInnerMethod).append("\n"); |
| 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; |
| } |
| |
| // 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]; |
| } |
| }; |
| } |
| 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 + "\n"; |
| } |
| |
| /** |
| * 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("\n"); |
| } |
| builder.append("EAPType: ").append(mEapType).append("\n"); |
| 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; |
| } |
| |
| /** |
| * 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; |
| } |
| } |
| |
| @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); |
| } |
| |
| @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); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mCreationTimeInMillis, mExpirationTimeInMillis, mRealm, |
| mCheckAaaServerCertStatus, mUserCredential, mCertCredential, mSimCredential, |
| mClientPrivateKey, Arrays.hashCode(mCaCertificates), |
| Arrays.hashCode(mClientCertificateChain)); |
| } |
| |
| /** |
| * 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, 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"); |
| } |
| 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)); |
| 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; |
| } |
| } |