/* * 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 static com.android.server.wifi.MboOceConstants.DEFAULT_BLOCKLIST_DURATION_MS; import android.annotation.Nullable; import android.net.wifi.EAPConstants; import android.net.wifi.ScanResult; import android.net.wifi.SecurityParams; 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.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import android.util.Pair; import com.android.modules.utils.build.SdkLevel; import com.android.server.wifi.Clock; 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 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; private final Clock mClock; private long mReauthDelay = 0; private List mBlockedBssids = new ArrayList<>(); private String mAnonymousIdentity = null; private String mConnectChoice = null; private int mConnectChoiceRssi = 0; public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, WifiCarrierInfoManager wifiCarrierInfoManager, long providerId, int creatorUid, String packageName, boolean isFromSuggestion, Clock clock) { this(config, keyStore, wifiCarrierInfoManager, providerId, creatorUid, packageName, isFromSuggestion, null, null, null, false, false, clock); } public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, WifiCarrierInfoManager wifiCarrierInfoManager, long providerId, int creatorUid, String packageName, boolean isFromSuggestion, List caCertificateAliases, String clientPrivateKeyAndCertificateAlias, String remediationCaCertificateAlias, boolean hasEverConnected, boolean isShared, Clock clock) { // 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; mClock = clock; // 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; } /** * Set Anonymous Identity for passpoint network. */ public void setAnonymousIdentity(String anonymousIdentity) { mAnonymousIdentity = anonymousIdentity; } public String getAnonymousIdentity() { return mAnonymousIdentity; } public PasspointConfiguration getConfig() { // Return a copy of the configuration to avoid it being updated by others. return new PasspointConfiguration(mConfig); } public List 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.getSubscriptionId() != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { matchingSIMImsi = mWifiCarrierInfoManager .getMatchingImsiBySubId(mConfig.getSubscriptionId()); } else if (mConfig.getCarrierId() != TelephonyManager.UNKNOWN_CARRIER_ID) { matchingSIMImsi = mWifiCarrierInfoManager.getMatchingImsiBySubId( mWifiCarrierInfoManager.getMatchingSubId(mConfig.getCarrierId())); } else { // Get the IMSI and carrier ID of SIM card which match with the IMSI prefix from // passpoint profile Pair 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 * @param scanResult Latest Scan result * @return {@link PasspointMatch} */ public PasspointMatch match(Map anqpElements, RoamingConsortium roamingConsortiumFromAp, ScanResult scanResult) { if (isProviderBlocked(scanResult)) { if (mVerboseLoggingEnabled) { Log.d(TAG, "Provider " + mConfig.getServiceFriendlyName() + " is blocked because reauthentication delay duration is still in" + " progess"); } return PasspointMatch.None; } // 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(); List paramsList = Arrays.asList( SecurityParams.createSecurityParamsBySecurityType( WifiConfiguration.SECURITY_TYPE_PASSPOINT_R1_R2), SecurityParams.createSecurityParamsBySecurityType( WifiConfiguration.SECURITY_TYPE_PASSPOINT_R3)); wifiConfig.setSecurityParams(paramsList); 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(); int carrierId = mConfig.getCarrierId(); if (carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) { carrierId = mBestGuessCarrierId; } wifiConfig.carrierId = carrierId; wifiConfig.subscriptionId = mConfig.getSubscriptionId() == SubscriptionManager.INVALID_SUBSCRIPTION_ID ? mWifiCarrierInfoManager.getMatchingSubId(carrierId) : mConfig.getSubscriptionId(); wifiConfig.carrierMerged = mConfig.isCarrierMerged(); wifiConfig.oemPaid = mConfig.isOemPaid(); wifiConfig.oemPrivate = mConfig.isOemPrivate(); 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()); enterpriseConfig.setAnonymousIdentity(mAnonymousIdentity); } // 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); } if (SdkLevel.isAtLeastS()) { enterpriseConfig.setDecoratedIdentityPrefix(mConfig.getDecoratedIdentityPrefix()); } 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.allowAutojoin = isAutojoinEnabled(); wifiConfig.shared = mIsShared; wifiConfig.fromWifiNetworkSuggestion = mIsFromSuggestion; wifiConfig.ephemeral = mIsFromSuggestion; wifiConfig.creatorName = mPackageName; wifiConfig.creatorUid = mCreatorUid; wifiConfig.trusted = mIsTrusted; if (mConfig.isMacRandomizationEnabled()) { if (mConfig.isEnhancedMacRandomizationEnabled()) { wifiConfig.macRandomizationSetting = WifiConfiguration.RANDOMIZATION_NON_PERSISTENT; } else { wifiConfig.macRandomizationSetting = WifiConfiguration.RANDOMIZATION_PERSISTENT; } } else { wifiConfig.macRandomizationSetting = WifiConfiguration.RANDOMIZATION_NONE; } wifiConfig.meteredOverride = mConfig.getMeteredOverride(); wifiConfig.getNetworkSelectionStatus().setConnectChoice(mConnectChoice); wifiConfig.getNetworkSelectionStatus().setConnectChoiceRssi(mConnectChoiceRssi); 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"); builder.append("UserConnectChoice: ").append(mConnectChoice).append("\n"); if (mReauthDelay != 0 && mClock.getElapsedSinceBootMillis() < mReauthDelay) { builder.append("Reauth delay remaining (seconds): ") .append((mReauthDelay - mClock.getElapsedSinceBootMillis()) / 1000) .append("\n"); if (mBlockedBssids.isEmpty()) { builder.append("ESS is blocked").append("\n"); } else { builder.append("List of blocked BSSIDs:").append("\n"); for (String bssid : mBlockedBssids) { builder.append(bssid).append("\n"); } } } else { builder.append("Provider is not blocked").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 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 enables verbose logging */ public void enableVerboseLogging(boolean verbose) { mVerboseLoggingEnabled = verbose; } /** * Block a BSS or ESS following a Deauthentication-Imminent WNM-Notification * * @param bssid BSSID of the source AP * @param isEss true: Block ESS, false: Block BSS * @param delayInSeconds Delay duration in seconds */ public void blockBssOrEss(long bssid, boolean isEss, int delayInSeconds) { if (delayInSeconds < 0 || bssid == 0) { return; } mReauthDelay = mClock.getElapsedSinceBootMillis(); if (delayInSeconds == 0) { // Section 3.2.1.2 in the specification defines that a Re-Auth Delay field // value of 0 means the delay value is chosen by the mobile device. mReauthDelay += DEFAULT_BLOCKLIST_DURATION_MS; } else { mReauthDelay += (delayInSeconds * 1000); } if (isEss) { // Deauth-imminent for the entire ESS, do not try to reauthenticate until the delay // is over. Clear the list of blocked BSSIDs. mBlockedBssids.clear(); } else { // Add this MAC address to the list of blocked BSSIDs. mBlockedBssids.add(Utils.macToString(bssid)); } } /** * Clear a block from a Passpoint provider. Used when Wi-Fi state is cleared, for example, * when turning Wi-Fi off. */ public void clearProviderBlock() { mReauthDelay = 0; mBlockedBssids.clear(); } /** * Checks if this provider is blocked or if there are any BSSes blocked * * @param scanResult Latest scan result * @return true if blocked, false otherwise */ private boolean isProviderBlocked(ScanResult scanResult) { if (mReauthDelay == 0) { return false; } if (mClock.getElapsedSinceBootMillis() >= mReauthDelay) { // Provider was blocked, but the delay duration have passed mReauthDelay = 0; mBlockedBssids.clear(); return false; } // Empty means the entire ESS is blocked if (mBlockedBssids.isEmpty() || mBlockedBssids.contains(scanResult.BSSID)) { return true; } // Trying to associate to another BSS in the ESS return false; } /** * Set the user connect choice on the passpoint network. * @param choice The {@link WifiConfiguration#getProfileKey()} of the user connect * network. * @param rssi The signal strength of the network. */ public void setUserConnectChoice(String choice, int rssi) { mConnectChoice = choice; mConnectChoiceRssi = rssi; } public String getConnectChoice() { return mConnectChoice; } public int getConnectChoiceRssi() { return mConnectChoiceRssi; } }