1 /*
2  * Copyright 2016, The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.managedprovisioning.task.wifi;
18 
19 import static android.net.ProxyInfo.buildDirectProxy;
20 import static android.net.ProxyInfo.buildPacProxy;
21 
22 import android.annotation.Nullable;
23 import android.net.IpConfiguration;
24 import android.net.IpConfiguration.ProxySettings;
25 import android.net.Uri;
26 import android.net.wifi.WifiConfiguration;
27 import android.net.wifi.WifiEnterpriseConfig;
28 import android.text.TextUtils;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.managedprovisioning.common.ProvisionLogger;
32 import com.android.managedprovisioning.model.WifiInfo;
33 import com.android.net.module.util.ProxyUtils;
34 
35 import java.io.ByteArrayInputStream;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.nio.charset.StandardCharsets;
39 import java.security.Key;
40 import java.security.KeyStore;
41 import java.security.KeyStoreException;
42 import java.security.NoSuchAlgorithmException;
43 import java.security.PrivateKey;
44 import java.security.UnrecoverableKeyException;
45 import java.security.cert.Certificate;
46 import java.security.cert.CertificateException;
47 import java.security.cert.CertificateFactory;
48 import java.security.cert.X509Certificate;
49 import java.util.Arrays;
50 import java.util.Base64;
51 import java.util.Collections;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.Map;
55 
56 /**
57  * Utility class for configuring a new {@link WifiConfiguration} object from the provisioning
58  * parameters represented via {@link WifiInfo}.
59  */
60 public class WifiConfigurationProvider {
61 
62     @VisibleForTesting
63     static final String WPA = "WPA";
64     @VisibleForTesting
65     static final String WEP = "WEP";
66     @VisibleForTesting
67     static final String EAP = "EAP";
68     @VisibleForTesting
69     static final String NONE = "NONE";
70     @VisibleForTesting
71     static final char[] PASSWORD = {};
72     public static final String KEYSTORE_TYPE_PKCS12 = "PKCS12";
73     private static Map<String, Integer> EAP_METHODS = buildEapMethodsMap();
74     private static Map<String, Integer> PHASE2_AUTH = buildPhase2AuthMap();
75 
buildEapMethodsMap()76     private static Map<String, Integer> buildEapMethodsMap() {
77         Map<String, Integer> map = new HashMap<>();
78         map.put("PEAP", WifiEnterpriseConfig.Eap.PEAP);
79         map.put("TLS", WifiEnterpriseConfig.Eap.TLS);
80         map.put("TTLS", WifiEnterpriseConfig.Eap.TTLS);
81         map.put("PWD", WifiEnterpriseConfig.Eap.PWD);
82         map.put("SIM", WifiEnterpriseConfig.Eap.SIM);
83         map.put("AKA", WifiEnterpriseConfig.Eap.AKA);
84         map.put("AKA_PRIME", WifiEnterpriseConfig.Eap.AKA_PRIME);
85         return map;
86     }
87 
buildPhase2AuthMap()88     private static Map<String, Integer> buildPhase2AuthMap() {
89         Map<String, Integer> map = new HashMap<>();
90         map.put(null, WifiEnterpriseConfig.Phase2.NONE);
91         map.put("", WifiEnterpriseConfig.Phase2.NONE);
92         map.put("NONE", WifiEnterpriseConfig.Phase2.NONE);
93         map.put("PAP", WifiEnterpriseConfig.Phase2.PAP);
94         map.put("MSCHAP", WifiEnterpriseConfig.Phase2.MSCHAP);
95         map.put("MSCHAPV2", WifiEnterpriseConfig.Phase2.MSCHAPV2);
96         map.put("GTC", WifiEnterpriseConfig.Phase2.GTC);
97         map.put("SIM", WifiEnterpriseConfig.Phase2.SIM);
98         map.put("AKA", WifiEnterpriseConfig.Phase2.AKA);
99         map.put("AKA_PRIME", WifiEnterpriseConfig.Phase2.AKA_PRIME);
100         return map;
101     }
102 
103     /**
104      * Create a {@link WifiConfiguration} object from the internal representation given via
105      * {@link WifiInfo}.
106      */
generateWifiConfiguration(WifiInfo wifiInfo)107     public WifiConfiguration generateWifiConfiguration(WifiInfo wifiInfo) {
108         WifiConfiguration wifiConf = new WifiConfiguration();
109         wifiConf.SSID = wifiInfo.ssid;
110         wifiConf.status = WifiConfiguration.Status.ENABLED;
111         wifiConf.hiddenSSID = wifiInfo.hidden;
112         String securityType = wifiInfo.securityType != null ? wifiInfo.securityType : NONE;
113         switch (securityType) {
114             case WPA:
115                 updateForWPAConfiguration(wifiConf, wifiInfo.password);
116                 break;
117             case WEP:
118                 updateForWEPConfiguration(wifiConf, wifiInfo.password);
119                 break;
120             case EAP:
121                 maybeUpdateForEAPConfiguration(wifiConf, wifiInfo);
122                 break;
123             default: // NONE
124                 wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
125                 wifiConf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
126                 break;
127         }
128 
129         updateForProxy(
130                 wifiConf,
131                 wifiInfo.proxyHost,
132                 wifiInfo.proxyPort,
133                 wifiInfo.proxyBypassHosts,
134                 wifiInfo.pacUrl);
135         return wifiConf;
136     }
137 
maybeUpdateForEAPConfiguration(WifiConfiguration wifiConf, WifiInfo wifiInfo)138     private void maybeUpdateForEAPConfiguration(WifiConfiguration wifiConf, WifiInfo wifiInfo) {
139         try {
140             maybeUpdateForEAPConfigurationOrThrow(wifiConf, wifiInfo);
141         } catch (IOException | CertificateException | NoSuchAlgorithmException
142                 | UnrecoverableKeyException | KeyStoreException e) {
143             ProvisionLogger.loge("Error while reading certificate", e);
144         }
145     }
146 
maybeUpdateForEAPConfigurationOrThrow( WifiConfiguration wifiConf, WifiInfo wifiInfo)147     private void maybeUpdateForEAPConfigurationOrThrow(
148             WifiConfiguration wifiConf, WifiInfo wifiInfo)
149             throws CertificateException, UnrecoverableKeyException, NoSuchAlgorithmException,
150             KeyStoreException, IOException {
151         if (!isEAPWifiInfoValid(wifiInfo.eapMethod)) {
152             ProvisionLogger.loge("Unknown EAP method: " + wifiInfo.eapMethod);
153             return;
154         }
155         if (!isPhase2AuthWifiInfoValid(wifiInfo.phase2Auth)) {
156             ProvisionLogger.loge(
157                     "Unknown phase 2 authentication method: " + wifiInfo.phase2Auth);
158             return;
159         }
160         wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
161         wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
162         WifiEnterpriseConfig wifiEnterpriseConfig = new WifiEnterpriseConfig();
163         updateWifiEnterpriseConfigFromWifiInfo(wifiEnterpriseConfig, wifiInfo);
164         maybeUpdateClientKeyForEAPConfiguration(wifiEnterpriseConfig, wifiInfo.userCertificate);
165         wifiConf.enterpriseConfig = wifiEnterpriseConfig;
166     }
167 
updateWifiEnterpriseConfigFromWifiInfo( WifiEnterpriseConfig wifiEnterpriseConfig, WifiInfo wifiInfo)168     private void updateWifiEnterpriseConfigFromWifiInfo(
169             WifiEnterpriseConfig wifiEnterpriseConfig, WifiInfo wifiInfo)
170             throws CertificateException, IOException {
171         wifiEnterpriseConfig.setEapMethod(getEAPMethodFromString(wifiInfo.eapMethod));
172         wifiEnterpriseConfig.setPhase2Method(getPhase2AuthFromString(wifiInfo.phase2Auth));
173         wifiEnterpriseConfig.setPassword(wifiInfo.password);
174         wifiEnterpriseConfig.setIdentity(wifiInfo.identity);
175         wifiEnterpriseConfig.setAnonymousIdentity(wifiInfo.anonymousIdentity);
176         wifiEnterpriseConfig.setDomainSuffixMatch(wifiInfo.domain);
177         if (!TextUtils.isEmpty(wifiInfo.caCertificate)) {
178             wifiEnterpriseConfig.setCaCertificate(buildCACertificate(wifiInfo.caCertificate));
179         }
180     }
181 
182     /**
183      * Updates client key information in EAP configuration if the key and certificate from {@code
184      * userCertificate} passes {@link #isKeyValidType(Key)} and {@link
185      * #isCertificateChainValidType(Certificate[])}.
186      */
maybeUpdateClientKeyForEAPConfiguration(WifiEnterpriseConfig wifiEnterpriseConfig, String userCertificate)187     private void maybeUpdateClientKeyForEAPConfiguration(WifiEnterpriseConfig wifiEnterpriseConfig,
188             String userCertificate)
189             throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException,
190             UnrecoverableKeyException {
191         if (TextUtils.isEmpty(userCertificate)) {
192             return;
193         }
194         KeyStore keyStore = loadKeystoreFromCertificate(userCertificate);
195         String alias = findAliasFromKeystore(keyStore);
196         if (TextUtils.isEmpty(alias) || !keyStore.isKeyEntry(alias)) {
197             return;
198         }
199         Key key = keyStore.getKey(alias, PASSWORD);
200         if (key == null) {
201             return;
202         }
203         if (!isKeyValidType(key)) {
204             ProvisionLogger.loge(
205                     "Key in user certificate must be non-null and PrivateKey type");
206             return;
207         }
208         Certificate[] certificates = keyStore.getCertificateChain(alias);
209         if (certificates == null) {
210             return;
211         }
212         if (!isCertificateChainValidType(certificates)) {
213             ProvisionLogger.loge(
214                     "All certificates in chain in user certificate must be non-null "
215                             + "X509Certificate type");
216             return;
217         }
218         wifiEnterpriseConfig.setClientKeyEntryWithCertificateChain(
219                 (PrivateKey) key, castX509Certificates(certificates));
220     }
221 
isCertificateChainValidType(Certificate[] certificates)222     private boolean isCertificateChainValidType(Certificate[] certificates) {
223         return !Arrays.stream(certificates).anyMatch(c -> !(c instanceof X509Certificate));
224     }
225 
isKeyValidType(Key key)226     private boolean isKeyValidType(Key key) {
227         return key instanceof PrivateKey;
228     }
229 
isPhase2AuthWifiInfoValid(String phase2Auth)230     private boolean isPhase2AuthWifiInfoValid(String phase2Auth) {
231         return PHASE2_AUTH.containsKey(phase2Auth);
232     }
233 
isEAPWifiInfoValid(String eapMethod)234     private boolean isEAPWifiInfoValid(String eapMethod) {
235         return EAP_METHODS.containsKey(eapMethod);
236     }
237 
updateForWPAConfiguration(WifiConfiguration wifiConf, String wifiPassword)238     private void updateForWPAConfiguration(WifiConfiguration wifiConf, String wifiPassword) {
239         wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
240         wifiConf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
241         wifiConf.allowedProtocols.set(WifiConfiguration.Protocol.WPA); // For WPA
242         wifiConf.allowedProtocols.set(WifiConfiguration.Protocol.RSN); // For WPA2
243         wifiConf.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
244         wifiConf.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
245         wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
246         wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
247         if (!TextUtils.isEmpty(wifiPassword)) {
248             wifiConf.preSharedKey = "\"" + wifiPassword + "\"";
249         }
250     }
251 
updateForWEPConfiguration(WifiConfiguration wifiConf, String password)252     private void updateForWEPConfiguration(WifiConfiguration wifiConf, String password) {
253         wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
254         wifiConf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
255         wifiConf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
256         wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
257         wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
258         wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
259         wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
260         int length = password.length();
261         if ((length == 10 || length == 26 || length == 58) && password.matches("[0-9A-Fa-f]*")) {
262             wifiConf.wepKeys[0] = password;
263         } else {
264             wifiConf.wepKeys[0] = '"' + password + '"';
265         }
266         wifiConf.wepTxKeyIndex = 0;
267     }
268 
269     /**
270      * Keystore must not contain more then one alias.
271      */
272     @Nullable
findAliasFromKeystore(KeyStore keyStore)273     private static String findAliasFromKeystore(KeyStore keyStore)
274             throws KeyStoreException, CertificateException {
275         List<String> aliases = Collections.list(keyStore.aliases());
276         if (aliases.isEmpty()) {
277             return null;
278         }
279         if (aliases.size() != 1) {
280             throw new CertificateException(
281                     "Configuration must contain only one certificate");
282         }
283         return aliases.get(0);
284     }
285 
loadKeystoreFromCertificate(String userCertificate)286     private static KeyStore loadKeystoreFromCertificate(String userCertificate)
287             throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
288         KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE_PKCS12);
289         try (InputStream inputStream = new ByteArrayInputStream(
290                 Base64.getDecoder().decode(userCertificate
291                         .getBytes(StandardCharsets.UTF_8)))) {
292             keyStore.load(inputStream, PASSWORD);
293         }
294         return keyStore;
295     }
296 
297     /**
298      * Casts the given certificate chain to a chain of {@link X509Certificate} objects. Assumes the
299      * given certificate chain passes {@link #isCertificateChainValidType(Certificate[])}.
300      */
castX509Certificates(Certificate[] certificateChain)301     private static X509Certificate[] castX509Certificates(Certificate[] certificateChain) {
302         return Arrays.stream(certificateChain)
303                 .map(certificate -> (X509Certificate) certificate)
304                 .toArray(X509Certificate[]::new);
305     }
306 
307     /**
308      * @param caCertificate String representation of CA certificate in the format described at
309      * {@link android.app.admin.DevicePolicyManager#EXTRA_PROVISIONING_WIFI_CA_CERTIFICATE}.
310      */
buildCACertificate(String caCertificate)311     private X509Certificate buildCACertificate(String caCertificate)
312             throws CertificateException, IOException {
313         CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
314         try (InputStream inputStream = new ByteArrayInputStream(Base64.getDecoder()
315                 .decode(caCertificate.getBytes(StandardCharsets.UTF_8)))) {
316         X509Certificate caCertificateX509 = (X509Certificate) certificateFactory
317                 .generateCertificate(inputStream);
318             return caCertificateX509;
319         }
320     }
321 
updateForProxy(WifiConfiguration wifiConf, String proxyHost, int proxyPort, String proxyBypassHosts, String pacUrl)322     private void updateForProxy(WifiConfiguration wifiConf, String proxyHost, int proxyPort,
323             String proxyBypassHosts, String pacUrl) {
324         if (TextUtils.isEmpty(proxyHost) && TextUtils.isEmpty(pacUrl)) {
325             return;
326         }
327         IpConfiguration ipConfig = wifiConf.getIpConfiguration();
328         if (!TextUtils.isEmpty(proxyHost)) {
329             ipConfig.setProxySettings(ProxySettings.STATIC);
330             ipConfig.setHttpProxy(buildDirectProxy(proxyHost, proxyPort,
331                     ProxyUtils.exclusionStringAsList(proxyBypassHosts)));
332         } else {
333             ipConfig.setProxySettings(ProxySettings.PAC);
334             ipConfig.setHttpProxy(buildPacProxy(Uri.parse(pacUrl)));
335         }
336         wifiConf.setIpConfiguration(ipConfig);
337     }
338 
getEAPMethodFromString(String eapMethod)339     private int getEAPMethodFromString(String eapMethod) {
340         if (EAP_METHODS.containsKey(eapMethod)) {
341             return EAP_METHODS.get(eapMethod);
342         }
343         throw new IllegalArgumentException("Unknown EAP method: " + eapMethod);
344     }
345 
getPhase2AuthFromString(String phase2Auth)346     private int getPhase2AuthFromString(String phase2Auth) {
347         if (PHASE2_AUTH.containsKey(phase2Auth)) {
348             return PHASE2_AUTH.get(phase2Auth);
349         }
350         throw new IllegalArgumentException("Unknown Phase 2 authentication method: " + phase2Auth);
351     }
352 }
353