From b815edf3aba63c2cd46f3ceb243ed13192102940 Mon Sep 17 00:00:00 2001 From: Irfan Sheriff Date: Tue, 5 Feb 2013 09:44:12 -0800 Subject: Track keys per config and allow cert push from apps Allow configuring keys for a configuration and update/delete from wificonfigstore. Change-Id: I79b43bf7ca58f7efc95f7dcc125fc84d7aa8c795 --- wifi/java/android/net/wifi/WifiConfigStore.java | 73 ++++-- wifi/java/android/net/wifi/WifiConfiguration.java | 48 +++- .../android/net/wifi/WifiEnterpriseConfig.java | 252 ++++++++++++++++++++- 3 files changed, 342 insertions(+), 31 deletions(-) (limited to 'wifi/java/android') diff --git a/wifi/java/android/net/wifi/WifiConfigStore.java b/wifi/java/android/net/wifi/WifiConfigStore.java index 9deacde..f119a4b 100644 --- a/wifi/java/android/net/wifi/WifiConfigStore.java +++ b/wifi/java/android/net/wifi/WifiConfigStore.java @@ -36,6 +36,7 @@ import android.os.Message; import android.os.Handler; import android.os.HandlerThread; import android.os.UserHandle; +import android.security.KeyStore; import android.text.TextUtils; import android.util.Log; @@ -143,6 +144,7 @@ class WifiConfigStore { private static final String EOS = "eos"; private WifiNative mWifiNative; + private final KeyStore mKeyStore = KeyStore.getInstance(); WifiConfigStore(Context c, WifiNative wn) { mContext = c; @@ -294,16 +296,7 @@ class WifiConfigStore { boolean forgetNetwork(int netId) { if (mWifiNative.removeNetwork(netId)) { mWifiNative.saveConfig(); - WifiConfiguration target = null; - WifiConfiguration config = mConfiguredNetworks.get(netId); - if (config != null) { - target = mConfiguredNetworks.remove(netId); - mNetworkIds.remove(configKey(config)); - } - if (target != null) { - writeIpAndProxyConfigurations(); - sendConfiguredNetworksChangedBroadcast(target, WifiManager.CHANGE_REASON_REMOVED); - } + removeConfigAndSendBroadcastIfNeeded(netId); return true; } else { loge("Failed to remove network " + netId); @@ -341,18 +334,25 @@ class WifiConfigStore { */ boolean removeNetwork(int netId) { boolean ret = mWifiNative.removeNetwork(netId); - WifiConfiguration config = null; if (ret) { - config = mConfiguredNetworks.get(netId); - if (config != null) { - config = mConfiguredNetworks.remove(netId); - mNetworkIds.remove(configKey(config)); - } + removeConfigAndSendBroadcastIfNeeded(netId); } + return ret; + } + + private void removeConfigAndSendBroadcastIfNeeded(int netId) { + WifiConfiguration config = mConfiguredNetworks.get(netId); if (config != null) { + // Remove any associated keys + if (config.enterpriseConfig != null) { + config.enterpriseConfig.removeKeys(mKeyStore); + } + mConfiguredNetworks.remove(netId); + mNetworkIds.remove(configKey(config)); + + writeIpAndProxyConfigurations(); sendConfiguredNetworksChangedBroadcast(config, WifiManager.CHANGE_REASON_REMOVED); } - return ret; } /** @@ -1122,13 +1122,48 @@ class WifiConfigStore { } if (config.enterpriseConfig != null) { - HashMap enterpriseFields = config.enterpriseConfig.getFields(); + + WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig; + + if (enterpriseConfig.needsKeyStore()) { + /** + * Keyguard settings may eventually be controlled by device policy. + * We check here if keystore is unlocked before installing + * credentials. + * TODO: Figure a way to store these credentials for wifi alone + * TODO: Do we need a dialog here ? + */ + if (mKeyStore.state() != KeyStore.State.UNLOCKED) { + loge(config.SSID + ": key store is locked"); + break setVariables; + } + + try { + /* config passed may include only fields being updated. + * In order to generate the key id, fetch uninitialized + * fields from the currently tracked configuration + */ + WifiConfiguration currentConfig = mConfiguredNetworks.get(netId); + String keyId = config.getKeyIdForCredentials(currentConfig); + + if (!enterpriseConfig.installKeys(mKeyStore, keyId)) { + loge(config.SSID + ": failed to install keys"); + break setVariables; + } + } catch (IllegalStateException e) { + loge(config.SSID + " invalid config for key installation"); + break setVariables; + } + } + + HashMap enterpriseFields = enterpriseConfig.getFields(); for (String key : enterpriseFields.keySet()) { String value = enterpriseFields.get(key); if (!mWifiNative.setNetworkVariable( netId, key, value)) { + enterpriseConfig.removeKeys(mKeyStore); loge(config.SSID + ": failed to set " + key + ": " + value); break setVariables; @@ -1136,7 +1171,7 @@ class WifiConfigStore { } } updateFailed = false; - } + } //end of setVariables if (updateFailed) { if (newNetwork) { diff --git a/wifi/java/android/net/wifi/WifiConfiguration.java b/wifi/java/android/net/wifi/WifiConfiguration.java index 552356c..bf82792 100644 --- a/wifi/java/android/net/wifi/WifiConfiguration.java +++ b/wifi/java/android/net/wifi/WifiConfiguration.java @@ -19,6 +19,7 @@ package android.net.wifi; import android.net.LinkProperties; import android.os.Parcelable; import android.os.Parcel; +import android.text.TextUtils; import java.util.BitSet; @@ -274,7 +275,8 @@ public class WifiConfiguration implements Parcelable { */ public BitSet allowedGroupCiphers; /** - * The enterprise configuration details + * The enterprise configuration details specifying the EAP method, + * certificates and other settings associated with the EAP. * @hide */ public WifiEnterpriseConfig enterpriseConfig; @@ -462,6 +464,47 @@ public class WifiConfiguration implements Parcelable { return SSID; } + /** + * Get an identifier for associating credentials with this config + * @param current configuration contains values for additional fields + * that are not part of this configuration. Used + * when a config with some fields is passed by an application. + * @throws IllegalStateException if config is invalid for key id generation + * @hide + */ + String getKeyIdForCredentials(WifiConfiguration current) { + String keyMgmt = null; + + try { + // Get current config details for fields that are not initialized + if (TextUtils.isEmpty(SSID)) SSID = current.SSID; + if (allowedKeyManagement.cardinality() == 0) { + allowedKeyManagement = current.allowedKeyManagement; + } + if (allowedKeyManagement.get(KeyMgmt.WPA_EAP)) { + keyMgmt = KeyMgmt.strings[KeyMgmt.WPA_EAP]; + } + if (allowedKeyManagement.get(KeyMgmt.IEEE8021X)) { + keyMgmt += KeyMgmt.strings[KeyMgmt.IEEE8021X]; + } + + if (TextUtils.isEmpty(keyMgmt)) { + throw new IllegalStateException("Not an EAP network"); + } + + return trimStringForKeyId(SSID) + "_" + keyMgmt + "_" + + trimStringForKeyId(enterpriseConfig.getKeyId(current != null ? + current.enterpriseConfig : null)); + } catch (NullPointerException e) { + throw new IllegalStateException("Invalid config details"); + } + } + + private String trimStringForKeyId(String string) { + // Remove quotes and spaces + return string.replace("\"", "").replace(" ", ""); + } + private static BitSet readBitSet(Parcel src) { int cardinality = src.readInt(); @@ -485,6 +528,9 @@ public class WifiConfiguration implements Parcelable { /** @hide */ public int getAuthType() { + if (allowedKeyManagement.cardinality() > 1) { + throw new IllegalStateException("More than one auth type set"); + } if (allowedKeyManagement.get(KeyMgmt.WPA_PSK)) { return KeyMgmt.WPA_PSK; } else if (allowedKeyManagement.get(KeyMgmt.WPA2_PSK)) { diff --git a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java index 4dca7ac..7313e7e 100644 --- a/wifi/java/android/net/wifi/WifiEnterpriseConfig.java +++ b/wifi/java/android/net/wifi/WifiEnterpriseConfig.java @@ -19,7 +19,26 @@ import android.os.Parcel; import android.os.Parcelable; import android.security.Credentials; import android.text.TextUtils; - +import android.util.Log; + +import com.android.org.bouncycastle.asn1.ASN1InputStream; +import com.android.org.bouncycastle.asn1.ASN1Sequence; +import com.android.org.bouncycastle.asn1.DEROctetString; +import com.android.org.bouncycastle.asn1.x509.BasicConstraints; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; import java.util.HashMap; import java.util.Map; @@ -70,6 +89,9 @@ public class WifiEnterpriseConfig implements Parcelable { private static final String PRIVATE_KEY_ID_KEY = "key_id"; private HashMap mFields = new HashMap(); + private X509Certificate mCaCert; + private PrivateKey mClientPrivateKey; + private X509Certificate mClientCertificate; /** This represents an empty value of an enterprise field. * NULL is used at wpa_supplicant to indicate an empty value @@ -103,6 +125,34 @@ public class WifiEnterpriseConfig implements Parcelable { dest.writeString(entry.getKey()); dest.writeString(entry.getValue()); } + + writeCertificate(dest, mCaCert); + + if (mClientPrivateKey != null) { + String algorithm = mClientPrivateKey.getAlgorithm(); + byte[] userKeyBytes = mClientPrivateKey.getEncoded(); + dest.writeInt(userKeyBytes.length); + dest.writeByteArray(userKeyBytes); + dest.writeString(algorithm); + } else { + dest.writeInt(0); + } + + writeCertificate(dest, mClientCertificate); + } + + private void writeCertificate(Parcel dest, X509Certificate cert) { + if (cert != null) { + try { + byte[] certBytes = cert.getEncoded(); + dest.writeInt(certBytes.length); + dest.writeByteArray(certBytes); + } catch (CertificateEncodingException e) { + dest.writeInt(0); + } + } else { + dest.writeInt(0); + } } public static final Creator CREATOR = @@ -115,9 +165,47 @@ public class WifiEnterpriseConfig implements Parcelable { String value = in.readString(); enterpriseConfig.mFields.put(key, value); } + + enterpriseConfig.mCaCert = readCertificate(in); + + PrivateKey userKey = null; + int len = in.readInt(); + if (len > 0) { + try { + byte[] bytes = new byte[len]; + in.readByteArray(bytes); + String algorithm = in.readString(); + KeyFactory keyFactory = KeyFactory.getInstance(algorithm); + userKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(bytes)); + } catch (NoSuchAlgorithmException e) { + userKey = null; + } catch (InvalidKeySpecException e) { + userKey = null; + } + } + + enterpriseConfig.mClientPrivateKey = userKey; + enterpriseConfig.mClientCertificate = readCertificate(in); return enterpriseConfig; } + private X509Certificate readCertificate(Parcel in) { + X509Certificate cert = null; + int len = in.readInt(); + if (len > 0) { + try { + byte[] bytes = new byte[len]; + in.readByteArray(bytes); + CertificateFactory cFactory = CertificateFactory.getInstance("X.509"); + cert = (X509Certificate) cFactory + .generateCertificate(new ByteArrayInputStream(bytes)); + } catch (CertificateException e) { + cert = null; + } + } + return cert; + } + public WifiEnterpriseConfig[] newArray(int size) { return new WifiEnterpriseConfig[size]; } @@ -145,13 +233,13 @@ public class WifiEnterpriseConfig implements Parcelable { public static final String[] strings = {EMPTY_VALUE, "PAP", "MSCHAP", "MSCHAPV2", "GTC" }; } - /** Internal use only @hide */ - public HashMap getFields() { + /** Internal use only */ + HashMap getFields() { return mFields; } - /** Internal use only @hide */ - public static String[] getSupplicantKeys() { + /** Internal use only */ + static String[] getSupplicantKeys() { return new String[] { EAP_KEY, PHASE2_KEY, IDENTITY_KEY, ANON_IDENTITY_KEY, PASSWORD_KEY, CLIENT_CERT_KEY, CA_CERT_KEY, SUBJECT_MATCH_KEY, ENGINE_KEY, ENGINE_ID_KEY, PRIVATE_KEY_ID_KEY }; @@ -271,28 +359,47 @@ public class WifiEnterpriseConfig implements Parcelable { * a certificate *

* @param alias identifies the certificate + * @hide */ - public void setCaCertificate(String alias) { + public void setCaCertificateAlias(String alias) { setFieldValue(CA_CERT_KEY, alias, CA_CERT_PREFIX); } /** * Get CA certificate alias * @return alias to the CA certificate + * @hide */ - public String getCaCertificate() { + public String getCaCertificateAlias() { return getFieldValue(CA_CERT_KEY, CA_CERT_PREFIX); } /** + * Specify a X.509 certificate that identifies the server. + * + *

A default name is automatically assigned to the certificate and used + * with this configuration. + * @param cert X.509 CA certificate + * @throws IllegalArgumentException if not a CA certificate + */ + public void setCaCertificate(X509Certificate cert) { + if (cert.getBasicConstraints() >= 0) { + mCaCert = cert; + } else { + throw new IllegalArgumentException("Not a CA certificate"); + } + } + + /** * Set Client certificate alias. * *

See the {@link android.security.KeyChain} for details on installing or choosing * a certificate *

* @param alias identifies the certificate + * @hide */ - public void setClientCertificate(String alias) { + public void setClientCertificateAlias(String alias) { setFieldValue(CLIENT_CERT_KEY, alias, CLIENT_CERT_PREFIX); setFieldValue(PRIVATE_KEY_ID_KEY, alias, Credentials.USER_PRIVATE_KEY); // Also, set engine parameters @@ -308,12 +415,118 @@ public class WifiEnterpriseConfig implements Parcelable { /** * Get client certificate alias * @return alias to the client certificate + * @hide */ - public String getClientCertificate() { + public String getClientCertificateAlias() { return getFieldValue(CLIENT_CERT_KEY, CLIENT_CERT_PREFIX); } /** + * Specify a private key and client certificate for client authorization. + * + *

A default name is automatically assigned to the key entry and used + * with this configuration. + * @param privateKey + * @param clientCertificate + */ + public void setClientKeyEntry(PrivateKey privateKey, X509Certificate clientCertificate) { + if (clientCertificate != null) { + if (clientCertificate.getBasicConstraints() != -1) { + throw new IllegalArgumentException("Cannot be a CA certificate"); + } + if (privateKey == null) { + throw new IllegalArgumentException("Client cert without a private key"); + } + if (privateKey.getEncoded() == null) { + throw new IllegalArgumentException("Private key cannot be encoded"); + } + } + + mClientPrivateKey = privateKey; + mClientCertificate = clientCertificate; + } + + boolean needsKeyStore() { + // Has no keys to be installed + if (mClientCertificate == null && mCaCert == null) return false; + return true; + } + + boolean installKeys(android.security.KeyStore keyStore, String name) { + boolean ret = true; + String privKeyName = Credentials.USER_PRIVATE_KEY + name; + String userCertName = Credentials.USER_CERTIFICATE + name; + String caCertName = Credentials.CA_CERTIFICATE + name; + if (mClientCertificate != null) { + byte[] privKeyData = mClientPrivateKey.getEncoded(); + ret = keyStore.importKey(privKeyName, privKeyData); + if (ret == false) { + return ret; + } + + ret = putCertInKeyStore(keyStore, userCertName, mClientCertificate); + if (ret == false) { + // Remove private key installed + keyStore.delKey(privKeyName); + return ret; + } + } + + if (mCaCert != null) { + ret = putCertInKeyStore(keyStore, caCertName, mCaCert); + if (ret == false) { + if (mClientCertificate != null) { + // Remove client key+cert + keyStore.delKey(privKeyName); + keyStore.delete(userCertName); + } + return ret; + } + } + + // Set alias names + if (mClientCertificate != null) { + setClientCertificateAlias(name); + mClientPrivateKey = null; + mClientCertificate = null; + } + + if (mCaCert != null) { + setCaCertificateAlias(name); + mCaCert = null; + } + + return ret; + } + + private boolean putCertInKeyStore(android.security.KeyStore keyStore, String name, + Certificate cert) { + try { + byte[] certData = Credentials.convertToPem(cert); + return keyStore.put(name, certData); + } catch (IOException e1) { + return false; + } catch (CertificateException e2) { + return false; + } + } + + void removeKeys(android.security.KeyStore keyStore) { + String client = getFieldValue(CLIENT_CERT_KEY, CLIENT_CERT_PREFIX); + // a valid client certificate is configured + if (!TextUtils.isEmpty(client)) { + keyStore.delKey(Credentials.USER_PRIVATE_KEY + client); + keyStore.delete(Credentials.USER_CERTIFICATE + client); + } + + String ca = getFieldValue(CA_CERT_KEY, CA_CERT_PREFIX); + // a valid ca certificate is configured + if (!TextUtils.isEmpty(ca)) { + keyStore.delete(Credentials.CA_CERTIFICATE + ca); + } + } + + /** * Set subject match. This is the substring to be matched against the subject of the * authentication server certificate. * @param subjectMatch substring to be matched @@ -330,13 +543,28 @@ public class WifiEnterpriseConfig implements Parcelable { return getFieldValue(SUBJECT_MATCH_KEY, ""); } + /** See {@link WifiConfiguration#getKeyIdForCredentials} @hide */ + String getKeyId(WifiEnterpriseConfig current) { + String eap = mFields.get(EAP_KEY); + String phase2 = mFields.get(PHASE2_KEY); + + // If either eap or phase2 are not initialized, use current config details + if (TextUtils.isEmpty((eap))) { + eap = current.mFields.get(EAP_KEY); + } + if (TextUtils.isEmpty(phase2)) { + phase2 = current.mFields.get(PHASE2_KEY); + } + return eap + "_" + phase2; + } + /** Migrates the old style TLS config to the new config style. This should only be used * when restoring an old wpa_supplicant.conf or upgrading from a previous * platform version. * @return true if the config was updated * @hide */ - public boolean migrateOldEapTlsNative(WifiNative wifiNative, int netId) { + boolean migrateOldEapTlsNative(WifiNative wifiNative, int netId) { String oldPrivateKey = wifiNative.getNetworkVariable(netId, OLD_PRIVATE_KEY_NAME); /* * If the old configuration value is not present, then there is nothing @@ -395,6 +623,7 @@ public class WifiEnterpriseConfig implements Parcelable { * @return the index into array */ private int getStringIndex(String arr[], String toBeFound, int defaultIndex) { + if (TextUtils.isEmpty(toBeFound)) return defaultIndex; for (int i = 0; i < arr.length; i++) { if (toBeFound.equals(arr[i])) return i; } @@ -408,7 +637,8 @@ public class WifiEnterpriseConfig implements Parcelable { */ private String getFieldValue(String key, String prefix) { String value = mFields.get(key); - if (EMPTY_VALUE.equals(value)) return ""; + // Uninitialized or known to be empty after reading from supplicant + if (TextUtils.isEmpty(value) || EMPTY_VALUE.equals(value)) return ""; return removeDoubleQuotes(value).substring(prefix.length()); } -- cgit v1.1