Home | History | Annotate | Download | only in osu
      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 
    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