1package com.android.hotspot2.osu;
2
3import android.content.Context;
4import android.net.Network;
5import android.net.NetworkInfo;
6import android.net.wifi.ScanResult;
7import android.net.wifi.WifiConfiguration;
8import android.net.wifi.WifiInfo;
9import android.util.Log;
10
11import com.android.anqp.Constants;
12import com.android.anqp.OSUProvider;
13import com.android.hotspot2.AppBridge;
14import com.android.hotspot2.OMADMAdapter;
15import com.android.hotspot2.PasspointMatch;
16import com.android.hotspot2.Utils;
17import com.android.hotspot2.WifiNetworkAdapter;
18import com.android.hotspot2.omadm.MOManager;
19import com.android.hotspot2.omadm.MOTree;
20import com.android.hotspot2.osu.commands.MOData;
21import com.android.hotspot2.osu.service.RedirectListener;
22import com.android.hotspot2.osu.service.SubscriptionTimer;
23import com.android.hotspot2.pps.HomeSP;
24import com.android.hotspot2.pps.UpdateInfo;
25
26import org.xml.sax.SAXException;
27
28import java.io.File;
29import java.io.FileInputStream;
30import java.io.FileOutputStream;
31import java.io.IOException;
32import java.net.MalformedURLException;
33import java.net.URL;
34import java.security.GeneralSecurityException;
35import java.security.KeyStore;
36import java.security.KeyStoreException;
37import java.security.PrivateKey;
38import java.security.cert.Certificate;
39import java.security.cert.CertificateException;
40import java.security.cert.CertificateFactory;
41import java.security.cert.X509Certificate;
42import java.util.ArrayList;
43import java.util.Arrays;
44import java.util.Collection;
45import java.util.Enumeration;
46import java.util.HashMap;
47import java.util.HashSet;
48import java.util.Iterator;
49import java.util.List;
50import java.util.Locale;
51import java.util.Map;
52import java.util.Set;
53import java.util.concurrent.atomic.AtomicInteger;
54
55import javax.net.ssl.KeyManager;
56
57public class OSUManager {
58    public static final String TAG = "OSUMGR";
59    public static final boolean R2_ENABLED = true;
60    public static final boolean R2_MOCK = true;
61    private static final boolean MATCH_BSSID = false;
62
63    private static final String KEYSTORE_FILE = "passpoint.ks";
64    private static final String WFA_CA_LOC = "/etc/security/wfa";
65
66    private static final String OSU_COUNT = "osu-count";
67    private static final String SP_NAME = "sp-name";
68    private static final String PROV_SUCCESS = "prov-success";
69    private static final String DEAUTH = "deauth";
70    private static final String DEAUTH_DELAY = "deauth-delay";
71    private static final String DEAUTH_URL = "deauth-url";
72    private static final String PROV_MESSAGE = "prov-message";
73
74    private static final long REMEDIATION_TIMEOUT = 120000L;
75    // How many scan result batches to hang on to
76
77    public static final int FLOW_PROVISIONING = 1;
78    public static final int FLOW_REMEDIATION = 2;
79    public static final int FLOW_POLICY = 3;
80
81    public static final String CERT_WFA_ALIAS = "wfa-root-";
82    public static final String CERT_REM_ALIAS = "rem-";
83    public static final String CERT_POLICY_ALIAS = "pol-";
84    public static final String CERT_SHARED_ALIAS = "shr-";
85    public static final String CERT_CLT_CERT_ALIAS = "clt-";
86    public static final String CERT_CLT_KEY_ALIAS = "prv-";
87    public static final String CERT_CLT_CA_ALIAS = "aaa-";
88
89    // Preferred icon parameters
90    private static final Set<String> ICON_TYPES =
91            new HashSet<>(Arrays.asList("image/png", "image/jpeg"));
92    private static final int ICON_WIDTH = 64;
93    private static final int ICON_HEIGHT = 64;
94    public static final Locale LOCALE = java.util.Locale.getDefault();
95
96    private final WifiNetworkAdapter mWifiNetworkAdapter;
97
98    private final AppBridge mAppBridge;
99    private final Context mContext;
100    private final IconCache mIconCache;
101    private final SubscriptionTimer mSubscriptionTimer;
102    private final Set<String> mOSUSSIDs = new HashSet<>();
103    private final Map<OSUProvider, OSUInfo> mOSUMap = new HashMap<>();
104    private final KeyStore mKeyStore;
105    private RedirectListener mRedirectListener;
106    private final AtomicInteger mOSUSequence = new AtomicInteger();
107    private OSUThread mProvisioningThread;
108    private final Map<String, OSUThread> mServiceThreads = new HashMap<>();
109    private volatile OSUInfo mPendingOSU;
110    private volatile Integer mOSUNwkID;
111
112    private final OSUCache mOSUCache;
113
114    public OSUManager(Context context) {
115        mContext = context;
116        mAppBridge = new AppBridge(context);
117        mIconCache = new IconCache(this);
118        mWifiNetworkAdapter = new WifiNetworkAdapter(context, this);
119        mSubscriptionTimer = new SubscriptionTimer(this, mWifiNetworkAdapter, context);
120        mOSUCache = new OSUCache();
121        KeyStore ks = null;
122        try {
123            //ks = loadKeyStore(KEYSTORE_FILE, readCertsFromDisk(WFA_CA_LOC));
124            ks = loadKeyStore(new File(context.getFilesDir(), KEYSTORE_FILE),
125                    OSUSocketFactory.buildCertSet());
126        } catch (IOException e) {
127            Log.e(TAG, "Failed to initialize Passpoint keystore, OSU disabled", e);
128        }
129        mKeyStore = ks;
130    }
131
132    private static KeyStore loadKeyStore(File ksFile, Set<X509Certificate> diskCerts)
133            throws IOException {
134        try {
135            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
136            if (ksFile.exists()) {
137                try (FileInputStream in = new FileInputStream(ksFile)) {
138                    keyStore.load(in, null);
139                }
140
141                // Note: comparing two sets of certs does not work.
142                boolean mismatch = false;
143                int loadCount = 0;
144                for (int n = 0; n < 1000; n++) {
145                    String alias = String.format("%s%d", CERT_WFA_ALIAS, n);
146                    Certificate cert = keyStore.getCertificate(alias);
147                    if (cert == null) {
148                        break;
149                    }
150
151                    loadCount++;
152                    boolean matched = false;
153                    Iterator<X509Certificate> iter = diskCerts.iterator();
154                    while (iter.hasNext()) {
155                        X509Certificate diskCert = iter.next();
156                        if (cert.equals(diskCert)) {
157                            iter.remove();
158                            matched = true;
159                            break;
160                        }
161                    }
162                    if (!matched) {
163                        mismatch = true;
164                        break;
165                    }
166                }
167                if (mismatch || !diskCerts.isEmpty()) {
168                    Log.d(TAG, "Re-seeding Passpoint key store with " +
169                            diskCerts.size() + " WFA certs");
170                    for (int n = 0; n < 1000; n++) {
171                        String alias = String.format("%s%d", CERT_WFA_ALIAS, n);
172                        Certificate cert = keyStore.getCertificate(alias);
173                        if (cert == null) {
174                            break;
175                        } else {
176                            keyStore.deleteEntry(alias);
177                        }
178                    }
179                    int index = 0;
180                    for (X509Certificate caCert : diskCerts) {
181                        keyStore.setCertificateEntry(
182                                String.format("%s%d", CERT_WFA_ALIAS, index), caCert);
183                        index++;
184                    }
185
186                    try (FileOutputStream out = new FileOutputStream(ksFile)) {
187                        keyStore.store(out, null);
188                    }
189                } else {
190                    Log.d(TAG, "Loaded Passpoint key store with " + loadCount + " CA certs");
191                    Enumeration<String> aliases = keyStore.aliases();
192                    while (aliases.hasMoreElements()) {
193                        Log.d("ZXC", "KS Alias '" + aliases.nextElement() + "'");
194                    }
195                }
196            } else {
197                keyStore.load(null, null);
198                int index = 0;
199                for (X509Certificate caCert : diskCerts) {
200                    keyStore.setCertificateEntry(
201                            String.format("%s%d", CERT_WFA_ALIAS, index), caCert);
202                    index++;
203                }
204
205                try (FileOutputStream out = new FileOutputStream(ksFile)) {
206                    keyStore.store(out, null);
207                }
208                Log.d(TAG, "Initialized Passpoint key store with " +
209                        diskCerts.size() + " CA certs");
210            }
211            return keyStore;
212        } catch (GeneralSecurityException gse) {
213            throw new IOException(gse);
214        }
215    }
216
217    private static Set<X509Certificate> readCertsFromDisk(String dir) throws CertificateException {
218        Set<X509Certificate> certs = new HashSet<>();
219        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
220        File caDir = new File(dir);
221        File[] certFiles = caDir.listFiles();
222        if (certFiles != null) {
223            for (File certFile : certFiles) {
224                try {
225                    try (FileInputStream in = new FileInputStream(certFile)) {
226                        Certificate cert = certFactory.generateCertificate(in);
227                        if (cert instanceof X509Certificate) {
228                            certs.add((X509Certificate) cert);
229                        }
230                    }
231                } catch (CertificateException | IOException e) {
232                            /* Ignore */
233                }
234            }
235        }
236        return certs;
237    }
238
239    public KeyStore getKeyStore() {
240        return mKeyStore;
241    }
242
243    private static class OSUThread extends Thread {
244        private final OSUClient mOSUClient;
245        private final OSUManager mOSUManager;
246        private final HomeSP mHomeSP;
247        private final String mSpName;
248        private final int mFlowType;
249        private final KeyManager mKeyManager;
250        private final long mLaunchTime;
251        private final Object mLock = new Object();
252        private boolean mLocalAddressSet;
253        private Network mNetwork;
254
255        private OSUThread(OSUInfo osuInfo, OSUManager osuManager, KeyManager km)
256                throws MalformedURLException {
257            mOSUClient = new OSUClient(osuInfo, osuManager.getKeyStore());
258            mOSUManager = osuManager;
259            mHomeSP = null;
260            mSpName = osuInfo.getName(LOCALE);
261            mFlowType = FLOW_PROVISIONING;
262            mKeyManager = km;
263            mLaunchTime = System.currentTimeMillis();
264
265            setDaemon(true);
266            setName("OSU Client Thread");
267        }
268
269        private OSUThread(String osuURL, OSUManager osuManager, KeyManager km, HomeSP homeSP,
270                          int flowType) throws MalformedURLException {
271            mOSUClient = new OSUClient(osuURL, osuManager.getKeyStore());
272            mOSUManager = osuManager;
273            mHomeSP = homeSP;
274            mSpName = homeSP.getFriendlyName();
275            mFlowType = flowType;
276            mKeyManager = km;
277            mLaunchTime = System.currentTimeMillis();
278
279            setDaemon(true);
280            setName("OSU Client Thread");
281        }
282
283        public long getLaunchTime() {
284            return mLaunchTime;
285        }
286
287        private void connect(Network network) {
288            synchronized (mLock) {
289                mNetwork = network;
290                mLocalAddressSet = true;
291                mLock.notifyAll();
292            }
293            Log.d(TAG, "Client notified...");
294        }
295
296        @Override
297        public void run() {
298            Log.d(TAG, mFlowType + "-" + getName() + " running.");
299            Network network;
300            synchronized (mLock) {
301                while (!mLocalAddressSet) {
302                    try {
303                        mLock.wait();
304                    } catch (InterruptedException ie) {
305                        /**/
306                    }
307                    Log.d(TAG, "OSU Thread running...");
308                }
309                network = mNetwork;
310            }
311
312            if (network == null) {
313                Log.d(TAG, "Association failed, exiting OSU flow");
314                mOSUManager.provisioningFailed(mSpName, "Network cannot be reached",
315                        mHomeSP, mFlowType);
316                return;
317            }
318
319            Log.d(TAG, "OSU SSID Associated at " + network.toString());
320            try {
321                if (mFlowType == FLOW_PROVISIONING) {
322                    mOSUClient.provision(mOSUManager, network, mKeyManager);
323                } else {
324                    mOSUClient.remediate(mOSUManager, network, mKeyManager, mHomeSP, mFlowType);
325                }
326            } catch (Throwable t) {
327                Log.w(TAG, "OSU flow failed: " + t, t);
328                mOSUManager.provisioningFailed(mSpName, t.getMessage(), mHomeSP, mFlowType);
329            }
330        }
331    }
332
333    /*
334    public void startOSU() {
335        registerUserInputListener(new UserInputListener() {
336            @Override
337            public void requestUserInput(URL target, Network network, URL endRedirect) {
338                Log.d(TAG, "Browser to " + target + ", land at " + endRedirect);
339
340                final Intent intent = new Intent(
341                        ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
342                intent.putExtra(ConnectivityManager.EXTRA_NETWORK, network);
343                intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
344                        new CaptivePortal(new ICaptivePortal.Stub() {
345                            @Override
346                            public void appResponse(int response) {
347                            }
348                        }));
349                //intent.setData(Uri.parse(target.toString()));     !!! Doesn't work!
350                intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, target.toString());
351                intent.setFlags(
352                        Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
353                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
354            }
355
356            @Override
357            public String operationStatus(String spIdentity, OSUOperationStatus status,
358                                          String message) {
359                Log.d(TAG, "OSU OP Status: " + status + ", message " + message);
360                Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION);
361                intent.putExtra(SP_NAME, spIdentity);
362                intent.putExtra(PROV_SUCCESS, status == OSUOperationStatus.ProvisioningSuccess);
363                if (message != null) {
364                    intent.putExtra(PROV_MESSAGE, message);
365                }
366                intent.setFlags(
367                        Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
368                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
369                return null;
370            }
371
372            @Override
373            public void deAuthNotification(String spIdentity, boolean ess, int delay, URL url) {
374                Log.i(TAG, "De-authentication imminent for " + (ess ? "ess" : "bss") +
375                        ", redirect to " + url);
376                Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION);
377                intent.putExtra(SP_NAME, spIdentity);
378                intent.putExtra(DEAUTH, ess);
379                intent.putExtra(DEAUTH_DELAY, delay);
380                intent.putExtra(DEAUTH_URL, url.toString());
381                intent.setFlags(
382                        Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
383                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
384            }
385        });
386        addOSUListener(new OSUListener() {
387            @Override
388            public void osuNotification(int count) {
389                Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION);
390                intent.putExtra(OSU_COUNT, count);
391                intent.setFlags(
392                        Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
393                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
394            }
395        });
396        mWifiNetworkAdapter.initialize();
397        mSubscriptionTimer.checkUpdates();
398    }
399    */
400
401    public List<OSUInfo> getAvailableOSUs() {
402        synchronized (mOSUMap) {
403            List<OSUInfo> completeOSUs = new ArrayList<>();
404            for (OSUInfo osuInfo : mOSUMap.values()) {
405                if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
406                    completeOSUs.add(osuInfo);
407                }
408            }
409            return completeOSUs;
410        }
411    }
412
413    public void recheckTimers() {
414        mSubscriptionTimer.checkUpdates();
415    }
416
417    public void setOSUSelection(int osuID) {
418        OSUInfo selection = null;
419        for (OSUInfo osuInfo : mOSUMap.values()) {
420            Log.d("ZXZ", "In select: " + osuInfo + ", id " + osuInfo.getOsuID());
421            if (osuInfo.getOsuID() == osuID &&
422                    osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
423                selection = osuInfo;
424                break;
425            }
426        }
427
428        Log.d(TAG, "Selected OSU ID " + osuID + ", matches " + selection);
429
430        if (selection == null) {
431            mPendingOSU = null;
432            return;
433        }
434
435        mPendingOSU = selection;
436        WifiConfiguration config = mWifiNetworkAdapter.getActiveWifiConfig();
437
438        if (config != null &&
439                bssidMatch(selection) &&
440                Utils.unquote(config.SSID).equals(selection.getSSID())) {
441
442            try {
443                // Go straight to provisioning if the network is already selected.
444                // Also note that mOSUNwkID is left unset to leave the network around after
445                // flow completion since it was not added by the OSU flow.
446                initiateProvisioning(mPendingOSU, mWifiNetworkAdapter.getCurrentNetwork());
447            } catch (IOException ioe) {
448                notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
449                        mPendingOSU.getName(LOCALE));
450            } finally {
451                mPendingOSU = null;
452            }
453        } else {
454            try {
455                mOSUNwkID = mWifiNetworkAdapter.connect(selection, mPendingOSU.getName(LOCALE));
456            } catch (IOException ioe) {
457                notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
458                        selection.getName(LOCALE));
459            }
460        }
461    }
462
463    public void networkConfigChange(WifiConfiguration configuration) {
464        mWifiNetworkAdapter.networkConfigChange(configuration);
465    }
466
467    public void networkConnectEvent(WifiInfo wifiInfo) {
468        if (wifiInfo != null) {
469            setActiveNetwork(mWifiNetworkAdapter.getActiveWifiConfig(),
470                    mWifiNetworkAdapter.getCurrentNetwork());
471        }
472    }
473
474    public void wifiStateChange(boolean on) {
475        if (!on) {
476            int current = mOSUMap.size();
477            mOSUMap.clear();
478            mOSUCache.clearAll();
479            mIconCache.clear();
480            if (current > 0) {
481                notifyOSUCount(0);
482            }
483        }
484    }
485
486    private boolean bssidMatch(OSUInfo osuInfo) {
487        if (MATCH_BSSID) {
488            WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo();
489            return wifiInfo != null && Utils.parseMac(wifiInfo.getBSSID()) == osuInfo.getBSSID();
490        } else {
491            return true;
492        }
493    }
494
495    public void setActiveNetwork(WifiConfiguration wifiConfiguration, Network network) {
496        Log.d(TAG, "Network change: " + network + ", cfg " +
497                (wifiConfiguration != null ? wifiConfiguration.SSID : "-") + ", osu " + mPendingOSU);
498        if (mPendingOSU != null &&
499                wifiConfiguration != null &&
500                network != null &&
501                bssidMatch(mPendingOSU) &&
502                Utils.unquote(wifiConfiguration.SSID).equals(mPendingOSU.getSSID())) {
503
504            try {
505                Log.d(TAG, "New network " + network + ", current OSU " + mPendingOSU);
506                initiateProvisioning(mPendingOSU, network);
507            } catch (IOException ioe) {
508                notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
509                        mPendingOSU.getName(LOCALE));
510            } finally {
511                mPendingOSU = null;
512            }
513            return;
514        }
515
516        /*
517        // !!! Hack to force start remediation at connection time
518        else if (wifiConfiguration != null && wifiConfiguration.isPasspoint()) {
519            HomeSP homeSP = mWifiConfigStore.getHomeSPForConfig(wifiConfiguration);
520            if (homeSP != null && homeSP.getSubscriptionUpdate() != null) {
521                if (!mServiceThreads.containsKey(homeSP.getFQDN())) {
522                    try {
523                        remediate(homeSP);
524                    } catch (IOException ioe) {
525                        Log.w(TAG, "Failed to remediate: " + ioe);
526                    }
527                }
528            }
529        }
530        */
531        else if (wifiConfiguration == null) {
532            mServiceThreads.clear();
533        }
534    }
535
536
537    /**
538     * Called when an OSU has been selected and the associated network is fully connected.
539     *
540     * @param osuInfo The selected OSUInfo or null if the current OSU flow is cancelled externally,
541     *                e.g. WiFi is turned off or the OSU network is otherwise detected as
542     *                unreachable.
543     * @param network The currently associated network (for the OSU SSID).
544     * @throws IOException
545     * @throws GeneralSecurityException
546     */
547    private void initiateProvisioning(OSUInfo osuInfo, Network network)
548            throws IOException {
549        synchronized (mWifiNetworkAdapter) {
550            if (mProvisioningThread != null) {
551                mProvisioningThread.connect(null);
552                mProvisioningThread = null;
553            }
554            if (mRedirectListener != null) {
555                mRedirectListener.abort();
556                mRedirectListener = null;
557            }
558            if (osuInfo != null) {
559                //new ConnMonitor().start();
560                mProvisioningThread = new OSUThread(osuInfo, this, getKeyManager(null, mKeyStore));
561                mProvisioningThread.start();
562                //mWifiNetworkAdapter.associate(osuInfo.getSSID(),
563                //        osuInfo.getBSSID(), osuInfo.getOSUProvider().getOsuNai());
564                mProvisioningThread.connect(network);
565            }
566        }
567    }
568
569    /**
570     * @param homeSP The Home SP associated with the keying material in question. Passing
571     *               null returns a "system wide" KeyManager to support pre-provisioned certs based
572     *               on names retrieved from the ClientCertInfo request.
573     * @return A key manager suitable for the given configuration (or pre-provisioned keys).
574     */
575    private static KeyManager getKeyManager(HomeSP homeSP, KeyStore keyStore)
576            throws IOException {
577        return homeSP != null ? new ClientKeyManager(homeSP, keyStore) :
578                new WiFiKeyManager(keyStore);
579    }
580
581    public boolean isOSU(String ssid) {
582        synchronized (mOSUMap) {
583            return mOSUSSIDs.contains(ssid);
584        }
585    }
586
587    public void tickleIconCache(boolean all) {
588        mIconCache.tickle(all);
589
590        if (all) {
591            synchronized (mOSUMap) {
592                int current = mOSUMap.size();
593                mOSUMap.clear();
594                mOSUCache.clearAll();
595                mIconCache.clear();
596                if (current > 0) {
597                    notifyOSUCount(0);
598                }
599            }
600        }
601    }
602
603    public void pushScanResults(Collection<ScanResult> scanResults) {
604        Map<OSUProvider, ScanResult> results = mOSUCache.pushScanResults(scanResults);
605        if (results != null) {
606            updateOSUInfoCache(results);
607        }
608    }
609
610    private void updateOSUInfoCache(Map<OSUProvider, ScanResult> results) {
611        Map<OSUProvider, OSUInfo> osus = new HashMap<>();
612        for (Map.Entry<OSUProvider, ScanResult> entry : results.entrySet()) {
613            OSUInfo existing = mOSUMap.get(entry.getKey());
614            long bssid = Utils.parseMac(entry.getValue().BSSID);
615
616            if (existing == null || existing.getBSSID() != bssid) {
617                osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey().getSSID(),
618                        entry.getKey(), mOSUSequence.getAndIncrement()));
619            } else {
620                // Maintain existing entries.
621                osus.put(entry.getKey(), existing);
622            }
623        }
624
625        mOSUMap.clear();
626        mOSUMap.putAll(osus);
627
628        mOSUSSIDs.clear();
629        for (OSUInfo osuInfo : mOSUMap.values()) {
630            mOSUSSIDs.add(osuInfo.getSSID());
631        }
632
633        if (mOSUMap.isEmpty()) {
634            notifyOSUCount(0);
635        }
636        initiateIconQueries();
637        Log.d(TAG, "Latest (app) OSU info: " + mOSUMap);
638    }
639
640    public void iconResults(List<OSUInfo> osuInfos) {
641        int newIcons = 0;
642        for (OSUInfo osuInfo : osuInfos) {
643            if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
644                newIcons++;
645            }
646        }
647        if (newIcons > 0) {
648            int count = 0;
649            for (OSUInfo existing : mOSUMap.values()) {
650                if (existing.getIconStatus() == OSUInfo.IconStatus.Available) {
651                    count++;
652                }
653            }
654            Log.d(TAG, "Icon results for " + count + " OSUs");
655            notifyOSUCount(count);
656        }
657    }
658
659    private void notifyOSUCount(int count) {
660        mAppBridge.showOsuCount(count, getAvailableOSUs());
661    }
662
663    private void initiateIconQueries() {
664        for (OSUInfo osuInfo : mOSUMap.values()) {
665            if (osuInfo.getIconStatus() == OSUInfo.IconStatus.NotQueried) {
666                mIconCache.startIconQuery(osuInfo,
667                        osuInfo.getIconInfo(LOCALE, ICON_TYPES, ICON_WIDTH, ICON_HEIGHT));
668            }
669        }
670    }
671
672    public void deauth(long bssid, boolean ess, int delay, String url) throws MalformedURLException {
673        Log.d(TAG, String.format("De-auth imminent on %s, delay %ss to '%s'",
674                ess ? "ess" : "bss",
675                delay,
676                url));
677        mWifiNetworkAdapter.setHoldoffTime(delay * Constants.MILLIS_IN_A_SEC, ess);
678        HomeSP homeSP = mWifiNetworkAdapter.getCurrentSP();
679        String spName = homeSP != null ? homeSP.getFriendlyName() : "unknown";
680        mAppBridge.showDeauth(spName, ess, delay, url);
681    }
682
683    // !!! Consistently check passpoint match.
684    // !!! Convert to a one-thread thread-pool
685    public void wnmRemediate(long bssid, String url, PasspointMatch match)
686            throws IOException, SAXException {
687        WifiConfiguration config = mWifiNetworkAdapter.getActiveWifiConfig();
688        HomeSP homeSP = MOManager.buildSP(config.getMoTree());
689        if (homeSP == null) {
690            throw new IOException("Remediation request for unidentified Passpoint network " +
691                    config.networkId);
692        }
693        Network network = mWifiNetworkAdapter.getCurrentNetwork();
694        if (network == null) {
695            throw new IOException("Failed to determine current network");
696        }
697        WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo();
698        if (wifiInfo == null || Utils.parseMac(wifiInfo.getBSSID()) != bssid) {
699            throw new IOException("Mismatching BSSID");
700        }
701        Log.d(TAG, "WNM Remediation on " + network.netId + " FQDN " + homeSP.getFQDN());
702
703        doRemediate(url, network, homeSP, false);
704    }
705
706    public void remediate(HomeSP homeSP, boolean policy) throws IOException, SAXException {
707        UpdateInfo updateInfo;
708        if (policy) {
709            if (homeSP.getPolicy() == null) {
710                throw new IOException("No policy object");
711            }
712            updateInfo = homeSP.getPolicy().getPolicyUpdate();
713        } else {
714            updateInfo = homeSP.getSubscriptionUpdate();
715        }
716        switch (updateInfo.getUpdateRestriction()) {
717            case HomeSP: {
718                Network network = mWifiNetworkAdapter.getCurrentNetwork();
719                if (network == null) {
720                    throw new IOException("Failed to determine current network");
721                }
722
723                WifiConfiguration config = mWifiNetworkAdapter.getActivePasspointNetwork();
724                HomeSP activeSP = MOManager.buildSP(config.getMoTree());
725
726                if (activeSP == null || !activeSP.getFQDN().equals(homeSP.getFQDN())) {
727                    throw new IOException("Remediation restricted to HomeSP");
728                }
729                doRemediate(updateInfo.getURI(), network, homeSP, policy);
730                break;
731            }
732            case RoamingPartner: {
733                Network network = mWifiNetworkAdapter.getCurrentNetwork();
734                if (network == null) {
735                    throw new IOException("Failed to determine current network");
736                }
737
738                WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo();
739                if (wifiInfo == null) {
740                    throw new IOException("Unable to determine WiFi info");
741                }
742
743                PasspointMatch match = mWifiNetworkAdapter.
744                        matchProviderWithCurrentNetwork(homeSP.getFQDN());
745
746                if (match == PasspointMatch.HomeProvider ||
747                        match == PasspointMatch.RoamingProvider) {
748                    doRemediate(updateInfo.getURI(), network, homeSP, policy);
749                } else {
750                    throw new IOException("No roaming network match: " + match);
751                }
752                break;
753            }
754            case Unrestricted: {
755                Network network = mWifiNetworkAdapter.getCurrentNetwork();
756                doRemediate(updateInfo.getURI(), network, homeSP, policy);
757                break;
758            }
759        }
760    }
761
762    private void doRemediate(String url, Network network, HomeSP homeSP, boolean policy)
763            throws IOException {
764        synchronized (mWifiNetworkAdapter) {
765            OSUThread existing = mServiceThreads.get(homeSP.getFQDN());
766            if (existing != null) {
767                if (System.currentTimeMillis() - existing.getLaunchTime() > REMEDIATION_TIMEOUT) {
768                    throw new IOException("Ignoring recurring remediation request");
769                } else {
770                    existing.connect(null);
771                }
772            }
773
774            try {
775                OSUThread osuThread = new OSUThread(url, this,
776                        getKeyManager(homeSP, mKeyStore),
777                        homeSP, policy ? FLOW_POLICY : FLOW_REMEDIATION);
778                osuThread.start();
779                osuThread.connect(network);
780                mServiceThreads.put(homeSP.getFQDN(), osuThread);
781            } catch (MalformedURLException me) {
782                throw new IOException("Failed to start remediation: " + me);
783            }
784        }
785    }
786
787    public MOTree getMOTree(HomeSP homeSP) throws IOException {
788        return mWifiNetworkAdapter.getMOTree(homeSP);
789    }
790
791    public void notifyIconReceived(long bssid, String fileName, byte[] data) {
792        mIconCache.notifyIconReceived(bssid, fileName, data);
793    }
794
795    public void doIconQuery(long bssid, String fileName) {
796        mWifiNetworkAdapter.doIconQuery(bssid, fileName);
797    }
798
799    protected URL prepareUserInput(String spName) throws IOException {
800        mRedirectListener = new RedirectListener(this, spName);
801        return mRedirectListener.getURL();
802    }
803
804    protected boolean startUserInput(URL target, Network network) throws IOException {
805        mRedirectListener.startService();
806        mWifiNetworkAdapter.launchBrowser(target, network, mRedirectListener.getURL());
807
808        return mRedirectListener.waitForUser();
809    }
810
811    public String notifyUser(OSUOperationStatus status, String message, String spName) {
812        if (status == OSUOperationStatus.UserInputComplete) {
813            return null;
814        }
815        if (mOSUNwkID != null) {
816            // Delete the OSU network if it was added by the OSU flow
817            mWifiNetworkAdapter.deleteNetwork(mOSUNwkID);
818            mOSUNwkID = null;
819        }
820        mAppBridge.showStatus(status, spName, message, null);
821        return null;
822    }
823
824    public void provisioningFailed(String spName, String message, HomeSP homeSP,
825                                   int flowType) {
826        synchronized (mWifiNetworkAdapter) {
827            switch (flowType) {
828                case FLOW_PROVISIONING:
829                    mProvisioningThread = null;
830                    if (mRedirectListener != null) {
831                        mRedirectListener.abort();
832                        mRedirectListener = null;
833                    }
834                    break;
835                case FLOW_REMEDIATION:
836                case FLOW_POLICY:
837                    mServiceThreads.remove(homeSP.getFQDN());
838                    if (mServiceThreads.isEmpty() && mRedirectListener != null) {
839                        mRedirectListener.abort();
840                        mRedirectListener = null;
841                    }
842                    break;
843            }
844        }
845        notifyUser(OSUOperationStatus.ProvisioningFailure, message, spName);
846    }
847
848    public void provisioningComplete(OSUInfo osuInfo,
849                                     MOData moData, Map<OSUCertType, List<X509Certificate>> certs,
850                                     PrivateKey privateKey, Network osuNetwork) {
851        synchronized (mWifiNetworkAdapter) {
852            mProvisioningThread = null;
853        }
854        try {
855            Log.d("ZXZ", "MOTree.toXML: " + moData.getMOTree().toXml());
856            HomeSP homeSP = mWifiNetworkAdapter.addSP(moData.getMOTree());
857
858            Integer spNwk = mWifiNetworkAdapter.addNetwork(homeSP, certs, privateKey, osuNetwork);
859            if (spNwk == null) {
860                notifyUser(OSUOperationStatus.ProvisioningFailure,
861                        "Failed to save network configuration", osuInfo.getName(LOCALE));
862                mWifiNetworkAdapter.removeSP(homeSP.getFQDN());
863            } else {
864                Set<X509Certificate> rootCerts = OSUSocketFactory.getRootCerts(mKeyStore);
865                X509Certificate remCert = getCert(certs, OSUCertType.Remediation);
866                X509Certificate polCert = getCert(certs, OSUCertType.Policy);
867                if (privateKey != null) {
868                    X509Certificate cltCert = getCert(certs, OSUCertType.Client);
869                    mKeyStore.setKeyEntry(CERT_CLT_KEY_ALIAS + homeSP,
870                            privateKey.getEncoded(),
871                            new X509Certificate[]{cltCert});
872                    mKeyStore.setCertificateEntry(CERT_CLT_CERT_ALIAS, cltCert);
873                }
874                boolean usingShared = false;
875                int newCerts = 0;
876                if (remCert != null) {
877                    if (!rootCerts.contains(remCert)) {
878                        if (remCert.equals(polCert)) {
879                            mKeyStore.setCertificateEntry(CERT_SHARED_ALIAS + homeSP.getFQDN(),
880                                    remCert);
881                            usingShared = true;
882                            newCerts++;
883                        } else {
884                            mKeyStore.setCertificateEntry(CERT_REM_ALIAS + homeSP.getFQDN(),
885                                    remCert);
886                            newCerts++;
887                        }
888                    }
889                }
890                if (!usingShared && polCert != null) {
891                    if (!rootCerts.contains(polCert)) {
892                        mKeyStore.setCertificateEntry(CERT_POLICY_ALIAS + homeSP.getFQDN(),
893                                remCert);
894                        newCerts++;
895                    }
896                }
897
898                if (newCerts > 0) {
899                    try (FileOutputStream out = new FileOutputStream(KEYSTORE_FILE)) {
900                        mKeyStore.store(out, null);
901                    }
902                }
903                notifyUser(OSUOperationStatus.ProvisioningSuccess, null, osuInfo.getName(LOCALE));
904                Log.d(TAG, "Provisioning complete.");
905            }
906        } catch (IOException | GeneralSecurityException | SAXException e) {
907            Log.e(TAG, "Failed to provision: " + e, e);
908            notifyUser(OSUOperationStatus.ProvisioningFailure, e.toString(),
909                    osuInfo.getName(LOCALE));
910        }
911    }
912
913    private static X509Certificate getCert(Map<OSUCertType, List<X509Certificate>> certMap,
914                                           OSUCertType certType) {
915        List<X509Certificate> certs = certMap.get(certType);
916        if (certs == null || certs.isEmpty()) {
917            return null;
918        }
919        return certs.iterator().next();
920    }
921
922    public void spDeleted(String fqdn) {
923        int count = deleteCerts(mKeyStore, fqdn,
924                CERT_REM_ALIAS, CERT_POLICY_ALIAS, CERT_SHARED_ALIAS);
925
926        if (count > 0) {
927            try (FileOutputStream out = new FileOutputStream(KEYSTORE_FILE)) {
928                mKeyStore.store(out, null);
929            } catch (IOException | GeneralSecurityException e) {
930                Log.w(TAG, "Failed to remove certs from key store: " + e);
931            }
932        }
933    }
934
935    private static int deleteCerts(KeyStore keyStore, String fqdn, String... prefixes) {
936        int count = 0;
937        for (String prefix : prefixes) {
938            try {
939                String alias = prefix + fqdn;
940                Certificate cert = keyStore.getCertificate(alias);
941                if (cert != null) {
942                    keyStore.deleteEntry(alias);
943                    count++;
944                }
945            } catch (KeyStoreException kse) {
946                /**/
947            }
948        }
949        return count;
950    }
951
952    public void remediationComplete(HomeSP homeSP, Collection<MOData> mods,
953                                    Map<OSUCertType, List<X509Certificate>> certs,
954                                    PrivateKey privateKey)
955            throws IOException, GeneralSecurityException {
956
957        HomeSP altSP = mWifiNetworkAdapter.modifySP(homeSP, mods);
958        X509Certificate caCert = null;
959        List<X509Certificate> clientCerts = null;
960        if (certs != null) {
961            List<X509Certificate> certList = certs.get(OSUCertType.AAA);
962            caCert = certList != null && !certList.isEmpty() ? certList.iterator().next() : null;
963            clientCerts = certs.get(OSUCertType.Client);
964        }
965        if (altSP != null || certs != null) {
966            if (altSP == null) {
967                altSP = homeSP;     // No MO mods, only certs and key
968            }
969            mWifiNetworkAdapter.updateNetwork(altSP, caCert, clientCerts, privateKey);
970        }
971        notifyUser(OSUOperationStatus.ProvisioningSuccess, null, homeSP.getFriendlyName());
972    }
973
974    protected OMADMAdapter getOMADMAdapter() {
975        return OMADMAdapter.getInstance(mContext);
976    }
977}
978