PasspointManager.java revision b48796131ddd016071144f77f208360e8c408f0e
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.WifiConfiguration;
33import android.net.wifi.WifiEnterpriseConfig;
34import android.net.wifi.hotspot2.PasspointConfiguration;
35import android.os.UserHandle;
36import android.text.TextUtils;
37import android.util.Log;
38import android.util.Pair;
39
40import com.android.server.wifi.Clock;
41import com.android.server.wifi.SIMAccessor;
42import com.android.server.wifi.ScanDetail;
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;
49
50import java.util.ArrayList;
51import java.util.HashMap;
52import java.util.List;
53import java.util.Map;
54
55/**
56 * This class provides the APIs to manage Passpoint provider configurations.
57 * It deals with the following:
58 * - Maintaining a list of configured Passpoint providers for provider matching.
59 * - Persisting the providers configurations to store when required.
60 * - matching Passpoint providers based on the scan results
61 * - Supporting WifiManager Public API calls:
62 *   > addOrUpdatePasspointConfiguration()
63 *   > removePasspointConfiguration()
64 *   > getPasspointConfigurations()
65 *
66 * The provider matching requires obtaining additional information from the AP (ANQP elements).
67 * The ANQP elements will be cached using {@link AnqpCache} to avoid unnecessary requests.
68 *
69 * NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
70 */
71public class PasspointManager {
72    private static final String TAG = "PasspointManager";
73
74    /**
75     * Handle for the current {@link PasspointManager} instance.  This is needed to avoid
76     * circular dependency with the WifiConfigManger, it will be used for adding the
77     * legacy Passpoint configurations.
78     *
79     * This can be eliminated once we can remove the dependency for WifiConfigManager (for
80     * triggering config store write) from this class.
81     */
82    private static PasspointManager sPasspointManager;
83
84    private final PasspointEventHandler mHandler;
85    private final SIMAccessor mSimAccessor;
86    private final WifiKeyStore mKeyStore;
87    private final PasspointObjectFactory mObjectFactory;
88    private final Map<String, PasspointProvider> mProviders;
89    private final AnqpCache mAnqpCache;
90    private final ANQPRequestManager mAnqpRequestManager;
91    private final WifiConfigManager mWifiConfigManager;
92
93    // Counter used for assigning unique identifier to each provider.
94    private long mProviderIndex;
95
96    private class CallbackHandler implements PasspointEventHandler.Callbacks {
97        private final Context mContext;
98        CallbackHandler(Context context) {
99            mContext = context;
100        }
101
102        @Override
103        public void onANQPResponse(long bssid,
104                Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
105            // Notify request manager for the completion of a request.
106            ScanDetail scanDetail =
107                    mAnqpRequestManager.onRequestCompleted(bssid, anqpElements != null);
108            if (anqpElements == null || scanDetail == null) {
109                // Query failed or the request wasn't originated from us (not tracked by the
110                // request manager). Nothing to be done.
111                return;
112            }
113
114            // Add new entry to the cache.
115            NetworkDetail networkDetail = scanDetail.getNetworkDetail();
116            ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(networkDetail.getSSID(),
117                    networkDetail.getBSSID(), networkDetail.getHESSID(),
118                    networkDetail.getAnqpDomainID());
119            mAnqpCache.addEntry(anqpKey, anqpElements);
120
121            // Update ANQP elements in the ScanDetail.
122            scanDetail.propagateANQPInfo(anqpElements);
123        }
124
125        @Override
126        public void onIconResponse(long bssid, String fileName, byte[] data) {
127            Intent intent = new Intent(ACTION_PASSPOINT_ICON);
128            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
129            intent.putExtra(EXTRA_BSSID_LONG, bssid);
130            intent.putExtra(EXTRA_ICON_INFO, new IconInfo(fileName, data));
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        return true;
248    }
249
250    /**
251     * Remove a Passpoint provider identified by the given FQDN.
252     *
253     * @param fqdn The FQDN of the provider to remove
254     * @return true if a provider is removed, false otherwise
255     */
256    public boolean removeProvider(String fqdn) {
257        if (!mProviders.containsKey(fqdn)) {
258            Log.e(TAG, "Config doesn't exist");
259            return false;
260        }
261
262        mProviders.get(fqdn).uninstallCertsAndKeys();
263        mProviders.remove(fqdn);
264        mWifiConfigManager.saveToStore(true /* forceWrite */);
265        return true;
266    }
267
268    /**
269     * Return the installed Passpoint provider configurations.
270     *
271     * An empty list will be returned when no provider is installed.
272     *
273     * @return A list of {@link PasspointConfiguration}
274     */
275    public List<PasspointConfiguration> getProviderConfigs() {
276        List<PasspointConfiguration> configs = new ArrayList<>();
277        for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
278            configs.add(entry.getValue().getConfig());
279        }
280        return configs;
281    }
282
283    /**
284     * Find the providers that can provide service through the given AP, which means the
285     * providers contained credential to authenticate with the given AP.
286     *
287     * An empty list will returned in the case when no match is found.
288     *
289     * @param scanDetail The detail information of the AP
290     * @return List of {@link PasspointProvider}
291     */
292    public List<Pair<PasspointProvider, PasspointMatch>> matchProvider(ScanDetail scanDetail) {
293        // Nothing to be done if no Passpoint provider is installed.
294        if (mProviders.isEmpty()) {
295            return new ArrayList<Pair<PasspointProvider, PasspointMatch>>();
296        }
297
298        // Lookup ANQP data in the cache.
299        NetworkDetail networkDetail = scanDetail.getNetworkDetail();
300        ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(networkDetail.getSSID(),
301                networkDetail.getBSSID(), networkDetail.getHESSID(),
302                networkDetail.getAnqpDomainID());
303        ANQPData anqpEntry = mAnqpCache.getEntry(anqpKey);
304
305        if (anqpEntry == null) {
306            mAnqpRequestManager.requestANQPElements(networkDetail.getBSSID(), scanDetail,
307                    networkDetail.getAnqpOICount() > 0,
308                    networkDetail.getHSRelease() == NetworkDetail.HSRelease.R2);
309            return new ArrayList<Pair<PasspointProvider, PasspointMatch>>();
310        }
311
312        List<Pair<PasspointProvider, PasspointMatch>> results = new ArrayList<>();
313        for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
314            PasspointProvider provider = entry.getValue();
315            PasspointMatch matchStatus = provider.match(anqpEntry.getElements());
316            if (matchStatus == PasspointMatch.HomeProvider
317                    || matchStatus == PasspointMatch.RoamingProvider) {
318                results.add(new Pair<PasspointProvider, PasspointMatch>(provider, matchStatus));
319            }
320        }
321        return results;
322    }
323
324    /**
325     * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration} to the
326     * current {@link PasspointManager}.
327     *
328     * This will not trigger a config store write, since this will be invoked as part of the
329     * configuration migration, the caller will be responsible for triggering store write
330     * after the migration is completed.
331     *
332     * @param config {@link WifiConfiguration} representation of the Passpoint configuration
333     * @return true on success
334     */
335    public static boolean addLegacyPasspointConfig(WifiConfiguration config) {
336        if (sPasspointManager == null) {
337            Log.e(TAG, "PasspointManager have not been initialized yet");
338            return false;
339        }
340        return sPasspointManager.addWifiConfig(config);
341    }
342
343    /**
344     * Sweep the ANQP cache to remove expired entries.
345     */
346    public void sweepCache() {
347        mAnqpCache.sweep();
348    }
349
350    /**
351     * Notify the completion of an ANQP request.
352     * TODO(zqiu): currently the notification is done through WifiMonitor,
353     * will no longer be the case once we switch over to use wificond.
354     */
355    public void notifyANQPDone(AnqpEvent anqpEvent) {
356        mHandler.notifyANQPDone(anqpEvent);
357    }
358
359    /**
360     * Notify the completion of an icon request.
361     * TODO(zqiu): currently the notification is done through WifiMonitor,
362     * will no longer be the case once we switch over to use wificond.
363     */
364    public void notifyIconDone(IconEvent iconEvent) {
365        mHandler.notifyIconDone(iconEvent);
366    }
367
368    /**
369     * Notify the reception of a Wireless Network Management (WNM) frame.
370     * TODO(zqiu): currently the notification is done through WifiMonitor,
371     * will no longer be the case once we switch over to use wificond.
372     */
373    public void receivedWnmFrame(WnmData data) {
374        mHandler.notifyWnmFrameReceived(data);
375    }
376
377    /**
378     * Request the specified icon file |fileName| from the specified AP |bssid|.
379     * @return true if the request is sent successfully, false otherwise
380     */
381    public boolean queryPasspointIcon(long bssid, String fileName) {
382        return mHandler.requestIcon(bssid, fileName);
383    }
384
385    /**
386     * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration}.
387     *
388     * @param wifiConfig {@link WifiConfiguration} representation of the Passpoint configuration
389     * @return true on success
390     */
391    private boolean addWifiConfig(WifiConfiguration wifiConfig) {
392        if (wifiConfig == null) {
393            return false;
394        }
395
396        // Convert to PasspointConfiguration
397        PasspointConfiguration passpointConfig =
398                PasspointProvider.convertFromWifiConfig(wifiConfig);
399        if (passpointConfig == null) {
400            return false;
401        }
402
403        // Setup aliases for enterprise certificates and key.
404        WifiEnterpriseConfig enterpriseConfig = wifiConfig.enterpriseConfig;
405        String caCertificateAliasSuffix = enterpriseConfig.getCaCertificateAlias();
406        String clientCertAndKeyAliasSuffix = enterpriseConfig.getClientCertificateAlias();
407        if (passpointConfig.getCredential().getUserCredential() != null
408                && TextUtils.isEmpty(caCertificateAliasSuffix)) {
409            Log.e(TAG, "Missing CA Certificate for user credential");
410            return false;
411        }
412        if (passpointConfig.getCredential().getCertCredential() != null) {
413            if (TextUtils.isEmpty(caCertificateAliasSuffix)) {
414                Log.e(TAG, "Missing CA certificate for Certificate credential");
415                return false;
416            }
417            if (TextUtils.isEmpty(clientCertAndKeyAliasSuffix)) {
418                Log.e(TAG, "Missing client certificate and key for certificate credential");
419                return false;
420            }
421        }
422
423        // Note that for legacy configuration, the alias for client private key is the same as the
424        // alias for the client certificate.
425        PasspointProvider provider = new PasspointProvider(passpointConfig, mKeyStore,
426                mSimAccessor, mProviderIndex++, enterpriseConfig.getCaCertificateAlias(),
427                enterpriseConfig.getClientCertificateAlias(),
428                enterpriseConfig.getClientCertificateAlias());
429        mProviders.put(passpointConfig.getHomeSp().getFqdn(), provider);
430        return true;
431    }
432}
433