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