/* * 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.app.AppOpsManager.OPSTR_CHANGE_WIFI_STATE; import static android.net.wifi.WifiConfiguration.MeteredOverride; import static android.net.wifi.WifiInfo.DEFAULT_MAC_ADDRESS; import static java.security.cert.PKIXReason.NO_TRUST_ANCHOR; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AppOpsManager; import android.content.Context; import android.net.MacAddress; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.WifiManager; import android.net.wifi.WifiSsid; import android.net.wifi.hotspot2.IProvisioningCallback; import android.net.wifi.hotspot2.OsuProvider; import android.net.wifi.hotspot2.PasspointConfiguration; import android.os.Looper; import android.os.Process; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.build.SdkLevel; import com.android.server.wifi.Clock; import com.android.server.wifi.MacAddressUtil; import com.android.server.wifi.NetworkUpdateResult; import com.android.server.wifi.RunnerHandler; import com.android.server.wifi.WifiCarrierInfoManager; import com.android.server.wifi.WifiConfigManager; import com.android.server.wifi.WifiConfigStore; import com.android.server.wifi.WifiInjector; import com.android.server.wifi.WifiKeyStore; import com.android.server.wifi.WifiMetrics; import com.android.server.wifi.WifiNative; import com.android.server.wifi.WifiSettingsStore; import com.android.server.wifi.hotspot2.anqp.ANQPElement; import com.android.server.wifi.hotspot2.anqp.Constants; import com.android.server.wifi.hotspot2.anqp.HSOsuProvidersElement; import com.android.server.wifi.hotspot2.anqp.I18Name; import com.android.server.wifi.hotspot2.anqp.OsuProviderInfo; import com.android.server.wifi.hotspot2.anqp.VenueNameElement; import com.android.server.wifi.hotspot2.anqp.VenueUrlElement; import com.android.server.wifi.proto.nano.WifiMetricsProto.UserActionEvent; import com.android.server.wifi.util.InformationElementUtil; import com.android.server.wifi.util.WifiPermissionsUtil; import java.io.IOException; import java.io.PrintWriter; import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.cert.CertPath; import java.security.cert.CertPathValidator; import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateFactory; import java.security.cert.PKIXParameters; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** * This class provides the APIs to manage Passpoint provider configurations. * It deals with the following: * - Maintaining a list of configured Passpoint providers for provider matching. * - Persisting the providers configurations to store when required. * - matching Passpoint providers based on the scan results * - Supporting WifiManager Public API calls: * > addOrUpdatePasspointConfiguration() * > removePasspointConfiguration() * > getPasspointConfigurations() * * The provider matching requires obtaining additional information from the AP (ANQP elements). * The ANQP elements will be cached using {@link AnqpCache} to avoid unnecessary requests. * * NOTE: These API's are not thread safe and should only be used from the main Wifi thread. */ public class PasspointManager { private static final String TAG = "PasspointManager"; /** * Handle for the current {@link PasspointManager} instance. This is needed to avoid * circular dependency with the WifiConfigManger, it will be used for adding the * legacy Passpoint configurations. * * This can be eliminated once we can remove the dependency for WifiConfigManager (for * triggering config store write) from this class. */ private static PasspointManager sPasspointManager; private final PasspointEventHandler mPasspointEventHandler; private final WifiInjector mWifiInjector; private final RunnerHandler mHandler; private final WifiKeyStore mKeyStore; private final PasspointObjectFactory mObjectFactory; private final Map mProviders; private final AnqpCache mAnqpCache; private final ANQPRequestManager mAnqpRequestManager; private final WifiConfigManager mWifiConfigManager; private final WifiMetrics mWifiMetrics; private final PasspointProvisioner mPasspointProvisioner; private PasspointNetworkNominateHelper mPasspointNetworkNominateHelper; private final AppOpsManager mAppOps; private final WifiCarrierInfoManager mWifiCarrierInfoManager; private final MacAddressUtil mMacAddressUtil; private final Clock mClock; private final WifiPermissionsUtil mWifiPermissionsUtil; private final WifiSettingsStore mSettingsStore; private final boolean mIsLowMemory; /** * Map of package name of an app to the app ops changed listener for the app. */ private final Map mAppOpsChangedListenerPerApp = new HashMap<>(); // Counter used for assigning unique identifier to each provider. private long mProviderIndex; private boolean mVerboseLoggingEnabled = false; // Set default value to false before receiving boot completed event. private boolean mEnabled = false; private class CallbackHandler implements PasspointEventHandler.Callbacks { private final Context mContext; CallbackHandler(Context context) { mContext = context; } @Override public void onANQPResponse(long bssid, Map anqpElements) { if (mVerboseLoggingEnabled) { Log.d(TAG, "ANQP response received from BSSID " + Utils.macToString(bssid) + " - List of ANQP elements:"); int i = 0; if (anqpElements != null) { for (Constants.ANQPElementType type : anqpElements.keySet()) { Log.d(TAG, "#" + i++ + ": " + type); } } } // Notify request manager for the completion of a request. ANQPNetworkKey anqpKey = mAnqpRequestManager.onRequestCompleted(bssid, anqpElements != null); if (anqpElements == null || anqpKey == null) { // Query failed or the request wasn't originated from us (not tracked by the // request manager). Nothing to be done. return; } if (anqpElements.containsKey(Constants.ANQPElementType.ANQPVenueUrl)) { // Venue URL ANQP is requested and received only after the network is connected mWifiMetrics.incrementTotalNumberOfPasspointConnectionsWithVenueUrl(); } // Add new entry to the cache. mAnqpCache.addOrUpdateEntry(anqpKey, anqpElements); } @Override public void onIconResponse(long bssid, String fileName, byte[] data) { // Empty } @Override public void onWnmFrameReceived(WnmData event) { // Empty } } /** * Data provider for the Passpoint configuration store data * {@link PasspointConfigUserStoreData}. */ private class UserDataSourceHandler implements PasspointConfigUserStoreData.DataSource { @Override public List getProviders() { List providers = new ArrayList<>(); for (Map.Entry entry : mProviders.entrySet()) { providers.add(entry.getValue()); } return providers; } @Override public void setProviders(List providers) { mProviders.clear(); for (PasspointProvider provider : providers) { provider.enableVerboseLogging(mVerboseLoggingEnabled); mProviders.put(provider.getConfig().getUniqueId(), provider); if (provider.getPackageName() != null) { startTrackingAppOpsChange(provider.getPackageName(), provider.getCreatorUid()); } } } } /** * Data provider for the Passpoint configuration store data * {@link PasspointConfigSharedStoreData}. */ private class SharedDataSourceHandler implements PasspointConfigSharedStoreData.DataSource { @Override public long getProviderIndex() { return mProviderIndex; } @Override public void setProviderIndex(long providerIndex) { mProviderIndex = providerIndex; } } /** * Listener for app-ops changes for apps to remove the corresponding Passpoint profiles. */ private final class AppOpsChangedListener implements AppOpsManager.OnOpChangedListener { private final String mPackageName; private final int mUid; AppOpsChangedListener(@NonNull String packageName, int uid) { mPackageName = packageName; mUid = uid; } @Override public void onOpChanged(String op, String packageName) { mHandler.post(() -> { if (!mPackageName.equals(packageName)) return; if (!OPSTR_CHANGE_WIFI_STATE.equals(op)) return; // Ensures the uid to package mapping is still correct. try { mAppOps.checkPackage(mUid, mPackageName); } catch (SecurityException e) { Log.wtf(TAG, "Invalid uid/package" + packageName); return; } if (mAppOps.unsafeCheckOpNoThrow(OPSTR_CHANGE_WIFI_STATE, mUid, mPackageName) == AppOpsManager.MODE_IGNORED) { Log.i(TAG, "User disallowed change wifi state for " + packageName); // Removes the profiles installed by the app from database. removePasspointProviderWithPackage(mPackageName); } }); } } private class OnNetworkUpdateListener implements WifiConfigManager.OnNetworkUpdateListener { @Override public void onConnectChoiceSet(@NonNull List networks, String choiceKey, int rssi) { onUserConnectChoiceSet(networks, choiceKey, rssi); } @Override public void onConnectChoiceRemoved(@NonNull String choiceKey) { if (choiceKey == null) { return; } onUserConnectChoiceRemove(choiceKey); } } private void onUserConnectChoiceRemove(String choiceKey) { AtomicBoolean modified = new AtomicBoolean(false); mProviders.values().forEach(provider -> { if (TextUtils.equals(provider.getConnectChoice(), choiceKey)) { provider.setUserConnectChoice(null, 0); if (!modified.get()) { modified.set(true); } } }); if (modified.get()) { mWifiConfigManager.saveToStore(); } } private void onUserConnectChoiceSet(List networks, String choiceKey, int rssi) { for (WifiConfiguration config : networks) { PasspointProvider provider = mProviders.get(config.getProfileKey()); if (provider != null) { provider.setUserConnectChoice(choiceKey, rssi); } } PasspointProvider provider = mProviders.get(choiceKey); if (provider != null) { provider.setUserConnectChoice(null, 0); } mWifiConfigManager.saveToStore(); } /** * Remove all Passpoint profiles installed by the app that has been disabled or uninstalled. * * @param packageName Package name of the app to remove the corresponding Passpoint profiles. */ public void removePasspointProviderWithPackage(@NonNull String packageName) { stopTrackingAppOpsChange(packageName); for (PasspointProvider provider : getPasspointProviderWithPackage(packageName)) { removeProvider(Process.WIFI_UID /* ignored */, true, provider.getConfig().getUniqueId(), null); } } private List getPasspointProviderWithPackage( @NonNull String packageName) { List providers = new ArrayList<>(mProviders.values()); providers.removeIf(provider -> !TextUtils.equals(packageName, provider.getPackageName())); return providers; } private void startTrackingAppOpsChange(@NonNull String packageName, int uid) { // The package is already registered. if (mAppOpsChangedListenerPerApp.containsKey(packageName)) return; AppOpsChangedListener appOpsChangedListener = new AppOpsChangedListener(packageName, uid); mAppOps.startWatchingMode(OPSTR_CHANGE_WIFI_STATE, packageName, appOpsChangedListener); mAppOpsChangedListenerPerApp.put(packageName, appOpsChangedListener); } private void stopTrackingAppOpsChange(@NonNull String packageName) { AppOpsChangedListener appOpsChangedListener = mAppOpsChangedListenerPerApp.remove( packageName); if (appOpsChangedListener == null) { Log.i(TAG, "No app ops listener found for " + packageName); return; } mAppOps.stopWatchingMode(appOpsChangedListener); } public PasspointManager(Context context, WifiInjector wifiInjector, RunnerHandler handler, WifiNative wifiNative, WifiKeyStore keyStore, Clock clock, PasspointObjectFactory objectFactory, WifiConfigManager wifiConfigManager, WifiConfigStore wifiConfigStore, WifiSettingsStore wifiSettingsStore, WifiMetrics wifiMetrics, WifiCarrierInfoManager wifiCarrierInfoManager, MacAddressUtil macAddressUtil, WifiPermissionsUtil wifiPermissionsUtil) { mPasspointEventHandler = objectFactory.makePasspointEventHandler(wifiInjector, new CallbackHandler(context)); mWifiInjector = wifiInjector; mHandler = handler; mKeyStore = keyStore; mObjectFactory = objectFactory; mProviders = new HashMap<>(); mAnqpCache = objectFactory.makeAnqpCache(clock); mAnqpRequestManager = objectFactory.makeANQPRequestManager(mPasspointEventHandler, clock, wifiInjector, mHandler); mWifiConfigManager = wifiConfigManager; mWifiMetrics = wifiMetrics; mProviderIndex = 0; mWifiCarrierInfoManager = wifiCarrierInfoManager; wifiConfigStore.registerStoreData(objectFactory.makePasspointConfigUserStoreData( mKeyStore, mWifiCarrierInfoManager, new UserDataSourceHandler(), clock)); wifiConfigStore.registerStoreData(objectFactory.makePasspointConfigSharedStoreData( new SharedDataSourceHandler())); mPasspointProvisioner = objectFactory.makePasspointProvisioner(context, wifiNative, this, wifiMetrics); ActivityManager activityManager = context.getSystemService(ActivityManager.class); mIsLowMemory = activityManager.isLowRamDevice(); mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); sPasspointManager = this; mMacAddressUtil = macAddressUtil; mClock = clock; mHandler.postToFront(() -> mWifiConfigManager.addOnNetworkUpdateListener( new PasspointManager.OnNetworkUpdateListener())); mWifiPermissionsUtil = wifiPermissionsUtil; mSettingsStore = wifiSettingsStore; } /** * Initializes the provisioning flow with a looper. * This looper should be tied to a background worker thread since PasspointProvisioner has a * heavy workload. */ public void initializeProvisioner(Looper looper) { mPasspointProvisioner.init(looper); } /** * Sets the {@link PasspointNetworkNominateHelper} used by this PasspointManager. */ public void setPasspointNetworkNominateHelper( @Nullable PasspointNetworkNominateHelper nominateHelper) { mPasspointNetworkNominateHelper = nominateHelper; } /** * Enable verbose logging * @param verbose enables verbose logging */ public void enableVerboseLogging(boolean verbose) { mVerboseLoggingEnabled = verbose; mPasspointProvisioner.enableVerboseLogging(verbose); for (PasspointProvider provider : mProviders.values()) { provider.enableVerboseLogging(verbose); } } private void updateWifiConfigInWcmIfPresent( WifiConfiguration newConfig, int uid, String packageName, boolean isFromSuggestion) { WifiConfiguration configInWcm = mWifiConfigManager.getConfiguredNetwork(newConfig.getProfileKey()); if (configInWcm == null) return; // suggestion != saved if (isFromSuggestion != configInWcm.fromWifiNetworkSuggestion) return; // is suggestion from same app. if (isFromSuggestion && (configInWcm.creatorUid != uid || !TextUtils.equals(configInWcm.creatorName, packageName))) { return; } NetworkUpdateResult result = mWifiConfigManager.addOrUpdateNetwork( newConfig, uid, packageName, false); if (!result.isSuccess()) { Log.e(TAG, "Failed to update config in WifiConfigManager"); } else { mWifiConfigManager.allowAutojoin(result.getNetworkId(), newConfig.allowAutojoin); if (mVerboseLoggingEnabled) { Log.v(TAG, "Updated config in WifiConfigManager"); } } } /** * Add or update a Passpoint provider with the given configuration. * * Each provider is uniquely identified by its unique identifier, see * {@link PasspointConfiguration#getUniqueId()}. * In the case when there is an existing configuration with the same unique identifier, * a provider with the new configuration will replace the existing provider. * * @param config Configuration of the Passpoint provider to be added * @param uid Uid of the app adding/Updating {@code config} * @param packageName Package name of the app adding/Updating {@code config} * @param isFromSuggestion Whether this {@code config} is from suggestion API * @param isTrusted Whether this {@code config} is a trusted network, default should be true. * Only able set to false when {@code isFromSuggestion} is true, otherwise * adding {@code config} will fail. * @param isRestricted Whether this {@code config} is a restricted network, default should be * false. Only able set to false when {@code isFromSuggestion} is true, * otherwise adding {@code config} will fail * @return true if provider is added successfully, false otherwise */ public boolean addOrUpdateProvider(PasspointConfiguration config, int uid, String packageName, boolean isFromSuggestion, boolean isTrusted, boolean isRestricted) { mWifiMetrics.incrementNumPasspointProviderInstallation(); if (config == null) { Log.e(TAG, "Configuration not provided"); return false; } if (!config.validate()) { Log.e(TAG, "Invalid configuration"); return false; } if (!isFromSuggestion && (!isTrusted || isRestricted)) { Log.e(TAG, "Set isTrusted to false on a non suggestion passpoint is not allowed"); return false; } if (config.getServiceFriendlyNames() != null && isFromSuggestion) { Log.e(TAG, "Passpoint from suggestion should not have ServiceFriendlyNames"); return false; } if (!mWifiPermissionsUtil.doesUidBelongToCurrentUserOrDeviceOwner(uid)) { Log.e(TAG, "UID " + uid + " not visible to the current user"); return false; } if (getPasspointProviderWithPackage(packageName).size() >= WifiManager.getMaxNumberOfNetworkSuggestionsPerApp(mIsLowMemory)) { Log.e(TAG, "packageName " + packageName + " has too many passpoint with exceed the " + "limitation"); return false; } mWifiCarrierInfoManager.tryUpdateCarrierIdForPasspoint(config); // Create a provider and install the necessary certificates and keys. PasspointProvider newProvider = mObjectFactory.makePasspointProvider(config, mKeyStore, mWifiCarrierInfoManager, mProviderIndex++, uid, packageName, isFromSuggestion, mClock); newProvider.setTrusted(isTrusted); newProvider.setRestricted(isRestricted); boolean metricsNoRootCa = false; boolean metricsSelfSignedRootCa = false; boolean metricsSubscriptionExpiration = false; if (config.getCredential().getUserCredential() != null || config.getCredential().getCertCredential() != null) { X509Certificate[] x509Certificates = config.getCredential().getCaCertificates(); if (x509Certificates == null) { metricsNoRootCa = true; } else { try { for (X509Certificate certificate : x509Certificates) { verifyCaCert(certificate); } } catch (CertPathValidatorException e) { // A self signed Root CA will fail path validation checks with NO_TRUST_ANCHOR if (e.getReason() == NO_TRUST_ANCHOR) { metricsSelfSignedRootCa = true; } } catch (Exception e) { // Other exceptions, fall through, will be handled below } } } if (config.getSubscriptionExpirationTimeMillis() != Long.MIN_VALUE) { metricsSubscriptionExpiration = true; } if (!newProvider.installCertsAndKeys()) { Log.e(TAG, "Failed to install certificates and keys to keystore"); return false; } // Remove existing provider with the same unique ID. if (mProviders.containsKey(config.getUniqueId())) { PasspointProvider old = mProviders.get(config.getUniqueId()); // If new profile is from suggestion and from a different App, ignore new profile, // return false. // If from same app, update it. if (isFromSuggestion && !old.getPackageName().equals(packageName)) { newProvider.uninstallCertsAndKeys(); return false; } Log.d(TAG, "Replacing configuration for FQDN: " + config.getHomeSp().getFqdn() + " and unique ID: " + config.getUniqueId()); old.uninstallCertsAndKeys(); mProviders.remove(config.getUniqueId()); // Keep the user connect choice and AnonymousIdentity newProvider.setUserConnectChoice(old.getConnectChoice(), old.getConnectChoiceRssi()); newProvider.setAnonymousIdentity(old.getAnonymousIdentity()); // New profile changes the credential, remove the related WifiConfig. if (!old.equals(newProvider)) { mWifiConfigManager.removePasspointConfiguredNetwork( newProvider.getWifiConfig().getProfileKey()); } else { // If there is a config cached in WifiConfigManager, update it with new info. updateWifiConfigInWcmIfPresent( newProvider.getWifiConfig(), uid, packageName, isFromSuggestion); } } newProvider.enableVerboseLogging(mVerboseLoggingEnabled); mProviders.put(config.getUniqueId(), newProvider); if (!isFromSuggestion) { // Suggestions will be handled by the WifiNetworkSuggestionsManager mWifiConfigManager.saveToStore(); } if (!isFromSuggestion && newProvider.getPackageName() != null) { startTrackingAppOpsChange(newProvider.getPackageName(), uid); } Log.d(TAG, "Added/updated Passpoint configuration for FQDN: " + config.getHomeSp().getFqdn() + " with unique ID: " + config.getUniqueId() + " by UID: " + uid); if (metricsNoRootCa) { mWifiMetrics.incrementNumPasspointProviderWithNoRootCa(); } if (metricsSelfSignedRootCa) { mWifiMetrics.incrementNumPasspointProviderWithSelfSignedRootCa(); } if (metricsSubscriptionExpiration) { mWifiMetrics.incrementNumPasspointProviderWithSubscriptionExpiration(); } if (SdkLevel.isAtLeastS() && config.getDecoratedIdentityPrefix() != null) { mWifiMetrics.incrementTotalNumberOfPasspointProfilesWithDecoratedIdentity(); } mWifiMetrics.incrementNumPasspointProviderInstallSuccess(); if (mPasspointNetworkNominateHelper != null) { mPasspointNetworkNominateHelper.refreshWifiConfigsForProviders(); } return true; } private boolean removeProviderInternal(PasspointProvider provider, int callingUid, boolean privileged) { if (!privileged && callingUid != provider.getCreatorUid()) { Log.e(TAG, "UID " + callingUid + " cannot remove profile created by " + provider.getCreatorUid()); return false; } if (!mWifiPermissionsUtil.doesUidBelongToCurrentUserOrDeviceOwner(callingUid)) { Log.e(TAG, "UID " + callingUid + " not visible to the current user"); return false; } provider.uninstallCertsAndKeys(); String packageName = provider.getPackageName(); if (!provider.isFromSuggestion()) { // Remove non-suggestion configs corresponding to the profile in WifiConfigManager. // Suggestion passpoint will be handled by WifiNetworkSuggestionsManager mWifiConfigManager.removePasspointConfiguredNetwork( provider.getWifiConfig().getProfileKey()); } String uniqueId = provider.getConfig().getUniqueId(); mProviders.remove(uniqueId); mWifiConfigManager.removeConnectChoiceFromAllNetworks(uniqueId); if (!provider.isFromSuggestion()) { // Suggestions will be handled by the WifiNetworkSuggestionsManager mWifiConfigManager.saveToStore(); } // Stop monitoring the package if there is no Passpoint profile installed by the package if (mAppOpsChangedListenerPerApp.containsKey(packageName) && getPasspointProviderWithPackage(packageName).size() == 0) { stopTrackingAppOpsChange(packageName); } Log.d(TAG, "Removed Passpoint configuration: " + uniqueId); mWifiMetrics.incrementNumPasspointProviderUninstallSuccess(); return true; } /** * Remove a Passpoint provider identified by the given its unique identifier. * * @param callingUid Calling UID. * @param privileged Whether the caller is a privileged entity * @param uniqueId The ID of the provider to remove. Not required if FQDN is specified. * @param fqdn The FQDN of the provider to remove. Not required if unique ID is specified. * @return true if a provider is removed, false otherwise */ public boolean removeProvider(int callingUid, boolean privileged, String uniqueId, String fqdn) { if (uniqueId == null && fqdn == null) { mWifiMetrics.incrementNumPasspointProviderUninstallation(); Log.e(TAG, "Cannot remove provider, both FQDN and unique ID are null"); return false; } if (uniqueId != null) { // Unique identifier provided mWifiMetrics.incrementNumPasspointProviderUninstallation(); PasspointProvider provider = mProviders.get(uniqueId); if (provider == null) { Log.e(TAG, "Config doesn't exist"); return false; } return removeProviderInternal(provider, callingUid, privileged); } // FQDN provided, loop through all profiles with matching FQDN ArrayList passpointProviders = new ArrayList<>(mProviders.values()); int removedProviders = 0; int numOfUninstallations = 0; for (PasspointProvider provider : passpointProviders) { if (!TextUtils.equals(provider.getConfig().getHomeSp().getFqdn(), fqdn)) { continue; } mWifiMetrics.incrementNumPasspointProviderUninstallation(); numOfUninstallations++; if (removeProviderInternal(provider, callingUid, privileged)) { removedProviders++; } } if (numOfUninstallations == 0) { // Update uninstallation requests metrics here to cover the corner case of trying to // uninstall a non-existent provider. mWifiMetrics.incrementNumPasspointProviderUninstallation(); } return removedProviders > 0; } /** * Enable or disable the auto-join configuration. Auto-join controls whether or not the * passpoint configuration is used for auto connection (network selection). Note that even * when auto-join is disabled the configuration can still be used for manual connection. * * @param uniqueId The unique identifier of the configuration. Not required if FQDN is specified * @param fqdn The FQDN of the configuration. Not required if uniqueId is specified. * @param enableAutojoin true to enable auto-join, false to disable. * @return true on success, false otherwise (e.g. if no such provider exists). */ public boolean enableAutojoin(String uniqueId, String fqdn, boolean enableAutojoin) { if (uniqueId == null && fqdn == null) { return false; } if (uniqueId != null) { // Unique identifier provided PasspointProvider provider = mProviders.get(uniqueId); if (provider == null) { Log.e(TAG, "Config doesn't exist"); return false; } if (provider.setAutojoinEnabled(enableAutojoin)) { mWifiMetrics.logUserActionEvent(enableAutojoin ? UserActionEvent.EVENT_CONFIGURE_AUTO_CONNECT_ON : UserActionEvent.EVENT_CONFIGURE_AUTO_CONNECT_OFF, provider.isFromSuggestion(), true); // Update WifiConfigManager if changed. updateWifiConfigInWcmIfPresent(provider.getWifiConfig(), provider.getCreatorUid(), provider.getPackageName(), provider.isFromSuggestion()); } mWifiConfigManager.saveToStore(); return true; } ArrayList passpointProviders = new ArrayList<>(mProviders.values()); boolean found = false; // FQDN provided, loop through all profiles with matching FQDN for (PasspointProvider provider : passpointProviders) { if (TextUtils.equals(provider.getConfig().getHomeSp().getFqdn(), fqdn)) { if (provider.setAutojoinEnabled(enableAutojoin)) { mWifiMetrics.logUserActionEvent(enableAutojoin ? UserActionEvent.EVENT_CONFIGURE_AUTO_CONNECT_ON : UserActionEvent.EVENT_CONFIGURE_AUTO_CONNECT_OFF, provider.isFromSuggestion(), true); // Update WifiConfigManager if changed. updateWifiConfigInWcmIfPresent(provider.getWifiConfig(), provider.getCreatorUid(), provider.getPackageName(), provider.isFromSuggestion()); } found = true; } } if (found) { mWifiConfigManager.saveToStore(); } return found; } /** * Enable or disable MAC randomization for this passpoint profile. * @param fqdn The FQDN of the configuration * @param enable true to enable MAC randomization, false to disable * @return true on success, false otherwise (e.g. if no such provider exists). */ public boolean enableMacRandomization(@NonNull String fqdn, boolean enable) { ArrayList passpointProviders = new ArrayList<>(mProviders.values()); boolean found = false; // Loop through all profiles with matching FQDN for (PasspointProvider provider : passpointProviders) { if (TextUtils.equals(provider.getConfig().getHomeSp().getFqdn(), fqdn)) { boolean settingChanged = provider.setMacRandomizationEnabled(enable); if (settingChanged) { mWifiMetrics.logUserActionEvent(enable ? UserActionEvent.EVENT_CONFIGURE_MAC_RANDOMIZATION_ON : UserActionEvent.EVENT_CONFIGURE_MAC_RANDOMIZATION_OFF, provider.isFromSuggestion(), true); mWifiConfigManager.removePasspointConfiguredNetwork( provider.getWifiConfig().getProfileKey()); } found = true; } } if (found) { mWifiConfigManager.saveToStore(); } return found; } /** * Set the metered override value for this passpoint profile * @param fqdn The FQDN of the configuration * @param meteredOverride One of the values in {@link MeteredOverride} * @return true on success, false otherwise (e.g. if no such provider exists). */ public boolean setMeteredOverride(@NonNull String fqdn, @MeteredOverride int meteredOverride) { ArrayList passpointProviders = new ArrayList<>(mProviders.values()); boolean found = false; // Loop through all profiles with matching FQDN for (PasspointProvider provider : passpointProviders) { if (TextUtils.equals(provider.getConfig().getHomeSp().getFqdn(), fqdn)) { if (provider.setMeteredOverride(meteredOverride)) { mWifiMetrics.logUserActionEvent( WifiMetrics.convertMeteredOverrideEnumToUserActionEventType( meteredOverride), provider.isFromSuggestion(), true); } found = true; } } if (found) { mWifiConfigManager.saveToStore(); } return found; } /** * Return the installed Passpoint provider configurations. * An empty list will be returned when no provider is installed. * * @param callingUid Calling UID. * @param privileged Whether the caller is a privileged entity * @return A list of {@link PasspointConfiguration} */ public List getProviderConfigs(int callingUid, boolean privileged) { List configs = new ArrayList<>(); for (Map.Entry entry : mProviders.entrySet()) { PasspointProvider provider = entry.getValue(); if (privileged || callingUid == provider.getCreatorUid()) { if (provider.isFromSuggestion()) { continue; } configs.add(provider.getConfig()); } } return configs; } /** * Find all providers that can provide service through the given AP, which means the * providers contained credential to authenticate with the given AP. * * If there is any home provider available, will return a list of matched home providers. * Otherwise will return a list of matched roaming providers. * * A empty list will be returned if no matching is found. * * @param scanResult The scan result associated with the AP * @return a list of pairs of {@link PasspointProvider} and match status. */ public @NonNull List> matchProvider( ScanResult scanResult) { return matchProvider(scanResult, true); } /** * Find all providers that can provide service through the given AP, which means the * providers contained credential to authenticate with the given AP. * * A empty list will be returned if no matching is found. * * @param scanResult The scan result associated with the AP * @param anqpRequestAllowed Indicates if to allow ANQP request if the provider's entry is empty * @return a list of pairs of {@link PasspointProvider} and match status. */ public @NonNull List> matchProvider( ScanResult scanResult, boolean anqpRequestAllowed) { if (!mEnabled) { return Collections.emptyList(); } List> allMatches = getAllMatchedProviders( scanResult, anqpRequestAllowed); allMatches.removeIf(a -> isExpired(a.first.getConfig())); if (allMatches.isEmpty()) { if (mVerboseLoggingEnabled) { Log.d(TAG, "No service provider found for " + scanResult.SSID); } } return allMatches; } /** * Return a list of all providers that can provide service through the given AP. * * @param scanResult The scan result associated with the AP * @return a list of pairs of {@link PasspointProvider} and match status. */ public @NonNull List> getAllMatchedProviders( ScanResult scanResult) { return getAllMatchedProviders(scanResult, true); } /** * Return a list of all providers that can provide service through the given AP. * * @param scanResult The scan result associated with the AP * @param anqpRequestAllowed Indicates if to allow ANQP request if the provider's entry is empty * @return a list of pairs of {@link PasspointProvider} and match status. */ private @NonNull List> getAllMatchedProviders( ScanResult scanResult, boolean anqpRequestAllowed) { if (!mEnabled) { return Collections.emptyList(); } List> allMatches = new ArrayList<>(); // Retrieve the relevant information elements, mainly Roaming Consortium IE and Hotspot 2.0 // Vendor Specific IE. InformationElementUtil.RoamingConsortium roamingConsortium = InformationElementUtil.getRoamingConsortiumIE(scanResult.informationElements); InformationElementUtil.Vsa vsa = InformationElementUtil.getHS2VendorSpecificIE( scanResult.informationElements); // Lookup ANQP data in the cache. long bssid; try { bssid = Utils.parseMac(scanResult.BSSID); } catch (IllegalArgumentException e) { Log.e(TAG, "Invalid BSSID provided in the scan result: " + scanResult.BSSID); return allMatches; } ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(scanResult.SSID, bssid, scanResult.hessid, vsa.anqpDomainID); ANQPData anqpEntry = mAnqpCache.getEntry(anqpKey); if (anqpEntry == null) { if (anqpRequestAllowed) { mAnqpRequestManager.requestANQPElements(bssid, anqpKey, roamingConsortium.anqpOICount > 0, vsa.hsRelease); } Log.d(TAG, "ANQP entry not found for: " + anqpKey); return allMatches; } boolean anyProviderUpdated = false; for (Map.Entry entry : mProviders.entrySet()) { PasspointProvider provider = entry.getValue(); if (provider.tryUpdateCarrierId()) { anyProviderUpdated = true; } if (mVerboseLoggingEnabled) { Log.d(TAG, "Matching provider " + provider.getConfig().getHomeSp().getFqdn() + " with " + anqpEntry.getElements().get(Constants.ANQPElementType.ANQPDomName)); } PasspointMatch matchStatus = provider.match(anqpEntry.getElements(), roamingConsortium, scanResult); if (matchStatus == PasspointMatch.HomeProvider || matchStatus == PasspointMatch.RoamingProvider) { allMatches.add(Pair.create(provider, matchStatus)); } } if (anyProviderUpdated) { mWifiConfigManager.saveToStore(); } if (allMatches.size() != 0) { for (Pair match : allMatches) { Log.d(TAG, String.format("Matched %s to %s as %s", scanResult.SSID, match.first.getConfig().getHomeSp().getFqdn(), match.second == PasspointMatch.HomeProvider ? "Home Provider" : "Roaming Provider")); } } else { if (mVerboseLoggingEnabled) { Log.d(TAG, "No service providers found for " + scanResult.SSID); } } return allMatches; } /** * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration} to the * current {@link PasspointManager}. * * This will not trigger a config store write, since this will be invoked as part of the * configuration migration, the caller will be responsible for triggering store write * after the migration is completed. * * @param config {@link WifiConfiguration} representation of the Passpoint configuration * @return true on success */ public static boolean addLegacyPasspointConfig(WifiConfiguration config) { if (sPasspointManager == null) { Log.e(TAG, "PasspointManager have not been initialized yet"); return false; } Log.d(TAG, "Installing legacy Passpoint configuration: " + config.FQDN); return sPasspointManager.addWifiConfig(config); } /** * Sweep the ANQP cache to remove expired entries. */ public void sweepCache() { mAnqpCache.sweep(); } /** * Notify the completion of an ANQP request. * TODO(zqiu): currently the notification is done through WifiMonitor, * will no longer be the case once we switch over to use wificond. */ public void notifyANQPDone(AnqpEvent anqpEvent) { mPasspointEventHandler.notifyANQPDone(anqpEvent); } /** * Notify the completion of an icon request. * TODO(zqiu): currently the notification is done through WifiMonitor, * will no longer be the case once we switch over to use wificond. */ public void notifyIconDone(IconEvent iconEvent) { mPasspointEventHandler.notifyIconDone(iconEvent); } /** * Notify the reception of a Wireless Network Management (WNM) frame. */ public void receivedWnmFrame(WnmData data) { mPasspointEventHandler.notifyWnmFrameReceived(data); } /** * Request the specified icon file |fileName| from the specified AP |bssid|. * @return true if the request is sent successfully, false otherwise */ public boolean queryPasspointIcon(long bssid, String fileName) { return mPasspointEventHandler.requestIcon(bssid, fileName); } /** * Lookup the ANQP elements associated with the given AP from the cache. An empty map * will be returned if no match found in the cache. * * @param scanResult The scan result associated with the AP * @return Map of ANQP elements */ public Map getANQPElements(ScanResult scanResult) { // Retrieve the Hotspot 2.0 Vendor Specific IE. InformationElementUtil.Vsa vsa = InformationElementUtil.getHS2VendorSpecificIE(scanResult.informationElements); // Lookup ANQP data in the cache. long bssid; try { bssid = Utils.parseMac(scanResult.BSSID); } catch (IllegalArgumentException e) { Log.e(TAG, "Invalid BSSID provided in the scan result: " + scanResult.BSSID); return new HashMap<>(); } ANQPData anqpEntry = mAnqpCache.getEntry(ANQPNetworkKey.buildKey( scanResult.SSID, bssid, scanResult.hessid, vsa.anqpDomainID)); if (anqpEntry != null) { return anqpEntry.getElements(); } return new HashMap<>(); } /** * Return a map of all matching configurations keys with corresponding scanResults (or an empty * map if none). * * @param scanResults The list of scan results * @return Map that consists of identifies and corresponding scanResults per network type * ({@link WifiManager#PASSPOINT_HOME_NETWORK}, {@link WifiManager#PASSPOINT_ROAMING_NETWORK}). */ public Map>> getAllMatchingPasspointProfilesForScanResults(List scanResults) { if (scanResults == null) { Log.e(TAG, "Attempt to get matching config for a null ScanResults"); return new HashMap<>(); } Map>> configs = new HashMap<>(); for (ScanResult scanResult : scanResults) { if (!scanResult.isPasspointNetwork()) continue; List> matchedProviders = getAllMatchedProviders( scanResult); for (Pair matchedProvider : matchedProviders) { WifiConfiguration config = matchedProvider.first.getWifiConfig(); int type = WifiManager.PASSPOINT_HOME_NETWORK; if (!config.isHomeProviderNetwork) { type = WifiManager.PASSPOINT_ROAMING_NETWORK; } Map> scanResultsPerNetworkType = configs.computeIfAbsent(config.getProfileKey(), k -> new HashMap<>()); List matchingScanResults = scanResultsPerNetworkType.computeIfAbsent( type, k -> new ArrayList<>()); matchingScanResults.add(scanResult); } } return configs; } /** * Returns the list of Hotspot 2.0 OSU (Online Sign-Up) providers associated with the given list * of ScanResult. * * An empty map will be returned when an invalid scanResults are provided or no match is found. * * @param scanResults a list of ScanResult that has Passpoint APs. * @return Map that consists of {@link OsuProvider} and a matching list of {@link ScanResult} */ public Map> getMatchingOsuProviders( List scanResults) { if (scanResults == null) { Log.e(TAG, "Attempt to retrieve OSU providers for a null ScanResult"); return new HashMap(); } Map> osuProviders = new HashMap<>(); for (ScanResult scanResult : scanResults) { if (!scanResult.isPasspointNetwork()) continue; // Lookup OSU Providers ANQP element. Map anqpElements = getANQPElements(scanResult); if (!anqpElements.containsKey(Constants.ANQPElementType.HSOSUProviders)) { continue; } HSOsuProvidersElement element = (HSOsuProvidersElement) anqpElements.get( Constants.ANQPElementType.HSOSUProviders); for (OsuProviderInfo info : element.getProviders()) { // Set null for OSU-SSID in the class because OSU-SSID is a factor for hotspot // operator rather than service provider, which means it can be different for // each hotspot operators. OsuProvider provider = new OsuProvider((WifiSsid) null, info.getFriendlyNames(), info.getServiceDescription(), info.getServerUri(), info.getNetworkAccessIdentifier(), info.getMethodList()); List matchingScanResults = osuProviders.get(provider); if (matchingScanResults == null) { matchingScanResults = new ArrayList<>(); osuProviders.put(provider, matchingScanResults); } matchingScanResults.add(scanResult); } } return osuProviders; } /** * Returns the matching Passpoint configurations for given OSU(Online Sign-Up) providers * * An empty map will be returned when an invalid {@code osuProviders} are provided or no match * is found. * * @param osuProviders a list of {@link OsuProvider} * @return Map that consists of {@link OsuProvider} and matching {@link PasspointConfiguration}. */ public Map getMatchingPasspointConfigsForOsuProviders( List osuProviders) { Map matchingPasspointConfigs = new HashMap<>(); for (OsuProvider osuProvider : osuProviders) { Map friendlyNamesForOsuProvider = osuProvider.getFriendlyNameList(); if (friendlyNamesForOsuProvider == null) continue; for (PasspointProvider provider : mProviders.values()) { PasspointConfiguration passpointConfiguration = provider.getConfig(); Map serviceFriendlyNamesForPpsMo = passpointConfiguration.getServiceFriendlyNames(); if (serviceFriendlyNamesForPpsMo == null) continue; for (Map.Entry entry : serviceFriendlyNamesForPpsMo.entrySet()) { String lang = entry.getKey(); String friendlyName = entry.getValue(); if (friendlyName == null) continue; String osuFriendlyName = friendlyNamesForOsuProvider.get(lang); if (osuFriendlyName == null) continue; if (friendlyName.equals(osuFriendlyName)) { matchingPasspointConfigs.put(osuProvider, passpointConfiguration); break; } } } } return matchingPasspointConfigs; } /** * Returns the corresponding wifi configurations from {@link WifiConfigManager} for given a list * of Passpoint profile unique identifiers. * * Note: Not all matched Passpoint profile's WifiConfiguration will be returned, only the ones * already be added into the {@link WifiConfigManager} will be returned. As the returns of this * method is expected to show in Wifi Picker or use with * {@link WifiManager#connect(int, WifiManager.ActionListener)} API, each WifiConfiguration must * have a valid network Id. * * An empty list will be returned when no match is found. * * @param idList a list of unique identifiers * @return List of {@link WifiConfiguration} converted from {@link PasspointProvider} */ public List getWifiConfigsForPasspointProfiles(List idList) { if (mProviders.isEmpty()) { return Collections.emptyList(); } List configs = new ArrayList<>(); Set uniqueIdSet = new HashSet<>(idList); boolean refreshed = false; for (String uniqueId : uniqueIdSet) { PasspointProvider provider = mProviders.get(uniqueId); if (provider == null) { continue; } String profileKey = provider.getWifiConfig().getProfileKey(); WifiConfiguration config = mWifiConfigManager .getConfiguredNetwork(profileKey); if (config == null && !refreshed) { // Refresh the WifiConfigManager, this may caused by new ANQP response mPasspointNetworkNominateHelper.refreshWifiConfigsForProviders(); refreshed = true; config = mWifiConfigManager.getConfiguredNetwork(profileKey); } if (config == null) { Log.e(TAG, "After refresh, still not in the WifiConfig, ignore"); continue; } // If the Passpoint configuration is from a suggestion, check if the app shares this // suggestion with the user. if (provider.isFromSuggestion() && !mWifiInjector.getWifiNetworkSuggestionsManager() .isPasspointSuggestionSharedWithUser(config)) { continue; } if (mWifiConfigManager.shouldUseNonPersistentRandomization(config)) { config.setRandomizedMacAddress(MacAddress.fromString(DEFAULT_MAC_ADDRESS)); } else { MacAddress result = mMacAddressUtil.calculatePersistentMacForSta( config.getNetworkKey(), Process.WIFI_UID); if (result != null) { config.setRandomizedMacAddress(result); } } configs.add(config); } return configs; } /** * Returns the corresponding Wifi configurations for all non-suggestion Passpoint profiles. * * @param requireSsid If true, this method will only return Passpoint configs that include an * SSID. If false, this method will return all Passpoint configs, including those which do * not include an SSID. *

Note: Passpoint SSIDs are recorded upon successful connection to a network. Having an * SSID indicates that a Passpoint network has connected since the last reboot. * @return List of {@link WifiConfiguration} converted from {@link PasspointProvider}. */ public List getWifiConfigsForPasspointProfiles(boolean requireSsid) { if (mProviders.isEmpty()) return Collections.emptyList(); List sortedProviders = new ArrayList<>(mProviders.values()); Collections.sort(sortedProviders, new PasspointProvider.ConnectionTimeComparator()); List configs = new ArrayList<>(); for (PasspointProvider provider : sortedProviders) { if (provider == null || provider.isFromSuggestion() || (requireSsid && provider.getMostRecentSsid() == null)) { continue; } WifiConfiguration config = provider.getWifiConfig(); config.SSID = provider.getMostRecentSsid(); if (config.SSID != null) { config.getNetworkSelectionStatus().setHasEverConnected(true); } configs.add(config); } return configs; } /** * Get the most recent SSID observed for the specified Passpoint profile. * * @param uniqueId The unique identifier of the Passpoint profile. * @return The most recent SSID observed for this profile, or null. */ public @Nullable String getMostRecentSsidForProfile(String uniqueId) { PasspointProvider provider = mProviders.get(uniqueId); if (provider == null) return null; return provider.getMostRecentSsid(); } /** * Invoked when a Passpoint network was successfully connected based on the credentials * provided by the given Passpoint provider * * @param uniqueId The unique identifier of the Passpoint profile. * @param ssid The SSID of the connected Passpoint network. */ public void onPasspointNetworkConnected(String uniqueId, @Nullable String ssid) { PasspointProvider provider = mProviders.get(uniqueId); if (provider == null) { Log.e(TAG, "Passpoint network connected without provider: " + uniqueId); return; } if (!provider.getHasEverConnected()) { // First successful connection using this provider. provider.setHasEverConnected(true); } provider.setMostRecentSsid(ssid); provider.updateMostRecentConnectionTime(); } /** * Update metrics related to installed Passpoint providers, this includes the number of * installed providers and the number of those providers that results in a successful network * connection. */ public void updateMetrics() { int numProviders = mProviders.size(); int numConnectedProviders = 0; for (Map.Entry entry : mProviders.entrySet()) { if (entry.getValue().getHasEverConnected()) { numConnectedProviders++; } } mWifiMetrics.updateSavedPasspointProfilesInfo(mProviders); mWifiMetrics.updateSavedPasspointProfiles(numProviders, numConnectedProviders); } /** * Dump the current state of PasspointManager to the provided output stream. * * @param pw The output stream to write to */ public void dump(PrintWriter pw) { pw.println("Dump of PasspointManager"); pw.println("mEnabled: " + mEnabled); pw.println("PasspointManager - Providers Begin ---"); for (Map.Entry entry : mProviders.entrySet()) { pw.println(entry.getValue()); } pw.println("PasspointManager - Providers End ---"); pw.println("PasspointManager - Next provider ID to be assigned " + mProviderIndex); mAnqpCache.dump(pw); mAnqpRequestManager.dump(pw); } /** * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration}. * * @param wifiConfig {@link WifiConfiguration} representation of the Passpoint configuration * @return true on success */ private boolean addWifiConfig(WifiConfiguration wifiConfig) { if (wifiConfig == null) { return false; } // Convert to PasspointConfiguration PasspointConfiguration passpointConfig = PasspointProvider.convertFromWifiConfig(wifiConfig); if (passpointConfig == null) { return false; } // Setup aliases for enterprise certificates and key. WifiEnterpriseConfig enterpriseConfig = wifiConfig.enterpriseConfig; String caCertificateAliasSuffix = enterpriseConfig.getCaCertificateAlias(); String clientCertAndKeyAliasSuffix = enterpriseConfig.getClientCertificateAlias(); if (passpointConfig.getCredential().getUserCredential() != null && TextUtils.isEmpty(caCertificateAliasSuffix)) { Log.e(TAG, "Missing CA Certificate for user credential"); return false; } if (passpointConfig.getCredential().getCertCredential() != null) { if (TextUtils.isEmpty(caCertificateAliasSuffix)) { Log.e(TAG, "Missing CA certificate for Certificate credential"); return false; } if (TextUtils.isEmpty(clientCertAndKeyAliasSuffix)) { Log.e(TAG, "Missing client certificate and key for certificate credential"); return false; } } // Note that for legacy configuration, the alias for client private key is the same as the // alias for the client certificate. PasspointProvider provider = new PasspointProvider(passpointConfig, mKeyStore, mWifiCarrierInfoManager, mProviderIndex++, wifiConfig.creatorUid, null, false, Arrays.asList(enterpriseConfig.getCaCertificateAlias()), enterpriseConfig.getClientCertificateAlias(), null, false, false, mClock); provider.enableVerboseLogging(mVerboseLoggingEnabled); mProviders.put(passpointConfig.getUniqueId(), provider); return true; } /** * Start the subscription provisioning flow with a provider. * @param callingUid integer indicating the uid of the caller * @param provider {@link OsuProvider} the provider to subscribe to * @param callback {@link IProvisioningCallback} callback to update status to the caller * @return boolean return value from the provisioning method */ public boolean startSubscriptionProvisioning(int callingUid, OsuProvider provider, IProvisioningCallback callback) { return mPasspointProvisioner.startSubscriptionProvisioning(callingUid, provider, callback); } /** * Check if a Passpoint configuration is expired * * @param config {@link PasspointConfiguration} Passpoint configuration * @return True if the configuration is expired, false if not or expiration is unset */ private boolean isExpired(@NonNull PasspointConfiguration config) { long expirationTime = config.getSubscriptionExpirationTimeMillis(); if (expirationTime != Long.MIN_VALUE) { long curTime = System.currentTimeMillis(); // Check expiration and return true for expired profiles if (curTime >= expirationTime) { Log.d(TAG, "Profile for " + config.getServiceFriendlyName() + " has expired, " + "expiration time: " + expirationTime + ", current time: " + curTime); return true; } } return false; } /** * Get the filtered ScanResults which could be served by the {@link PasspointConfiguration}. * @param passpointConfiguration The instance of {@link PasspointConfiguration} * @param scanResults The list of {@link ScanResult} * @return The filtered ScanResults */ @NonNull public List getMatchingScanResults( @NonNull PasspointConfiguration passpointConfiguration, @NonNull List scanResults) { PasspointProvider provider = mObjectFactory.makePasspointProvider(passpointConfiguration, null, mWifiCarrierInfoManager, 0, 0, null, false, mClock); List filteredScanResults = new ArrayList<>(); for (ScanResult scanResult : scanResults) { PasspointMatch matchInfo = provider.match(getANQPElements(scanResult), InformationElementUtil.getRoamingConsortiumIE(scanResult.informationElements), scanResult); if (matchInfo == PasspointMatch.HomeProvider || matchInfo == PasspointMatch.RoamingProvider) { filteredScanResults.add(scanResult); } } return filteredScanResults; } /** * Check if the providers list is empty * * @return true if the providers list is empty, false otherwise */ public boolean isProvidersListEmpty() { return mProviders.isEmpty(); } /** * Clear ANQP requests and flush ANQP Cache (for factory reset) */ public void clearAnqpRequestsAndFlushCache() { mAnqpRequestManager.clear(); mAnqpCache.flush(); mProviders.values().forEach(PasspointProvider::clearProviderBlock); } private PKIXParameters mInjectedPKIXParameters; private boolean mUseInjectedPKIX = false; /** * Used to speedup unit test. */ @VisibleForTesting public void injectPKIXParameters(PKIXParameters params) { mInjectedPKIXParameters = params; } /** * Used to speedup unit test. */ @VisibleForTesting public void setUseInjectedPKIX(boolean value) { mUseInjectedPKIX = value; } /** * Verify that the given certificate is trusted by one of the pre-loaded public CAs in the * system key store. * * @param caCert The CA Certificate to verify * @throws CertPathValidatorException * @throws Exception */ private void verifyCaCert(X509Certificate caCert) throws GeneralSecurityException, IOException { CertificateFactory factory = CertificateFactory.getInstance("X.509"); CertPathValidator validator = CertPathValidator.getInstance(CertPathValidator.getDefaultType()); CertPath path = factory.generateCertPath(Arrays.asList(caCert)); PKIXParameters params; if (mUseInjectedPKIX) { params = mInjectedPKIXParameters; } else { KeyStore ks = KeyStore.getInstance("AndroidCAStore"); ks.load(null, null); params = new PKIXParameters(ks); params.setRevocationEnabled(false); } validator.validate(path, params); } /** * Request the Venue URL ANQP-element from the AP post connection * * @param scanResult Scan result associated to the requested AP */ public void requestVenueUrlAnqpElement(@NonNull ScanResult scanResult) { long bssid; try { bssid = Utils.parseMac(scanResult.BSSID); } catch (IllegalArgumentException e) { Log.e(TAG, "Invalid BSSID provided in the scan result: " + scanResult.BSSID); return; } InformationElementUtil.Vsa vsa = InformationElementUtil.getHS2VendorSpecificIE( scanResult.informationElements); ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(scanResult.SSID, bssid, scanResult.hessid, vsa.anqpDomainID); // TODO(haishalom@): Should we limit to R3 only? vsa.hsRelease > NetworkDetail.HSRelease.R2 // I am seeing R2's that respond to Venue URL request, so may keep it this way. // APs that do not support this ANQP request simply ignore it. mAnqpRequestManager.requestVenueUrlAnqpElement(bssid, anqpKey); } /** * Get the Venue URL associated to the scan result, matched to the system language. If no * Venue URL matches the system language, then entry number one is returned, which is considered * to be the venue's default language. * * @param scanResult Scan result * @return The Venue URL associated to the scan result or null if not found */ @Nullable public URL getVenueUrl(@NonNull ScanResult scanResult) { long bssid; try { bssid = Utils.parseMac(scanResult.BSSID); } catch (IllegalArgumentException e) { Log.e(TAG, "Invalid BSSID provided in the scan result: " + scanResult.BSSID); return null; } InformationElementUtil.Vsa vsa = InformationElementUtil.getHS2VendorSpecificIE( scanResult.informationElements); ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(scanResult.SSID, bssid, scanResult.hessid, vsa.anqpDomainID); ANQPData anqpEntry = mAnqpCache.getEntry(anqpKey); if (anqpEntry == null) { return null; } VenueUrlElement venueUrlElement = (VenueUrlElement) anqpEntry.getElements().get(Constants.ANQPElementType.ANQPVenueUrl); if (venueUrlElement == null || venueUrlElement.getVenueUrls().isEmpty()) { return null; // No Venue URL } VenueNameElement venueNameElement = (VenueNameElement) anqpEntry.getElements().get(Constants.ANQPElementType.ANQPVenueName); if (venueNameElement == null || venueUrlElement.getVenueUrls().size() != venueNameElement.getNames().size()) { Log.w(TAG, "Venue name list size mismatches the Venue URL list size"); return null; // No match between Venue names Venue URLs } // Find the Venue URL that matches the system language. Venue URLs are ordered by venue // names. Locale locale = Locale.getDefault(); URL venueUrl = null; int index = 1; for (I18Name venueName : venueNameElement.getNames()) { if (venueName.getLanguage().equals(locale.getISO3Language())) { venueUrl = venueUrlElement.getVenueUrls().get(index); break; } index++; } // If no venue URL for the system language is available, use entry number one if (venueUrl == null) { venueUrl = venueUrlElement.getVenueUrls().get(1); } if (mVerboseLoggingEnabled) { Log.d(TAG, "Venue URL to display (language = " + locale.getDisplayLanguage() + "): " + (venueUrl != null ? venueUrl : "None")); } return venueUrl; } /** * Handle Deauthentication Imminent WNM-Notification event * * @param event Deauthentication Imminent WNM-Notification data * @param config Configuration of the currently connected network */ public void handleDeauthImminentEvent(WnmData event, WifiConfiguration config) { if (event == null || config == null) { return; } blockProvider(config.getProfileKey(), event.getBssid(), event.isEss(), event.getDelay()); mWifiMetrics.incrementPasspointDeauthImminentScope(event.isEss()); } /** * Block a specific provider from network selection * * @param passpointUniqueId The unique ID of the Passpoint network * @param bssid BSSID of the AP * @param isEss Block the ESS or the BSS * @param delay Delay in seconds */ private void blockProvider(String passpointUniqueId, long bssid, boolean isEss, int delay) { PasspointProvider provider = mProviders.get(passpointUniqueId); if (provider != null) { provider.blockBssOrEss(bssid, isEss, delay); } } /** * Store the AnonymousIdentity for passpoint after connection. */ public void setAnonymousIdentity(WifiConfiguration configuration) { if (!configuration.isPasspoint()) { return; } PasspointProvider provider = mProviders.get(configuration.getProfileKey()); if (provider != null) { provider.setAnonymousIdentity(configuration.enterpriseConfig.getAnonymousIdentity()); mWifiConfigManager.saveToStore(); } } /** * Resets all sim networks state. */ public void resetSimPasspointNetwork() { mProviders.values().forEach(p -> p.setAnonymousIdentity(null)); mWifiConfigManager.saveToStore(); } /** * Handle Terms & Conditions acceptance required WNM-Notification event * * @param event Terms & Conditions WNM-Notification data * @param config Configuration of the currently connected Passpoint network * * @return The Terms & conditions URL if it is valid, null otherwise */ public URL handleTermsAndConditionsEvent(WnmData event, WifiConfiguration config) { if (event == null || config == null || !config.isPasspoint()) { return null; } final int oneHourInSeconds = 60 * 60; final int twentyFourHoursInSeconds = 24 * 60 * 60; final URL termsAndConditionsUrl; try { termsAndConditionsUrl = new URL(event.getUrl()); } catch (java.net.MalformedURLException e) { Log.e(TAG, "Malformed Terms and Conditions URL: " + event.getUrl() + " from BSSID: " + Utils.macToString(event.getBssid())); // Block this provider for an hour, this unlikely issue may be resolved shortly blockProvider(config.getProfileKey(), event.getBssid(), true, oneHourInSeconds); return null; } // Reject URLs that are not HTTPS if (!TextUtils.equals(termsAndConditionsUrl.getProtocol(), "https")) { Log.e(TAG, "Non-HTTPS Terms and Conditions URL rejected: " + termsAndConditionsUrl + " from BSSID: " + Utils.macToString(event.getBssid())); // Block this provider for 24 hours, it is unlikely to be changed blockProvider(config.getProfileKey(), event.getBssid(), true, twentyFourHoursInSeconds); return null; } Log.i(TAG, "Captive network, Terms and Conditions URL: " + termsAndConditionsUrl + " from BSSID: " + Utils.macToString(event.getBssid())); return termsAndConditionsUrl; } /** * Check if Wi-Fi Passpoint is enabled. * * @return true if Wi-Fi Passpoint is enabled. */ public boolean isWifiPasspointEnabled() { return mEnabled; } /** * Enable or disable Wi-Fi Passpoint globally. */ public void setWifiPasspointEnabled(boolean enabled) { if (enabled != mEnabled) { clearAnqpRequestsAndFlushCache(); mEnabled = enabled; mSettingsStore.handleWifiPasspointEnabled(enabled); } } /** * Get the selected RCOI for a particular Passpoint network connection * @param uniqueId The Unique ID of the Passpoint configuration * @param ssid The target SSID * @return Selected RCOI for a network, or 0 if none. */ public long getSelectedRcoiForNetwork(String uniqueId, String ssid) { if (TextUtils.isEmpty(uniqueId) || TextUtils.isEmpty(ssid)) return 0; PasspointProvider provider = mProviders.get(uniqueId); if (provider == null) return 0; return provider.getAndRemoveMatchedRcoi(ssid); } /** * Handle boot completed, read config flags. */ public void handleBootCompleted() { // Settings Store should be accessed after boot completed event. mEnabled = mSettingsStore.isWifiPasspointEnabled(); } }