PasspointManager.java revision b54f07e01c9daef8883c85e09f61436f2c06cc72
1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.server.wifi.hotspot2; 18 19import static android.net.wifi.WifiManager.ACTION_PASSPOINT_DEAUTH_IMMINENT; 20import static android.net.wifi.WifiManager.ACTION_PASSPOINT_ICON; 21import static android.net.wifi.WifiManager.ACTION_PASSPOINT_SUBSCRIPTION_REMEDIATION; 22import static android.net.wifi.WifiManager.EXTRA_BSSID_LONG; 23import static android.net.wifi.WifiManager.EXTRA_DELAY; 24import static android.net.wifi.WifiManager.EXTRA_ESS; 25import static android.net.wifi.WifiManager.EXTRA_ICON_INFO; 26import static android.net.wifi.WifiManager.EXTRA_SUBSCRIPTION_REMEDIATION_METHOD; 27import static android.net.wifi.WifiManager.EXTRA_URL; 28 29import android.content.Context; 30import android.content.Intent; 31import android.net.wifi.IconInfo; 32import android.net.wifi.ScanResult; 33import android.net.wifi.WifiConfiguration; 34import android.net.wifi.WifiEnterpriseConfig; 35import android.net.wifi.hotspot2.PasspointConfiguration; 36import android.os.UserHandle; 37import android.text.TextUtils; 38import android.util.Log; 39import android.util.Pair; 40 41import com.android.server.wifi.Clock; 42import com.android.server.wifi.SIMAccessor; 43import com.android.server.wifi.WifiConfigManager; 44import com.android.server.wifi.WifiConfigStore; 45import com.android.server.wifi.WifiKeyStore; 46import com.android.server.wifi.WifiNative; 47import com.android.server.wifi.hotspot2.anqp.ANQPElement; 48import com.android.server.wifi.hotspot2.anqp.Constants; 49import com.android.server.wifi.util.InformationElementUtil; 50 51import java.util.ArrayList; 52import java.util.HashMap; 53import java.util.List; 54import java.util.Map; 55 56/** 57 * This class provides the APIs to manage Passpoint provider configurations. 58 * It deals with the following: 59 * - Maintaining a list of configured Passpoint providers for provider matching. 60 * - Persisting the providers configurations to store when required. 61 * - matching Passpoint providers based on the scan results 62 * - Supporting WifiManager Public API calls: 63 * > addOrUpdatePasspointConfiguration() 64 * > removePasspointConfiguration() 65 * > getPasspointConfigurations() 66 * 67 * The provider matching requires obtaining additional information from the AP (ANQP elements). 68 * The ANQP elements will be cached using {@link AnqpCache} to avoid unnecessary requests. 69 * 70 * NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread. 71 */ 72public class PasspointManager { 73 private static final String TAG = "PasspointManager"; 74 75 /** 76 * Handle for the current {@link PasspointManager} instance. This is needed to avoid 77 * circular dependency with the WifiConfigManger, it will be used for adding the 78 * legacy Passpoint configurations. 79 * 80 * This can be eliminated once we can remove the dependency for WifiConfigManager (for 81 * triggering config store write) from this class. 82 */ 83 private static PasspointManager sPasspointManager; 84 85 private final PasspointEventHandler mHandler; 86 private final SIMAccessor mSimAccessor; 87 private final WifiKeyStore mKeyStore; 88 private final PasspointObjectFactory mObjectFactory; 89 private final Map<String, PasspointProvider> mProviders; 90 private final AnqpCache mAnqpCache; 91 private final ANQPRequestManager mAnqpRequestManager; 92 private final WifiConfigManager mWifiConfigManager; 93 94 // Counter used for assigning unique identifier to each provider. 95 private long mProviderIndex; 96 97 private class CallbackHandler implements PasspointEventHandler.Callbacks { 98 private final Context mContext; 99 CallbackHandler(Context context) { 100 mContext = context; 101 } 102 103 @Override 104 public void onANQPResponse(long bssid, 105 Map<Constants.ANQPElementType, ANQPElement> anqpElements) { 106 // Notify request manager for the completion of a request. 107 ANQPNetworkKey anqpKey = 108 mAnqpRequestManager.onRequestCompleted(bssid, anqpElements != null); 109 if (anqpElements == null || anqpKey == null) { 110 // Query failed or the request wasn't originated from us (not tracked by the 111 // request manager). Nothing to be done. 112 return; 113 } 114 115 // Add new entry to the cache. 116 mAnqpCache.addEntry(anqpKey, anqpElements); 117 } 118 119 @Override 120 public void onIconResponse(long bssid, String fileName, byte[] data) { 121 Intent intent = new Intent(ACTION_PASSPOINT_ICON); 122 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 123 intent.putExtra(EXTRA_BSSID_LONG, bssid); 124 intent.putExtra(EXTRA_ICON_INFO, new IconInfo(fileName, data)); 125 mContext.sendBroadcastAsUser(intent, UserHandle.ALL, 126 android.Manifest.permission.ACCESS_WIFI_STATE); 127 } 128 129 @Override 130 public void onWnmFrameReceived(WnmData event) { 131 // %012x HS20-SUBSCRIPTION-REMEDIATION "%u %s", osu_method, url 132 // %012x HS20-DEAUTH-IMMINENT-NOTICE "%u %u %s", code, reauth_delay, url 133 Intent intent; 134 if (event.isDeauthEvent()) { 135 intent = new Intent(ACTION_PASSPOINT_DEAUTH_IMMINENT); 136 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 137 intent.putExtra(EXTRA_BSSID_LONG, event.getBssid()); 138 intent.putExtra(EXTRA_URL, event.getUrl()); 139 intent.putExtra(EXTRA_ESS, event.isEss()); 140 intent.putExtra(EXTRA_DELAY, event.getDelay()); 141 } else { 142 intent = new Intent(ACTION_PASSPOINT_SUBSCRIPTION_REMEDIATION); 143 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 144 intent.putExtra(EXTRA_BSSID_LONG, event.getBssid()); 145 intent.putExtra(EXTRA_SUBSCRIPTION_REMEDIATION_METHOD, event.getMethod()); 146 intent.putExtra(EXTRA_URL, event.getUrl()); 147 } 148 mContext.sendBroadcastAsUser(intent, UserHandle.ALL, 149 android.Manifest.permission.ACCESS_WIFI_STATE); 150 } 151 } 152 153 /** 154 * Data provider for the Passpoint configuration store data {@link PasspointConfigStoreData}. 155 */ 156 private class DataSourceHandler implements PasspointConfigStoreData.DataSource { 157 @Override 158 public List<PasspointProvider> getProviders() { 159 List<PasspointProvider> providers = new ArrayList<>(); 160 for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) { 161 providers.add(entry.getValue()); 162 } 163 return providers; 164 } 165 166 @Override 167 public void setProviders(List<PasspointProvider> providers) { 168 mProviders.clear(); 169 for (PasspointProvider provider : providers) { 170 mProviders.put(provider.getConfig().getHomeSp().getFqdn(), provider); 171 } 172 } 173 174 @Override 175 public long getProviderIndex() { 176 return mProviderIndex; 177 } 178 179 @Override 180 public void setProviderIndex(long providerIndex) { 181 mProviderIndex = providerIndex; 182 } 183 } 184 185 public PasspointManager(Context context, WifiNative wifiNative, WifiKeyStore keyStore, 186 Clock clock, SIMAccessor simAccessor, PasspointObjectFactory objectFactory, 187 WifiConfigManager wifiConfigManager, WifiConfigStore wifiConfigStore) { 188 mHandler = objectFactory.makePasspointEventHandler(wifiNative, 189 new CallbackHandler(context)); 190 mKeyStore = keyStore; 191 mSimAccessor = simAccessor; 192 mObjectFactory = objectFactory; 193 mProviders = new HashMap<>(); 194 mAnqpCache = objectFactory.makeAnqpCache(clock); 195 mAnqpRequestManager = objectFactory.makeANQPRequestManager(mHandler, clock); 196 mWifiConfigManager = wifiConfigManager; 197 mProviderIndex = 0; 198 wifiConfigStore.registerStoreData(objectFactory.makePasspointConfigStoreData( 199 mKeyStore, mSimAccessor, new DataSourceHandler())); 200 sPasspointManager = this; 201 } 202 203 /** 204 * Add or update a Passpoint provider with the given configuration. 205 * 206 * Each provider is uniquely identified by its FQDN (Fully Qualified Domain Name). 207 * In the case when there is an existing configuration with the same FQDN 208 * a provider with the new configuration will replace the existing provider. 209 * 210 * @param config Configuration of the Passpoint provider to be added 211 * @return true if provider is added, false otherwise 212 */ 213 public boolean addOrUpdateProvider(PasspointConfiguration config) { 214 if (config == null) { 215 Log.e(TAG, "Configuration not provided"); 216 return false; 217 } 218 if (!config.validate()) { 219 Log.e(TAG, "Invalid configuration"); 220 return false; 221 } 222 223 // Create a provider and install the necessary certificates and keys. 224 PasspointProvider newProvider = mObjectFactory.makePasspointProvider( 225 config, mKeyStore, mSimAccessor, mProviderIndex++); 226 227 if (!newProvider.installCertsAndKeys()) { 228 Log.e(TAG, "Failed to install certificates and keys to keystore"); 229 return false; 230 } 231 232 // Remove existing provider with the same FQDN. 233 if (mProviders.containsKey(config.getHomeSp().getFqdn())) { 234 Log.d(TAG, "Replacing configuration for " + config.getHomeSp().getFqdn()); 235 mProviders.get(config.getHomeSp().getFqdn()).uninstallCertsAndKeys(); 236 mProviders.remove(config.getHomeSp().getFqdn()); 237 } 238 239 mProviders.put(config.getHomeSp().getFqdn(), newProvider); 240 mWifiConfigManager.saveToStore(true /* forceWrite */); 241 return true; 242 } 243 244 /** 245 * Remove a Passpoint provider identified by the given FQDN. 246 * 247 * @param fqdn The FQDN of the provider to remove 248 * @return true if a provider is removed, false otherwise 249 */ 250 public boolean removeProvider(String fqdn) { 251 if (!mProviders.containsKey(fqdn)) { 252 Log.e(TAG, "Config doesn't exist"); 253 return false; 254 } 255 256 mProviders.get(fqdn).uninstallCertsAndKeys(); 257 mProviders.remove(fqdn); 258 mWifiConfigManager.saveToStore(true /* forceWrite */); 259 return true; 260 } 261 262 /** 263 * Return the installed Passpoint provider configurations. 264 * 265 * An empty list will be returned when no provider is installed. 266 * 267 * @return A list of {@link PasspointConfiguration} 268 */ 269 public List<PasspointConfiguration> getProviderConfigs() { 270 List<PasspointConfiguration> configs = new ArrayList<>(); 271 for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) { 272 configs.add(entry.getValue().getConfig()); 273 } 274 return configs; 275 } 276 277 /** 278 * Find the best provider that can provide service through the given AP, which means the 279 * provider contained credential to authenticate with the given AP. 280 * 281 * Here is the current precedence of the matching rule in descending order: 282 * 1. Home Provider 283 * 2. Roaming Provider 284 * 285 * A {code null} will be returned if no matching is found. 286 * 287 * @param scanResult The scan result associated with the AP 288 * @return A pair of {@link PasspointProvider} and match status. 289 */ 290 public Pair<PasspointProvider, PasspointMatch> matchProvider(ScanResult scanResult) { 291 // Nothing to be done if no Passpoint provider is installed. 292 if (mProviders.isEmpty()) { 293 return null; 294 } 295 296 // Retrieve the relevant information elements, mainly Roaming Consortium IE and Hotspot 2.0 297 // Vendor Specific IE. 298 InformationElementUtil.RoamingConsortium roamingConsortium = 299 InformationElementUtil.getRoamingConsortiumIE(scanResult.informationElements); 300 InformationElementUtil.Vsa vsa = InformationElementUtil.getHS2VendorSpecificIE( 301 scanResult.informationElements); 302 303 // Lookup ANQP data in the cache. 304 long bssid = Utils.parseMac(scanResult.BSSID); 305 ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(scanResult.SSID, bssid, scanResult.hessid, 306 vsa.anqpDomainID); 307 ANQPData anqpEntry = mAnqpCache.getEntry(anqpKey); 308 309 if (anqpEntry == null) { 310 mAnqpRequestManager.requestANQPElements(bssid, anqpKey, 311 roamingConsortium.anqpOICount > 0, 312 vsa.hsRelease == NetworkDetail.HSRelease.R2); 313 return null; 314 } 315 316 Pair<PasspointProvider, PasspointMatch> bestMatch = null; 317 for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) { 318 PasspointProvider provider = entry.getValue(); 319 PasspointMatch matchStatus = provider.match(anqpEntry.getElements()); 320 if (matchStatus == PasspointMatch.HomeProvider) { 321 bestMatch = Pair.create(provider, matchStatus); 322 break; 323 } 324 if (matchStatus == PasspointMatch.RoamingProvider && bestMatch == null) { 325 bestMatch = Pair.create(provider, matchStatus); 326 } 327 } 328 return bestMatch; 329 } 330 331 /** 332 * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration} to the 333 * current {@link PasspointManager}. 334 * 335 * This will not trigger a config store write, since this will be invoked as part of the 336 * configuration migration, the caller will be responsible for triggering store write 337 * after the migration is completed. 338 * 339 * @param config {@link WifiConfiguration} representation of the Passpoint configuration 340 * @return true on success 341 */ 342 public static boolean addLegacyPasspointConfig(WifiConfiguration config) { 343 if (sPasspointManager == null) { 344 Log.e(TAG, "PasspointManager have not been initialized yet"); 345 return false; 346 } 347 return sPasspointManager.addWifiConfig(config); 348 } 349 350 /** 351 * Sweep the ANQP cache to remove expired entries. 352 */ 353 public void sweepCache() { 354 mAnqpCache.sweep(); 355 } 356 357 /** 358 * Notify the completion of an ANQP request. 359 * TODO(zqiu): currently the notification is done through WifiMonitor, 360 * will no longer be the case once we switch over to use wificond. 361 */ 362 public void notifyANQPDone(AnqpEvent anqpEvent) { 363 mHandler.notifyANQPDone(anqpEvent); 364 } 365 366 /** 367 * Notify the completion of an icon request. 368 * TODO(zqiu): currently the notification is done through WifiMonitor, 369 * will no longer be the case once we switch over to use wificond. 370 */ 371 public void notifyIconDone(IconEvent iconEvent) { 372 mHandler.notifyIconDone(iconEvent); 373 } 374 375 /** 376 * Notify the reception of a Wireless Network Management (WNM) frame. 377 * TODO(zqiu): currently the notification is done through WifiMonitor, 378 * will no longer be the case once we switch over to use wificond. 379 */ 380 public void receivedWnmFrame(WnmData data) { 381 mHandler.notifyWnmFrameReceived(data); 382 } 383 384 /** 385 * Request the specified icon file |fileName| from the specified AP |bssid|. 386 * @return true if the request is sent successfully, false otherwise 387 */ 388 public boolean queryPasspointIcon(long bssid, String fileName) { 389 return mHandler.requestIcon(bssid, fileName); 390 } 391 392 /** 393 * Lookup the ANQP elements associated with the given AP from the cache. An empty map 394 * will be returned if no match found in the cache. 395 * 396 * @param scanResult The scan result associated with the AP 397 * @return Map of ANQP elements 398 */ 399 public Map<Constants.ANQPElementType, ANQPElement> getANQPElements(ScanResult scanResult) { 400 // Retrieve the Hotspot 2.0 Vendor Specific IE. 401 InformationElementUtil.Vsa vsa = 402 InformationElementUtil.getHS2VendorSpecificIE(scanResult.informationElements); 403 404 // Lookup ANQP data in the cache. 405 long bssid = Utils.parseMac(scanResult.BSSID); 406 ANQPData anqpEntry = mAnqpCache.getEntry(ANQPNetworkKey.buildKey( 407 scanResult.SSID, bssid, scanResult.hessid, vsa.anqpDomainID)); 408 if (anqpEntry != null) { 409 return anqpEntry.getElements(); 410 } 411 return new HashMap<Constants.ANQPElementType, ANQPElement>(); 412 } 413 414 /** 415 * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration}. 416 * 417 * @param wifiConfig {@link WifiConfiguration} representation of the Passpoint configuration 418 * @return true on success 419 */ 420 private boolean addWifiConfig(WifiConfiguration wifiConfig) { 421 if (wifiConfig == null) { 422 return false; 423 } 424 425 // Convert to PasspointConfiguration 426 PasspointConfiguration passpointConfig = 427 PasspointProvider.convertFromWifiConfig(wifiConfig); 428 if (passpointConfig == null) { 429 return false; 430 } 431 432 // Setup aliases for enterprise certificates and key. 433 WifiEnterpriseConfig enterpriseConfig = wifiConfig.enterpriseConfig; 434 String caCertificateAliasSuffix = enterpriseConfig.getCaCertificateAlias(); 435 String clientCertAndKeyAliasSuffix = enterpriseConfig.getClientCertificateAlias(); 436 if (passpointConfig.getCredential().getUserCredential() != null 437 && TextUtils.isEmpty(caCertificateAliasSuffix)) { 438 Log.e(TAG, "Missing CA Certificate for user credential"); 439 return false; 440 } 441 if (passpointConfig.getCredential().getCertCredential() != null) { 442 if (TextUtils.isEmpty(caCertificateAliasSuffix)) { 443 Log.e(TAG, "Missing CA certificate for Certificate credential"); 444 return false; 445 } 446 if (TextUtils.isEmpty(clientCertAndKeyAliasSuffix)) { 447 Log.e(TAG, "Missing client certificate and key for certificate credential"); 448 return false; 449 } 450 } 451 452 // Note that for legacy configuration, the alias for client private key is the same as the 453 // alias for the client certificate. 454 PasspointProvider provider = new PasspointProvider(passpointConfig, mKeyStore, 455 mSimAccessor, mProviderIndex++, enterpriseConfig.getCaCertificateAlias(), 456 enterpriseConfig.getClientCertificateAlias(), 457 enterpriseConfig.getClientCertificateAlias()); 458 mProviders.put(passpointConfig.getHomeSp().getFqdn(), provider); 459 return true; 460 } 461} 462