• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.hotspot2.osu;
2 
3 import android.content.Context;
4 import android.net.Network;
5 import android.net.NetworkInfo;
6 import android.net.wifi.ScanResult;
7 import android.net.wifi.WifiConfiguration;
8 import android.net.wifi.WifiInfo;
9 import android.util.Log;
10 
11 import com.android.anqp.Constants;
12 import com.android.anqp.OSUProvider;
13 import com.android.hotspot2.AppBridge;
14 import com.android.hotspot2.OMADMAdapter;
15 import com.android.hotspot2.PasspointMatch;
16 import com.android.hotspot2.Utils;
17 import com.android.hotspot2.WifiNetworkAdapter;
18 import com.android.hotspot2.omadm.MOManager;
19 import com.android.hotspot2.omadm.MOTree;
20 import com.android.hotspot2.osu.commands.MOData;
21 import com.android.hotspot2.osu.service.RedirectListener;
22 import com.android.hotspot2.osu.service.SubscriptionTimer;
23 import com.android.hotspot2.pps.HomeSP;
24 import com.android.hotspot2.pps.UpdateInfo;
25 
26 import org.xml.sax.SAXException;
27 
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.FileOutputStream;
31 import java.io.IOException;
32 import java.net.MalformedURLException;
33 import java.net.URL;
34 import java.security.GeneralSecurityException;
35 import java.security.KeyStore;
36 import java.security.KeyStoreException;
37 import java.security.PrivateKey;
38 import java.security.cert.Certificate;
39 import java.security.cert.CertificateException;
40 import java.security.cert.CertificateFactory;
41 import java.security.cert.X509Certificate;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Collection;
45 import java.util.Enumeration;
46 import java.util.HashMap;
47 import java.util.HashSet;
48 import java.util.Iterator;
49 import java.util.List;
50 import java.util.Locale;
51 import java.util.Map;
52 import java.util.Set;
53 import java.util.concurrent.atomic.AtomicInteger;
54 
55 import javax.net.ssl.KeyManager;
56 
57 public 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 
OSUManager(Context context)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 
loadKeyStore(File ksFile, Set<X509Certificate> diskCerts)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 
readCertsFromDisk(String dir)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 
getKeyStore()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 
OSUThread(OSUInfo osuInfo, OSUManager osuManager, KeyManager km)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 
OSUThread(String osuURL, OSUManager osuManager, KeyManager km, HomeSP homeSP, int flowType)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 
getLaunchTime()283         public long getLaunchTime() {
284             return mLaunchTime;
285         }
286 
connect(Network network)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
run()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 
getAvailableOSUs()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 
recheckTimers()413     public void recheckTimers() {
414         mSubscriptionTimer.checkUpdates();
415     }
416 
setOSUSelection(int osuID)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 
networkConfigChange(WifiConfiguration configuration)463     public void networkConfigChange(WifiConfiguration configuration) {
464         mWifiNetworkAdapter.networkConfigChange(configuration);
465     }
466 
networkConnectEvent(WifiInfo wifiInfo)467     public void networkConnectEvent(WifiInfo wifiInfo) {
468         if (wifiInfo != null) {
469             setActiveNetwork(mWifiNetworkAdapter.getActiveWifiConfig(),
470                     mWifiNetworkAdapter.getCurrentNetwork());
471         }
472     }
473 
wifiStateChange(boolean on)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 
bssidMatch(OSUInfo osuInfo)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 
setActiveNetwork(WifiConfiguration wifiConfiguration, Network network)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      */
initiateProvisioning(OSUInfo osuInfo, Network network)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      */
getKeyManager(HomeSP homeSP, KeyStore keyStore)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 
isOSU(String ssid)581     public boolean isOSU(String ssid) {
582         synchronized (mOSUMap) {
583             return mOSUSSIDs.contains(ssid);
584         }
585     }
586 
tickleIconCache(boolean all)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 
pushScanResults(Collection<ScanResult> scanResults)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 
updateOSUInfoCache(Map<OSUProvider, ScanResult> results)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 
iconResults(List<OSUInfo> osuInfos)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 
notifyOSUCount(int count)659     private void notifyOSUCount(int count) {
660         mAppBridge.showOsuCount(count, getAvailableOSUs());
661     }
662 
initiateIconQueries()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 
deauth(long bssid, boolean ess, int delay, String url)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
wnmRemediate(long bssid, String url, PasspointMatch match)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 
remediate(HomeSP homeSP, boolean policy)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 
doRemediate(String url, Network network, HomeSP homeSP, boolean policy)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 
getMOTree(HomeSP homeSP)787     public MOTree getMOTree(HomeSP homeSP) throws IOException {
788         return mWifiNetworkAdapter.getMOTree(homeSP);
789     }
790 
notifyIconReceived(long bssid, String fileName, byte[] data)791     public void notifyIconReceived(long bssid, String fileName, byte[] data) {
792         mIconCache.notifyIconReceived(bssid, fileName, data);
793     }
794 
doIconQuery(long bssid, String fileName)795     public void doIconQuery(long bssid, String fileName) {
796         mWifiNetworkAdapter.doIconQuery(bssid, fileName);
797     }
798 
prepareUserInput(String spName)799     protected URL prepareUserInput(String spName) throws IOException {
800         mRedirectListener = new RedirectListener(this, spName);
801         return mRedirectListener.getURL();
802     }
803 
startUserInput(URL target, Network network)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 
notifyUser(OSUOperationStatus status, String message, String spName)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 
provisioningFailed(String spName, String message, HomeSP homeSP, int flowType)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 
provisioningComplete(OSUInfo osuInfo, MOData moData, Map<OSUCertType, List<X509Certificate>> certs, PrivateKey privateKey, Network osuNetwork)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 
getCert(Map<OSUCertType, List<X509Certificate>> certMap, OSUCertType certType)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 
spDeleted(String fqdn)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 
deleteCerts(KeyStore keyStore, String fqdn, String... prefixes)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 
remediationComplete(HomeSP homeSP, Collection<MOData> mods, Map<OSUCertType, List<X509Certificate>> certs, PrivateKey privateKey)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 
getOMADMAdapter()974     protected OMADMAdapter getOMADMAdapter() {
975         return OMADMAdapter.getInstance(mContext);
976     }
977 }
978