Home | History | Annotate | Download | only in configparse
      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