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