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