package com.android.server.wifi.configparse; import android.content.Context; import android.net.Uri; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiEnterpriseConfig; import android.provider.DocumentsContract; import android.util.Base64; import android.util.Log; import com.android.server.wifi.IMSIParameter; import com.android.server.wifi.anqp.eap.AuthParam; import com.android.server.wifi.anqp.eap.EAP; import com.android.server.wifi.anqp.eap.EAPMethod; import com.android.server.wifi.anqp.eap.NonEAPInnerAuth; import com.android.server.wifi.hotspot2.omadm.PasspointManagementObjectManager; import com.android.server.wifi.hotspot2.pps.Credential; import com.android.server.wifi.hotspot2.pps.HomeSP; import org.xml.sax.SAXException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.MessageDigest; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.HashSet; import java.util.List; public class ConfigBuilder { public static final String WifiConfigType = "application/x-wifi-config"; private static final String ProfileTag = "application/x-passpoint-profile"; private static final String KeyTag = "application/x-pkcs12"; private static final String CATag = "application/x-x509-ca-cert"; private static final String X509 = "X.509"; private static final String TAG = "WCFG"; public static WifiConfiguration buildConfig(String uriString, byte[] data, Context context) throws IOException, GeneralSecurityException, SAXException { Log.d(TAG, "Content: " + (data != null ? data.length : -1)); byte[] b64 = Base64.decode(new String(data, StandardCharsets.ISO_8859_1), Base64.DEFAULT); Log.d(TAG, "Decoded: " + b64.length + " bytes."); dropFile(Uri.parse(uriString), context); MIMEContainer mimeContainer = new MIMEContainer(new LineNumberReader( new InputStreamReader(new ByteArrayInputStream(b64), StandardCharsets.ISO_8859_1)), null); if (!mimeContainer.isBase64()) { throw new IOException("Encoding for " + mimeContainer.getContentType() + " is not base64"); } MIMEContainer inner; if (mimeContainer.getContentType().equals(WifiConfigType)) { byte[] wrappedContent = Base64.decode(mimeContainer.getText(), Base64.DEFAULT); Log.d(TAG, "Building container from '" + new String(wrappedContent, StandardCharsets.ISO_8859_1) + "'"); inner = new MIMEContainer(new LineNumberReader( new InputStreamReader(new ByteArrayInputStream(wrappedContent), StandardCharsets.ISO_8859_1)), null); } else { inner = mimeContainer; } return parse(inner); } private static void dropFile(Uri uri, Context context) { if (DocumentsContract.isDocumentUri(context, uri)) { DocumentsContract.deleteDocument(context.getContentResolver(), uri); } else { context.getContentResolver().delete(uri, null, null); } } private static WifiConfiguration parse(MIMEContainer root) throws IOException, GeneralSecurityException, SAXException { if (root.getMimeContainers() == null) { throw new IOException("Malformed MIME content: not multipart"); } String moText = null; X509Certificate caCert = null; PrivateKey clientKey = null; List clientChain = null; for (MIMEContainer subContainer : root.getMimeContainers()) { Log.d(TAG, " + Content Type: " + subContainer.getContentType()); switch (subContainer.getContentType()) { case ProfileTag: if (subContainer.isBase64()) { byte[] octets = Base64.decode(subContainer.getText(), Base64.DEFAULT); moText = new String(octets, StandardCharsets.UTF_8); } else { moText = subContainer.getText(); } Log.d(TAG, "OMA: " + moText); break; case CATag: { if (!subContainer.isBase64()) { throw new IOException("Can't read non base64 encoded cert"); } byte[] octets = Base64.decode(subContainer.getText(), Base64.DEFAULT); CertificateFactory factory = CertificateFactory.getInstance(X509); caCert = (X509Certificate) factory.generateCertificate( new ByteArrayInputStream(octets)); Log.d(TAG, "Cert subject " + caCert.getSubjectX500Principal()); Log.d(TAG, "Full Cert: " + caCert); break; } case KeyTag: { if (!subContainer.isBase64()) { throw new IOException("Can't read non base64 encoded key"); } byte[] octets = Base64.decode(subContainer.getText(), Base64.DEFAULT); KeyStore ks = KeyStore.getInstance("PKCS12"); ByteArrayInputStream in = new ByteArrayInputStream(octets); ks.load(in, new char[0]); in.close(); Log.d(TAG, "---- Start PKCS12 info " + octets.length + ", size " + ks.size()); Enumeration aliases = ks.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); clientKey = (PrivateKey) ks.getKey(alias, null); Log.d(TAG, "Key: " + clientKey.getFormat()); Certificate[] chain = ks.getCertificateChain(alias); if (chain != null) { clientChain = new ArrayList<>(); for (Certificate certificate : chain) { if (!(certificate instanceof X509Certificate)) { Log.w(TAG, "Element in cert chain is not an X509Certificate: " + certificate.getClass()); } clientChain.add((X509Certificate) certificate); } Log.d(TAG, "Chain: " + clientChain.size()); } } Log.d(TAG, "---- End PKCS12 info."); break; } } } if (moText == null) { throw new IOException("Missing profile"); } HomeSP homeSP = PasspointManagementObjectManager.buildSP(moText); return buildConfig(homeSP, caCert, clientChain, clientKey); } private static WifiConfiguration buildConfig(HomeSP homeSP, X509Certificate caCert, List clientChain, PrivateKey key) throws IOException, GeneralSecurityException { WifiConfiguration config; EAP.EAPMethodID eapMethodID = homeSP.getCredential().getEAPMethod().getEAPMethodID(); switch (eapMethodID) { case EAP_TTLS: if (key != null || clientChain != null) { Log.w(TAG, "Client cert and/or key unnecessarily included with EAP-TTLS "+ "profile"); } config = buildTTLSConfig(homeSP, caCert); break; case EAP_TLS: config = buildTLSConfig(homeSP, clientChain, key, caCert); break; case EAP_AKA: case EAP_AKAPrim: case EAP_SIM: if (key != null || clientChain != null || caCert != null) { Log.i(TAG, "Client/CA cert and/or key unnecessarily included with " + eapMethodID + " profile"); } config = buildSIMConfig(homeSP); break; default: throw new IOException("Unsupported EAP Method: " + eapMethodID); } return config; } // Retain for debugging purposes /* private static void xIterateCerts(KeyStore ks, X509Certificate caCert) throws GeneralSecurityException { Enumeration aliases = ks.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); Certificate cert = ks.getCertificate(alias); Log.d("HS2J", "Checking " + alias); if (cert instanceof X509Certificate) { X509Certificate x509Certificate = (X509Certificate) cert; boolean sm = x509Certificate.getSubjectX500Principal().equals( caCert.getSubjectX500Principal()); boolean eq = false; if (sm) { eq = Arrays.equals(x509Certificate.getEncoded(), caCert.getEncoded()); } Log.d("HS2J", "Subject: " + x509Certificate.getSubjectX500Principal() + ": " + sm + "/" + eq); } } } */ private static void setAnonymousIdentityToNaiRealm( WifiConfiguration config, Credential credential) { /** * Set WPA supplicant's anonymous identity field to a string containing the NAI realm, so * that this value will be sent to the EAP server as part of the EAP-Response/ Identity * packet. WPA supplicant will reset this field after using it for the EAP-Response/Identity * packet, and revert to using the (real) identity field for subsequent transactions that * request an identity (e.g. in EAP-TTLS). * * This NAI realm value (the portion of the identity after the '@') is used to tell the * AAA server which AAA/H to forward packets to. The hardcoded username, "anonymous", is a * placeholder that is not used--it is set to this value by convention. See Section 5.1 of * RFC3748 for more details. * * NOTE: we do not set this value for EAP-SIM/AKA/AKA', since the EAP server expects the * EAP-Response/Identity packet to contain an actual, IMSI-based identity, in order to * identify the device. */ config.enterpriseConfig.setAnonymousIdentity("anonymous@" + credential.getRealm()); } private static WifiConfiguration buildTTLSConfig(HomeSP homeSP, X509Certificate caCert) throws IOException { Credential credential = homeSP.getCredential(); if (credential.getUserName() == null || credential.getPassword() == null) { throw new IOException("EAP-TTLS provisioned without user name or password"); } EAPMethod eapMethod = credential.getEAPMethod(); AuthParam authParam = eapMethod.getAuthParam(); if (authParam == null || authParam.getAuthInfoID() != EAP.AuthInfoID.NonEAPInnerAuthType) { throw new IOException("Bad auth parameter for EAP-TTLS: " + authParam); } WifiConfiguration config = buildBaseConfiguration(homeSP); NonEAPInnerAuth ttlsParam = (NonEAPInnerAuth) authParam; WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig; enterpriseConfig.setPhase2Method(remapInnerMethod(ttlsParam.getType())); enterpriseConfig.setIdentity(credential.getUserName()); enterpriseConfig.setPassword(credential.getPassword()); enterpriseConfig.setCaCertificate(caCert); setAnonymousIdentityToNaiRealm(config, credential); return config; } private static WifiConfiguration buildTLSConfig(HomeSP homeSP, List clientChain, PrivateKey clientKey, X509Certificate caCert) throws IOException, GeneralSecurityException { Credential credential = homeSP.getCredential(); X509Certificate clientCertificate = null; if (clientKey == null || clientChain == null) { throw new IOException("No key and/or cert passed for EAP-TLS"); } if (credential.getCertType() != Credential.CertType.x509v3) { throw new IOException("Invalid certificate type for TLS: " + credential.getCertType()); } byte[] reference = credential.getFingerPrint(); MessageDigest digester = MessageDigest.getInstance("SHA-256"); for (X509Certificate certificate : clientChain) { digester.reset(); byte[] fingerprint = digester.digest(certificate.getEncoded()); if (Arrays.equals(reference, fingerprint)) { clientCertificate = certificate; break; } } if (clientCertificate == null) { throw new IOException("No certificate in chain matches supplied fingerprint"); } String alias = Base64.encodeToString(reference, Base64.DEFAULT); WifiConfiguration config = buildBaseConfiguration(homeSP); WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig; enterpriseConfig.setClientCertificateAlias(alias); enterpriseConfig.setClientKeyEntry(clientKey, clientCertificate); enterpriseConfig.setCaCertificate(caCert); setAnonymousIdentityToNaiRealm(config, credential); return config; } private static WifiConfiguration buildSIMConfig(HomeSP homeSP) throws IOException { Credential credential = homeSP.getCredential(); IMSIParameter credImsi = credential.getImsi(); /* * Uncomment to enforce strict IMSI matching with currently installed SIM cards. * TelephonyManager tm = TelephonyManager.from(context); SubscriptionManager sub = SubscriptionManager.from(context); boolean match = false; for (int subId : sub.getActiveSubscriptionIdList()) { String imsi = tm.getSubscriberId(subId); if (credImsi.matches(imsi)) { match = true; break; } } if (!match) { throw new IOException("Supplied IMSI does not match any SIM card"); } */ WifiConfiguration config = buildBaseConfiguration(homeSP); config.enterpriseConfig.setPlmn(credImsi.toString()); return config; } private static WifiConfiguration buildBaseConfiguration(HomeSP homeSP) throws IOException { EAP.EAPMethodID eapMethodID = homeSP.getCredential().getEAPMethod().getEAPMethodID(); WifiConfiguration config = new WifiConfiguration(); config.FQDN = homeSP.getFQDN(); HashSet roamingConsortiumIds = homeSP.getRoamingConsortiums(); config.roamingConsortiumIds = new long[roamingConsortiumIds.size()]; int i = 0; for (long id : roamingConsortiumIds) { config.roamingConsortiumIds[i] = id; i++; } config.providerFriendlyName = homeSP.getFriendlyName(); config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP); config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X); WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig(); enterpriseConfig.setEapMethod(remapEAPMethod(eapMethodID)); enterpriseConfig.setRealm(homeSP.getCredential().getRealm()); config.enterpriseConfig = enterpriseConfig; // The framework based config builder only ever builds r1 configs: config.updateIdentifier = null; return config; } private static int remapEAPMethod(EAP.EAPMethodID eapMethodID) throws IOException { switch (eapMethodID) { case EAP_TTLS: return WifiEnterpriseConfig.Eap.TTLS; case EAP_TLS: return WifiEnterpriseConfig.Eap.TLS; case EAP_SIM: return WifiEnterpriseConfig.Eap.SIM; case EAP_AKA: return WifiEnterpriseConfig.Eap.AKA; case EAP_AKAPrim: return WifiEnterpriseConfig.Eap.AKA_PRIME; default: throw new IOException("Bad EAP method: " + eapMethodID); } } private static int remapInnerMethod(NonEAPInnerAuth.NonEAPType type) throws IOException { switch (type) { case PAP: return WifiEnterpriseConfig.Phase2.PAP; case MSCHAP: return WifiEnterpriseConfig.Phase2.MSCHAP; case MSCHAPv2: return WifiEnterpriseConfig.Phase2.MSCHAPV2; case CHAP: default: throw new IOException("Inner method " + type + " not supported"); } } }