1package com.android.server.wifi.configparse;
2
3import android.content.Context;
4import android.net.Uri;
5import android.net.wifi.WifiConfiguration;
6import android.net.wifi.WifiEnterpriseConfig;
7import android.provider.DocumentsContract;
8import android.util.Base64;
9import android.util.Log;
10
11import com.android.server.wifi.IMSIParameter;
12import com.android.server.wifi.anqp.eap.AuthParam;
13import com.android.server.wifi.anqp.eap.EAP;
14import com.android.server.wifi.anqp.eap.EAPMethod;
15import com.android.server.wifi.anqp.eap.NonEAPInnerAuth;
16import com.android.server.wifi.hotspot2.omadm.PasspointManagementObjectManager;
17import com.android.server.wifi.hotspot2.pps.Credential;
18import com.android.server.wifi.hotspot2.pps.HomeSP;
19
20import org.xml.sax.SAXException;
21
22import java.io.ByteArrayInputStream;
23import java.io.IOException;
24import java.io.InputStreamReader;
25import java.io.LineNumberReader;
26import java.nio.charset.StandardCharsets;
27import java.security.GeneralSecurityException;
28import java.security.KeyStore;
29import java.security.MessageDigest;
30import java.security.PrivateKey;
31import java.security.cert.Certificate;
32import java.security.cert.CertificateFactory;
33import java.security.cert.X509Certificate;
34import java.util.ArrayList;
35import java.util.Arrays;
36import java.util.Enumeration;
37import java.util.HashSet;
38import java.util.List;
39
40public 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