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