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