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