| package com.android.hotspot2.osu; |
| |
| import android.content.Context; |
| import android.net.Network; |
| import android.net.NetworkInfo; |
| import android.net.wifi.ScanResult; |
| import android.net.wifi.WifiConfiguration; |
| import android.net.wifi.WifiInfo; |
| import android.util.Log; |
| |
| import com.android.anqp.Constants; |
| import com.android.anqp.OSUProvider; |
| import com.android.hotspot2.AppBridge; |
| import com.android.hotspot2.OMADMAdapter; |
| import com.android.hotspot2.PasspointMatch; |
| import com.android.hotspot2.Utils; |
| import com.android.hotspot2.WifiNetworkAdapter; |
| import com.android.hotspot2.omadm.MOManager; |
| import com.android.hotspot2.omadm.MOTree; |
| import com.android.hotspot2.osu.commands.MOData; |
| import com.android.hotspot2.osu.service.RedirectListener; |
| import com.android.hotspot2.osu.service.SubscriptionTimer; |
| import com.android.hotspot2.pps.HomeSP; |
| import com.android.hotspot2.pps.UpdateInfo; |
| |
| import org.xml.sax.SAXException; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.security.GeneralSecurityException; |
| import java.security.KeyStore; |
| import java.security.KeyStoreException; |
| import java.security.PrivateKey; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import javax.net.ssl.KeyManager; |
| |
| public class OSUManager { |
| public static final String TAG = "OSUMGR"; |
| public static final boolean R2_ENABLED = true; |
| public static final boolean R2_MOCK = true; |
| private static final boolean MATCH_BSSID = false; |
| |
| private static final String KEYSTORE_FILE = "passpoint.ks"; |
| private static final String WFA_CA_LOC = "/etc/security/wfa"; |
| |
| private static final String OSU_COUNT = "osu-count"; |
| private static final String SP_NAME = "sp-name"; |
| private static final String PROV_SUCCESS = "prov-success"; |
| private static final String DEAUTH = "deauth"; |
| private static final String DEAUTH_DELAY = "deauth-delay"; |
| private static final String DEAUTH_URL = "deauth-url"; |
| private static final String PROV_MESSAGE = "prov-message"; |
| |
| private static final long REMEDIATION_TIMEOUT = 120000L; |
| // How many scan result batches to hang on to |
| |
| public static final int FLOW_PROVISIONING = 1; |
| public static final int FLOW_REMEDIATION = 2; |
| public static final int FLOW_POLICY = 3; |
| |
| public static final String CERT_WFA_ALIAS = "wfa-root-"; |
| public static final String CERT_REM_ALIAS = "rem-"; |
| public static final String CERT_POLICY_ALIAS = "pol-"; |
| public static final String CERT_SHARED_ALIAS = "shr-"; |
| public static final String CERT_CLT_CERT_ALIAS = "clt-"; |
| public static final String CERT_CLT_KEY_ALIAS = "prv-"; |
| public static final String CERT_CLT_CA_ALIAS = "aaa-"; |
| |
| // Preferred icon parameters |
| private static final Set<String> ICON_TYPES = |
| new HashSet<>(Arrays.asList("image/png", "image/jpeg")); |
| private static final int ICON_WIDTH = 64; |
| private static final int ICON_HEIGHT = 64; |
| public static final Locale LOCALE = java.util.Locale.getDefault(); |
| |
| private final WifiNetworkAdapter mWifiNetworkAdapter; |
| |
| private final AppBridge mAppBridge; |
| private final Context mContext; |
| private final IconCache mIconCache; |
| private final SubscriptionTimer mSubscriptionTimer; |
| private final Set<String> mOSUSSIDs = new HashSet<>(); |
| private final Map<OSUProvider, OSUInfo> mOSUMap = new HashMap<>(); |
| private final KeyStore mKeyStore; |
| private RedirectListener mRedirectListener; |
| private final AtomicInteger mOSUSequence = new AtomicInteger(); |
| private OSUThread mProvisioningThread; |
| private final Map<String, OSUThread> mServiceThreads = new HashMap<>(); |
| private volatile OSUInfo mPendingOSU; |
| private volatile Integer mOSUNwkID; |
| |
| private final OSUCache mOSUCache; |
| |
| public OSUManager(Context context) { |
| mContext = context; |
| mAppBridge = new AppBridge(context); |
| mIconCache = new IconCache(this); |
| mWifiNetworkAdapter = new WifiNetworkAdapter(context, this); |
| mSubscriptionTimer = new SubscriptionTimer(this, mWifiNetworkAdapter, context); |
| mOSUCache = new OSUCache(); |
| KeyStore ks = null; |
| try { |
| //ks = loadKeyStore(KEYSTORE_FILE, readCertsFromDisk(WFA_CA_LOC)); |
| ks = loadKeyStore(new File(context.getFilesDir(), KEYSTORE_FILE), |
| OSUSocketFactory.buildCertSet()); |
| } catch (IOException e) { |
| Log.e(TAG, "Failed to initialize Passpoint keystore, OSU disabled", e); |
| } |
| mKeyStore = ks; |
| } |
| |
| private static KeyStore loadKeyStore(File ksFile, Set<X509Certificate> diskCerts) |
| throws IOException { |
| try { |
| KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
| if (ksFile.exists()) { |
| try (FileInputStream in = new FileInputStream(ksFile)) { |
| keyStore.load(in, null); |
| } |
| |
| // Note: comparing two sets of certs does not work. |
| boolean mismatch = false; |
| int loadCount = 0; |
| for (int n = 0; n < 1000; n++) { |
| String alias = String.format("%s%d", CERT_WFA_ALIAS, n); |
| Certificate cert = keyStore.getCertificate(alias); |
| if (cert == null) { |
| break; |
| } |
| |
| loadCount++; |
| boolean matched = false; |
| Iterator<X509Certificate> iter = diskCerts.iterator(); |
| while (iter.hasNext()) { |
| X509Certificate diskCert = iter.next(); |
| if (cert.equals(diskCert)) { |
| iter.remove(); |
| matched = true; |
| break; |
| } |
| } |
| if (!matched) { |
| mismatch = true; |
| break; |
| } |
| } |
| if (mismatch || !diskCerts.isEmpty()) { |
| Log.d(TAG, "Re-seeding Passpoint key store with " + |
| diskCerts.size() + " WFA certs"); |
| for (int n = 0; n < 1000; n++) { |
| String alias = String.format("%s%d", CERT_WFA_ALIAS, n); |
| Certificate cert = keyStore.getCertificate(alias); |
| if (cert == null) { |
| break; |
| } else { |
| keyStore.deleteEntry(alias); |
| } |
| } |
| int index = 0; |
| for (X509Certificate caCert : diskCerts) { |
| keyStore.setCertificateEntry( |
| String.format("%s%d", CERT_WFA_ALIAS, index), caCert); |
| index++; |
| } |
| |
| try (FileOutputStream out = new FileOutputStream(ksFile)) { |
| keyStore.store(out, null); |
| } |
| } else { |
| Log.d(TAG, "Loaded Passpoint key store with " + loadCount + " CA certs"); |
| Enumeration<String> aliases = keyStore.aliases(); |
| while (aliases.hasMoreElements()) { |
| Log.d("ZXC", "KS Alias '" + aliases.nextElement() + "'"); |
| } |
| } |
| } else { |
| keyStore.load(null, null); |
| int index = 0; |
| for (X509Certificate caCert : diskCerts) { |
| keyStore.setCertificateEntry( |
| String.format("%s%d", CERT_WFA_ALIAS, index), caCert); |
| index++; |
| } |
| |
| try (FileOutputStream out = new FileOutputStream(ksFile)) { |
| keyStore.store(out, null); |
| } |
| Log.d(TAG, "Initialized Passpoint key store with " + |
| diskCerts.size() + " CA certs"); |
| } |
| return keyStore; |
| } catch (GeneralSecurityException gse) { |
| throw new IOException(gse); |
| } |
| } |
| |
| private static Set<X509Certificate> readCertsFromDisk(String dir) throws CertificateException { |
| Set<X509Certificate> certs = new HashSet<>(); |
| CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); |
| File caDir = new File(dir); |
| File[] certFiles = caDir.listFiles(); |
| if (certFiles != null) { |
| for (File certFile : certFiles) { |
| try { |
| try (FileInputStream in = new FileInputStream(certFile)) { |
| Certificate cert = certFactory.generateCertificate(in); |
| if (cert instanceof X509Certificate) { |
| certs.add((X509Certificate) cert); |
| } |
| } |
| } catch (CertificateException | IOException e) { |
| /* Ignore */ |
| } |
| } |
| } |
| return certs; |
| } |
| |
| public KeyStore getKeyStore() { |
| return mKeyStore; |
| } |
| |
| private static class OSUThread extends Thread { |
| private final OSUClient mOSUClient; |
| private final OSUManager mOSUManager; |
| private final HomeSP mHomeSP; |
| private final String mSpName; |
| private final int mFlowType; |
| private final KeyManager mKeyManager; |
| private final long mLaunchTime; |
| private final Object mLock = new Object(); |
| private boolean mLocalAddressSet; |
| private Network mNetwork; |
| |
| private OSUThread(OSUInfo osuInfo, OSUManager osuManager, KeyManager km) |
| throws MalformedURLException { |
| mOSUClient = new OSUClient(osuInfo, osuManager.getKeyStore()); |
| mOSUManager = osuManager; |
| mHomeSP = null; |
| mSpName = osuInfo.getName(LOCALE); |
| mFlowType = FLOW_PROVISIONING; |
| mKeyManager = km; |
| mLaunchTime = System.currentTimeMillis(); |
| |
| setDaemon(true); |
| setName("OSU Client Thread"); |
| } |
| |
| private OSUThread(String osuURL, OSUManager osuManager, KeyManager km, HomeSP homeSP, |
| int flowType) throws MalformedURLException { |
| mOSUClient = new OSUClient(osuURL, osuManager.getKeyStore()); |
| mOSUManager = osuManager; |
| mHomeSP = homeSP; |
| mSpName = homeSP.getFriendlyName(); |
| mFlowType = flowType; |
| mKeyManager = km; |
| mLaunchTime = System.currentTimeMillis(); |
| |
| setDaemon(true); |
| setName("OSU Client Thread"); |
| } |
| |
| public long getLaunchTime() { |
| return mLaunchTime; |
| } |
| |
| private void connect(Network network) { |
| synchronized (mLock) { |
| mNetwork = network; |
| mLocalAddressSet = true; |
| mLock.notifyAll(); |
| } |
| Log.d(TAG, "Client notified..."); |
| } |
| |
| @Override |
| public void run() { |
| Log.d(TAG, mFlowType + "-" + getName() + " running."); |
| Network network; |
| synchronized (mLock) { |
| while (!mLocalAddressSet) { |
| try { |
| mLock.wait(); |
| } catch (InterruptedException ie) { |
| /**/ |
| } |
| Log.d(TAG, "OSU Thread running..."); |
| } |
| network = mNetwork; |
| } |
| |
| if (network == null) { |
| Log.d(TAG, "Association failed, exiting OSU flow"); |
| mOSUManager.provisioningFailed(mSpName, "Network cannot be reached", |
| mHomeSP, mFlowType); |
| return; |
| } |
| |
| Log.d(TAG, "OSU SSID Associated at " + network.toString()); |
| try { |
| if (mFlowType == FLOW_PROVISIONING) { |
| mOSUClient.provision(mOSUManager, network, mKeyManager); |
| } else { |
| mOSUClient.remediate(mOSUManager, network, mKeyManager, mHomeSP, mFlowType); |
| } |
| } catch (Throwable t) { |
| Log.w(TAG, "OSU flow failed: " + t, t); |
| mOSUManager.provisioningFailed(mSpName, t.getMessage(), mHomeSP, mFlowType); |
| } |
| } |
| } |
| |
| /* |
| public void startOSU() { |
| registerUserInputListener(new UserInputListener() { |
| @Override |
| public void requestUserInput(URL target, Network network, URL endRedirect) { |
| Log.d(TAG, "Browser to " + target + ", land at " + endRedirect); |
| |
| final Intent intent = new Intent( |
| ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN); |
| intent.putExtra(ConnectivityManager.EXTRA_NETWORK, network); |
| intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL, |
| new CaptivePortal(new ICaptivePortal.Stub() { |
| @Override |
| public void appResponse(int response) { |
| } |
| })); |
| //intent.setData(Uri.parse(target.toString())); !!! Doesn't work! |
| intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, target.toString()); |
| intent.setFlags( |
| Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); |
| mContext.startActivityAsUser(intent, UserHandle.CURRENT); |
| } |
| |
| @Override |
| public String operationStatus(String spIdentity, OSUOperationStatus status, |
| String message) { |
| Log.d(TAG, "OSU OP Status: " + status + ", message " + message); |
| Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION); |
| intent.putExtra(SP_NAME, spIdentity); |
| intent.putExtra(PROV_SUCCESS, status == OSUOperationStatus.ProvisioningSuccess); |
| if (message != null) { |
| intent.putExtra(PROV_MESSAGE, message); |
| } |
| intent.setFlags( |
| Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); |
| mContext.startActivityAsUser(intent, UserHandle.CURRENT); |
| return null; |
| } |
| |
| @Override |
| public void deAuthNotification(String spIdentity, boolean ess, int delay, URL url) { |
| Log.i(TAG, "De-authentication imminent for " + (ess ? "ess" : "bss") + |
| ", redirect to " + url); |
| Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION); |
| intent.putExtra(SP_NAME, spIdentity); |
| intent.putExtra(DEAUTH, ess); |
| intent.putExtra(DEAUTH_DELAY, delay); |
| intent.putExtra(DEAUTH_URL, url.toString()); |
| intent.setFlags( |
| Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); |
| mContext.startActivityAsUser(intent, UserHandle.CURRENT); |
| } |
| }); |
| addOSUListener(new OSUListener() { |
| @Override |
| public void osuNotification(int count) { |
| Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION); |
| intent.putExtra(OSU_COUNT, count); |
| intent.setFlags( |
| Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); |
| mContext.startActivityAsUser(intent, UserHandle.CURRENT); |
| } |
| }); |
| mWifiNetworkAdapter.initialize(); |
| mSubscriptionTimer.checkUpdates(); |
| } |
| */ |
| |
| public List<OSUInfo> getAvailableOSUs() { |
| synchronized (mOSUMap) { |
| List<OSUInfo> completeOSUs = new ArrayList<>(); |
| for (OSUInfo osuInfo : mOSUMap.values()) { |
| if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) { |
| completeOSUs.add(osuInfo); |
| } |
| } |
| return completeOSUs; |
| } |
| } |
| |
| public void recheckTimers() { |
| mSubscriptionTimer.checkUpdates(); |
| } |
| |
| public void setOSUSelection(int osuID) { |
| OSUInfo selection = null; |
| for (OSUInfo osuInfo : mOSUMap.values()) { |
| Log.d("ZXZ", "In select: " + osuInfo + ", id " + osuInfo.getOsuID()); |
| if (osuInfo.getOsuID() == osuID && |
| osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) { |
| selection = osuInfo; |
| break; |
| } |
| } |
| |
| Log.d(TAG, "Selected OSU ID " + osuID + ", matches " + selection); |
| |
| if (selection == null) { |
| mPendingOSU = null; |
| return; |
| } |
| |
| mPendingOSU = selection; |
| WifiConfiguration config = mWifiNetworkAdapter.getActiveWifiConfig(); |
| |
| if (config != null && |
| bssidMatch(selection) && |
| Utils.unquote(config.SSID).equals(selection.getSSID())) { |
| |
| try { |
| // Go straight to provisioning if the network is already selected. |
| // Also note that mOSUNwkID is left unset to leave the network around after |
| // flow completion since it was not added by the OSU flow. |
| initiateProvisioning(mPendingOSU, mWifiNetworkAdapter.getCurrentNetwork()); |
| } catch (IOException ioe) { |
| notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(), |
| mPendingOSU.getName(LOCALE)); |
| } finally { |
| mPendingOSU = null; |
| } |
| } else { |
| try { |
| mOSUNwkID = mWifiNetworkAdapter.connect(selection, mPendingOSU.getName(LOCALE)); |
| } catch (IOException ioe) { |
| notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(), |
| selection.getName(LOCALE)); |
| } |
| } |
| } |
| |
| public void networkConfigChange(WifiConfiguration configuration) { |
| mWifiNetworkAdapter.networkConfigChange(configuration); |
| } |
| |
| public void networkConnectEvent(WifiInfo wifiInfo) { |
| if (wifiInfo != null) { |
| setActiveNetwork(mWifiNetworkAdapter.getActiveWifiConfig(), |
| mWifiNetworkAdapter.getCurrentNetwork()); |
| } |
| } |
| |
| public void wifiStateChange(boolean on) { |
| if (!on) { |
| int current = mOSUMap.size(); |
| mOSUMap.clear(); |
| mOSUCache.clearAll(); |
| mIconCache.clear(); |
| if (current > 0) { |
| notifyOSUCount(0); |
| } |
| } |
| } |
| |
| private boolean bssidMatch(OSUInfo osuInfo) { |
| if (MATCH_BSSID) { |
| WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo(); |
| return wifiInfo != null && Utils.parseMac(wifiInfo.getBSSID()) == osuInfo.getBSSID(); |
| } else { |
| return true; |
| } |
| } |
| |
| public void setActiveNetwork(WifiConfiguration wifiConfiguration, Network network) { |
| Log.d(TAG, "Network change: " + network + ", cfg " + |
| (wifiConfiguration != null ? wifiConfiguration.SSID : "-") + ", osu " + mPendingOSU); |
| if (mPendingOSU != null && |
| wifiConfiguration != null && |
| network != null && |
| bssidMatch(mPendingOSU) && |
| Utils.unquote(wifiConfiguration.SSID).equals(mPendingOSU.getSSID())) { |
| |
| try { |
| Log.d(TAG, "New network " + network + ", current OSU " + mPendingOSU); |
| initiateProvisioning(mPendingOSU, network); |
| } catch (IOException ioe) { |
| notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(), |
| mPendingOSU.getName(LOCALE)); |
| } finally { |
| mPendingOSU = null; |
| } |
| return; |
| } |
| |
| /* |
| // !!! Hack to force start remediation at connection time |
| else if (wifiConfiguration != null && wifiConfiguration.isPasspoint()) { |
| HomeSP homeSP = mWifiConfigStore.getHomeSPForConfig(wifiConfiguration); |
| if (homeSP != null && homeSP.getSubscriptionUpdate() != null) { |
| if (!mServiceThreads.containsKey(homeSP.getFQDN())) { |
| try { |
| remediate(homeSP); |
| } catch (IOException ioe) { |
| Log.w(TAG, "Failed to remediate: " + ioe); |
| } |
| } |
| } |
| } |
| */ |
| else if (wifiConfiguration == null) { |
| mServiceThreads.clear(); |
| } |
| } |
| |
| |
| /** |
| * Called when an OSU has been selected and the associated network is fully connected. |
| * |
| * @param osuInfo The selected OSUInfo or null if the current OSU flow is cancelled externally, |
| * e.g. WiFi is turned off or the OSU network is otherwise detected as |
| * unreachable. |
| * @param network The currently associated network (for the OSU SSID). |
| * @throws IOException |
| * @throws GeneralSecurityException |
| */ |
| private void initiateProvisioning(OSUInfo osuInfo, Network network) |
| throws IOException { |
| synchronized (mWifiNetworkAdapter) { |
| if (mProvisioningThread != null) { |
| mProvisioningThread.connect(null); |
| mProvisioningThread = null; |
| } |
| if (mRedirectListener != null) { |
| mRedirectListener.abort(); |
| mRedirectListener = null; |
| } |
| if (osuInfo != null) { |
| //new ConnMonitor().start(); |
| mProvisioningThread = new OSUThread(osuInfo, this, getKeyManager(null, mKeyStore)); |
| mProvisioningThread.start(); |
| //mWifiNetworkAdapter.associate(osuInfo.getSSID(), |
| // osuInfo.getBSSID(), osuInfo.getOSUProvider().getOsuNai()); |
| mProvisioningThread.connect(network); |
| } |
| } |
| } |
| |
| /** |
| * @param homeSP The Home SP associated with the keying material in question. Passing |
| * null returns a "system wide" KeyManager to support pre-provisioned certs based |
| * on names retrieved from the ClientCertInfo request. |
| * @return A key manager suitable for the given configuration (or pre-provisioned keys). |
| */ |
| private static KeyManager getKeyManager(HomeSP homeSP, KeyStore keyStore) |
| throws IOException { |
| return homeSP != null ? new ClientKeyManager(homeSP, keyStore) : |
| new WiFiKeyManager(keyStore); |
| } |
| |
| public boolean isOSU(String ssid) { |
| synchronized (mOSUMap) { |
| return mOSUSSIDs.contains(ssid); |
| } |
| } |
| |
| public void tickleIconCache(boolean all) { |
| mIconCache.tickle(all); |
| |
| if (all) { |
| synchronized (mOSUMap) { |
| int current = mOSUMap.size(); |
| mOSUMap.clear(); |
| mOSUCache.clearAll(); |
| mIconCache.clear(); |
| if (current > 0) { |
| notifyOSUCount(0); |
| } |
| } |
| } |
| } |
| |
| public void pushScanResults(Collection<ScanResult> scanResults) { |
| Map<OSUProvider, ScanResult> results = mOSUCache.pushScanResults(scanResults); |
| if (results != null) { |
| updateOSUInfoCache(results); |
| } |
| } |
| |
| private void updateOSUInfoCache(Map<OSUProvider, ScanResult> results) { |
| Map<OSUProvider, OSUInfo> osus = new HashMap<>(); |
| for (Map.Entry<OSUProvider, ScanResult> entry : results.entrySet()) { |
| OSUInfo existing = mOSUMap.get(entry.getKey()); |
| long bssid = Utils.parseMac(entry.getValue().BSSID); |
| |
| if (existing == null || existing.getBSSID() != bssid) { |
| osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey().getSSID(), |
| entry.getKey(), mOSUSequence.getAndIncrement())); |
| } else { |
| // Maintain existing entries. |
| osus.put(entry.getKey(), existing); |
| } |
| } |
| |
| mOSUMap.clear(); |
| mOSUMap.putAll(osus); |
| |
| mOSUSSIDs.clear(); |
| for (OSUInfo osuInfo : mOSUMap.values()) { |
| mOSUSSIDs.add(osuInfo.getSSID()); |
| } |
| |
| if (mOSUMap.isEmpty()) { |
| notifyOSUCount(0); |
| } |
| initiateIconQueries(); |
| Log.d(TAG, "Latest (app) OSU info: " + mOSUMap); |
| } |
| |
| public void iconResults(List<OSUInfo> osuInfos) { |
| int newIcons = 0; |
| for (OSUInfo osuInfo : osuInfos) { |
| if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) { |
| newIcons++; |
| } |
| } |
| if (newIcons > 0) { |
| int count = 0; |
| for (OSUInfo existing : mOSUMap.values()) { |
| if (existing.getIconStatus() == OSUInfo.IconStatus.Available) { |
| count++; |
| } |
| } |
| Log.d(TAG, "Icon results for " + count + " OSUs"); |
| notifyOSUCount(count); |
| } |
| } |
| |
| private void notifyOSUCount(int count) { |
| mAppBridge.showOsuCount(count, getAvailableOSUs()); |
| } |
| |
| private void initiateIconQueries() { |
| for (OSUInfo osuInfo : mOSUMap.values()) { |
| if (osuInfo.getIconStatus() == OSUInfo.IconStatus.NotQueried) { |
| mIconCache.startIconQuery(osuInfo, |
| osuInfo.getIconInfo(LOCALE, ICON_TYPES, ICON_WIDTH, ICON_HEIGHT)); |
| } |
| } |
| } |
| |
| public void deauth(long bssid, boolean ess, int delay, String url) throws MalformedURLException { |
| Log.d(TAG, String.format("De-auth imminent on %s, delay %ss to '%s'", |
| ess ? "ess" : "bss", |
| delay, |
| url)); |
| mWifiNetworkAdapter.setHoldoffTime(delay * Constants.MILLIS_IN_A_SEC, ess); |
| HomeSP homeSP = mWifiNetworkAdapter.getCurrentSP(); |
| String spName = homeSP != null ? homeSP.getFriendlyName() : "unknown"; |
| mAppBridge.showDeauth(spName, ess, delay, url); |
| } |
| |
| // !!! Consistently check passpoint match. |
| // !!! Convert to a one-thread thread-pool |
| public void wnmRemediate(long bssid, String url, PasspointMatch match) |
| throws IOException, SAXException { |
| WifiConfiguration config = mWifiNetworkAdapter.getActiveWifiConfig(); |
| HomeSP homeSP = MOManager.buildSP(config.getMoTree()); |
| if (homeSP == null) { |
| throw new IOException("Remediation request for unidentified Passpoint network " + |
| config.networkId); |
| } |
| Network network = mWifiNetworkAdapter.getCurrentNetwork(); |
| if (network == null) { |
| throw new IOException("Failed to determine current network"); |
| } |
| WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo(); |
| if (wifiInfo == null || Utils.parseMac(wifiInfo.getBSSID()) != bssid) { |
| throw new IOException("Mismatching BSSID"); |
| } |
| Log.d(TAG, "WNM Remediation on " + network.netId + " FQDN " + homeSP.getFQDN()); |
| |
| doRemediate(url, network, homeSP, false); |
| } |
| |
| public void remediate(HomeSP homeSP, boolean policy) throws IOException, SAXException { |
| UpdateInfo updateInfo; |
| if (policy) { |
| if (homeSP.getPolicy() == null) { |
| throw new IOException("No policy object"); |
| } |
| updateInfo = homeSP.getPolicy().getPolicyUpdate(); |
| } else { |
| updateInfo = homeSP.getSubscriptionUpdate(); |
| } |
| switch (updateInfo.getUpdateRestriction()) { |
| case HomeSP: { |
| Network network = mWifiNetworkAdapter.getCurrentNetwork(); |
| if (network == null) { |
| throw new IOException("Failed to determine current network"); |
| } |
| |
| WifiConfiguration config = mWifiNetworkAdapter.getActivePasspointNetwork(); |
| HomeSP activeSP = MOManager.buildSP(config.getMoTree()); |
| |
| if (activeSP == null || !activeSP.getFQDN().equals(homeSP.getFQDN())) { |
| throw new IOException("Remediation restricted to HomeSP"); |
| } |
| doRemediate(updateInfo.getURI(), network, homeSP, policy); |
| break; |
| } |
| case RoamingPartner: { |
| Network network = mWifiNetworkAdapter.getCurrentNetwork(); |
| if (network == null) { |
| throw new IOException("Failed to determine current network"); |
| } |
| |
| WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo(); |
| if (wifiInfo == null) { |
| throw new IOException("Unable to determine WiFi info"); |
| } |
| |
| PasspointMatch match = mWifiNetworkAdapter. |
| matchProviderWithCurrentNetwork(homeSP.getFQDN()); |
| |
| if (match == PasspointMatch.HomeProvider || |
| match == PasspointMatch.RoamingProvider) { |
| doRemediate(updateInfo.getURI(), network, homeSP, policy); |
| } else { |
| throw new IOException("No roaming network match: " + match); |
| } |
| break; |
| } |
| case Unrestricted: { |
| Network network = mWifiNetworkAdapter.getCurrentNetwork(); |
| doRemediate(updateInfo.getURI(), network, homeSP, policy); |
| break; |
| } |
| } |
| } |
| |
| private void doRemediate(String url, Network network, HomeSP homeSP, boolean policy) |
| throws IOException { |
| synchronized (mWifiNetworkAdapter) { |
| OSUThread existing = mServiceThreads.get(homeSP.getFQDN()); |
| if (existing != null) { |
| if (System.currentTimeMillis() - existing.getLaunchTime() > REMEDIATION_TIMEOUT) { |
| throw new IOException("Ignoring recurring remediation request"); |
| } else { |
| existing.connect(null); |
| } |
| } |
| |
| try { |
| OSUThread osuThread = new OSUThread(url, this, |
| getKeyManager(homeSP, mKeyStore), |
| homeSP, policy ? FLOW_POLICY : FLOW_REMEDIATION); |
| osuThread.start(); |
| osuThread.connect(network); |
| mServiceThreads.put(homeSP.getFQDN(), osuThread); |
| } catch (MalformedURLException me) { |
| throw new IOException("Failed to start remediation: " + me); |
| } |
| } |
| } |
| |
| public MOTree getMOTree(HomeSP homeSP) throws IOException { |
| return mWifiNetworkAdapter.getMOTree(homeSP); |
| } |
| |
| public void notifyIconReceived(long bssid, String fileName, byte[] data) { |
| mIconCache.notifyIconReceived(bssid, fileName, data); |
| } |
| |
| public void doIconQuery(long bssid, String fileName) { |
| mWifiNetworkAdapter.doIconQuery(bssid, fileName); |
| } |
| |
| protected URL prepareUserInput(String spName) throws IOException { |
| mRedirectListener = new RedirectListener(this, spName); |
| return mRedirectListener.getURL(); |
| } |
| |
| protected boolean startUserInput(URL target, Network network) throws IOException { |
| mRedirectListener.startService(); |
| mWifiNetworkAdapter.launchBrowser(target, network, mRedirectListener.getURL()); |
| |
| return mRedirectListener.waitForUser(); |
| } |
| |
| public String notifyUser(OSUOperationStatus status, String message, String spName) { |
| if (status == OSUOperationStatus.UserInputComplete) { |
| return null; |
| } |
| if (mOSUNwkID != null) { |
| // Delete the OSU network if it was added by the OSU flow |
| mWifiNetworkAdapter.deleteNetwork(mOSUNwkID); |
| mOSUNwkID = null; |
| } |
| mAppBridge.showStatus(status, spName, message, null); |
| return null; |
| } |
| |
| public void provisioningFailed(String spName, String message, HomeSP homeSP, |
| int flowType) { |
| synchronized (mWifiNetworkAdapter) { |
| switch (flowType) { |
| case FLOW_PROVISIONING: |
| mProvisioningThread = null; |
| if (mRedirectListener != null) { |
| mRedirectListener.abort(); |
| mRedirectListener = null; |
| } |
| break; |
| case FLOW_REMEDIATION: |
| case FLOW_POLICY: |
| mServiceThreads.remove(homeSP.getFQDN()); |
| if (mServiceThreads.isEmpty() && mRedirectListener != null) { |
| mRedirectListener.abort(); |
| mRedirectListener = null; |
| } |
| break; |
| } |
| } |
| notifyUser(OSUOperationStatus.ProvisioningFailure, message, spName); |
| } |
| |
| public void provisioningComplete(OSUInfo osuInfo, |
| MOData moData, Map<OSUCertType, List<X509Certificate>> certs, |
| PrivateKey privateKey, Network osuNetwork) { |
| synchronized (mWifiNetworkAdapter) { |
| mProvisioningThread = null; |
| } |
| try { |
| Log.d("ZXZ", "MOTree.toXML: " + moData.getMOTree().toXml()); |
| HomeSP homeSP = mWifiNetworkAdapter.addSP(moData.getMOTree()); |
| |
| Integer spNwk = mWifiNetworkAdapter.addNetwork(homeSP, certs, privateKey, osuNetwork); |
| if (spNwk == null) { |
| notifyUser(OSUOperationStatus.ProvisioningFailure, |
| "Failed to save network configuration", osuInfo.getName(LOCALE)); |
| mWifiNetworkAdapter.removeSP(homeSP.getFQDN()); |
| } else { |
| Set<X509Certificate> rootCerts = OSUSocketFactory.getRootCerts(mKeyStore); |
| X509Certificate remCert = getCert(certs, OSUCertType.Remediation); |
| X509Certificate polCert = getCert(certs, OSUCertType.Policy); |
| if (privateKey != null) { |
| X509Certificate cltCert = getCert(certs, OSUCertType.Client); |
| mKeyStore.setKeyEntry(CERT_CLT_KEY_ALIAS + homeSP, |
| privateKey.getEncoded(), |
| new X509Certificate[]{cltCert}); |
| mKeyStore.setCertificateEntry(CERT_CLT_CERT_ALIAS, cltCert); |
| } |
| boolean usingShared = false; |
| int newCerts = 0; |
| if (remCert != null) { |
| if (!rootCerts.contains(remCert)) { |
| if (remCert.equals(polCert)) { |
| mKeyStore.setCertificateEntry(CERT_SHARED_ALIAS + homeSP.getFQDN(), |
| remCert); |
| usingShared = true; |
| newCerts++; |
| } else { |
| mKeyStore.setCertificateEntry(CERT_REM_ALIAS + homeSP.getFQDN(), |
| remCert); |
| newCerts++; |
| } |
| } |
| } |
| if (!usingShared && polCert != null) { |
| if (!rootCerts.contains(polCert)) { |
| mKeyStore.setCertificateEntry(CERT_POLICY_ALIAS + homeSP.getFQDN(), |
| remCert); |
| newCerts++; |
| } |
| } |
| |
| if (newCerts > 0) { |
| try (FileOutputStream out = new FileOutputStream(KEYSTORE_FILE)) { |
| mKeyStore.store(out, null); |
| } |
| } |
| notifyUser(OSUOperationStatus.ProvisioningSuccess, null, osuInfo.getName(LOCALE)); |
| Log.d(TAG, "Provisioning complete."); |
| } |
| } catch (IOException | GeneralSecurityException | SAXException e) { |
| Log.e(TAG, "Failed to provision: " + e, e); |
| notifyUser(OSUOperationStatus.ProvisioningFailure, e.toString(), |
| osuInfo.getName(LOCALE)); |
| } |
| } |
| |
| private static X509Certificate getCert(Map<OSUCertType, List<X509Certificate>> certMap, |
| OSUCertType certType) { |
| List<X509Certificate> certs = certMap.get(certType); |
| if (certs == null || certs.isEmpty()) { |
| return null; |
| } |
| return certs.iterator().next(); |
| } |
| |
| public void spDeleted(String fqdn) { |
| int count = deleteCerts(mKeyStore, fqdn, |
| CERT_REM_ALIAS, CERT_POLICY_ALIAS, CERT_SHARED_ALIAS); |
| |
| if (count > 0) { |
| try (FileOutputStream out = new FileOutputStream(KEYSTORE_FILE)) { |
| mKeyStore.store(out, null); |
| } catch (IOException | GeneralSecurityException e) { |
| Log.w(TAG, "Failed to remove certs from key store: " + e); |
| } |
| } |
| } |
| |
| private static int deleteCerts(KeyStore keyStore, String fqdn, String... prefixes) { |
| int count = 0; |
| for (String prefix : prefixes) { |
| try { |
| String alias = prefix + fqdn; |
| Certificate cert = keyStore.getCertificate(alias); |
| if (cert != null) { |
| keyStore.deleteEntry(alias); |
| count++; |
| } |
| } catch (KeyStoreException kse) { |
| /**/ |
| } |
| } |
| return count; |
| } |
| |
| public void remediationComplete(HomeSP homeSP, Collection<MOData> mods, |
| Map<OSUCertType, List<X509Certificate>> certs, |
| PrivateKey privateKey) |
| throws IOException, GeneralSecurityException { |
| |
| HomeSP altSP = mWifiNetworkAdapter.modifySP(homeSP, mods); |
| X509Certificate caCert = null; |
| List<X509Certificate> clientCerts = null; |
| if (certs != null) { |
| List<X509Certificate> certList = certs.get(OSUCertType.AAA); |
| caCert = certList != null && !certList.isEmpty() ? certList.iterator().next() : null; |
| clientCerts = certs.get(OSUCertType.Client); |
| } |
| if (altSP != null || certs != null) { |
| if (altSP == null) { |
| altSP = homeSP; // No MO mods, only certs and key |
| } |
| mWifiNetworkAdapter.updateNetwork(altSP, caCert, clientCerts, privateKey); |
| } |
| notifyUser(OSUOperationStatus.ProvisioningSuccess, null, homeSP.getFriendlyName()); |
| } |
| |
| protected OMADMAdapter getOMADMAdapter() { |
| return OMADMAdapter.getInstance(mContext); |
| } |
| } |