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