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