1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.wifi.hotspot2; 18 19 import android.annotation.Nullable; 20 import android.net.wifi.EAPConstants; 21 import android.net.wifi.WifiConfiguration; 22 import android.net.wifi.WifiEnterpriseConfig; 23 import android.net.wifi.hotspot2.PasspointConfiguration; 24 import android.net.wifi.hotspot2.pps.Credential; 25 import android.net.wifi.hotspot2.pps.Credential.SimCredential; 26 import android.net.wifi.hotspot2.pps.Credential.UserCredential; 27 import android.net.wifi.hotspot2.pps.HomeSp; 28 import android.security.Credentials; 29 import android.text.TextUtils; 30 import android.util.Base64; 31 import android.util.Log; 32 33 import com.android.server.wifi.IMSIParameter; 34 import com.android.server.wifi.SIMAccessor; 35 import com.android.server.wifi.WifiKeyStore; 36 import com.android.server.wifi.hotspot2.anqp.ANQPElement; 37 import com.android.server.wifi.hotspot2.anqp.Constants.ANQPElementType; 38 import com.android.server.wifi.hotspot2.anqp.DomainNameElement; 39 import com.android.server.wifi.hotspot2.anqp.NAIRealmElement; 40 import com.android.server.wifi.hotspot2.anqp.RoamingConsortiumElement; 41 import com.android.server.wifi.hotspot2.anqp.ThreeGPPNetworkElement; 42 import com.android.server.wifi.hotspot2.anqp.eap.AuthParam; 43 import com.android.server.wifi.hotspot2.anqp.eap.NonEAPInnerAuth; 44 import com.android.server.wifi.util.InformationElementUtil.RoamingConsortium; 45 46 import java.nio.charset.StandardCharsets; 47 import java.security.MessageDigest; 48 import java.security.NoSuchAlgorithmException; 49 import java.security.cert.CertificateEncodingException; 50 import java.security.cert.X509Certificate; 51 import java.util.ArrayList; 52 import java.util.Arrays; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Objects; 56 57 /** 58 * Abstraction for Passpoint service provider. This class contains the both static 59 * Passpoint configuration data and the runtime data (e.g. blacklisted SSIDs, statistics). 60 */ 61 public class PasspointProvider { 62 private static final String TAG = "PasspointProvider"; 63 64 /** 65 * Used as part of alias string for certificates and keys. The alias string is in the format 66 * of: [KEY_TYPE]_HS2_[ProviderID] 67 * For example: "CACERT_HS2_0", "USRCERT_HS2_0", "USRPKEY_HS2_0", "CACERT_HS2_REMEDIATION_0" 68 */ 69 private static final String ALIAS_HS_TYPE = "HS2_"; 70 private static final String ALIAS_ALIAS_REMEDIATION_TYPE = "REMEDIATION_"; 71 72 private final PasspointConfiguration mConfig; 73 private final WifiKeyStore mKeyStore; 74 75 /** 76 * Aliases for the private keys and certificates installed in the keystore. Each alias 77 * is a suffix of the actual certificate or key name installed in the keystore. The 78 * certificate or key name in the keystore is consist of |Type|_|alias|. 79 * This will be consistent with the usage of the term "alias" in {@link WifiEnterpriseConfig}. 80 */ 81 private List<String> mCaCertificateAliases; 82 private String mClientPrivateKeyAlias; 83 private String mClientCertificateAlias; 84 private String mRemediationCaCertificateAlias; 85 86 private final long mProviderId; 87 private final int mCreatorUid; 88 private final String mPackageName; 89 90 private final IMSIParameter mImsiParameter; 91 private final List<String> mMatchingSIMImsiList; 92 93 private final int mEAPMethodID; 94 private final AuthParam mAuthParam; 95 96 private boolean mHasEverConnected; 97 private boolean mIsShared; 98 private boolean mVerboseLoggingEnabled; 99 100 /** 101 * This is a flag to indicate if the Provider is created temporarily. 102 * Thus, it is not saved permanently unlike normal Passpoint profile. 103 */ 104 private boolean mIsEphemeral = false; 105 PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, SIMAccessor simAccessor, long providerId, int creatorUid, String packageName)106 public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, 107 SIMAccessor simAccessor, long providerId, int creatorUid, String packageName) { 108 this(config, keyStore, simAccessor, providerId, creatorUid, packageName, null, null, null, 109 null, false, false); 110 } 111 PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, SIMAccessor simAccessor, long providerId, int creatorUid, String packageName, List<String> caCertificateAliases, String clientCertificateAlias, String clientPrivateKeyAlias, String remediationCaCertificateAlias, boolean hasEverConnected, boolean isShared)112 public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore, 113 SIMAccessor simAccessor, long providerId, int creatorUid, String packageName, 114 List<String> caCertificateAliases, 115 String clientCertificateAlias, String clientPrivateKeyAlias, 116 String remediationCaCertificateAlias, 117 boolean hasEverConnected, boolean isShared) { 118 // Maintain a copy of the configuration to avoid it being updated by others. 119 mConfig = new PasspointConfiguration(config); 120 mKeyStore = keyStore; 121 mProviderId = providerId; 122 mCreatorUid = creatorUid; 123 mPackageName = packageName; 124 mCaCertificateAliases = caCertificateAliases; 125 mClientCertificateAlias = clientCertificateAlias; 126 mClientPrivateKeyAlias = clientPrivateKeyAlias; 127 mRemediationCaCertificateAlias = remediationCaCertificateAlias; 128 mHasEverConnected = hasEverConnected; 129 mIsShared = isShared; 130 131 // Setup EAP method and authentication parameter based on the credential. 132 if (mConfig.getCredential().getUserCredential() != null) { 133 mEAPMethodID = EAPConstants.EAP_TTLS; 134 mAuthParam = new NonEAPInnerAuth(NonEAPInnerAuth.getAuthTypeID( 135 mConfig.getCredential().getUserCredential().getNonEapInnerMethod())); 136 mImsiParameter = null; 137 mMatchingSIMImsiList = null; 138 } else if (mConfig.getCredential().getCertCredential() != null) { 139 mEAPMethodID = EAPConstants.EAP_TLS; 140 mAuthParam = null; 141 mImsiParameter = null; 142 mMatchingSIMImsiList = null; 143 } else { 144 mEAPMethodID = mConfig.getCredential().getSimCredential().getEapType(); 145 mAuthParam = null; 146 mImsiParameter = IMSIParameter.build( 147 mConfig.getCredential().getSimCredential().getImsi()); 148 mMatchingSIMImsiList = simAccessor.getMatchingImsis(mImsiParameter); 149 } 150 } 151 getConfig()152 public PasspointConfiguration getConfig() { 153 // Return a copy of the configuration to avoid it being updated by others. 154 return new PasspointConfiguration(mConfig); 155 } 156 getCaCertificateAliases()157 public List<String> getCaCertificateAliases() { 158 return mCaCertificateAliases; 159 } 160 getClientPrivateKeyAlias()161 public String getClientPrivateKeyAlias() { 162 return mClientPrivateKeyAlias; 163 } 164 getClientCertificateAlias()165 public String getClientCertificateAlias() { 166 return mClientCertificateAlias; 167 } 168 getRemediationCaCertificateAlias()169 public String getRemediationCaCertificateAlias() { 170 return mRemediationCaCertificateAlias; 171 } 172 getProviderId()173 public long getProviderId() { 174 return mProviderId; 175 } 176 getCreatorUid()177 public int getCreatorUid() { 178 return mCreatorUid; 179 } 180 181 @Nullable getPackageName()182 public String getPackageName() { 183 return mPackageName; 184 } 185 getHasEverConnected()186 public boolean getHasEverConnected() { 187 return mHasEverConnected; 188 } 189 setHasEverConnected(boolean hasEverConnected)190 public void setHasEverConnected(boolean hasEverConnected) { 191 mHasEverConnected = hasEverConnected; 192 } 193 isEphemeral()194 public boolean isEphemeral() { 195 return mIsEphemeral; 196 } 197 setEphemeral(boolean isEphemeral)198 public void setEphemeral(boolean isEphemeral) { 199 mIsEphemeral = isEphemeral; 200 } 201 getImsiParameter()202 public IMSIParameter getImsiParameter() { 203 return mImsiParameter; 204 } 205 206 /** 207 * Install certificates and key based on current configuration. 208 * Note: the certificates and keys in the configuration will get cleared once 209 * they're installed in the keystore. 210 * 211 * @return true on success 212 */ installCertsAndKeys()213 public boolean installCertsAndKeys() { 214 // Install CA certificate. 215 X509Certificate[] x509Certificates = mConfig.getCredential().getCaCertificates(); 216 if (x509Certificates != null) { 217 mCaCertificateAliases = new ArrayList<>(); 218 for (int i = 0; i < x509Certificates.length; i++) { 219 String alias = String.format("%s%s_%d", ALIAS_HS_TYPE, mProviderId, i); 220 if (!mKeyStore.putCertInKeyStore(Credentials.CA_CERTIFICATE + alias, 221 x509Certificates[i])) { 222 Log.e(TAG, "Failed to install CA Certificate"); 223 uninstallCertsAndKeys(); 224 return false; 225 } else { 226 mCaCertificateAliases.add(alias); 227 } 228 } 229 } 230 231 // Install the client private key. 232 if (mConfig.getCredential().getClientPrivateKey() != null) { 233 String keyName = Credentials.USER_PRIVATE_KEY + ALIAS_HS_TYPE + mProviderId; 234 if (!mKeyStore.putKeyInKeyStore(keyName, 235 mConfig.getCredential().getClientPrivateKey())) { 236 Log.e(TAG, "Failed to install client private key"); 237 uninstallCertsAndKeys(); 238 return false; 239 } 240 mClientPrivateKeyAlias = ALIAS_HS_TYPE + mProviderId; 241 } 242 243 // Install the client certificate. 244 if (mConfig.getCredential().getClientCertificateChain() != null) { 245 X509Certificate clientCert = getClientCertificate( 246 mConfig.getCredential().getClientCertificateChain(), 247 mConfig.getCredential().getCertCredential().getCertSha256Fingerprint()); 248 if (clientCert == null) { 249 Log.e(TAG, "Failed to locate client certificate"); 250 uninstallCertsAndKeys(); 251 return false; 252 } 253 String certName = Credentials.USER_CERTIFICATE + ALIAS_HS_TYPE + mProviderId; 254 if (!mKeyStore.putCertInKeyStore(certName, clientCert)) { 255 Log.e(TAG, "Failed to install client certificate"); 256 uninstallCertsAndKeys(); 257 return false; 258 } 259 mClientCertificateAlias = ALIAS_HS_TYPE + mProviderId; 260 } 261 262 if (mConfig.getSubscriptionUpdate() != null) { 263 X509Certificate certificate = mConfig.getSubscriptionUpdate().getCaCertificate(); 264 if (certificate == null) { 265 Log.e(TAG, "Failed to locate CA certificate for remediation"); 266 uninstallCertsAndKeys(); 267 return false; 268 } 269 mRemediationCaCertificateAlias = 270 ALIAS_HS_TYPE + ALIAS_ALIAS_REMEDIATION_TYPE + mProviderId; 271 String certName = Credentials.CA_CERTIFICATE + mRemediationCaCertificateAlias; 272 if (!mKeyStore.putCertInKeyStore(certName, certificate)) { 273 Log.e(TAG, "Failed to install CA certificate for remediation"); 274 mRemediationCaCertificateAlias = null; 275 uninstallCertsAndKeys(); 276 return false; 277 } 278 } 279 280 // Clear the keys and certificates in the configuration. 281 mConfig.getCredential().setCaCertificates(null); 282 mConfig.getCredential().setClientPrivateKey(null); 283 mConfig.getCredential().setClientCertificateChain(null); 284 if (mConfig.getSubscriptionUpdate() != null) { 285 mConfig.getSubscriptionUpdate().setCaCertificate(null); 286 } 287 return true; 288 } 289 290 /** 291 * Remove any installed certificates and key. 292 */ uninstallCertsAndKeys()293 public void uninstallCertsAndKeys() { 294 if (mCaCertificateAliases != null) { 295 for (String certificateAlias : mCaCertificateAliases) { 296 if (!mKeyStore.removeEntryFromKeyStore( 297 Credentials.CA_CERTIFICATE + certificateAlias)) { 298 Log.e(TAG, "Failed to remove entry: " + certificateAlias); 299 } 300 } 301 mCaCertificateAliases = null; 302 } 303 if (mClientPrivateKeyAlias != null) { 304 if (!mKeyStore.removeEntryFromKeyStore( 305 Credentials.USER_PRIVATE_KEY + mClientPrivateKeyAlias)) { 306 Log.e(TAG, "Failed to remove entry: " + mClientPrivateKeyAlias); 307 } 308 mClientPrivateKeyAlias = null; 309 } 310 if (mClientCertificateAlias != null) { 311 if (!mKeyStore.removeEntryFromKeyStore( 312 Credentials.USER_CERTIFICATE + mClientCertificateAlias)) { 313 Log.e(TAG, "Failed to remove entry: " + mClientCertificateAlias); 314 } 315 mClientCertificateAlias = null; 316 } 317 318 if (mRemediationCaCertificateAlias != null) { 319 if (!mKeyStore.removeEntryFromKeyStore( 320 Credentials.CA_CERTIFICATE + mRemediationCaCertificateAlias)) { 321 Log.e(TAG, "Failed to remove entry: " + mRemediationCaCertificateAlias); 322 } 323 mRemediationCaCertificateAlias = null; 324 } 325 } 326 327 /** 328 * Return the matching status with the given AP, based on the ANQP elements from the AP. 329 * 330 * @param anqpElements ANQP elements from the AP 331 * @param roamingConsortiumFromAp Roaming Consortium information element from the AP 332 * @return {@link PasspointMatch} 333 */ match(Map<ANQPElementType, ANQPElement> anqpElements, RoamingConsortium roamingConsortiumFromAp)334 public PasspointMatch match(Map<ANQPElementType, ANQPElement> anqpElements, 335 RoamingConsortium roamingConsortiumFromAp) { 336 // Match FQDN for Home provider or RCOI(s) for Roaming provider 337 // For SIM credential, the FQDN is in the format of wlan.mnc*.mcc*.3gppnetwork.org 338 PasspointMatch providerMatch = matchFqdnAndRcoi(anqpElements, roamingConsortiumFromAp); 339 340 // 3GPP Network matching. 341 if (providerMatch == PasspointMatch.None && ANQPMatcher.matchThreeGPPNetwork( 342 (ThreeGPPNetworkElement) anqpElements.get(ANQPElementType.ANQP3GPPNetwork), 343 mImsiParameter, mMatchingSIMImsiList)) { 344 if (mVerboseLoggingEnabled) { 345 Log.d(TAG, "Final RoamingProvider match with " 346 + anqpElements.get(ANQPElementType.ANQP3GPPNetwork)); 347 } 348 return PasspointMatch.RoamingProvider; 349 } 350 351 // Perform NAI Realm matching 352 boolean realmMatch = ANQPMatcher.matchNAIRealm( 353 (NAIRealmElement) anqpElements.get(ANQPElementType.ANQPNAIRealm), 354 mConfig.getCredential().getRealm()); 355 356 // In case of no realm match, return provider match as is. 357 if (!realmMatch) { 358 if (mVerboseLoggingEnabled) { 359 Log.d(TAG, "No NAI realm match, final match: " + providerMatch); 360 } 361 return providerMatch; 362 } 363 364 if (mVerboseLoggingEnabled) { 365 Log.d(TAG, "NAI realm match with " + mConfig.getCredential().getRealm()); 366 } 367 368 // Promote the provider match to RoamingProvider if provider match is not found, but NAI 369 // realm is matched. 370 if (providerMatch == PasspointMatch.None) { 371 providerMatch = PasspointMatch.RoamingProvider; 372 } 373 374 if (mVerboseLoggingEnabled) { 375 Log.d(TAG, "Final match: " + providerMatch); 376 } 377 return providerMatch; 378 } 379 380 /** 381 * Generate a WifiConfiguration based on the provider's configuration. The generated 382 * WifiConfiguration will include all the necessary credentials for network connection except 383 * the SSID, which should be added by the caller when the config is being used for network 384 * connection. 385 * 386 * @return {@link WifiConfiguration} 387 */ getWifiConfig()388 public WifiConfiguration getWifiConfig() { 389 WifiConfiguration wifiConfig = new WifiConfiguration(); 390 wifiConfig.FQDN = mConfig.getHomeSp().getFqdn(); 391 if (mConfig.getHomeSp().getRoamingConsortiumOis() != null) { 392 wifiConfig.roamingConsortiumIds = Arrays.copyOf( 393 mConfig.getHomeSp().getRoamingConsortiumOis(), 394 mConfig.getHomeSp().getRoamingConsortiumOis().length); 395 } 396 if (mConfig.getUpdateIdentifier() != Integer.MIN_VALUE) { 397 // R2 profile, it needs to set updateIdentifier HS2.0 Indication element as PPS MO 398 // ID in Association Request. 399 wifiConfig.updateIdentifier = Integer.toString(mConfig.getUpdateIdentifier()); 400 if (isMeteredNetwork(mConfig)) { 401 wifiConfig.meteredOverride = WifiConfiguration.METERED_OVERRIDE_METERED; 402 } 403 } 404 wifiConfig.providerFriendlyName = mConfig.getHomeSp().getFriendlyName(); 405 wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP); 406 wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X); 407 408 // Set RSN only to tell wpa_supplicant that this network is for Passpoint. 409 wifiConfig.allowedProtocols.set(WifiConfiguration.Protocol.RSN); 410 411 WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig(); 412 enterpriseConfig.setRealm(mConfig.getCredential().getRealm()); 413 enterpriseConfig.setDomainSuffixMatch(mConfig.getHomeSp().getFqdn()); 414 if (mConfig.getCredential().getUserCredential() != null) { 415 buildEnterpriseConfigForUserCredential(enterpriseConfig, 416 mConfig.getCredential().getUserCredential()); 417 setAnonymousIdentityToNaiRealm(enterpriseConfig, mConfig.getCredential().getRealm()); 418 } else if (mConfig.getCredential().getCertCredential() != null) { 419 buildEnterpriseConfigForCertCredential(enterpriseConfig); 420 setAnonymousIdentityToNaiRealm(enterpriseConfig, mConfig.getCredential().getRealm()); 421 } else { 422 buildEnterpriseConfigForSimCredential(enterpriseConfig, 423 mConfig.getCredential().getSimCredential()); 424 } 425 wifiConfig.enterpriseConfig = enterpriseConfig; 426 wifiConfig.shared = mIsShared; 427 return wifiConfig; 428 } 429 430 /** 431 * @return true if provider is backed by a SIM credential. 432 */ isSimCredential()433 public boolean isSimCredential() { 434 return mConfig.getCredential().getSimCredential() != null; 435 } 436 437 /** 438 * Convert a legacy {@link WifiConfiguration} representation of a Passpoint configuration to 439 * a {@link PasspointConfiguration}. This is used for migrating legacy Passpoint 440 * configuration (release N and older). 441 * 442 * @param wifiConfig The {@link WifiConfiguration} to convert 443 * @return {@link PasspointConfiguration} 444 */ convertFromWifiConfig(WifiConfiguration wifiConfig)445 public static PasspointConfiguration convertFromWifiConfig(WifiConfiguration wifiConfig) { 446 PasspointConfiguration passpointConfig = new PasspointConfiguration(); 447 448 // Setup HomeSP. 449 HomeSp homeSp = new HomeSp(); 450 if (TextUtils.isEmpty(wifiConfig.FQDN)) { 451 Log.e(TAG, "Missing FQDN"); 452 return null; 453 } 454 homeSp.setFqdn(wifiConfig.FQDN); 455 homeSp.setFriendlyName(wifiConfig.providerFriendlyName); 456 if (wifiConfig.roamingConsortiumIds != null) { 457 homeSp.setRoamingConsortiumOis(Arrays.copyOf( 458 wifiConfig.roamingConsortiumIds, wifiConfig.roamingConsortiumIds.length)); 459 } 460 passpointConfig.setHomeSp(homeSp); 461 462 // Setup Credential. 463 Credential credential = new Credential(); 464 credential.setRealm(wifiConfig.enterpriseConfig.getRealm()); 465 switch (wifiConfig.enterpriseConfig.getEapMethod()) { 466 case WifiEnterpriseConfig.Eap.TTLS: 467 credential.setUserCredential(buildUserCredentialFromEnterpriseConfig( 468 wifiConfig.enterpriseConfig)); 469 break; 470 case WifiEnterpriseConfig.Eap.TLS: 471 Credential.CertificateCredential certCred = new Credential.CertificateCredential(); 472 certCred.setCertType(Credential.CertificateCredential.CERT_TYPE_X509V3); 473 credential.setCertCredential(certCred); 474 break; 475 case WifiEnterpriseConfig.Eap.SIM: 476 credential.setSimCredential(buildSimCredentialFromEnterpriseConfig( 477 EAPConstants.EAP_SIM, wifiConfig.enterpriseConfig)); 478 break; 479 case WifiEnterpriseConfig.Eap.AKA: 480 credential.setSimCredential(buildSimCredentialFromEnterpriseConfig( 481 EAPConstants.EAP_AKA, wifiConfig.enterpriseConfig)); 482 break; 483 case WifiEnterpriseConfig.Eap.AKA_PRIME: 484 credential.setSimCredential(buildSimCredentialFromEnterpriseConfig( 485 EAPConstants.EAP_AKA_PRIME, wifiConfig.enterpriseConfig)); 486 break; 487 default: 488 Log.e(TAG, "Unsupport EAP method: " + wifiConfig.enterpriseConfig.getEapMethod()); 489 return null; 490 } 491 if (credential.getUserCredential() == null && credential.getCertCredential() == null 492 && credential.getSimCredential() == null) { 493 Log.e(TAG, "Missing credential"); 494 return null; 495 } 496 passpointConfig.setCredential(credential); 497 498 return passpointConfig; 499 } 500 501 @Override equals(Object thatObject)502 public boolean equals(Object thatObject) { 503 if (this == thatObject) { 504 return true; 505 } 506 if (!(thatObject instanceof PasspointProvider)) { 507 return false; 508 } 509 PasspointProvider that = (PasspointProvider) thatObject; 510 return mProviderId == that.mProviderId 511 && (mCaCertificateAliases == null ? that.mCaCertificateAliases == null 512 : mCaCertificateAliases.equals(that.mCaCertificateAliases)) 513 && TextUtils.equals(mClientCertificateAlias, that.mClientCertificateAlias) 514 && TextUtils.equals(mClientPrivateKeyAlias, that.mClientPrivateKeyAlias) 515 && (mConfig == null ? that.mConfig == null : mConfig.equals(that.mConfig)) 516 && TextUtils.equals(mRemediationCaCertificateAlias, 517 that.mRemediationCaCertificateAlias); 518 } 519 520 @Override hashCode()521 public int hashCode() { 522 return Objects.hash(mProviderId, mCaCertificateAliases, mClientCertificateAlias, 523 mClientPrivateKeyAlias, mConfig, mRemediationCaCertificateAlias); 524 } 525 526 @Override toString()527 public String toString() { 528 StringBuilder builder = new StringBuilder(); 529 builder.append("ProviderId: ").append(mProviderId).append("\n"); 530 builder.append("CreatorUID: ").append(mCreatorUid).append("\n"); 531 if (mPackageName != null) { 532 builder.append("PackageName: ").append(mPackageName).append("\n"); 533 } 534 builder.append("Configuration Begin ---\n"); 535 builder.append(mConfig); 536 builder.append("Configuration End ---\n"); 537 return builder.toString(); 538 } 539 540 /** 541 * Retrieve the client certificate from the certificates chain. The certificate 542 * with the matching SHA256 digest is the client certificate. 543 * 544 * @param certChain The client certificates chain 545 * @param expectedSha256Fingerprint The expected SHA256 digest of the client certificate 546 * @return {@link java.security.cert.X509Certificate} 547 */ getClientCertificate(X509Certificate[] certChain, byte[] expectedSha256Fingerprint)548 private static X509Certificate getClientCertificate(X509Certificate[] certChain, 549 byte[] expectedSha256Fingerprint) { 550 if (certChain == null) { 551 return null; 552 } 553 try { 554 MessageDigest digester = MessageDigest.getInstance("SHA-256"); 555 for (X509Certificate certificate : certChain) { 556 digester.reset(); 557 byte[] fingerprint = digester.digest(certificate.getEncoded()); 558 if (Arrays.equals(expectedSha256Fingerprint, fingerprint)) { 559 return certificate; 560 } 561 } 562 } catch (CertificateEncodingException | NoSuchAlgorithmException e) { 563 return null; 564 } 565 566 return null; 567 } 568 569 /** 570 * Determines the Passpoint network is a metered network. 571 * 572 * Expiration date -> non-metered 573 * Data limit -> metered 574 * Time usage limit -> metered 575 * @param passpointConfig instance of {@link PasspointConfiguration} 576 * @return {@code true} if the network is a metered network, {@code false} otherwise. 577 */ isMeteredNetwork(PasspointConfiguration passpointConfig)578 private boolean isMeteredNetwork(PasspointConfiguration passpointConfig) { 579 if (passpointConfig == null) return false; 580 581 // If DataLimit is zero, there is unlimited data usage for the account. 582 // If TimeLimit is zero, there is unlimited time usage for the account. 583 return passpointConfig.getUsageLimitDataLimit() > 0 584 || passpointConfig.getUsageLimitTimeLimitInMinutes() > 0; 585 } 586 587 /** 588 * Match given OIs to the Roaming Consortium OIs 589 * 590 * @param providerOis Provider OIs to match against 591 * @param roamingConsortiumElement RCOIs in the ANQP element 592 * @param roamingConsortiumFromAp RCOIs in the AP scan results 593 * @param matchAll Indicates if all providerOis must match the RCOIs elements 594 * @return {@code true} if there is a match, {@code false} otherwise. 595 */ matchOis(long[] providerOis, RoamingConsortiumElement roamingConsortiumElement, RoamingConsortium roamingConsortiumFromAp, boolean matchAll)596 private boolean matchOis(long[] providerOis, 597 RoamingConsortiumElement roamingConsortiumElement, 598 RoamingConsortium roamingConsortiumFromAp, 599 boolean matchAll) { 600 601 602 // ANQP Roaming Consortium OI matching. 603 if (ANQPMatcher.matchRoamingConsortium(roamingConsortiumElement, providerOis, matchAll)) { 604 if (mVerboseLoggingEnabled) { 605 Log.e(TAG, "ANQP RCOI match " + roamingConsortiumElement); 606 } 607 return true; 608 } 609 610 // AP Roaming Consortium OI matching. 611 long[] apRoamingConsortiums = roamingConsortiumFromAp.getRoamingConsortiums(); 612 if (apRoamingConsortiums == null || providerOis == null) { 613 return false; 614 } 615 // Roaming Consortium OI information element matching. 616 for (long apOi: apRoamingConsortiums) { 617 boolean matched = false; 618 for (long providerOi: providerOis) { 619 if (apOi == providerOi) { 620 if (mVerboseLoggingEnabled) { 621 Log.e(TAG, "AP RCOI match: " + apOi); 622 } 623 if (!matchAll) { 624 return true; 625 } else { 626 matched = true; 627 break; 628 } 629 } 630 } 631 if (matchAll && !matched) { 632 return false; 633 } 634 } 635 return matchAll; 636 } 637 638 /** 639 * Perform a provider match based on the given ANQP elements except for matching 3GPP Network. 640 * 641 * @param anqpElements List of ANQP elements 642 * @param roamingConsortiumFromAp Roaming Consortium information element from the AP 643 * @return {@link PasspointMatch} 644 */ matchFqdnAndRcoi( Map<ANQPElementType, ANQPElement> anqpElements, RoamingConsortium roamingConsortiumFromAp)645 private PasspointMatch matchFqdnAndRcoi( 646 Map<ANQPElementType, ANQPElement> anqpElements, 647 RoamingConsortium roamingConsortiumFromAp) { 648 // Domain name matching. 649 if (ANQPMatcher.matchDomainName( 650 (DomainNameElement) anqpElements.get(ANQPElementType.ANQPDomName), 651 mConfig.getHomeSp().getFqdn(), mImsiParameter, mMatchingSIMImsiList)) { 652 if (mVerboseLoggingEnabled) { 653 Log.d(TAG, "Domain name " + mConfig.getHomeSp().getFqdn() 654 + " match: HomeProvider"); 655 } 656 return PasspointMatch.HomeProvider; 657 } 658 659 // Other Home Partners matching. 660 if (mConfig.getHomeSp().getOtherHomePartners() != null) { 661 for (String otherHomePartner : mConfig.getHomeSp().getOtherHomePartners()) { 662 if (ANQPMatcher.matchDomainName( 663 (DomainNameElement) anqpElements.get(ANQPElementType.ANQPDomName), 664 otherHomePartner, null, null)) { 665 if (mVerboseLoggingEnabled) { 666 Log.d(TAG, "Other Home Partner " + otherHomePartner 667 + " match: HomeProvider"); 668 } 669 return PasspointMatch.HomeProvider; 670 } 671 } 672 } 673 674 // HomeOI matching 675 if (mConfig.getHomeSp().getMatchAllOis() != null) { 676 // Ensure that every HomeOI whose corresponding HomeOIRequired value is true shall match 677 // an OI in the Roaming Consortium advertised by the hotspot operator. 678 if (matchOis(mConfig.getHomeSp().getMatchAllOis(), (RoamingConsortiumElement) 679 anqpElements.get(ANQPElementType.ANQPRoamingConsortium), 680 roamingConsortiumFromAp, true)) { 681 if (mVerboseLoggingEnabled) { 682 Log.e(TAG, "All HomeOI RCOI match: HomeProvider"); 683 } 684 return PasspointMatch.HomeProvider; 685 } 686 } else if (mConfig.getHomeSp().getMatchAnyOis() != null) { 687 // Ensure that any HomeOI whose corresponding HomeOIRequired value is false shall match 688 // an OI in the Roaming Consortium advertised by the hotspot operator. 689 if (matchOis(mConfig.getHomeSp().getMatchAnyOis(), (RoamingConsortiumElement) 690 anqpElements.get(ANQPElementType.ANQPRoamingConsortium), 691 roamingConsortiumFromAp, false)) { 692 if (mVerboseLoggingEnabled) { 693 Log.e(TAG, "Any HomeOI RCOI match: HomeProvider"); 694 } 695 return PasspointMatch.HomeProvider; 696 } 697 } 698 699 // Roaming Consortium OI matching. 700 if (matchOis(mConfig.getHomeSp().getRoamingConsortiumOis(), (RoamingConsortiumElement) 701 anqpElements.get(ANQPElementType.ANQPRoamingConsortium), 702 roamingConsortiumFromAp, false)) { 703 if (mVerboseLoggingEnabled) { 704 Log.e(TAG, "ANQP RCOI match: RoamingProvider"); 705 } 706 return PasspointMatch.RoamingProvider; 707 } 708 if (mVerboseLoggingEnabled) { 709 Log.e(TAG, "No domain name or RCOI match"); 710 } 711 return PasspointMatch.None; 712 } 713 714 /** 715 * Fill in WifiEnterpriseConfig with information from an user credential. 716 * 717 * @param config Instance of {@link WifiEnterpriseConfig} 718 * @param credential Instance of {@link UserCredential} 719 */ buildEnterpriseConfigForUserCredential(WifiEnterpriseConfig config, Credential.UserCredential credential)720 private void buildEnterpriseConfigForUserCredential(WifiEnterpriseConfig config, 721 Credential.UserCredential credential) { 722 byte[] pwOctets = Base64.decode(credential.getPassword(), Base64.DEFAULT); 723 String decodedPassword = new String(pwOctets, StandardCharsets.UTF_8); 724 config.setEapMethod(WifiEnterpriseConfig.Eap.TTLS); 725 config.setIdentity(credential.getUsername()); 726 config.setPassword(decodedPassword); 727 config.setCaCertificateAliases(mCaCertificateAliases.toArray(new String[0])); 728 int phase2Method = WifiEnterpriseConfig.Phase2.NONE; 729 switch (credential.getNonEapInnerMethod()) { 730 case Credential.UserCredential.AUTH_METHOD_PAP: 731 phase2Method = WifiEnterpriseConfig.Phase2.PAP; 732 break; 733 case Credential.UserCredential.AUTH_METHOD_MSCHAP: 734 phase2Method = WifiEnterpriseConfig.Phase2.MSCHAP; 735 break; 736 case Credential.UserCredential.AUTH_METHOD_MSCHAPV2: 737 phase2Method = WifiEnterpriseConfig.Phase2.MSCHAPV2; 738 break; 739 default: 740 // Should never happen since this is already validated when the provider is 741 // added. 742 Log.wtf(TAG, "Unsupported Auth: " + credential.getNonEapInnerMethod()); 743 break; 744 } 745 config.setPhase2Method(phase2Method); 746 } 747 748 /** 749 * Fill in WifiEnterpriseConfig with information from a certificate credential. 750 * 751 * @param config Instance of {@link WifiEnterpriseConfig} 752 */ buildEnterpriseConfigForCertCredential(WifiEnterpriseConfig config)753 private void buildEnterpriseConfigForCertCredential(WifiEnterpriseConfig config) { 754 config.setEapMethod(WifiEnterpriseConfig.Eap.TLS); 755 config.setClientCertificateAlias(mClientCertificateAlias); 756 config.setCaCertificateAliases(mCaCertificateAliases.toArray(new String[0])); 757 } 758 759 /** 760 * Fill in WifiEnterpriseConfig with information from a SIM credential. 761 * 762 * @param config Instance of {@link WifiEnterpriseConfig} 763 * @param credential Instance of {@link SimCredential} 764 */ buildEnterpriseConfigForSimCredential(WifiEnterpriseConfig config, Credential.SimCredential credential)765 private void buildEnterpriseConfigForSimCredential(WifiEnterpriseConfig config, 766 Credential.SimCredential credential) { 767 int eapMethod = WifiEnterpriseConfig.Eap.NONE; 768 switch(credential.getEapType()) { 769 case EAPConstants.EAP_SIM: 770 eapMethod = WifiEnterpriseConfig.Eap.SIM; 771 break; 772 case EAPConstants.EAP_AKA: 773 eapMethod = WifiEnterpriseConfig.Eap.AKA; 774 break; 775 case EAPConstants.EAP_AKA_PRIME: 776 eapMethod = WifiEnterpriseConfig.Eap.AKA_PRIME; 777 break; 778 default: 779 // Should never happen since this is already validated when the provider is 780 // added. 781 Log.wtf(TAG, "Unsupported EAP Method: " + credential.getEapType()); 782 break; 783 } 784 config.setEapMethod(eapMethod); 785 config.setPlmn(credential.getImsi()); 786 } 787 setAnonymousIdentityToNaiRealm(WifiEnterpriseConfig config, String realm)788 private static void setAnonymousIdentityToNaiRealm(WifiEnterpriseConfig config, String realm) { 789 /** 790 * Set WPA supplicant's anonymous identity field to a string containing the NAI realm, so 791 * that this value will be sent to the EAP server as part of the EAP-Response/ Identity 792 * packet. WPA supplicant will reset this field after using it for the EAP-Response/Identity 793 * packet, and revert to using the (real) identity field for subsequent transactions that 794 * request an identity (e.g. in EAP-TTLS). 795 * 796 * This NAI realm value (the portion of the identity after the '@') is used to tell the 797 * AAA server which AAA/H to forward packets to. The hardcoded username, "anonymous", is a 798 * placeholder that is not used--it is set to this value by convention. See Section 5.1 of 799 * RFC3748 for more details. 800 * 801 * NOTE: we do not set this value for EAP-SIM/AKA/AKA', since the EAP server expects the 802 * EAP-Response/Identity packet to contain an actual, IMSI-based identity, in order to 803 * identify the device. 804 */ 805 config.setAnonymousIdentity("anonymous@" + realm); 806 } 807 808 /** 809 * Helper function for creating a 810 * {@link android.net.wifi.hotspot2.pps.Credential.UserCredential} from the given 811 * {@link WifiEnterpriseConfig} 812 * 813 * @param config The enterprise configuration containing the credential 814 * @return {@link android.net.wifi.hotspot2.pps.Credential.UserCredential} 815 */ buildUserCredentialFromEnterpriseConfig( WifiEnterpriseConfig config)816 private static Credential.UserCredential buildUserCredentialFromEnterpriseConfig( 817 WifiEnterpriseConfig config) { 818 Credential.UserCredential userCredential = new Credential.UserCredential(); 819 userCredential.setEapType(EAPConstants.EAP_TTLS); 820 821 if (TextUtils.isEmpty(config.getIdentity())) { 822 Log.e(TAG, "Missing username for user credential"); 823 return null; 824 } 825 userCredential.setUsername(config.getIdentity()); 826 827 if (TextUtils.isEmpty(config.getPassword())) { 828 Log.e(TAG, "Missing password for user credential"); 829 return null; 830 } 831 String encodedPassword = 832 new String(Base64.encode(config.getPassword().getBytes(StandardCharsets.UTF_8), 833 Base64.DEFAULT), StandardCharsets.UTF_8); 834 userCredential.setPassword(encodedPassword); 835 836 switch(config.getPhase2Method()) { 837 case WifiEnterpriseConfig.Phase2.PAP: 838 userCredential.setNonEapInnerMethod(Credential.UserCredential.AUTH_METHOD_PAP); 839 break; 840 case WifiEnterpriseConfig.Phase2.MSCHAP: 841 userCredential.setNonEapInnerMethod(Credential.UserCredential.AUTH_METHOD_MSCHAP); 842 break; 843 case WifiEnterpriseConfig.Phase2.MSCHAPV2: 844 userCredential.setNonEapInnerMethod(Credential.UserCredential.AUTH_METHOD_MSCHAPV2); 845 break; 846 default: 847 Log.e(TAG, "Unsupported phase2 method for TTLS: " + config.getPhase2Method()); 848 return null; 849 } 850 return userCredential; 851 } 852 853 /** 854 * Helper function for creating a 855 * {@link android.net.wifi.hotspot2.pps.Credential.SimCredential} from the given 856 * {@link WifiEnterpriseConfig} 857 * 858 * @param eapType The EAP type of the SIM credential 859 * @param config The enterprise configuration containing the credential 860 * @return {@link android.net.wifi.hotspot2.pps.Credential.SimCredential} 861 */ buildSimCredentialFromEnterpriseConfig( int eapType, WifiEnterpriseConfig config)862 private static Credential.SimCredential buildSimCredentialFromEnterpriseConfig( 863 int eapType, WifiEnterpriseConfig config) { 864 Credential.SimCredential simCredential = new Credential.SimCredential(); 865 if (TextUtils.isEmpty(config.getPlmn())) { 866 Log.e(TAG, "Missing IMSI for SIM credential"); 867 return null; 868 } 869 simCredential.setImsi(config.getPlmn()); 870 simCredential.setEapType(eapType); 871 return simCredential; 872 } 873 874 /** 875 * Enable verbose logging 876 * @param verbose more than 0 enables verbose logging 877 */ enableVerboseLogging(int verbose)878 public void enableVerboseLogging(int verbose) { 879 mVerboseLoggingEnabled = (verbose > 0) ? true : false; 880 } 881 } 882