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