PasspointManager.java revision b54f07e01c9daef8883c85e09f61436f2c06cc72
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;
50
51import java.util.ArrayList;
52import java.util.HashMap;
53import java.util.List;
54import java.util.Map;
55
56/**
57 * This class provides the APIs to manage Passpoint provider configurations.
58 * It deals with the following:
59 * - Maintaining a list of configured Passpoint providers for provider matching.
60 * - Persisting the providers configurations to store when required.
61 * - matching Passpoint providers based on the scan results
62 * - Supporting WifiManager Public API calls:
63 *   > addOrUpdatePasspointConfiguration()
64 *   > removePasspointConfiguration()
65 *   > getPasspointConfigurations()
66 *
67 * The provider matching requires obtaining additional information from the AP (ANQP elements).
68 * The ANQP elements will be cached using {@link AnqpCache} to avoid unnecessary requests.
69 *
70 * NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
71 */
72public class PasspointManager {
73    private static final String TAG = "PasspointManager";
74
75    /**
76     * Handle for the current {@link PasspointManager} instance.  This is needed to avoid
77     * circular dependency with the WifiConfigManger, it will be used for adding the
78     * legacy Passpoint configurations.
79     *
80     * This can be eliminated once we can remove the dependency for WifiConfigManager (for
81     * triggering config store write) from this class.
82     */
83    private static PasspointManager sPasspointManager;
84
85    private final PasspointEventHandler mHandler;
86    private final SIMAccessor mSimAccessor;
87    private final WifiKeyStore mKeyStore;
88    private final PasspointObjectFactory mObjectFactory;
89    private final Map<String, PasspointProvider> mProviders;
90    private final AnqpCache mAnqpCache;
91    private final ANQPRequestManager mAnqpRequestManager;
92    private final WifiConfigManager mWifiConfigManager;
93
94    // Counter used for assigning unique identifier to each provider.
95    private long mProviderIndex;
96
97    private class CallbackHandler implements PasspointEventHandler.Callbacks {
98        private final Context mContext;
99        CallbackHandler(Context context) {
100            mContext = context;
101        }
102
103        @Override
104        public void onANQPResponse(long bssid,
105                Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
106            // Notify request manager for the completion of a request.
107            ANQPNetworkKey anqpKey =
108                    mAnqpRequestManager.onRequestCompleted(bssid, anqpElements != null);
109            if (anqpElements == null || anqpKey == null) {
110                // Query failed or the request wasn't originated from us (not tracked by the
111                // request manager). Nothing to be done.
112                return;
113            }
114
115            // Add new entry to the cache.
116            mAnqpCache.addEntry(anqpKey, anqpElements);
117        }
118
119        @Override
120        public void onIconResponse(long bssid, String fileName, byte[] data) {
121            Intent intent = new Intent(ACTION_PASSPOINT_ICON);
122            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
123            intent.putExtra(EXTRA_BSSID_LONG, bssid);
124            intent.putExtra(EXTRA_ICON_INFO, new IconInfo(fileName, data));
125            mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
126                    android.Manifest.permission.ACCESS_WIFI_STATE);
127        }
128
129        @Override
130        public void onWnmFrameReceived(WnmData event) {
131            // %012x HS20-SUBSCRIPTION-REMEDIATION "%u %s", osu_method, url
132            // %012x HS20-DEAUTH-IMMINENT-NOTICE "%u %u %s", code, reauth_delay, url
133            Intent intent;
134            if (event.isDeauthEvent()) {
135                intent = new Intent(ACTION_PASSPOINT_DEAUTH_IMMINENT);
136                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
137                intent.putExtra(EXTRA_BSSID_LONG, event.getBssid());
138                intent.putExtra(EXTRA_URL, event.getUrl());
139                intent.putExtra(EXTRA_ESS, event.isEss());
140                intent.putExtra(EXTRA_DELAY, event.getDelay());
141            } else {
142                intent = new Intent(ACTION_PASSPOINT_SUBSCRIPTION_REMEDIATION);
143                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
144                intent.putExtra(EXTRA_BSSID_LONG, event.getBssid());
145                intent.putExtra(EXTRA_SUBSCRIPTION_REMEDIATION_METHOD, event.getMethod());
146                intent.putExtra(EXTRA_URL, event.getUrl());
147            }
148            mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
149                    android.Manifest.permission.ACCESS_WIFI_STATE);
150        }
151    }
152
153    /**
154     * Data provider for the Passpoint configuration store data {@link PasspointConfigStoreData}.
155     */
156    private class DataSourceHandler implements PasspointConfigStoreData.DataSource {
157        @Override
158        public List<PasspointProvider> getProviders() {
159            List<PasspointProvider> providers = new ArrayList<>();
160            for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
161                providers.add(entry.getValue());
162            }
163            return providers;
164        }
165
166        @Override
167        public void setProviders(List<PasspointProvider> providers) {
168            mProviders.clear();
169            for (PasspointProvider provider : providers) {
170                mProviders.put(provider.getConfig().getHomeSp().getFqdn(), provider);
171            }
172        }
173
174        @Override
175        public long getProviderIndex() {
176            return mProviderIndex;
177        }
178
179        @Override
180        public void setProviderIndex(long providerIndex) {
181            mProviderIndex = providerIndex;
182        }
183    }
184
185    public PasspointManager(Context context, WifiNative wifiNative, WifiKeyStore keyStore,
186            Clock clock, SIMAccessor simAccessor, PasspointObjectFactory objectFactory,
187            WifiConfigManager wifiConfigManager, WifiConfigStore wifiConfigStore) {
188        mHandler = objectFactory.makePasspointEventHandler(wifiNative,
189                new CallbackHandler(context));
190        mKeyStore = keyStore;
191        mSimAccessor = simAccessor;
192        mObjectFactory = objectFactory;
193        mProviders = new HashMap<>();
194        mAnqpCache = objectFactory.makeAnqpCache(clock);
195        mAnqpRequestManager = objectFactory.makeANQPRequestManager(mHandler, clock);
196        mWifiConfigManager = wifiConfigManager;
197        mProviderIndex = 0;
198        wifiConfigStore.registerStoreData(objectFactory.makePasspointConfigStoreData(
199                mKeyStore, mSimAccessor, new DataSourceHandler()));
200        sPasspointManager = this;
201    }
202
203    /**
204     * Add or update a Passpoint provider with the given configuration.
205     *
206     * Each provider is uniquely identified by its FQDN (Fully Qualified Domain Name).
207     * In the case when there is an existing configuration with the same FQDN
208     * a provider with the new configuration will replace the existing provider.
209     *
210     * @param config Configuration of the Passpoint provider to be added
211     * @return true if provider is added, false otherwise
212     */
213    public boolean addOrUpdateProvider(PasspointConfiguration config) {
214        if (config == null) {
215            Log.e(TAG, "Configuration not provided");
216            return false;
217        }
218        if (!config.validate()) {
219            Log.e(TAG, "Invalid configuration");
220            return false;
221        }
222
223        // Create a provider and install the necessary certificates and keys.
224        PasspointProvider newProvider = mObjectFactory.makePasspointProvider(
225                config, mKeyStore, mSimAccessor, mProviderIndex++);
226
227        if (!newProvider.installCertsAndKeys()) {
228            Log.e(TAG, "Failed to install certificates and keys to keystore");
229            return false;
230        }
231
232        // Remove existing provider with the same FQDN.
233        if (mProviders.containsKey(config.getHomeSp().getFqdn())) {
234            Log.d(TAG, "Replacing configuration for " + config.getHomeSp().getFqdn());
235            mProviders.get(config.getHomeSp().getFqdn()).uninstallCertsAndKeys();
236            mProviders.remove(config.getHomeSp().getFqdn());
237        }
238
239        mProviders.put(config.getHomeSp().getFqdn(), newProvider);
240        mWifiConfigManager.saveToStore(true /* forceWrite */);
241        return true;
242    }
243
244    /**
245     * Remove a Passpoint provider identified by the given FQDN.
246     *
247     * @param fqdn The FQDN of the provider to remove
248     * @return true if a provider is removed, false otherwise
249     */
250    public boolean removeProvider(String fqdn) {
251        if (!mProviders.containsKey(fqdn)) {
252            Log.e(TAG, "Config doesn't exist");
253            return false;
254        }
255
256        mProviders.get(fqdn).uninstallCertsAndKeys();
257        mProviders.remove(fqdn);
258        mWifiConfigManager.saveToStore(true /* forceWrite */);
259        return true;
260    }
261
262    /**
263     * Return the installed Passpoint provider configurations.
264     *
265     * An empty list will be returned when no provider is installed.
266     *
267     * @return A list of {@link PasspointConfiguration}
268     */
269    public List<PasspointConfiguration> getProviderConfigs() {
270        List<PasspointConfiguration> configs = new ArrayList<>();
271        for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
272            configs.add(entry.getValue().getConfig());
273        }
274        return configs;
275    }
276
277    /**
278     * Find the best provider that can provide service through the given AP, which means the
279     * provider contained credential to authenticate with the given AP.
280     *
281     * Here is the current precedence of the matching rule in descending order:
282     * 1. Home Provider
283     * 2. Roaming Provider
284     *
285     * A {code null} will be returned if no matching is found.
286     *
287     * @param scanResult The scan result associated with the AP
288     * @return A pair of {@link PasspointProvider} and match status.
289     */
290    public Pair<PasspointProvider, PasspointMatch> matchProvider(ScanResult scanResult) {
291        // Nothing to be done if no Passpoint provider is installed.
292        if (mProviders.isEmpty()) {
293            return null;
294        }
295
296        // Retrieve the relevant information elements, mainly Roaming Consortium IE and Hotspot 2.0
297        // Vendor Specific IE.
298        InformationElementUtil.RoamingConsortium roamingConsortium =
299                InformationElementUtil.getRoamingConsortiumIE(scanResult.informationElements);
300        InformationElementUtil.Vsa vsa = InformationElementUtil.getHS2VendorSpecificIE(
301                scanResult.informationElements);
302
303        // Lookup ANQP data in the cache.
304        long bssid = Utils.parseMac(scanResult.BSSID);
305        ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(scanResult.SSID, bssid, scanResult.hessid,
306                vsa.anqpDomainID);
307        ANQPData anqpEntry = mAnqpCache.getEntry(anqpKey);
308
309        if (anqpEntry == null) {
310            mAnqpRequestManager.requestANQPElements(bssid, anqpKey,
311                    roamingConsortium.anqpOICount > 0,
312                    vsa.hsRelease  == NetworkDetail.HSRelease.R2);
313            return null;
314        }
315
316        Pair<PasspointProvider, PasspointMatch> bestMatch = null;
317        for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
318            PasspointProvider provider = entry.getValue();
319            PasspointMatch matchStatus = provider.match(anqpEntry.getElements());
320            if (matchStatus == PasspointMatch.HomeProvider) {
321                bestMatch = Pair.create(provider, matchStatus);
322                break;
323            }
324            if (matchStatus == PasspointMatch.RoamingProvider && bestMatch == null) {
325                bestMatch = Pair.create(provider, matchStatus);
326            }
327        }
328        return bestMatch;
329    }
330
331    /**
332     * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration} to the
333     * current {@link PasspointManager}.
334     *
335     * This will not trigger a config store write, since this will be invoked as part of the
336     * configuration migration, the caller will be responsible for triggering store write
337     * after the migration is completed.
338     *
339     * @param config {@link WifiConfiguration} representation of the Passpoint configuration
340     * @return true on success
341     */
342    public static boolean addLegacyPasspointConfig(WifiConfiguration config) {
343        if (sPasspointManager == null) {
344            Log.e(TAG, "PasspointManager have not been initialized yet");
345            return false;
346        }
347        return sPasspointManager.addWifiConfig(config);
348    }
349
350    /**
351     * Sweep the ANQP cache to remove expired entries.
352     */
353    public void sweepCache() {
354        mAnqpCache.sweep();
355    }
356
357    /**
358     * Notify the completion of an ANQP request.
359     * TODO(zqiu): currently the notification is done through WifiMonitor,
360     * will no longer be the case once we switch over to use wificond.
361     */
362    public void notifyANQPDone(AnqpEvent anqpEvent) {
363        mHandler.notifyANQPDone(anqpEvent);
364    }
365
366    /**
367     * Notify the completion of an icon request.
368     * TODO(zqiu): currently the notification is done through WifiMonitor,
369     * will no longer be the case once we switch over to use wificond.
370     */
371    public void notifyIconDone(IconEvent iconEvent) {
372        mHandler.notifyIconDone(iconEvent);
373    }
374
375    /**
376     * Notify the reception of a Wireless Network Management (WNM) frame.
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 receivedWnmFrame(WnmData data) {
381        mHandler.notifyWnmFrameReceived(data);
382    }
383
384    /**
385     * Request the specified icon file |fileName| from the specified AP |bssid|.
386     * @return true if the request is sent successfully, false otherwise
387     */
388    public boolean queryPasspointIcon(long bssid, String fileName) {
389        return mHandler.requestIcon(bssid, fileName);
390    }
391
392    /**
393     * Lookup the ANQP elements associated with the given AP from the cache. An empty map
394     * will be returned if no match found in the cache.
395     *
396     * @param scanResult The scan result associated with the AP
397     * @return Map of ANQP elements
398     */
399    public Map<Constants.ANQPElementType, ANQPElement> getANQPElements(ScanResult scanResult) {
400        // Retrieve the Hotspot 2.0 Vendor Specific IE.
401        InformationElementUtil.Vsa vsa =
402                InformationElementUtil.getHS2VendorSpecificIE(scanResult.informationElements);
403
404        // Lookup ANQP data in the cache.
405        long bssid = Utils.parseMac(scanResult.BSSID);
406        ANQPData anqpEntry = mAnqpCache.getEntry(ANQPNetworkKey.buildKey(
407                scanResult.SSID, bssid, scanResult.hessid, vsa.anqpDomainID));
408        if (anqpEntry != null) {
409            return anqpEntry.getElements();
410        }
411        return new HashMap<Constants.ANQPElementType, ANQPElement>();
412    }
413
414    /**
415     * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration}.
416     *
417     * @param wifiConfig {@link WifiConfiguration} representation of the Passpoint configuration
418     * @return true on success
419     */
420    private boolean addWifiConfig(WifiConfiguration wifiConfig) {
421        if (wifiConfig == null) {
422            return false;
423        }
424
425        // Convert to PasspointConfiguration
426        PasspointConfiguration passpointConfig =
427                PasspointProvider.convertFromWifiConfig(wifiConfig);
428        if (passpointConfig == null) {
429            return false;
430        }
431
432        // Setup aliases for enterprise certificates and key.
433        WifiEnterpriseConfig enterpriseConfig = wifiConfig.enterpriseConfig;
434        String caCertificateAliasSuffix = enterpriseConfig.getCaCertificateAlias();
435        String clientCertAndKeyAliasSuffix = enterpriseConfig.getClientCertificateAlias();
436        if (passpointConfig.getCredential().getUserCredential() != null
437                && TextUtils.isEmpty(caCertificateAliasSuffix)) {
438            Log.e(TAG, "Missing CA Certificate for user credential");
439            return false;
440        }
441        if (passpointConfig.getCredential().getCertCredential() != null) {
442            if (TextUtils.isEmpty(caCertificateAliasSuffix)) {
443                Log.e(TAG, "Missing CA certificate for Certificate credential");
444                return false;
445            }
446            if (TextUtils.isEmpty(clientCertAndKeyAliasSuffix)) {
447                Log.e(TAG, "Missing client certificate and key for certificate credential");
448                return false;
449            }
450        }
451
452        // Note that for legacy configuration, the alias for client private key is the same as the
453        // alias for the client certificate.
454        PasspointProvider provider = new PasspointProvider(passpointConfig, mKeyStore,
455                mSimAccessor, mProviderIndex++, enterpriseConfig.getCaCertificateAlias(),
456                enterpriseConfig.getClientCertificateAlias(),
457                enterpriseConfig.getClientCertificateAlias());
458        mProviders.put(passpointConfig.getHomeSp().getFqdn(), provider);
459        return true;
460    }
461}
462