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