1 /* 2 * Copyright (C) 2013 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 package android.net.wifi; 17 18 import android.os.Parcel; 19 import android.os.Parcelable; 20 import android.security.Credentials; 21 import android.text.TextUtils; 22 23 import java.io.ByteArrayInputStream; 24 import java.security.KeyFactory; 25 import java.security.NoSuchAlgorithmException; 26 import java.security.PrivateKey; 27 import java.security.cert.CertificateEncodingException; 28 import java.security.cert.CertificateException; 29 import java.security.cert.CertificateFactory; 30 import java.security.cert.X509Certificate; 31 import java.security.spec.InvalidKeySpecException; 32 import java.security.spec.PKCS8EncodedKeySpec; 33 import java.util.HashMap; 34 import java.util.Map; 35 36 /** 37 * Enterprise configuration details for Wi-Fi. Stores details about the EAP method 38 * and any associated credentials. 39 */ 40 public class WifiEnterpriseConfig implements Parcelable { 41 42 /** @hide */ 43 public static final String EMPTY_VALUE = "NULL"; 44 /** @hide */ 45 public static final String EAP_KEY = "eap"; 46 /** @hide */ 47 public static final String PHASE2_KEY = "phase2"; 48 /** @hide */ 49 public static final String IDENTITY_KEY = "identity"; 50 /** @hide */ 51 public static final String ANON_IDENTITY_KEY = "anonymous_identity"; 52 /** @hide */ 53 public static final String PASSWORD_KEY = "password"; 54 /** @hide */ 55 public static final String SUBJECT_MATCH_KEY = "subject_match"; 56 /** @hide */ 57 public static final String OPP_KEY_CACHING = "proactive_key_caching"; 58 /** 59 * String representing the keystore OpenSSL ENGINE's ID. 60 * @hide 61 */ 62 public static final String ENGINE_ID_KEYSTORE = "keystore"; 63 64 /** 65 * String representing the keystore URI used for wpa_supplicant. 66 * @hide 67 */ 68 public static final String KEYSTORE_URI = "keystore://"; 69 70 /** 71 * String to set the engine value to when it should be enabled. 72 * @hide 73 */ 74 public static final String ENGINE_ENABLE = "1"; 75 76 /** 77 * String to set the engine value to when it should be disabled. 78 * @hide 79 */ 80 public static final String ENGINE_DISABLE = "0"; 81 82 /** @hide */ 83 public static final String CA_CERT_PREFIX = KEYSTORE_URI + Credentials.CA_CERTIFICATE; 84 /** @hide */ 85 public static final String CLIENT_CERT_PREFIX = KEYSTORE_URI + Credentials.USER_CERTIFICATE; 86 /** @hide */ 87 public static final String CLIENT_CERT_KEY = "client_cert"; 88 /** @hide */ 89 public static final String CA_CERT_KEY = "ca_cert"; 90 /** @hide */ 91 public static final String ENGINE_KEY = "engine"; 92 /** @hide */ 93 public static final String ENGINE_ID_KEY = "engine_id"; 94 /** @hide */ 95 public static final String PRIVATE_KEY_ID_KEY = "key_id"; 96 97 private HashMap<String, String> mFields = new HashMap<String, String>(); 98 private X509Certificate mCaCert; 99 private PrivateKey mClientPrivateKey; 100 private X509Certificate mClientCertificate; 101 WifiEnterpriseConfig()102 public WifiEnterpriseConfig() { 103 // Do not set defaults so that the enterprise fields that are not changed 104 // by API are not changed underneath 105 // This is essential because an app may not have all fields like password 106 // available. It allows modification of subset of fields. 107 108 } 109 110 /** Copy constructor */ WifiEnterpriseConfig(WifiEnterpriseConfig source)111 public WifiEnterpriseConfig(WifiEnterpriseConfig source) { 112 for (String key : source.mFields.keySet()) { 113 mFields.put(key, source.mFields.get(key)); 114 } 115 } 116 117 @Override describeContents()118 public int describeContents() { 119 return 0; 120 } 121 122 @Override writeToParcel(Parcel dest, int flags)123 public void writeToParcel(Parcel dest, int flags) { 124 dest.writeInt(mFields.size()); 125 for (Map.Entry<String, String> entry : mFields.entrySet()) { 126 dest.writeString(entry.getKey()); 127 dest.writeString(entry.getValue()); 128 } 129 130 writeCertificate(dest, mCaCert); 131 132 if (mClientPrivateKey != null) { 133 String algorithm = mClientPrivateKey.getAlgorithm(); 134 byte[] userKeyBytes = mClientPrivateKey.getEncoded(); 135 dest.writeInt(userKeyBytes.length); 136 dest.writeByteArray(userKeyBytes); 137 dest.writeString(algorithm); 138 } else { 139 dest.writeInt(0); 140 } 141 142 writeCertificate(dest, mClientCertificate); 143 } 144 writeCertificate(Parcel dest, X509Certificate cert)145 private void writeCertificate(Parcel dest, X509Certificate cert) { 146 if (cert != null) { 147 try { 148 byte[] certBytes = cert.getEncoded(); 149 dest.writeInt(certBytes.length); 150 dest.writeByteArray(certBytes); 151 } catch (CertificateEncodingException e) { 152 dest.writeInt(0); 153 } 154 } else { 155 dest.writeInt(0); 156 } 157 } 158 159 public static final Creator<WifiEnterpriseConfig> CREATOR = 160 new Creator<WifiEnterpriseConfig>() { 161 public WifiEnterpriseConfig createFromParcel(Parcel in) { 162 WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig(); 163 int count = in.readInt(); 164 for (int i = 0; i < count; i++) { 165 String key = in.readString(); 166 String value = in.readString(); 167 enterpriseConfig.mFields.put(key, value); 168 } 169 170 enterpriseConfig.mCaCert = readCertificate(in); 171 172 PrivateKey userKey = null; 173 int len = in.readInt(); 174 if (len > 0) { 175 try { 176 byte[] bytes = new byte[len]; 177 in.readByteArray(bytes); 178 String algorithm = in.readString(); 179 KeyFactory keyFactory = KeyFactory.getInstance(algorithm); 180 userKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(bytes)); 181 } catch (NoSuchAlgorithmException e) { 182 userKey = null; 183 } catch (InvalidKeySpecException e) { 184 userKey = null; 185 } 186 } 187 188 enterpriseConfig.mClientPrivateKey = userKey; 189 enterpriseConfig.mClientCertificate = readCertificate(in); 190 return enterpriseConfig; 191 } 192 193 private X509Certificate readCertificate(Parcel in) { 194 X509Certificate cert = null; 195 int len = in.readInt(); 196 if (len > 0) { 197 try { 198 byte[] bytes = new byte[len]; 199 in.readByteArray(bytes); 200 CertificateFactory cFactory = CertificateFactory.getInstance("X.509"); 201 cert = (X509Certificate) cFactory 202 .generateCertificate(new ByteArrayInputStream(bytes)); 203 } catch (CertificateException e) { 204 cert = null; 205 } 206 } 207 return cert; 208 } 209 210 public WifiEnterpriseConfig[] newArray(int size) { 211 return new WifiEnterpriseConfig[size]; 212 } 213 }; 214 215 /** The Extensible Authentication Protocol method used */ 216 public static final class Eap { 217 /** No EAP method used. Represents an empty config */ 218 public static final int NONE = -1; 219 /** Protected EAP */ 220 public static final int PEAP = 0; 221 /** EAP-Transport Layer Security */ 222 public static final int TLS = 1; 223 /** EAP-Tunneled Transport Layer Security */ 224 public static final int TTLS = 2; 225 /** EAP-Password */ 226 public static final int PWD = 3; 227 /** EAP-Subscriber Identity Module */ 228 public static final int SIM = 4; 229 /** EAP-Authentication and Key Agreement */ 230 public static final int AKA = 5; 231 /** @hide */ 232 public static final String[] strings = { "PEAP", "TLS", "TTLS", "PWD", "SIM", "AKA" }; 233 234 /** Prevent initialization */ Eap()235 private Eap() {} 236 } 237 238 /** The inner authentication method used */ 239 public static final class Phase2 { 240 public static final int NONE = 0; 241 /** Password Authentication Protocol */ 242 public static final int PAP = 1; 243 /** Microsoft Challenge Handshake Authentication Protocol */ 244 public static final int MSCHAP = 2; 245 /** Microsoft Challenge Handshake Authentication Protocol v2 */ 246 public static final int MSCHAPV2 = 3; 247 /** Generic Token Card */ 248 public static final int GTC = 4; 249 private static final String PREFIX = "auth="; 250 /** @hide */ 251 public static final String[] strings = {EMPTY_VALUE, "PAP", "MSCHAP", 252 "MSCHAPV2", "GTC" }; 253 254 /** Prevent initialization */ Phase2()255 private Phase2() {} 256 } 257 258 /** Internal use only 259 * @hide 260 */ getFields()261 public HashMap<String, String> getFields() { 262 return mFields; 263 } 264 265 /** 266 * Set the EAP authentication method. 267 * @param eapMethod is one {@link Eap#PEAP}, {@link Eap#TLS}, {@link Eap#TTLS} or 268 * {@link Eap#PWD} 269 * @throws IllegalArgumentException on an invalid eap method 270 */ setEapMethod(int eapMethod)271 public void setEapMethod(int eapMethod) { 272 switch (eapMethod) { 273 /** Valid methods */ 274 case Eap.TLS: 275 setPhase2Method(Phase2.NONE); 276 /* fall through */ 277 case Eap.PEAP: 278 case Eap.PWD: 279 case Eap.TTLS: 280 case Eap.SIM: 281 case Eap.AKA: 282 mFields.put(EAP_KEY, Eap.strings[eapMethod]); 283 mFields.put(OPP_KEY_CACHING, "1"); 284 break; 285 default: 286 throw new IllegalArgumentException("Unknown EAP method"); 287 } 288 } 289 290 /** 291 * Get the eap method. 292 * @return eap method configured 293 */ getEapMethod()294 public int getEapMethod() { 295 String eapMethod = mFields.get(EAP_KEY); 296 return getStringIndex(Eap.strings, eapMethod, Eap.NONE); 297 } 298 299 /** 300 * Set Phase 2 authentication method. Sets the inner authentication method to be used in 301 * phase 2 after setting up a secure channel 302 * @param phase2Method is the inner authentication method and can be one of {@link Phase2#NONE}, 303 * {@link Phase2#PAP}, {@link Phase2#MSCHAP}, {@link Phase2#MSCHAPV2}, 304 * {@link Phase2#GTC} 305 * @throws IllegalArgumentException on an invalid phase2 method 306 * 307 */ setPhase2Method(int phase2Method)308 public void setPhase2Method(int phase2Method) { 309 switch (phase2Method) { 310 case Phase2.NONE: 311 mFields.put(PHASE2_KEY, EMPTY_VALUE); 312 break; 313 /** Valid methods */ 314 case Phase2.PAP: 315 case Phase2.MSCHAP: 316 case Phase2.MSCHAPV2: 317 case Phase2.GTC: 318 mFields.put(PHASE2_KEY, convertToQuotedString( 319 Phase2.PREFIX + Phase2.strings[phase2Method])); 320 break; 321 default: 322 throw new IllegalArgumentException("Unknown Phase 2 method"); 323 } 324 } 325 326 /** 327 * Get the phase 2 authentication method. 328 * @return a phase 2 method defined at {@link Phase2} 329 * */ getPhase2Method()330 public int getPhase2Method() { 331 String phase2Method = removeDoubleQuotes(mFields.get(PHASE2_KEY)); 332 // Remove auth= prefix 333 if (phase2Method.startsWith(Phase2.PREFIX)) { 334 phase2Method = phase2Method.substring(Phase2.PREFIX.length()); 335 } 336 return getStringIndex(Phase2.strings, phase2Method, Phase2.NONE); 337 } 338 339 /** 340 * Set the identity 341 * @param identity 342 */ setIdentity(String identity)343 public void setIdentity(String identity) { 344 setFieldValue(IDENTITY_KEY, identity, ""); 345 } 346 347 /** 348 * Get the identity 349 * @return the identity 350 */ getIdentity()351 public String getIdentity() { 352 return getFieldValue(IDENTITY_KEY, ""); 353 } 354 355 /** 356 * Set anonymous identity. This is used as the unencrypted identity with 357 * certain EAP types 358 * @param anonymousIdentity the anonymous identity 359 */ setAnonymousIdentity(String anonymousIdentity)360 public void setAnonymousIdentity(String anonymousIdentity) { 361 setFieldValue(ANON_IDENTITY_KEY, anonymousIdentity, ""); 362 } 363 364 /** Get the anonymous identity 365 * @return anonymous identity 366 */ getAnonymousIdentity()367 public String getAnonymousIdentity() { 368 return getFieldValue(ANON_IDENTITY_KEY, ""); 369 } 370 371 /** 372 * Set the password. 373 * @param password the password 374 */ setPassword(String password)375 public void setPassword(String password) { 376 setFieldValue(PASSWORD_KEY, password, ""); 377 } 378 379 /** 380 * Get the password. 381 * 382 * Returns locally set password value. For networks fetched from 383 * framework, returns "*". 384 */ getPassword()385 public String getPassword() { 386 return getFieldValue(PASSWORD_KEY, ""); 387 } 388 389 /** 390 * Set CA certificate alias. 391 * 392 * <p> See the {@link android.security.KeyChain} for details on installing or choosing 393 * a certificate 394 * </p> 395 * @param alias identifies the certificate 396 * @hide 397 */ setCaCertificateAlias(String alias)398 public void setCaCertificateAlias(String alias) { 399 setFieldValue(CA_CERT_KEY, alias, CA_CERT_PREFIX); 400 } 401 402 /** 403 * Get CA certificate alias 404 * @return alias to the CA certificate 405 * @hide 406 */ getCaCertificateAlias()407 public String getCaCertificateAlias() { 408 return getFieldValue(CA_CERT_KEY, CA_CERT_PREFIX); 409 } 410 411 /** 412 * Specify a X.509 certificate that identifies the server. 413 * 414 * <p>A default name is automatically assigned to the certificate and used 415 * with this configuration. The framework takes care of installing the 416 * certificate when the config is saved and removing the certificate when 417 * the config is removed. 418 * 419 * @param cert X.509 CA certificate 420 * @throws IllegalArgumentException if not a CA certificate 421 */ setCaCertificate(X509Certificate cert)422 public void setCaCertificate(X509Certificate cert) { 423 if (cert != null) { 424 if (cert.getBasicConstraints() >= 0) { 425 mCaCert = cert; 426 } else { 427 throw new IllegalArgumentException("Not a CA certificate"); 428 } 429 } else { 430 mCaCert = null; 431 } 432 } 433 434 /** 435 * Get CA certificate 436 * @return X.509 CA certificate 437 */ getCaCertificate()438 public X509Certificate getCaCertificate() { 439 return mCaCert; 440 } 441 442 /** 443 * @hide 444 */ resetCaCertificate()445 public void resetCaCertificate() { 446 mCaCert = null; 447 } 448 449 /** Set Client certificate alias. 450 * 451 * <p> See the {@link android.security.KeyChain} for details on installing or choosing 452 * a certificate 453 * </p> 454 * @param alias identifies the certificate 455 * @hide 456 */ setClientCertificateAlias(String alias)457 public void setClientCertificateAlias(String alias) { 458 setFieldValue(CLIENT_CERT_KEY, alias, CLIENT_CERT_PREFIX); 459 setFieldValue(PRIVATE_KEY_ID_KEY, alias, Credentials.USER_PRIVATE_KEY); 460 // Also, set engine parameters 461 if (TextUtils.isEmpty(alias)) { 462 mFields.put(ENGINE_KEY, ENGINE_DISABLE); 463 mFields.put(ENGINE_ID_KEY, EMPTY_VALUE); 464 } else { 465 mFields.put(ENGINE_KEY, ENGINE_ENABLE); 466 mFields.put(ENGINE_ID_KEY, convertToQuotedString(ENGINE_ID_KEYSTORE)); 467 } 468 } 469 470 /** 471 * Get client certificate alias 472 * @return alias to the client certificate 473 * @hide 474 */ getClientCertificateAlias()475 public String getClientCertificateAlias() { 476 return getFieldValue(CLIENT_CERT_KEY, CLIENT_CERT_PREFIX); 477 } 478 479 /** 480 * Specify a private key and client certificate for client authorization. 481 * 482 * <p>A default name is automatically assigned to the key entry and used 483 * with this configuration. The framework takes care of installing the 484 * key entry when the config is saved and removing the key entry when 485 * the config is removed. 486 487 * @param privateKey 488 * @param clientCertificate 489 * @throws IllegalArgumentException for an invalid key or certificate. 490 */ setClientKeyEntry(PrivateKey privateKey, X509Certificate clientCertificate)491 public void setClientKeyEntry(PrivateKey privateKey, X509Certificate clientCertificate) { 492 if (clientCertificate != null) { 493 if (clientCertificate.getBasicConstraints() != -1) { 494 throw new IllegalArgumentException("Cannot be a CA certificate"); 495 } 496 if (privateKey == null) { 497 throw new IllegalArgumentException("Client cert without a private key"); 498 } 499 if (privateKey.getEncoded() == null) { 500 throw new IllegalArgumentException("Private key cannot be encoded"); 501 } 502 } 503 504 mClientPrivateKey = privateKey; 505 mClientCertificate = clientCertificate; 506 } 507 508 /** 509 * Get client certificate 510 * 511 * @return X.509 client certificate 512 */ getClientCertificate()513 public X509Certificate getClientCertificate() { 514 return mClientCertificate; 515 } 516 517 /** 518 * @hide 519 */ resetClientKeyEntry()520 public void resetClientKeyEntry() { 521 mClientPrivateKey = null; 522 mClientCertificate = null; 523 } 524 525 /** 526 * @hide 527 */ getClientPrivateKey()528 public PrivateKey getClientPrivateKey() { 529 return mClientPrivateKey; 530 } 531 532 /** 533 * Set subject match. This is the substring to be matched against the subject of the 534 * authentication server certificate. 535 * @param subjectMatch substring to be matched 536 */ setSubjectMatch(String subjectMatch)537 public void setSubjectMatch(String subjectMatch) { 538 setFieldValue(SUBJECT_MATCH_KEY, subjectMatch, ""); 539 } 540 541 /** 542 * Get subject match 543 * @return the subject match string 544 */ getSubjectMatch()545 public String getSubjectMatch() { 546 return getFieldValue(SUBJECT_MATCH_KEY, ""); 547 } 548 549 /** See {@link WifiConfiguration#getKeyIdForCredentials} @hide */ getKeyId(WifiEnterpriseConfig current)550 String getKeyId(WifiEnterpriseConfig current) { 551 String eap = mFields.get(EAP_KEY); 552 String phase2 = mFields.get(PHASE2_KEY); 553 554 // If either eap or phase2 are not initialized, use current config details 555 if (TextUtils.isEmpty((eap))) { 556 eap = current.mFields.get(EAP_KEY); 557 } 558 if (TextUtils.isEmpty(phase2)) { 559 phase2 = current.mFields.get(PHASE2_KEY); 560 } 561 return eap + "_" + phase2; 562 } 563 removeDoubleQuotes(String string)564 private String removeDoubleQuotes(String string) { 565 if (TextUtils.isEmpty(string)) return ""; 566 int length = string.length(); 567 if ((length > 1) && (string.charAt(0) == '"') 568 && (string.charAt(length - 1) == '"')) { 569 return string.substring(1, length - 1); 570 } 571 return string; 572 } 573 convertToQuotedString(String string)574 private String convertToQuotedString(String string) { 575 return "\"" + string + "\""; 576 } 577 578 /** Returns the index at which the toBeFound string is found in the array. 579 * @param arr array of strings 580 * @param toBeFound string to be found 581 * @param defaultIndex default index to be returned when string is not found 582 * @return the index into array 583 */ getStringIndex(String arr[], String toBeFound, int defaultIndex)584 private int getStringIndex(String arr[], String toBeFound, int defaultIndex) { 585 if (TextUtils.isEmpty(toBeFound)) return defaultIndex; 586 for (int i = 0; i < arr.length; i++) { 587 if (toBeFound.equals(arr[i])) return i; 588 } 589 return defaultIndex; 590 } 591 592 /** Returns the field value for the key. 593 * @param key into the hash 594 * @param prefix is the prefix that the value may have 595 * @return value 596 * @hide 597 */ getFieldValue(String key, String prefix)598 public String getFieldValue(String key, String prefix) { 599 String value = mFields.get(key); 600 // Uninitialized or known to be empty after reading from supplicant 601 if (TextUtils.isEmpty(value) || EMPTY_VALUE.equals(value)) return ""; 602 603 value = removeDoubleQuotes(value); 604 if (value.startsWith(prefix)) { 605 return value.substring(prefix.length()); 606 } else { 607 return value; 608 } 609 } 610 611 /** Set a value with an optional prefix at key 612 * @param key into the hash 613 * @param value to be set 614 * @param prefix an optional value to be prefixed to actual value 615 * @hide 616 */ setFieldValue(String key, String value, String prefix)617 public void setFieldValue(String key, String value, String prefix) { 618 if (TextUtils.isEmpty(value)) { 619 mFields.put(key, EMPTY_VALUE); 620 } else { 621 mFields.put(key, convertToQuotedString(prefix + value)); 622 } 623 } 624 625 626 /** Set a value with an optional prefix at key 627 * @param key into the hash 628 * @param value to be set 629 * @param prefix an optional value to be prefixed to actual value 630 * @hide 631 */ setFieldValue(String key, String value)632 public void setFieldValue(String key, String value) { 633 if (TextUtils.isEmpty(value)) { 634 mFields.put(key, EMPTY_VALUE); 635 } else { 636 mFields.put(key, convertToQuotedString(value)); 637 } 638 } 639 640 @Override toString()641 public String toString() { 642 StringBuffer sb = new StringBuffer(); 643 for (String key : mFields.keySet()) { 644 sb.append(key).append(" ").append(mFields.get(key)).append("\n"); 645 } 646 return sb.toString(); 647 } 648 } 649