| /* |
| * 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 com.android.server.wifi.hotspot2; |
| |
| import static android.net.wifi.WifiConfiguration.MeteredOverride; |
| |
| import android.annotation.Nullable; |
| import android.net.wifi.EAPConstants; |
| import android.net.wifi.WifiConfiguration; |
| import android.net.wifi.WifiEnterpriseConfig; |
| import android.net.wifi.hotspot2.PasspointConfiguration; |
| import android.net.wifi.hotspot2.pps.Credential; |
| import android.net.wifi.hotspot2.pps.Credential.SimCredential; |
| import android.net.wifi.hotspot2.pps.Credential.UserCredential; |
| import android.net.wifi.hotspot2.pps.HomeSp; |
| import android.telephony.TelephonyManager; |
| import android.text.TextUtils; |
| import android.util.Base64; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| import com.android.server.wifi.IMSIParameter; |
| import com.android.server.wifi.WifiCarrierInfoManager; |
| import com.android.server.wifi.WifiKeyStore; |
| import com.android.server.wifi.hotspot2.anqp.ANQPElement; |
| import com.android.server.wifi.hotspot2.anqp.Constants.ANQPElementType; |
| import com.android.server.wifi.hotspot2.anqp.DomainNameElement; |
| import com.android.server.wifi.hotspot2.anqp.NAIRealmElement; |
| import com.android.server.wifi.hotspot2.anqp.RoamingConsortiumElement; |
| import com.android.server.wifi.hotspot2.anqp.ThreeGPPNetworkElement; |
| import com.android.server.wifi.hotspot2.anqp.eap.AuthParam; |
| import com.android.server.wifi.hotspot2.anqp.eap.NonEAPInnerAuth; |
| import com.android.server.wifi.util.ArrayUtils; |
| import com.android.server.wifi.util.InformationElementUtil.RoamingConsortium; |
| |
| import java.nio.charset.StandardCharsets; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PrivateKey; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| /** |
| * Abstraction for Passpoint service provider. This class contains the both static |
| * Passpoint configuration data and the runtime data (e.g. blacklisted SSIDs, statistics). |
| */ |
| public class PasspointProvider { |
| private static final String TAG = "PasspointProvider"; |
| |
| /** |
| * Used as part of alias string for certificates and keys. The alias string is in the format |
| * of: [KEY_TYPE]_HS2_[ProviderID] |
| * For example: "CACERT_HS2_0", "USRCERT_HS2_0", "USRPKEY_HS2_0", "CACERT_HS2_REMEDIATION_0" |
| */ |
| private static final String ALIAS_HS_TYPE = "HS2_"; |
| private static final String ALIAS_ALIAS_REMEDIATION_TYPE = "REMEDIATION_"; |
| |
| private static final String SYSTEM_CA_STORE_PATH = "/system/etc/security/cacerts"; |
| |
| private final PasspointConfiguration mConfig; |
| private final WifiKeyStore mKeyStore; |
| |
| /** |
| * Aliases for the private keys and certificates installed in the keystore. Each alias |
| * is a suffix of the actual certificate or key name installed in the keystore. The |
| * certificate or key name in the keystore is consist of |Type|_|alias|. |
| * This will be consistent with the usage of the term "alias" in {@link WifiEnterpriseConfig}. |
| */ |
| private List<String> mCaCertificateAliases; |
| private String mClientPrivateKeyAndCertificateAlias; |
| private String mRemediationCaCertificateAlias; |
| |
| private final long mProviderId; |
| private final int mCreatorUid; |
| private final String mPackageName; |
| |
| private final IMSIParameter mImsiParameter; |
| |
| private final int mEAPMethodID; |
| private final AuthParam mAuthParam; |
| private final WifiCarrierInfoManager mWifiCarrierInfoManager; |
| |
| private int mBestGuessCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID; |
| private boolean mHasEverConnected; |
| private boolean mIsShared; |
| private boolean mIsFromSuggestion; |
| private boolean mIsTrusted; |
| private boolean mVerboseLoggingEnabled; |
| |
| public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, |
| WifiCarrierInfoManager wifiCarrierInfoManager, long providerId, int creatorUid, |
| String packageName, boolean isFromSuggestion) { |
| this(config, keyStore, wifiCarrierInfoManager, providerId, creatorUid, packageName, |
| isFromSuggestion, null, null, null, false, false); |
| } |
| |
| public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, |
| WifiCarrierInfoManager wifiCarrierInfoManager, long providerId, int creatorUid, |
| String packageName, boolean isFromSuggestion, List<String> caCertificateAliases, |
| String clientPrivateKeyAndCertificateAlias, String remediationCaCertificateAlias, |
| boolean hasEverConnected, boolean isShared) { |
| // Maintain a copy of the configuration to avoid it being updated by others. |
| mConfig = new PasspointConfiguration(config); |
| mKeyStore = keyStore; |
| mProviderId = providerId; |
| mCreatorUid = creatorUid; |
| mPackageName = packageName; |
| mCaCertificateAliases = caCertificateAliases; |
| mClientPrivateKeyAndCertificateAlias = clientPrivateKeyAndCertificateAlias; |
| mRemediationCaCertificateAlias = remediationCaCertificateAlias; |
| mHasEverConnected = hasEverConnected; |
| mIsShared = isShared; |
| mIsFromSuggestion = isFromSuggestion; |
| mWifiCarrierInfoManager = wifiCarrierInfoManager; |
| mIsTrusted = true; |
| |
| // Setup EAP method and authentication parameter based on the credential. |
| if (mConfig.getCredential().getUserCredential() != null) { |
| mEAPMethodID = EAPConstants.EAP_TTLS; |
| mAuthParam = new NonEAPInnerAuth(NonEAPInnerAuth.getAuthTypeID( |
| mConfig.getCredential().getUserCredential().getNonEapInnerMethod())); |
| mImsiParameter = null; |
| } else if (mConfig.getCredential().getCertCredential() != null) { |
| mEAPMethodID = EAPConstants.EAP_TLS; |
| mAuthParam = null; |
| mImsiParameter = null; |
| } else { |
| mEAPMethodID = mConfig.getCredential().getSimCredential().getEapType(); |
| mAuthParam = null; |
| mImsiParameter = IMSIParameter.build( |
| mConfig.getCredential().getSimCredential().getImsi()); |
| } |
| } |
| |
| /** |
| * Set passpoint network trusted or not. |
| * Default is true. Only allows to change when it is from suggestion. |
| */ |
| public void setTrusted(boolean trusted) { |
| if (!mIsFromSuggestion) { |
| Log.e(TAG, "setTrusted can only be called for suggestion passpoint network"); |
| return; |
| } |
| mIsTrusted = trusted; |
| } |
| |
| public boolean isTrusted() { |
| return mIsTrusted; |
| } |
| |
| public PasspointConfiguration getConfig() { |
| // Return a copy of the configuration to avoid it being updated by others. |
| return new PasspointConfiguration(mConfig); |
| } |
| |
| public List<String> getCaCertificateAliases() { |
| return mCaCertificateAliases; |
| } |
| |
| public String getClientPrivateKeyAndCertificateAlias() { |
| return mClientPrivateKeyAndCertificateAlias; |
| } |
| |
| public String getRemediationCaCertificateAlias() { |
| return mRemediationCaCertificateAlias; |
| } |
| |
| public long getProviderId() { |
| return mProviderId; |
| } |
| |
| public int getCreatorUid() { |
| return mCreatorUid; |
| } |
| |
| @Nullable |
| public String getPackageName() { |
| return mPackageName; |
| } |
| |
| public boolean getHasEverConnected() { |
| return mHasEverConnected; |
| } |
| |
| public void setHasEverConnected(boolean hasEverConnected) { |
| mHasEverConnected = hasEverConnected; |
| } |
| |
| public boolean isFromSuggestion() { |
| return mIsFromSuggestion; |
| } |
| |
| /** |
| * Enable/disable the auto-join configuration of the corresponding passpoint configuration. |
| * |
| * @return true if the setting has changed |
| */ |
| public boolean setAutojoinEnabled(boolean autoJoinEnabled) { |
| boolean changed = mConfig.isAutojoinEnabled() != autoJoinEnabled; |
| mConfig.setAutojoinEnabled(autoJoinEnabled); |
| return changed; |
| } |
| |
| public boolean isAutojoinEnabled() { |
| return mConfig.isAutojoinEnabled(); |
| } |
| |
| /** |
| * Enable/disable mac randomization for this passpoint profile. |
| * |
| * @return true if the setting has changed |
| */ |
| public boolean setMacRandomizationEnabled(boolean enabled) { |
| boolean changed = mConfig.isMacRandomizationEnabled() != enabled; |
| mConfig.setMacRandomizationEnabled(enabled); |
| return changed; |
| } |
| |
| /** |
| * Get whether mac randomization is enabled for this passpoint profile. |
| */ |
| public boolean isMacRandomizationEnabled() { |
| return mConfig.isMacRandomizationEnabled(); |
| } |
| |
| /** |
| * Get the metered override for this passpoint profile. |
| * |
| * @return true if the setting has changed |
| */ |
| public boolean setMeteredOverride(@MeteredOverride int meteredOverride) { |
| boolean changed = mConfig.getMeteredOverride() != meteredOverride; |
| mConfig.setMeteredOverride(meteredOverride); |
| return changed; |
| } |
| |
| /** |
| * Install certificates and key based on current configuration. |
| * Note: the certificates and keys in the configuration will get cleared once |
| * they're installed in the keystore. |
| * |
| * @return true on success |
| */ |
| public boolean installCertsAndKeys() { |
| // Install CA certificate. |
| X509Certificate[] x509Certificates = mConfig.getCredential().getCaCertificates(); |
| if (x509Certificates != null) { |
| mCaCertificateAliases = new ArrayList<>(); |
| for (int i = 0; i < x509Certificates.length; i++) { |
| String alias = String.format("%s%s_%d", ALIAS_HS_TYPE, mProviderId, i); |
| if (!mKeyStore.putCaCertInKeyStore(alias, x509Certificates[i])) { |
| Log.e(TAG, "Failed to install CA Certificate " + alias); |
| uninstallCertsAndKeys(); |
| return false; |
| } else { |
| mCaCertificateAliases.add(alias); |
| } |
| } |
| } |
| |
| // Install the client private key & certificate. |
| if (mConfig.getCredential().getClientPrivateKey() != null |
| && mConfig.getCredential().getClientCertificateChain() != null) { |
| String keyName = ALIAS_HS_TYPE + mProviderId; |
| PrivateKey clientKey = mConfig.getCredential().getClientPrivateKey(); |
| X509Certificate clientCert = getClientCertificate( |
| mConfig.getCredential().getClientCertificateChain(), |
| mConfig.getCredential().getCertCredential().getCertSha256Fingerprint()); |
| if (clientCert == null) { |
| Log.e(TAG, "Failed to locate client certificate"); |
| uninstallCertsAndKeys(); |
| return false; |
| } |
| if (!mKeyStore.putUserPrivKeyAndCertsInKeyStore( |
| keyName, clientKey, new Certificate[] {clientCert})) { |
| Log.e(TAG, "Failed to install client private key or certificate"); |
| uninstallCertsAndKeys(); |
| return false; |
| } |
| mClientPrivateKeyAndCertificateAlias = keyName; |
| } |
| |
| if (mConfig.getSubscriptionUpdate() != null) { |
| X509Certificate certificate = mConfig.getSubscriptionUpdate().getCaCertificate(); |
| if (certificate == null) { |
| Log.e(TAG, "Failed to locate CA certificate for remediation"); |
| uninstallCertsAndKeys(); |
| return false; |
| } |
| String certName = ALIAS_HS_TYPE + ALIAS_ALIAS_REMEDIATION_TYPE + mProviderId; |
| if (!mKeyStore.putCaCertInKeyStore(certName, certificate)) { |
| Log.e(TAG, "Failed to install CA certificate for remediation"); |
| uninstallCertsAndKeys(); |
| return false; |
| } |
| mRemediationCaCertificateAlias = certName; |
| } |
| |
| // Clear the keys and certificates in the configuration. |
| mConfig.getCredential().setCaCertificates(null); |
| mConfig.getCredential().setClientPrivateKey(null); |
| mConfig.getCredential().setClientCertificateChain(null); |
| if (mConfig.getSubscriptionUpdate() != null) { |
| mConfig.getSubscriptionUpdate().setCaCertificate(null); |
| } |
| return true; |
| } |
| |
| /** |
| * Remove any installed certificates and key. |
| */ |
| public void uninstallCertsAndKeys() { |
| if (mCaCertificateAliases != null) { |
| for (String certificateAlias : mCaCertificateAliases) { |
| if (!mKeyStore.removeEntryFromKeyStore(certificateAlias)) { |
| Log.e(TAG, "Failed to remove entry: " + certificateAlias); |
| } |
| } |
| mCaCertificateAliases = null; |
| } |
| if (mClientPrivateKeyAndCertificateAlias != null) { |
| if (!mKeyStore.removeEntryFromKeyStore(mClientPrivateKeyAndCertificateAlias)) { |
| Log.e(TAG, "Failed to remove entry: " + mClientPrivateKeyAndCertificateAlias); |
| } |
| mClientPrivateKeyAndCertificateAlias = null; |
| } |
| if (mRemediationCaCertificateAlias != null) { |
| if (!mKeyStore.removeEntryFromKeyStore(mRemediationCaCertificateAlias)) { |
| Log.e(TAG, "Failed to remove entry: " + mRemediationCaCertificateAlias); |
| } |
| mRemediationCaCertificateAlias = null; |
| } |
| } |
| |
| /** |
| * Try to update the carrier ID according to the IMSI parameter of passpoint configuration. |
| * |
| * @return true if the carrier ID is updated, otherwise false. |
| */ |
| public boolean tryUpdateCarrierId() { |
| return mWifiCarrierInfoManager.tryUpdateCarrierIdForPasspoint(mConfig); |
| } |
| |
| private @Nullable String getMatchingSimImsi() { |
| String matchingSIMImsi = null; |
| if (mConfig.getCarrierId() != TelephonyManager.UNKNOWN_CARRIER_ID) { |
| matchingSIMImsi = mWifiCarrierInfoManager |
| .getMatchingImsi(mConfig.getCarrierId()); |
| } else { |
| // Get the IMSI and carrier ID of SIM card which match with the IMSI prefix from |
| // passpoint profile |
| Pair<String, Integer> imsiCarrierIdPair = mWifiCarrierInfoManager |
| .getMatchingImsiCarrierId(mConfig.getCredential().getSimCredential().getImsi()); |
| if (imsiCarrierIdPair != null) { |
| matchingSIMImsi = imsiCarrierIdPair.first; |
| mBestGuessCarrierId = imsiCarrierIdPair.second; |
| } |
| } |
| |
| return matchingSIMImsi; |
| } |
| |
| /** |
| * Return the matching status with the given AP, based on the ANQP elements from the AP. |
| * |
| * @param anqpElements ANQP elements from the AP |
| * @param roamingConsortiumFromAp Roaming Consortium information element from the AP |
| * @return {@link PasspointMatch} |
| */ |
| public PasspointMatch match(Map<ANQPElementType, ANQPElement> anqpElements, |
| RoamingConsortium roamingConsortiumFromAp) { |
| // If the profile requires a SIM credential, make sure that the installed SIM matches |
| String matchingSimImsi = null; |
| if (mConfig.getCredential().getSimCredential() != null) { |
| matchingSimImsi = getMatchingSimImsi(); |
| if (TextUtils.isEmpty(matchingSimImsi)) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "No SIM card with IMSI " |
| + mConfig.getCredential().getSimCredential().getImsi() |
| + " is installed, final match: " + PasspointMatch.None); |
| } |
| return PasspointMatch.None; |
| } |
| } |
| |
| // Match FQDN for Home provider or RCOI(s) for Roaming provider |
| // For SIM credential, the FQDN is in the format of wlan.mnc*.mcc*.3gppnetwork.org |
| PasspointMatch providerMatch = matchFqdnAndRcoi(anqpElements, roamingConsortiumFromAp, |
| matchingSimImsi); |
| |
| // 3GPP Network matching |
| if (providerMatch == PasspointMatch.None && ANQPMatcher.matchThreeGPPNetwork( |
| (ThreeGPPNetworkElement) anqpElements.get(ANQPElementType.ANQP3GPPNetwork), |
| mImsiParameter, matchingSimImsi)) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "Final RoamingProvider match with " |
| + anqpElements.get(ANQPElementType.ANQP3GPPNetwork)); |
| } |
| return PasspointMatch.RoamingProvider; |
| } |
| |
| // Perform NAI Realm matching |
| boolean realmMatch = ANQPMatcher.matchNAIRealm( |
| (NAIRealmElement) anqpElements.get(ANQPElementType.ANQPNAIRealm), |
| mConfig.getCredential().getRealm()); |
| |
| // In case of no realm match, return provider match as is. |
| if (!realmMatch) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "No NAI realm match, final match: " + providerMatch); |
| } |
| return providerMatch; |
| } |
| |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "NAI realm match with " + mConfig.getCredential().getRealm()); |
| } |
| |
| // Promote the provider match to RoamingProvider if provider match is not found, but NAI |
| // realm is matched. |
| if (providerMatch == PasspointMatch.None) { |
| providerMatch = PasspointMatch.RoamingProvider; |
| } |
| |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "Final match: " + providerMatch); |
| } |
| return providerMatch; |
| } |
| |
| /** |
| * Generate a WifiConfiguration based on the provider's configuration. The generated |
| * WifiConfiguration will include all the necessary credentials for network connection except |
| * the SSID, which should be added by the caller when the config is being used for network |
| * connection. |
| * |
| * @return {@link WifiConfiguration} |
| */ |
| public WifiConfiguration getWifiConfig() { |
| WifiConfiguration wifiConfig = new WifiConfiguration(); |
| wifiConfig.FQDN = mConfig.getHomeSp().getFqdn(); |
| wifiConfig.setPasspointUniqueId(mConfig.getUniqueId()); |
| if (mConfig.getHomeSp().getRoamingConsortiumOis() != null) { |
| wifiConfig.roamingConsortiumIds = Arrays.copyOf( |
| mConfig.getHomeSp().getRoamingConsortiumOis(), |
| mConfig.getHomeSp().getRoamingConsortiumOis().length); |
| } |
| if (mConfig.getUpdateIdentifier() != Integer.MIN_VALUE) { |
| // R2 profile, it needs to set updateIdentifier HS2.0 Indication element as PPS MO |
| // ID in Association Request. |
| wifiConfig.updateIdentifier = Integer.toString(mConfig.getUpdateIdentifier()); |
| if (isMeteredNetwork(mConfig)) { |
| wifiConfig.meteredOverride = WifiConfiguration.METERED_OVERRIDE_METERED; |
| } |
| } |
| wifiConfig.providerFriendlyName = mConfig.getHomeSp().getFriendlyName(); |
| wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP); |
| wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X); |
| int carrierId = mConfig.getCarrierId(); |
| if (carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) { |
| carrierId = mBestGuessCarrierId; |
| } |
| wifiConfig.carrierId = carrierId; |
| |
| // Set RSN only to tell wpa_supplicant that this network is for Passpoint. |
| wifiConfig.allowedProtocols.set(WifiConfiguration.Protocol.RSN); |
| |
| WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig(); |
| enterpriseConfig.setRealm(mConfig.getCredential().getRealm()); |
| enterpriseConfig.setDomainSuffixMatch(mConfig.getHomeSp().getFqdn()); |
| if (mConfig.getCredential().getUserCredential() != null) { |
| buildEnterpriseConfigForUserCredential(enterpriseConfig, |
| mConfig.getCredential().getUserCredential()); |
| setAnonymousIdentityToNaiRealm(enterpriseConfig, mConfig.getCredential().getRealm()); |
| } else if (mConfig.getCredential().getCertCredential() != null) { |
| buildEnterpriseConfigForCertCredential(enterpriseConfig); |
| setAnonymousIdentityToNaiRealm(enterpriseConfig, mConfig.getCredential().getRealm()); |
| } else { |
| buildEnterpriseConfigForSimCredential(enterpriseConfig, |
| mConfig.getCredential().getSimCredential()); |
| } |
| // If AAA server trusted names are specified, use it to replace HOME SP FQDN |
| // and use system CA regardless of provisioned CA certificate. |
| if (!ArrayUtils.isEmpty(mConfig.getAaaServerTrustedNames())) { |
| enterpriseConfig.setDomainSuffixMatch( |
| String.join(";", mConfig.getAaaServerTrustedNames())); |
| enterpriseConfig.setCaPath(SYSTEM_CA_STORE_PATH); |
| } |
| wifiConfig.enterpriseConfig = enterpriseConfig; |
| // PPS MO Credential/CheckAAAServerCertStatus node contains a flag which indicates |
| // if the mobile device needs to check the AAA server certificate's revocation status |
| // during EAP authentication. |
| if (mConfig.getCredential().getCheckAaaServerCertStatus()) { |
| // Check server certificate using OCSP (Online Certificate Status Protocol). |
| wifiConfig.enterpriseConfig.setOcsp(WifiEnterpriseConfig.OCSP_REQUIRE_CERT_STATUS); |
| } |
| wifiConfig.shared = mIsShared; |
| wifiConfig.fromWifiNetworkSuggestion = mIsFromSuggestion; |
| wifiConfig.ephemeral = mIsFromSuggestion; |
| wifiConfig.creatorName = mPackageName; |
| wifiConfig.creatorUid = mCreatorUid; |
| wifiConfig.trusted = mIsTrusted; |
| if (mConfig.isMacRandomizationEnabled()) { |
| wifiConfig.macRandomizationSetting = WifiConfiguration.RANDOMIZATION_PERSISTENT; |
| } else { |
| wifiConfig.macRandomizationSetting = WifiConfiguration.RANDOMIZATION_NONE; |
| } |
| wifiConfig.meteredOverride = mConfig.getMeteredOverride(); |
| return wifiConfig; |
| } |
| |
| /** |
| * @return true if provider is backed by a SIM credential. |
| */ |
| public boolean isSimCredential() { |
| return mConfig.getCredential().getSimCredential() != null; |
| } |
| |
| /** |
| * Convert a legacy {@link WifiConfiguration} representation of a Passpoint configuration to |
| * a {@link PasspointConfiguration}. This is used for migrating legacy Passpoint |
| * configuration (release N and older). |
| * |
| * @param wifiConfig The {@link WifiConfiguration} to convert |
| * @return {@link PasspointConfiguration} |
| */ |
| public static PasspointConfiguration convertFromWifiConfig(WifiConfiguration wifiConfig) { |
| PasspointConfiguration passpointConfig = new PasspointConfiguration(); |
| |
| // Setup HomeSP. |
| HomeSp homeSp = new HomeSp(); |
| if (TextUtils.isEmpty(wifiConfig.FQDN)) { |
| Log.e(TAG, "Missing FQDN"); |
| return null; |
| } |
| homeSp.setFqdn(wifiConfig.FQDN); |
| homeSp.setFriendlyName(wifiConfig.providerFriendlyName); |
| if (wifiConfig.roamingConsortiumIds != null) { |
| homeSp.setRoamingConsortiumOis(Arrays.copyOf( |
| wifiConfig.roamingConsortiumIds, wifiConfig.roamingConsortiumIds.length)); |
| } |
| passpointConfig.setHomeSp(homeSp); |
| passpointConfig.setCarrierId(wifiConfig.carrierId); |
| |
| // Setup Credential. |
| Credential credential = new Credential(); |
| credential.setRealm(wifiConfig.enterpriseConfig.getRealm()); |
| switch (wifiConfig.enterpriseConfig.getEapMethod()) { |
| case WifiEnterpriseConfig.Eap.TTLS: |
| credential.setUserCredential(buildUserCredentialFromEnterpriseConfig( |
| wifiConfig.enterpriseConfig)); |
| break; |
| case WifiEnterpriseConfig.Eap.TLS: |
| Credential.CertificateCredential certCred = new Credential.CertificateCredential(); |
| certCred.setCertType(Credential.CertificateCredential.CERT_TYPE_X509V3); |
| credential.setCertCredential(certCred); |
| break; |
| case WifiEnterpriseConfig.Eap.SIM: |
| credential.setSimCredential(buildSimCredentialFromEnterpriseConfig( |
| EAPConstants.EAP_SIM, wifiConfig.enterpriseConfig)); |
| break; |
| case WifiEnterpriseConfig.Eap.AKA: |
| credential.setSimCredential(buildSimCredentialFromEnterpriseConfig( |
| EAPConstants.EAP_AKA, wifiConfig.enterpriseConfig)); |
| break; |
| case WifiEnterpriseConfig.Eap.AKA_PRIME: |
| credential.setSimCredential(buildSimCredentialFromEnterpriseConfig( |
| EAPConstants.EAP_AKA_PRIME, wifiConfig.enterpriseConfig)); |
| break; |
| default: |
| Log.e(TAG, "Unsupported EAP method: " |
| + wifiConfig.enterpriseConfig.getEapMethod()); |
| return null; |
| } |
| if (credential.getUserCredential() == null && credential.getCertCredential() == null |
| && credential.getSimCredential() == null) { |
| Log.e(TAG, "Missing credential"); |
| return null; |
| } |
| passpointConfig.setCredential(credential); |
| |
| return passpointConfig; |
| } |
| |
| @Override |
| public boolean equals(Object thatObject) { |
| if (this == thatObject) { |
| return true; |
| } |
| if (!(thatObject instanceof PasspointProvider)) { |
| return false; |
| } |
| PasspointProvider that = (PasspointProvider) thatObject; |
| return mProviderId == that.mProviderId |
| && (mCaCertificateAliases == null ? that.mCaCertificateAliases == null |
| : mCaCertificateAliases.equals(that.mCaCertificateAliases)) |
| && TextUtils.equals(mClientPrivateKeyAndCertificateAlias, |
| that.mClientPrivateKeyAndCertificateAlias) |
| && (mConfig == null ? that.mConfig == null : mConfig.equals(that.mConfig)) |
| && TextUtils.equals(mRemediationCaCertificateAlias, |
| that.mRemediationCaCertificateAlias); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mProviderId, mCaCertificateAliases, |
| mClientPrivateKeyAndCertificateAlias, mConfig, mRemediationCaCertificateAlias); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| builder.append("ProviderId: ").append(mProviderId).append("\n"); |
| builder.append("CreatorUID: ").append(mCreatorUid).append("\n"); |
| builder.append("Best guess Carrier ID: ").append(mBestGuessCarrierId).append("\n"); |
| builder.append("Ever connected: ").append(mHasEverConnected).append("\n"); |
| builder.append("Shared: ").append(mIsShared).append("\n"); |
| builder.append("Suggestion: ").append(mIsFromSuggestion).append("\n"); |
| builder.append("Trusted: ").append(mIsTrusted).append("\n"); |
| |
| if (mPackageName != null) { |
| builder.append("PackageName: ").append(mPackageName).append("\n"); |
| } |
| builder.append("Configuration Begin ---\n"); |
| builder.append(mConfig); |
| builder.append("Configuration End ---\n"); |
| builder.append("WifiConfiguration Begin ---\n"); |
| builder.append(getWifiConfig()); |
| builder.append("WifiConfiguration End ---\n"); |
| return builder.toString(); |
| } |
| |
| /** |
| * Retrieve the client certificate from the certificates chain. The certificate |
| * with the matching SHA256 digest is the client certificate. |
| * |
| * @param certChain The client certificates chain |
| * @param expectedSha256Fingerprint The expected SHA256 digest of the client certificate |
| * @return {@link java.security.cert.X509Certificate} |
| */ |
| private static X509Certificate getClientCertificate(X509Certificate[] certChain, |
| byte[] expectedSha256Fingerprint) { |
| if (certChain == null) { |
| return null; |
| } |
| try { |
| MessageDigest digester = MessageDigest.getInstance("SHA-256"); |
| for (X509Certificate certificate : certChain) { |
| digester.reset(); |
| byte[] fingerprint = digester.digest(certificate.getEncoded()); |
| if (Arrays.equals(expectedSha256Fingerprint, fingerprint)) { |
| return certificate; |
| } |
| } |
| } catch (CertificateEncodingException | NoSuchAlgorithmException e) { |
| return null; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Determines the Passpoint network is a metered network. |
| * |
| * Expiration date -> non-metered |
| * Data limit -> metered |
| * Time usage limit -> metered |
| * @param passpointConfig instance of {@link PasspointConfiguration} |
| * @return {@code true} if the network is a metered network, {@code false} otherwise. |
| */ |
| private boolean isMeteredNetwork(PasspointConfiguration passpointConfig) { |
| if (passpointConfig == null) return false; |
| |
| // If DataLimit is zero, there is unlimited data usage for the account. |
| // If TimeLimit is zero, there is unlimited time usage for the account. |
| return passpointConfig.getUsageLimitDataLimit() > 0 |
| || passpointConfig.getUsageLimitTimeLimitInMinutes() > 0; |
| } |
| |
| /** |
| * Match given OIs to the Roaming Consortium OIs |
| * |
| * @param providerOis Provider OIs to match against |
| * @param roamingConsortiumElement RCOIs in the ANQP element |
| * @param roamingConsortiumFromAp RCOIs in the AP scan results |
| * @param matchAll Indicates if all providerOis must match the RCOIs elements |
| * @return {@code true} if there is a match, {@code false} otherwise. |
| */ |
| private boolean matchOis(long[] providerOis, |
| RoamingConsortiumElement roamingConsortiumElement, |
| RoamingConsortium roamingConsortiumFromAp, |
| boolean matchAll) { |
| |
| |
| // ANQP Roaming Consortium OI matching. |
| if (ANQPMatcher.matchRoamingConsortium(roamingConsortiumElement, providerOis, matchAll)) { |
| if (mVerboseLoggingEnabled) { |
| Log.e(TAG, "ANQP RCOI match " + roamingConsortiumElement); |
| } |
| return true; |
| } |
| |
| // AP Roaming Consortium OI matching. |
| long[] apRoamingConsortiums = roamingConsortiumFromAp.getRoamingConsortiums(); |
| if (apRoamingConsortiums == null || providerOis == null) { |
| return false; |
| } |
| // Roaming Consortium OI information element matching. |
| for (long apOi: apRoamingConsortiums) { |
| boolean matched = false; |
| for (long providerOi: providerOis) { |
| if (apOi == providerOi) { |
| if (mVerboseLoggingEnabled) { |
| Log.e(TAG, "AP RCOI match: " + apOi); |
| } |
| if (!matchAll) { |
| return true; |
| } else { |
| matched = true; |
| break; |
| } |
| } |
| } |
| if (matchAll && !matched) { |
| return false; |
| } |
| } |
| return matchAll; |
| } |
| |
| /** |
| * Perform a provider match based on the given ANQP elements for FQDN and RCOI |
| * |
| * @param anqpElements List of ANQP elements |
| * @param roamingConsortiumFromAp Roaming Consortium information element from the AP |
| * @param matchingSIMImsi Installed SIM IMSI that matches the SIM credential ANQP element |
| * @return {@link PasspointMatch} |
| */ |
| private PasspointMatch matchFqdnAndRcoi(Map<ANQPElementType, ANQPElement> anqpElements, |
| RoamingConsortium roamingConsortiumFromAp, String matchingSIMImsi) { |
| // Domain name matching. |
| if (ANQPMatcher.matchDomainName( |
| (DomainNameElement) anqpElements.get(ANQPElementType.ANQPDomName), |
| mConfig.getHomeSp().getFqdn(), mImsiParameter, matchingSIMImsi)) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "Domain name " + mConfig.getHomeSp().getFqdn() |
| + " match: HomeProvider"); |
| } |
| return PasspointMatch.HomeProvider; |
| } |
| |
| // Other Home Partners matching. |
| if (mConfig.getHomeSp().getOtherHomePartners() != null) { |
| for (String otherHomePartner : mConfig.getHomeSp().getOtherHomePartners()) { |
| if (ANQPMatcher.matchDomainName( |
| (DomainNameElement) anqpElements.get(ANQPElementType.ANQPDomName), |
| otherHomePartner, null, null)) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "Other Home Partner " + otherHomePartner |
| + " match: HomeProvider"); |
| } |
| return PasspointMatch.HomeProvider; |
| } |
| } |
| } |
| |
| // HomeOI matching |
| if (mConfig.getHomeSp().getMatchAllOis() != null) { |
| // Ensure that every HomeOI whose corresponding HomeOIRequired value is true shall match |
| // an OI in the Roaming Consortium advertised by the hotspot operator. |
| if (matchOis(mConfig.getHomeSp().getMatchAllOis(), (RoamingConsortiumElement) |
| anqpElements.get(ANQPElementType.ANQPRoamingConsortium), |
| roamingConsortiumFromAp, true)) { |
| if (mVerboseLoggingEnabled) { |
| Log.e(TAG, "All HomeOI RCOI match: HomeProvider"); |
| } |
| return PasspointMatch.HomeProvider; |
| } |
| } else if (mConfig.getHomeSp().getMatchAnyOis() != null) { |
| // Ensure that any HomeOI whose corresponding HomeOIRequired value is false shall match |
| // an OI in the Roaming Consortium advertised by the hotspot operator. |
| if (matchOis(mConfig.getHomeSp().getMatchAnyOis(), (RoamingConsortiumElement) |
| anqpElements.get(ANQPElementType.ANQPRoamingConsortium), |
| roamingConsortiumFromAp, false)) { |
| if (mVerboseLoggingEnabled) { |
| Log.e(TAG, "Any HomeOI RCOI match: HomeProvider"); |
| } |
| return PasspointMatch.HomeProvider; |
| } |
| } |
| |
| // Roaming Consortium OI matching. |
| if (matchOis(mConfig.getHomeSp().getRoamingConsortiumOis(), (RoamingConsortiumElement) |
| anqpElements.get(ANQPElementType.ANQPRoamingConsortium), |
| roamingConsortiumFromAp, false)) { |
| if (mVerboseLoggingEnabled) { |
| Log.e(TAG, "ANQP RCOI match: RoamingProvider"); |
| } |
| return PasspointMatch.RoamingProvider; |
| } |
| if (mVerboseLoggingEnabled) { |
| Log.e(TAG, "No domain name or RCOI match"); |
| } |
| return PasspointMatch.None; |
| } |
| |
| /** |
| * Fill in WifiEnterpriseConfig with information from an user credential. |
| * |
| * @param config Instance of {@link WifiEnterpriseConfig} |
| * @param credential Instance of {@link UserCredential} |
| */ |
| private void buildEnterpriseConfigForUserCredential(WifiEnterpriseConfig config, |
| Credential.UserCredential credential) { |
| String password; |
| try { |
| byte[] pwOctets = Base64.decode(credential.getPassword(), Base64.DEFAULT); |
| password = new String(pwOctets, StandardCharsets.UTF_8); |
| } catch (IllegalArgumentException e) { |
| Log.w(TAG, "Failed to decode password"); |
| password = credential.getPassword(); |
| } |
| config.setEapMethod(WifiEnterpriseConfig.Eap.TTLS); |
| config.setIdentity(credential.getUsername()); |
| config.setPassword(password); |
| if (!ArrayUtils.isEmpty(mCaCertificateAliases)) { |
| config.setCaCertificateAliases(mCaCertificateAliases.toArray(new String[0])); |
| } else { |
| config.setCaPath(SYSTEM_CA_STORE_PATH); |
| } |
| int phase2Method = WifiEnterpriseConfig.Phase2.NONE; |
| switch (credential.getNonEapInnerMethod()) { |
| case Credential.UserCredential.AUTH_METHOD_PAP: |
| phase2Method = WifiEnterpriseConfig.Phase2.PAP; |
| break; |
| case Credential.UserCredential.AUTH_METHOD_MSCHAP: |
| phase2Method = WifiEnterpriseConfig.Phase2.MSCHAP; |
| break; |
| case Credential.UserCredential.AUTH_METHOD_MSCHAPV2: |
| phase2Method = WifiEnterpriseConfig.Phase2.MSCHAPV2; |
| break; |
| default: |
| // Should never happen since this is already validated when the provider is |
| // added. |
| Log.wtf(TAG, "Unsupported Auth: " + credential.getNonEapInnerMethod()); |
| break; |
| } |
| config.setPhase2Method(phase2Method); |
| } |
| |
| /** |
| * Fill in WifiEnterpriseConfig with information from a certificate credential. |
| * |
| * @param config Instance of {@link WifiEnterpriseConfig} |
| */ |
| private void buildEnterpriseConfigForCertCredential(WifiEnterpriseConfig config) { |
| config.setEapMethod(WifiEnterpriseConfig.Eap.TLS); |
| config.setClientCertificateAlias(mClientPrivateKeyAndCertificateAlias); |
| if (!ArrayUtils.isEmpty(mCaCertificateAliases)) { |
| config.setCaCertificateAliases(mCaCertificateAliases.toArray(new String[0])); |
| } else { |
| config.setCaPath(SYSTEM_CA_STORE_PATH); |
| } |
| } |
| |
| /** |
| * Fill in WifiEnterpriseConfig with information from a SIM credential. |
| * |
| * @param config Instance of {@link WifiEnterpriseConfig} |
| * @param credential Instance of {@link SimCredential} |
| */ |
| private void buildEnterpriseConfigForSimCredential(WifiEnterpriseConfig config, |
| Credential.SimCredential credential) { |
| int eapMethod = WifiEnterpriseConfig.Eap.NONE; |
| switch(credential.getEapType()) { |
| case EAPConstants.EAP_SIM: |
| eapMethod = WifiEnterpriseConfig.Eap.SIM; |
| break; |
| case EAPConstants.EAP_AKA: |
| eapMethod = WifiEnterpriseConfig.Eap.AKA; |
| break; |
| case EAPConstants.EAP_AKA_PRIME: |
| eapMethod = WifiEnterpriseConfig.Eap.AKA_PRIME; |
| break; |
| default: |
| // Should never happen since this is already validated when the provider is |
| // added. |
| Log.wtf(TAG, "Unsupported EAP Method: " + credential.getEapType()); |
| break; |
| } |
| config.setEapMethod(eapMethod); |
| config.setPlmn(credential.getImsi()); |
| } |
| |
| private static void setAnonymousIdentityToNaiRealm(WifiEnterpriseConfig config, String realm) { |
| /** |
| * Set WPA supplicant's anonymous identity field to a string containing the NAI realm, so |
| * that this value will be sent to the EAP server as part of the EAP-Response/ Identity |
| * packet. WPA supplicant will reset this field after using it for the EAP-Response/Identity |
| * packet, and revert to using the (real) identity field for subsequent transactions that |
| * request an identity (e.g. in EAP-TTLS). |
| * |
| * This NAI realm value (the portion of the identity after the '@') is used to tell the |
| * AAA server which AAA/H to forward packets to. The hardcoded username, "anonymous", is a |
| * placeholder that is not used--it is set to this value by convention. See Section 5.1 of |
| * RFC3748 for more details. |
| * |
| * NOTE: we do not set this value for EAP-SIM/AKA/AKA', since the EAP server expects the |
| * EAP-Response/Identity packet to contain an actual, IMSI-based identity, in order to |
| * identify the device. |
| */ |
| config.setAnonymousIdentity("anonymous@" + realm); |
| } |
| |
| /** |
| * Helper function for creating a |
| * {@link android.net.wifi.hotspot2.pps.Credential.UserCredential} from the given |
| * {@link WifiEnterpriseConfig} |
| * |
| * @param config The enterprise configuration containing the credential |
| * @return {@link android.net.wifi.hotspot2.pps.Credential.UserCredential} |
| */ |
| private static Credential.UserCredential buildUserCredentialFromEnterpriseConfig( |
| WifiEnterpriseConfig config) { |
| Credential.UserCredential userCredential = new Credential.UserCredential(); |
| userCredential.setEapType(EAPConstants.EAP_TTLS); |
| |
| if (TextUtils.isEmpty(config.getIdentity())) { |
| Log.e(TAG, "Missing username for user credential"); |
| return null; |
| } |
| userCredential.setUsername(config.getIdentity()); |
| |
| if (TextUtils.isEmpty(config.getPassword())) { |
| Log.e(TAG, "Missing password for user credential"); |
| return null; |
| } |
| String encodedPassword = |
| new String(Base64.encode(config.getPassword().getBytes(StandardCharsets.UTF_8), |
| Base64.DEFAULT), StandardCharsets.UTF_8); |
| userCredential.setPassword(encodedPassword); |
| |
| switch(config.getPhase2Method()) { |
| case WifiEnterpriseConfig.Phase2.PAP: |
| userCredential.setNonEapInnerMethod(Credential.UserCredential.AUTH_METHOD_PAP); |
| break; |
| case WifiEnterpriseConfig.Phase2.MSCHAP: |
| userCredential.setNonEapInnerMethod(Credential.UserCredential.AUTH_METHOD_MSCHAP); |
| break; |
| case WifiEnterpriseConfig.Phase2.MSCHAPV2: |
| userCredential.setNonEapInnerMethod(Credential.UserCredential.AUTH_METHOD_MSCHAPV2); |
| break; |
| default: |
| Log.e(TAG, "Unsupported phase2 method for TTLS: " + config.getPhase2Method()); |
| return null; |
| } |
| return userCredential; |
| } |
| |
| /** |
| * Helper function for creating a |
| * {@link android.net.wifi.hotspot2.pps.Credential.SimCredential} from the given |
| * {@link WifiEnterpriseConfig} |
| * |
| * @param eapType The EAP type of the SIM credential |
| * @param config The enterprise configuration containing the credential |
| * @return {@link android.net.wifi.hotspot2.pps.Credential.SimCredential} |
| */ |
| private static Credential.SimCredential buildSimCredentialFromEnterpriseConfig( |
| int eapType, WifiEnterpriseConfig config) { |
| Credential.SimCredential simCredential = new Credential.SimCredential(); |
| if (TextUtils.isEmpty(config.getPlmn())) { |
| Log.e(TAG, "Missing IMSI for SIM credential"); |
| return null; |
| } |
| simCredential.setImsi(config.getPlmn()); |
| simCredential.setEapType(eapType); |
| return simCredential; |
| } |
| |
| /** |
| * Enable verbose logging |
| * @param verbose more than 0 enables verbose logging |
| */ |
| public void enableVerboseLogging(int verbose) { |
| mVerboseLoggingEnabled = (verbose > 0) ? true : false; |
| } |
| } |