1 package com.android.server.wifi.configparse; 2 3 import android.content.Context; 4 import android.net.Uri; 5 import android.net.wifi.WifiConfiguration; 6 import android.net.wifi.WifiEnterpriseConfig; 7 import android.provider.DocumentsContract; 8 import android.util.Base64; 9 import android.util.Log; 10 11 import com.android.server.wifi.IMSIParameter; 12 import com.android.server.wifi.anqp.eap.AuthParam; 13 import com.android.server.wifi.anqp.eap.EAP; 14 import com.android.server.wifi.anqp.eap.EAPMethod; 15 import com.android.server.wifi.anqp.eap.NonEAPInnerAuth; 16 import com.android.server.wifi.hotspot2.omadm.PasspointManagementObjectManager; 17 import com.android.server.wifi.hotspot2.pps.Credential; 18 import com.android.server.wifi.hotspot2.pps.HomeSP; 19 20 import org.xml.sax.SAXException; 21 22 import java.io.ByteArrayInputStream; 23 import java.io.IOException; 24 import java.io.InputStreamReader; 25 import java.io.LineNumberReader; 26 import java.nio.charset.StandardCharsets; 27 import java.security.GeneralSecurityException; 28 import java.security.KeyStore; 29 import java.security.MessageDigest; 30 import java.security.PrivateKey; 31 import java.security.cert.Certificate; 32 import java.security.cert.CertificateFactory; 33 import java.security.cert.X509Certificate; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.Enumeration; 37 import java.util.HashSet; 38 import java.util.List; 39 40 public class ConfigBuilder { 41 public static final String WifiConfigType = "application/x-wifi-config"; 42 private static final String ProfileTag = "application/x-passpoint-profile"; 43 private static final String KeyTag = "application/x-pkcs12"; 44 private static final String CATag = "application/x-x509-ca-cert"; 45 46 private static final String X509 = "X.509"; 47 48 private static final String TAG = "WCFG"; 49 50 public static WifiConfiguration buildConfig(String uriString, byte[] data, Context context) 51 throws IOException, GeneralSecurityException, SAXException { 52 Log.d(TAG, "Content: " + (data != null ? data.length : -1)); 53 54 byte[] b64 = Base64.decode(new String(data, StandardCharsets.ISO_8859_1), Base64.DEFAULT); 55 Log.d(TAG, "Decoded: " + b64.length + " bytes."); 56 57 dropFile(Uri.parse(uriString), context); 58 59 MIMEContainer mimeContainer = new 60 MIMEContainer(new LineNumberReader( 61 new InputStreamReader(new ByteArrayInputStream(b64), StandardCharsets.ISO_8859_1)), 62 null); 63 if (!mimeContainer.isBase64()) { 64 throw new IOException("Encoding for " + 65 mimeContainer.getContentType() + " is not base64"); 66 } 67 MIMEContainer inner; 68 if (mimeContainer.getContentType().equals(WifiConfigType)) { 69 byte[] wrappedContent = Base64.decode(mimeContainer.getText(), Base64.DEFAULT); 70 Log.d(TAG, "Building container from '" + 71 new String(wrappedContent, StandardCharsets.ISO_8859_1) + "'"); 72 inner = new MIMEContainer(new LineNumberReader( 73 new InputStreamReader(new ByteArrayInputStream(wrappedContent), 74 StandardCharsets.ISO_8859_1)), null); 75 } 76 else { 77 inner = mimeContainer; 78 } 79 return parse(inner); 80 } 81 82 private static void dropFile(Uri uri, Context context) { 83 if (DocumentsContract.isDocumentUri(context, uri)) { 84 DocumentsContract.deleteDocument(context.getContentResolver(), uri); 85 } else { 86 context.getContentResolver().delete(uri, null, null); 87 } 88 } 89 90 private static WifiConfiguration parse(MIMEContainer root) 91 throws IOException, GeneralSecurityException, SAXException { 92 93 if (root.getMimeContainers() == null) { 94 throw new IOException("Malformed MIME content: not multipart"); 95 } 96 97 String moText = null; 98 X509Certificate caCert = null; 99 PrivateKey clientKey = null; 100 List<X509Certificate> clientChain = null; 101 102 for (MIMEContainer subContainer : root.getMimeContainers()) { 103 Log.d(TAG, " + Content Type: " + subContainer.getContentType()); 104 switch (subContainer.getContentType()) { 105 case ProfileTag: 106 if (subContainer.isBase64()) { 107 byte[] octets = Base64.decode(subContainer.getText(), Base64.DEFAULT); 108 moText = new String(octets, StandardCharsets.UTF_8); 109 } else { 110 moText = subContainer.getText(); 111 } 112 Log.d(TAG, "OMA: " + moText); 113 break; 114 case CATag: { 115 if (!subContainer.isBase64()) { 116 throw new IOException("Can't read non base64 encoded cert"); 117 } 118 119 byte[] octets = Base64.decode(subContainer.getText(), Base64.DEFAULT); 120 CertificateFactory factory = CertificateFactory.getInstance(X509); 121 caCert = (X509Certificate) factory.generateCertificate( 122 new ByteArrayInputStream(octets)); 123 Log.d(TAG, "Cert subject " + caCert.getSubjectX500Principal()); 124 Log.d(TAG, "Full Cert: " + caCert); 125 break; 126 } 127 case KeyTag: { 128 if (!subContainer.isBase64()) { 129 throw new IOException("Can't read non base64 encoded key"); 130 } 131 132 byte[] octets = Base64.decode(subContainer.getText(), Base64.DEFAULT); 133 134 KeyStore ks = KeyStore.getInstance("PKCS12"); 135 ByteArrayInputStream in = new ByteArrayInputStream(octets); 136 ks.load(in, new char[0]); 137 in.close(); 138 Log.d(TAG, "---- Start PKCS12 info " + octets.length + ", size " + ks.size()); 139 Enumeration<String> aliases = ks.aliases(); 140 while (aliases.hasMoreElements()) { 141 String alias = aliases.nextElement(); 142 clientKey = (PrivateKey) ks.getKey(alias, null); 143 Log.d(TAG, "Key: " + clientKey.getFormat()); 144 Certificate[] chain = ks.getCertificateChain(alias); 145 if (chain != null) { 146 clientChain = new ArrayList<>(); 147 for (Certificate certificate : chain) { 148 if (!(certificate instanceof X509Certificate)) { 149 Log.w(TAG, "Element in cert chain is not an X509Certificate: " + 150 certificate.getClass()); 151 } 152 clientChain.add((X509Certificate) certificate); 153 } 154 Log.d(TAG, "Chain: " + clientChain.size()); 155 } 156 } 157 Log.d(TAG, "---- End PKCS12 info."); 158 break; 159 } 160 } 161 } 162 163 if (moText == null) { 164 throw new IOException("Missing profile"); 165 } 166 167 HomeSP homeSP = PasspointManagementObjectManager.buildSP(moText); 168 169 return buildConfig(homeSP, caCert, clientChain, clientKey); 170 } 171 172 private static WifiConfiguration buildConfig(HomeSP homeSP, X509Certificate caCert, 173 List<X509Certificate> clientChain, PrivateKey key) 174 throws IOException, GeneralSecurityException { 175 176 WifiConfiguration config; 177 178 EAP.EAPMethodID eapMethodID = homeSP.getCredential().getEAPMethod().getEAPMethodID(); 179 switch (eapMethodID) { 180 case EAP_TTLS: 181 if (key != null || clientChain != null) { 182 Log.w(TAG, "Client cert and/or key unnecessarily included with EAP-TTLS "+ 183 "profile"); 184 } 185 config = buildTTLSConfig(homeSP, caCert); 186 break; 187 case EAP_TLS: 188 config = buildTLSConfig(homeSP, clientChain, key, caCert); 189 break; 190 case EAP_AKA: 191 case EAP_AKAPrim: 192 case EAP_SIM: 193 if (key != null || clientChain != null || caCert != null) { 194 Log.i(TAG, "Client/CA cert and/or key unnecessarily included with " + 195 eapMethodID + " profile"); 196 } 197 config = buildSIMConfig(homeSP); 198 break; 199 default: 200 throw new IOException("Unsupported EAP Method: " + eapMethodID); 201 } 202 203 return config; 204 } 205 206 // Retain for debugging purposes 207 /* 208 private static void xIterateCerts(KeyStore ks, X509Certificate caCert) 209 throws GeneralSecurityException { 210 Enumeration<String> aliases = ks.aliases(); 211 while (aliases.hasMoreElements()) { 212 String alias = aliases.nextElement(); 213 Certificate cert = ks.getCertificate(alias); 214 Log.d("HS2J", "Checking " + alias); 215 if (cert instanceof X509Certificate) { 216 X509Certificate x509Certificate = (X509Certificate) cert; 217 boolean sm = x509Certificate.getSubjectX500Principal().equals( 218 caCert.getSubjectX500Principal()); 219 boolean eq = false; 220 if (sm) { 221 eq = Arrays.equals(x509Certificate.getEncoded(), caCert.getEncoded()); 222 } 223 Log.d("HS2J", "Subject: " + x509Certificate.getSubjectX500Principal() + 224 ": " + sm + "/" + eq); 225 } 226 } 227 } 228 */ 229 230 private static void setAnonymousIdentityToNaiRealm( 231 WifiConfiguration config, Credential credential) { 232 /** 233 * Set WPA supplicant's anonymous identity field to a string containing the NAI realm, so 234 * that this value will be sent to the EAP server as part of the EAP-Response/ Identity 235 * packet. WPA supplicant will reset this field after using it for the EAP-Response/Identity 236 * packet, and revert to using the (real) identity field for subsequent transactions that 237 * request an identity (e.g. in EAP-TTLS). 238 * 239 * This NAI realm value (the portion of the identity after the '@') is used to tell the 240 * AAA server which AAA/H to forward packets to. The hardcoded username, "anonymous", is a 241 * placeholder that is not used--it is set to this value by convention. See Section 5.1 of 242 * RFC3748 for more details. 243 * 244 * NOTE: we do not set this value for EAP-SIM/AKA/AKA', since the EAP server expects the 245 * EAP-Response/Identity packet to contain an actual, IMSI-based identity, in order to 246 * identify the device. 247 */ 248 config.enterpriseConfig.setAnonymousIdentity("anonymous@" + credential.getRealm()); 249 } 250 251 private static WifiConfiguration buildTTLSConfig(HomeSP homeSP, X509Certificate caCert) 252 throws IOException { 253 Credential credential = homeSP.getCredential(); 254 255 if (credential.getUserName() == null || credential.getPassword() == null) { 256 throw new IOException("EAP-TTLS provisioned without user name or password"); 257 } 258 259 EAPMethod eapMethod = credential.getEAPMethod(); 260 261 AuthParam authParam = eapMethod.getAuthParam(); 262 if (authParam == null || 263 authParam.getAuthInfoID() != EAP.AuthInfoID.NonEAPInnerAuthType) { 264 throw new IOException("Bad auth parameter for EAP-TTLS: " + authParam); 265 } 266 267 WifiConfiguration config = buildBaseConfiguration(homeSP); 268 NonEAPInnerAuth ttlsParam = (NonEAPInnerAuth) authParam; 269 WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig; 270 enterpriseConfig.setPhase2Method(remapInnerMethod(ttlsParam.getType())); 271 enterpriseConfig.setIdentity(credential.getUserName()); 272 enterpriseConfig.setPassword(credential.getPassword()); 273 enterpriseConfig.setCaCertificate(caCert); 274 275 setAnonymousIdentityToNaiRealm(config, credential); 276 277 return config; 278 } 279 280 private static WifiConfiguration buildTLSConfig(HomeSP homeSP, 281 List<X509Certificate> clientChain, 282 PrivateKey clientKey, 283 X509Certificate caCert) 284 throws IOException, GeneralSecurityException { 285 286 Credential credential = homeSP.getCredential(); 287 288 X509Certificate clientCertificate = null; 289 290 if (clientKey == null || clientChain == null) { 291 throw new IOException("No key and/or cert passed for EAP-TLS"); 292 } 293 if (credential.getCertType() != Credential.CertType.x509v3) { 294 throw new IOException("Invalid certificate type for TLS: " + 295 credential.getCertType()); 296 } 297 298 byte[] reference = credential.getFingerPrint(); 299 MessageDigest digester = MessageDigest.getInstance("SHA-256"); 300 for (X509Certificate certificate : clientChain) { 301 digester.reset(); 302 byte[] fingerprint = digester.digest(certificate.getEncoded()); 303 if (Arrays.equals(reference, fingerprint)) { 304 clientCertificate = certificate; 305 break; 306 } 307 } 308 if (clientCertificate == null) { 309 throw new IOException("No certificate in chain matches supplied fingerprint"); 310 } 311 312 String alias = Base64.encodeToString(reference, Base64.DEFAULT); 313 314 WifiConfiguration config = buildBaseConfiguration(homeSP); 315 WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig; 316 enterpriseConfig.setClientCertificateAlias(alias); 317 enterpriseConfig.setClientKeyEntry(clientKey, clientCertificate); 318 enterpriseConfig.setCaCertificate(caCert); 319 320 setAnonymousIdentityToNaiRealm(config, credential); 321 322 return config; 323 } 324 325 private static WifiConfiguration buildSIMConfig(HomeSP homeSP) 326 throws IOException { 327 328 Credential credential = homeSP.getCredential(); 329 IMSIParameter credImsi = credential.getImsi(); 330 331 /* 332 * Uncomment to enforce strict IMSI matching with currently installed SIM cards. 333 * 334 TelephonyManager tm = TelephonyManager.from(context); 335 SubscriptionManager sub = SubscriptionManager.from(context); 336 boolean match = false; 337 338 for (int subId : sub.getActiveSubscriptionIdList()) { 339 String imsi = tm.getSubscriberId(subId); 340 if (credImsi.matches(imsi)) { 341 match = true; 342 break; 343 } 344 } 345 if (!match) { 346 throw new IOException("Supplied IMSI does not match any SIM card"); 347 } 348 */ 349 350 WifiConfiguration config = buildBaseConfiguration(homeSP); 351 config.enterpriseConfig.setPlmn(credImsi.toString()); 352 return config; 353 } 354 355 private static WifiConfiguration buildBaseConfiguration(HomeSP homeSP) throws IOException { 356 EAP.EAPMethodID eapMethodID = homeSP.getCredential().getEAPMethod().getEAPMethodID(); 357 358 WifiConfiguration config = new WifiConfiguration(); 359 360 config.FQDN = homeSP.getFQDN(); 361 362 HashSet<Long> roamingConsortiumIds = homeSP.getRoamingConsortiums(); 363 config.roamingConsortiumIds = new long[roamingConsortiumIds.size()]; 364 int i = 0; 365 for (long id : roamingConsortiumIds) { 366 config.roamingConsortiumIds[i] = id; 367 i++; 368 } 369 config.providerFriendlyName = homeSP.getFriendlyName(); 370 371 config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP); 372 config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X); 373 374 WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig(); 375 enterpriseConfig.setEapMethod(remapEAPMethod(eapMethodID)); 376 enterpriseConfig.setRealm(homeSP.getCredential().getRealm()); 377 config.enterpriseConfig = enterpriseConfig; 378 // The framework based config builder only ever builds r1 configs: 379 config.updateIdentifier = null; 380 381 return config; 382 } 383 384 private static int remapEAPMethod(EAP.EAPMethodID eapMethodID) throws IOException { 385 switch (eapMethodID) { 386 case EAP_TTLS: 387 return WifiEnterpriseConfig.Eap.TTLS; 388 case EAP_TLS: 389 return WifiEnterpriseConfig.Eap.TLS; 390 case EAP_SIM: 391 return WifiEnterpriseConfig.Eap.SIM; 392 case EAP_AKA: 393 return WifiEnterpriseConfig.Eap.AKA; 394 case EAP_AKAPrim: 395 return WifiEnterpriseConfig.Eap.AKA_PRIME; 396 default: 397 throw new IOException("Bad EAP method: " + eapMethodID); 398 } 399 } 400 401 private static int remapInnerMethod(NonEAPInnerAuth.NonEAPType type) throws IOException { 402 switch (type) { 403 case PAP: 404 return WifiEnterpriseConfig.Phase2.PAP; 405 case MSCHAP: 406 return WifiEnterpriseConfig.Phase2.MSCHAP; 407 case MSCHAPv2: 408 return WifiEnterpriseConfig.Phase2.MSCHAPV2; 409 case CHAP: 410 default: 411 throw new IOException("Inner method " + type + " not supported"); 412 } 413 } 414 } 415