From 8d28f4ab801a59732869f81c57f0d516ffaf3443 Mon Sep 17 00:00:00 2001 From: Sefa Ilkimen Date: Fri, 5 Apr 2019 05:22:34 +0200 Subject: [PATCH] WIP: implementing X509 client cert authentication --- plugin.xml | 8 +- .../cordovahttp/CordovaClientAuth.java | 75 +++++++++ .../silkimen/cordovahttp/CordovaHttpBase.java | 31 ++-- .../cordovahttp/CordovaHttpDownload.java | 9 +- .../cordovahttp/CordovaHttpOperation.java | 14 +- .../cordovahttp/CordovaHttpPlugin.java | 152 +++++------------- .../cordovahttp/CordovaHttpUpload.java | 10 +- .../cordovahttp/CordovaServerTrust.java | 129 +++++++++++++++ .../http/HostnameVerifierFactory.java | 20 --- .../com/silkimen/http/KeyChainKeyManager.java | 57 +++++++ .../com/silkimen/http/TLSConfiguration.java | 69 ++++++++ .../silkimen/http/TrustManagersFactory.java | 64 -------- www/helpers.js | 6 + www/messages.js | 1 + www/public-interface.js | 5 + 15 files changed, 410 insertions(+), 240 deletions(-) create mode 100644 src/android/com/silkimen/cordovahttp/CordovaClientAuth.java create mode 100644 src/android/com/silkimen/cordovahttp/CordovaServerTrust.java delete mode 100644 src/android/com/silkimen/http/HostnameVerifierFactory.java create mode 100644 src/android/com/silkimen/http/KeyChainKeyManager.java create mode 100644 src/android/com/silkimen/http/TLSConfiguration.java delete mode 100644 src/android/com/silkimen/http/TrustManagersFactory.java diff --git a/plugin.xml b/plugin.xml index dcdad4d..127bef2 100644 --- a/plugin.xml +++ b/plugin.xml @@ -59,19 +59,21 @@ + - + + + - @@ -84,4 +86,4 @@ - \ No newline at end of file + diff --git a/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java b/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java new file mode 100644 index 0000000..f75accd --- /dev/null +++ b/src/android/com/silkimen/cordovahttp/CordovaClientAuth.java @@ -0,0 +1,75 @@ +package com.silkimen.cordovahttp; + +import android.app.Activity; +import android.content.Context; +import android.security.KeyChain; +import android.security.KeyChainAliasCallback; +import android.util.Log; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import javax.net.ssl.KeyManager; + +import org.apache.cordova.CallbackContext; + +import com.silkimen.http.KeyChainKeyManager; +import com.silkimen.http.TLSConfiguration; + +class CordovaClientAuth implements Runnable, KeyChainAliasCallback { + private static final String TAG = "Cordova-Plugin-HTTP"; + + private String mode; + private Activity activity; + private Context context; + private TLSConfiguration tlsConfiguration; + private CallbackContext callbackContext; + + public CordovaClientAuth(final String mode, final Activity activity, final Context context, + final TLSConfiguration configContainer, final CallbackContext callbackContext) { + + this.mode = mode; + this.activity = activity; + this.tlsConfiguration = configContainer; + this.context = context; + this.callbackContext = callbackContext; + } + + @Override + public void run() { + switch (this.mode) { + case "systemstore": + KeyChain.choosePrivateKeyAlias(this.activity, this, null, null, null, -1, null); + break; + case "bundle": + // @todo use pfx in bundle + this.callbackContext.error("Not implemented, yet"); + break; + default: + this.tlsConfiguration.setKeyManagers(null); + this.callbackContext.success(); + break; + } + } + + @Override + public void alias(final String alias) { + try { + if (alias == null) { + throw new Exception("Couldn't get a consent for private key access"); + } + + PrivateKey key = KeyChain.getPrivateKey(this.context, alias); + X509Certificate[] chain = KeyChain.getCertificateChain(this.context, alias); + KeyManager keyManager = new KeyChainKeyManager(alias, key, chain); + + this.tlsConfiguration.setKeyManagers(new KeyManager[] { keyManager }); + + this.callbackContext.success(); + } catch (Exception e) { + Log.e(TAG, "Couldn't load private key and certificate pair for authentication", e); + this.callbackContext.error("Couldn't load private key and certificate pair for authentication"); + } + } +} diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java b/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java index af2bcfa..27e8a67 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java @@ -1,5 +1,6 @@ package com.silkimen.cordovahttp; +import java.io.IOException; import java.io.ByteArrayOutputStream; import java.net.SocketTimeoutException; @@ -7,15 +8,14 @@ import java.net.UnknownHostException; import java.nio.ByteBuffer; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLSocketFactory; import com.silkimen.http.HttpBodyDecoder; import com.silkimen.http.HttpRequest; import com.silkimen.http.HttpRequest.HttpRequestException; import com.silkimen.http.JsonUtils; import com.silkimen.http.OkConnectionFactory; +import com.silkimen.http.TLSConfiguration; import org.apache.cordova.CallbackContext; @@ -34,13 +34,11 @@ abstract class CordovaHttpBase implements Runnable { protected JSONObject headers; protected int timeout; protected boolean followRedirects; - protected SSLSocketFactory customSSLSocketFactory; - protected HostnameVerifier customHostnameVerifier; + protected TLSConfiguration tlsConfiguration; protected CallbackContext callbackContext; public CordovaHttpBase(String method, String url, String serializer, Object data, JSONObject headers, int timeout, - boolean followRedirects, SSLSocketFactory customSSLSocketFactory, HostnameVerifier customHostnameVerifier, - CallbackContext callbackContext) { + boolean followRedirects, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { this.method = method; this.url = url; @@ -49,22 +47,19 @@ abstract class CordovaHttpBase implements Runnable { this.headers = headers; this.timeout = timeout; this.followRedirects = followRedirects; - this.customSSLSocketFactory = customSSLSocketFactory; - this.customHostnameVerifier = customHostnameVerifier; + this.tlsConfiguration = tlsConfiguration; this.callbackContext = callbackContext; } - public CordovaHttpBase(String method, String url, JSONObject headers, int timeout, - boolean followRedirects, SSLSocketFactory customSSLSocketFactory, HostnameVerifier customHostnameVerifier, - CallbackContext callbackContext) { + public CordovaHttpBase(String method, String url, JSONObject headers, int timeout, boolean followRedirects, + TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { this.method = method; this.url = url; this.headers = headers; this.timeout = timeout; this.followRedirects = followRedirects; - this.customSSLSocketFactory = customSSLSocketFactory; - this.customHostnameVerifier = customHostnameVerifier; + this.tlsConfiguration = tlsConfiguration; this.callbackContext = callbackContext; } @@ -116,20 +111,18 @@ abstract class CordovaHttpBase implements Runnable { return new HttpRequest(this.url, this.method); } - protected void prepareRequest(HttpRequest request) throws JSONException { + protected void prepareRequest(HttpRequest request) throws JSONException, IOException { request.followRedirects(this.followRedirects); request.readTimeout(this.timeout); request.acceptCharset("UTF-8"); request.uncompress(true); request.setConnectionFactory(new OkConnectionFactory()); - if (this.customHostnameVerifier != null) { - request.setHostnameVerifier(this.customHostnameVerifier); + if (this.tlsConfiguration.getHostnameVerifier() != null) { + request.setHostnameVerifier(this.tlsConfiguration.getHostnameVerifier()); } - if (this.customSSLSocketFactory != null) { - request.setSSLSocketFactory(this.customSSLSocketFactory); - } + request.setSSLSocketFactory(this.tlsConfiguration.getTLSSocketFactory()); // setup content type before applying headers, so user can override it this.setContentType(request); diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java index 425faa9..3b30bfe 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java @@ -7,6 +7,7 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; import com.silkimen.http.HttpRequest; +import com.silkimen.http.TLSConfiguration; import org.apache.cordova.CallbackContext; import org.apache.cordova.file.FileUtils; @@ -15,12 +16,10 @@ import org.json.JSONObject; class CordovaHttpDownload extends CordovaHttpBase { private String filePath; - public CordovaHttpDownload(String url, JSONObject headers, String filePath, int timeout, - boolean followRedirects, SSLSocketFactory customSSLSocketFactory, HostnameVerifier customHostnameVerifier, - CallbackContext callbackContext) { + public CordovaHttpDownload(String url, JSONObject headers, String filePath, int timeout, boolean followRedirects, + TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { - super("GET", url, headers, timeout, followRedirects, customSSLSocketFactory, customHostnameVerifier, - callbackContext); + super("GET", url, headers, timeout, followRedirects, tlsConfiguration, callbackContext); this.filePath = filePath; } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java b/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java index 98c3569..2a9a34c 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java @@ -3,23 +3,21 @@ package com.silkimen.cordovahttp; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; +import com.silkimen.http.TLSConfiguration; + import org.apache.cordova.CallbackContext; import org.json.JSONObject; class CordovaHttpOperation extends CordovaHttpBase { public CordovaHttpOperation(String method, String url, String serializer, Object data, JSONObject headers, - int timeout, boolean followRedirects, SSLSocketFactory customSSLSocketFactory, - HostnameVerifier customHostnameVerifier, CallbackContext callbackContext) { + int timeout, boolean followRedirects, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { - super(method, url, serializer, data, headers, timeout, followRedirects, customSSLSocketFactory, - customHostnameVerifier, callbackContext); + super(method, url, serializer, data, headers, timeout, followRedirects, tlsConfiguration, callbackContext); } public CordovaHttpOperation(String method, String url, JSONObject headers, int timeout, boolean followRedirects, - SSLSocketFactory customSSLSocketFactory, HostnameVerifier customHostnameVerifier, - CallbackContext callbackContext) { + TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { - super(method, url, headers, timeout, followRedirects, customSSLSocketFactory, customHostnameVerifier, - callbackContext); + super(method, url, headers, timeout, followRedirects, tlsConfiguration, callbackContext); } } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java index ccf5745..47e848d 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java @@ -1,27 +1,8 @@ package com.silkimen.cordovahttp; -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; - -import java.security.cert.CertificateFactory; -import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.security.KeyStore.TrustedCertificateEntry; -import java.security.SecureRandom; -import java.security.cert.Certificate; -import java.util.ArrayList; -import java.util.Enumeration; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; - -import com.silkimen.http.HostnameVerifierFactory; -import com.silkimen.http.TLSSocketFactory; -import com.silkimen.http.TrustManagersFactory; +import com.silkimen.http.TLSConfiguration; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; @@ -31,26 +12,32 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import android.content.res.AssetManager; import android.util.Log; +import javax.net.ssl.TrustManagerFactory; + public class CordovaHttpPlugin extends CordovaPlugin { private static final String TAG = "Cordova-Plugin-HTTP"; - private final TrustManagersFactory trustManagersFactory = new TrustManagersFactory(); - private final HostnameVerifierFactory hostnameVerifierFactory = new HostnameVerifierFactory(); - private boolean followRedirects = true; - private SSLSocketFactory customSSLSocketFactory; - private HostnameVerifier customHostnameVerifier; + private TLSConfiguration tlsConfiguration; @Override public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); + this.tlsConfiguration = new TLSConfiguration(); + try { - this.customSSLSocketFactory = this.createSocketFactory( - this.trustManagersFactory.getPinnedTrustManagers(this.getCertsFromKeyStore("AndroidCAStore"))); + KeyStore store = KeyStore.getInstance("AndroidCAStore"); + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + + store.load(null); + tmf.init(store); + + this.tlsConfiguration.setHostnameVerifier(null); + this.tlsConfiguration.setTrustManagers(tmf.getTrustManagers()); } catch (Exception e) { Log.e(TAG, "An error occured while loading system's CA certificates", e); } @@ -83,6 +70,8 @@ public class CordovaHttpPlugin extends CordovaPlugin { return this.downloadFile(args, callbackContext); case "setSSLCertMode": return this.setSSLCertMode(args, callbackContext); + case "setClientAuthMode": + return this.setClientAuthMode(args, callbackContext); case "disableRedirect": return this.disableRedirect(args, callbackContext); default: @@ -98,7 +87,7 @@ public class CordovaHttpPlugin extends CordovaPlugin { int timeout = args.getInt(2) * 1000; CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, - this.followRedirects, this.customSSLSocketFactory, this.customHostnameVerifier, callbackContext); + this.followRedirects, this.tlsConfiguration, callbackContext); cordova.getThreadPool().execute(request); @@ -115,7 +104,7 @@ public class CordovaHttpPlugin extends CordovaPlugin { int timeout = args.getInt(4) * 1000; CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, serializer, data, headers, - timeout, this.followRedirects, this.customSSLSocketFactory, this.customHostnameVerifier, callbackContext); + timeout, this.followRedirects, this.tlsConfiguration, callbackContext); cordova.getThreadPool().execute(request); @@ -129,8 +118,8 @@ public class CordovaHttpPlugin extends CordovaPlugin { String uploadName = args.getString(3); int timeout = args.getInt(4) * 1000; - CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePath, uploadName, timeout, - this.followRedirects, this.customSSLSocketFactory, this.customHostnameVerifier, callbackContext); + CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePath, uploadName, timeout, this.followRedirects, + this.tlsConfiguration, callbackContext); cordova.getThreadPool().execute(upload); @@ -143,42 +132,28 @@ public class CordovaHttpPlugin extends CordovaPlugin { String filePath = args.getString(2); int timeout = args.getInt(3) * 1000; - CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, - this.followRedirects, this.customSSLSocketFactory, this.customHostnameVerifier, callbackContext); + CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, this.followRedirects, + this.tlsConfiguration, callbackContext); cordova.getThreadPool().execute(download); return true; } - private boolean setSSLCertMode(final JSONArray args, final CallbackContext callbackContext) { - try { - switch (args.getString(0)) { - case "legacy": - this.customHostnameVerifier = null; - this.customSSLSocketFactory = null; - break; - case "nocheck": - this.customHostnameVerifier = this.hostnameVerifierFactory.getNoOpVerifier(); - this.customSSLSocketFactory = this.createSocketFactory(this.trustManagersFactory.getNoopTrustManagers()); - break; - case "pinned": - this.customHostnameVerifier = null; - this.customSSLSocketFactory = this.createSocketFactory( - this.trustManagersFactory.getPinnedTrustManagers(this.getCertsFromBundle("www/certificates"))); - break; - default: - this.customHostnameVerifier = null; - this.customSSLSocketFactory = this.createSocketFactory( - this.trustManagersFactory.getPinnedTrustManagers(this.getCertsFromKeyStore("AndroidCAStore"))); - break; - } + private boolean setSSLCertMode(final JSONArray args, final CallbackContext callbackContext) throws JSONException { + CordovaServerTrust runnable = new CordovaServerTrust(args.getString(0), this.cordova.getActivity(), + this.tlsConfiguration, callbackContext); - callbackContext.success(); - } catch (Exception e) { - Log.e(TAG, "An error occured while configuring SSL cert mode", e); - callbackContext.error("An error occured while configuring SSL cert mode"); - } + cordova.getThreadPool().execute(runnable); + + return true; + } + + private boolean setClientAuthMode(final JSONArray args, final CallbackContext callbackContext) throws JSONException { + CordovaClientAuth runnable = new CordovaClientAuth(args.getString(0), this.cordova.getActivity(), + this.cordova.getContext(), this.tlsConfiguration, callbackContext); + + cordova.getThreadPool().execute(runnable); return true; } @@ -190,59 +165,4 @@ public class CordovaHttpPlugin extends CordovaPlugin { return true; } - - private ArrayList getCertsFromKeyStore(String storeType) throws GeneralSecurityException, IOException { - ArrayList certList = new ArrayList(); - KeyStore keyStore = KeyStore.getInstance(storeType); - keyStore.load(null); - - Enumeration aliases = keyStore.aliases(); - - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - TrustedCertificateEntry certEntry = (TrustedCertificateEntry) keyStore.getEntry(alias, null); - Certificate cert = certEntry.getTrustedCertificate(); - certList.add(cert); - } - - return certList; - } - - private ArrayList getCertsFromBundle(String path) throws GeneralSecurityException, IOException { - AssetManager assetManager = cordova.getActivity().getAssets(); - String[] files = assetManager.list(path); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - ArrayList certList = new ArrayList(); - - for (int i = 0; i < files.length; i++) { - int index = files[i].lastIndexOf('.'); - - if (index == -1 || !files[i].substring(index).equals(".cer")) { - continue; - } - - certList.add(cf.generateCertificate(assetManager.open(path + "/" + files[i]))); - } - - return certList; - } - - private SSLSocketFactory createSocketFactory(TrustManager[] trustManagers) throws IOException { - try { - SSLContext context = SSLContext.getInstance("TLS"); - - /* @TODO implement custom KeyManager */ - context.init(null, trustManagers, new SecureRandom()); - - if (android.os.Build.VERSION.SDK_INT < 20) { - return new TLSSocketFactory(context); - } else { - return context.getSocketFactory(); - } - } catch (GeneralSecurityException e) { - IOException ioException = new IOException("Security exception occured while configuring SSL context"); - ioException.initCause(e); - throw ioException; - } - } } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java index 77449dc..623e025 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java @@ -10,6 +10,8 @@ import java.net.URI; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; +import com.silkimen.http.TLSConfiguration; + import org.apache.cordova.CallbackContext; import org.json.JSONObject; @@ -17,12 +19,10 @@ class CordovaHttpUpload extends CordovaHttpBase { private String filePath; private String uploadName; - public CordovaHttpUpload(String url, JSONObject headers, String filePath, String uploadName, - int timeout, boolean followRedirects, SSLSocketFactory customSSLSocketFactory, - HostnameVerifier customHostnameVerifier, CallbackContext callbackContext) { + public CordovaHttpUpload(String url, JSONObject headers, String filePath, String uploadName, int timeout, + boolean followRedirects, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { - super("POST", url, headers, timeout, followRedirects, customSSLSocketFactory, customHostnameVerifier, - callbackContext); + super("POST", url, headers, timeout, followRedirects, tlsConfiguration, callbackContext); this.filePath = filePath; this.uploadName = uploadName; } diff --git a/src/android/com/silkimen/cordovahttp/CordovaServerTrust.java b/src/android/com/silkimen/cordovahttp/CordovaServerTrust.java new file mode 100644 index 0000000..cfd4b49 --- /dev/null +++ b/src/android/com/silkimen/cordovahttp/CordovaServerTrust.java @@ -0,0 +1,129 @@ +package com.silkimen.cordovahttp; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import com.silkimen.http.TLSConfiguration; + +import org.apache.cordova.CallbackContext; + +import android.app.Activity; +import android.util.Log; +import android.content.res.AssetManager; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +class CordovaServerTrust implements Runnable { + private static final String TAG = "Cordova-Plugin-HTTP"; + + private final TrustManager[] noOpTrustManagers; + private final HostnameVerifier noOpVerifier; + + private String mode; + private Activity activity; + private TLSConfiguration tlsConfiguration; + private CallbackContext callbackContext; + + public CordovaServerTrust(final String mode, final Activity activity, final TLSConfiguration configContainer, + final CallbackContext callbackContext) { + + this.mode = mode; + this.activity = activity; + this.tlsConfiguration = configContainer; + this.callbackContext = callbackContext; + + this.noOpTrustManagers = new TrustManager[] { new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) { + // intentionally left blank + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) { + // intentionally left blank + } + } }; + + this.noOpVerifier = new HostnameVerifier() { + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; + } + + @Override + public void run() { + try { + switch (this.mode) { + case "legacy": + this.tlsConfiguration.setHostnameVerifier(null); + this.tlsConfiguration.setTrustManagers(null); + break; + case "nocheck": + this.tlsConfiguration.setHostnameVerifier(this.noOpVerifier); + this.tlsConfiguration.setTrustManagers(this.noOpTrustManagers); + break; + case "pinned": + this.tlsConfiguration.setHostnameVerifier(null); + this.tlsConfiguration.setTrustManagers(this.getTrustManagers(this.getCertsFromBundle("www/certificates"))); + break; + default: + this.tlsConfiguration.setHostnameVerifier(null); + this.tlsConfiguration.setTrustManagers(this.getTrustManagers(this.getCertsFromKeyStore("AndroidCAStore"))); + break; + } + + callbackContext.success(); + } catch (Exception e) { + Log.e(TAG, "An error occured while configuring SSL cert mode", e); + callbackContext.error("An error occured while configuring SSL cert mode"); + } + } + + private TrustManager[] getTrustManagers(KeyStore store) throws GeneralSecurityException { + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init(store); + + return tmf.getTrustManagers(); + } + + private KeyStore getCertsFromBundle(String path) throws GeneralSecurityException, IOException { + AssetManager assetManager = this.activity.getAssets(); + String[] files = assetManager.list(path); + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + String keyStoreType = KeyStore.getDefaultType(); + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + + keyStore.load(null, null); + + for (int i = 0; i < files.length; i++) { + int index = files[i].lastIndexOf('.'); + + if (index == -1 || !files[i].substring(index).equals(".cer")) { + continue; + } + + keyStore.setCertificateEntry("CA" + i, cf.generateCertificate(assetManager.open(path + "/" + files[i]))); + } + + return keyStore; + } + + private KeyStore getCertsFromKeyStore(String storeType) throws GeneralSecurityException, IOException { + KeyStore store = KeyStore.getInstance(storeType); + store.load(null); + + return store; + } +} diff --git a/src/android/com/silkimen/http/HostnameVerifierFactory.java b/src/android/com/silkimen/http/HostnameVerifierFactory.java deleted file mode 100644 index f0cf16c..0000000 --- a/src/android/com/silkimen/http/HostnameVerifierFactory.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.silkimen.http; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSession; - -public class HostnameVerifierFactory { - private final HostnameVerifier noOpVerifier; - - public HostnameVerifierFactory() { - this.noOpVerifier = new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { - return true; - } - }; - } - - public HostnameVerifier getNoOpVerifier() { - return this.noOpVerifier; - } -} diff --git a/src/android/com/silkimen/http/KeyChainKeyManager.java b/src/android/com/silkimen/http/KeyChainKeyManager.java new file mode 100644 index 0000000..ecdaa38 --- /dev/null +++ b/src/android/com/silkimen/http/KeyChainKeyManager.java @@ -0,0 +1,57 @@ +package com.silkimen.http; + +import android.content.Context; +import android.security.KeyChain; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509ExtendedKeyManager; + +public class KeyChainKeyManager extends X509ExtendedKeyManager { + private final String alias; + private final X509Certificate[] chain; + private final PrivateKey key; + + public KeyChainKeyManager(String alias, PrivateKey key, X509Certificate[] chain) { + this.alias = alias; + this.key = key; + this.chain = chain; + } + + @Override + public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) { + return this.alias; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return chain; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return key; + } + + @Override + public final String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + // not a client SSLSocket callback + throw new UnsupportedOperationException(); + } + + @Override + public final String[] getClientAliases(String keyType, Principal[] issuers) { + // not a client SSLSocket callback + throw new UnsupportedOperationException(); + } + + @Override + public final String[] getServerAliases(String keyType, Principal[] issuers) { + // not a client SSLSocket callback + throw new UnsupportedOperationException(); + } +} diff --git a/src/android/com/silkimen/http/TLSConfiguration.java b/src/android/com/silkimen/http/TLSConfiguration.java new file mode 100644 index 0000000..33c8634 --- /dev/null +++ b/src/android/com/silkimen/http/TLSConfiguration.java @@ -0,0 +1,69 @@ +package com.silkimen.http; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import com.silkimen.http.TLSSocketFactory; + +public class TLSConfiguration { + private TrustManager[] trustManagers; + private KeyManager[] keyManagers; + private HostnameVerifier hostnameVerifier; + + private SSLSocketFactory socketFactory; + + public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + } + + public void setKeyManagers(KeyManager[] keyManagers) { + this.keyManagers = keyManagers; + this.socketFactory = null; + } + + public void setTrustManagers(TrustManager[] trustManagers) { + this.trustManagers = trustManagers; + this.socketFactory = null; + } + + public HostnameVerifier getHostnameVerifier() { + return this.hostnameVerifier; + } + + public SSLSocketFactory getTLSSocketFactory() throws IOException { + if (this.socketFactory != null) { + return this.socketFactory; + } + + try { + SSLContext context = SSLContext.getInstance("TLS"); + + context.init(this.keyManagers, this.trustManagers, new SecureRandom()); + + if (android.os.Build.VERSION.SDK_INT < 20) { + this.socketFactory = new TLSSocketFactory(context); + } else { + this.socketFactory = context.getSocketFactory(); + } + + return this.socketFactory; + } catch (GeneralSecurityException e) { + IOException ioException = new IOException("Security exception occured while configuring TLS context"); + ioException.initCause(e); + throw ioException; + } + } +} diff --git a/src/android/com/silkimen/http/TrustManagersFactory.java b/src/android/com/silkimen/http/TrustManagersFactory.java deleted file mode 100644 index ca9fa50..0000000 --- a/src/android/com/silkimen/http/TrustManagersFactory.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.silkimen.http; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.util.ArrayList; - -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -public class TrustManagersFactory { - private final TrustManager[] noOpTrustManager; - - public TrustManagersFactory() { - this.noOpTrustManager = new TrustManager[] { new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - - public void checkClientTrusted(X509Certificate[] chain, String authType) { - // intentionally left blank - } - - public void checkServerTrusted(X509Certificate[] chain, String authType) { - // intentionally left blank - } - } }; - } - - public TrustManager[] getNoopTrustManagers() { - return this.noOpTrustManager; - } - - public TrustManager[] getPinnedTrustManagers(ArrayList pinnedCerts) throws IOException { - if (pinnedCerts == null || pinnedCerts.size() == 0) { - throw new IOException("You must add at least 1 certificate in order to pin to certificates"); - } - - try { - String keyStoreType = KeyStore.getDefaultType(); - KeyStore keyStore = KeyStore.getInstance(keyStoreType); - keyStore.load(null, null); - - for (int i = 0; i < pinnedCerts.size(); i++) { - keyStore.setCertificateEntry("CA" + i, pinnedCerts.get(i)); - } - - // Create a TrustManager that trusts the CAs in our KeyStore - String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); - tmf.init(keyStore); - - return tmf.getTrustManagers(); - } catch (GeneralSecurityException e) { - IOException ioException = new IOException("Security exception configuring SSL trust managers"); - ioException.initCause(e); - - throw ioException; - } - } -} diff --git a/www/helpers.js b/www/helpers.js index 06e82ee..14313b5 100644 --- a/www/helpers.js +++ b/www/helpers.js @@ -1,6 +1,7 @@ module.exports = function init(cookieHandler, messages) { var validSerializers = ['urlencoded', 'json', 'utf8']; var validCertModes = ['default', 'nocheck', 'pinned', 'legacy']; + var validClientAuthModes = ['none', 'systemstore', 'bundle']; var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'upload', 'download']; return { @@ -8,6 +9,7 @@ module.exports = function init(cookieHandler, messages) { getTypeOf: getTypeOf, checkSerializer: checkSerializer, checkSSLCertMode: checkSSLCertMode, + checkClientAuthMode: checkClientAuthMode, checkForBlacklistedHeaderKey: checkForBlacklistedHeaderKey, checkForInvalidHeaderValue: checkForInvalidHeaderValue, injectCookieHandler: injectCookieHandler, @@ -82,6 +84,10 @@ module.exports = function init(cookieHandler, messages) { return checkForValidStringValue(validCertModes, mode, messages.INVALID_SSL_CERT_MODE); } + function checkClientAuthMode(mode) { + return checkForValidStringValue(validClientAuthModes, mode, messages.INVALID_CLIENT_AUTH_MODE); + } + function checkForBlacklistedHeaderKey(key) { if (key.toLowerCase() === 'cookie') { throw new Error(messages.ADDING_COOKIES_NOT_SUPPORTED); diff --git a/www/messages.js b/www/messages.js index da37826..25a76d0 100644 --- a/www/messages.js +++ b/www/messages.js @@ -6,6 +6,7 @@ module.exports = { INVALID_HTTP_METHOD: 'advanced-http: invalid HTTP method, supported methods are:', INVALID_DATA_SERIALIZER: 'advanced-http: invalid serializer, supported serializers are:', INVALID_SSL_CERT_MODE: 'advanced-http: invalid SSL cert mode, supported modes are:', + INVALID_CLIENT_AUTH_MODE: 'advanced-http: invalid client certificate authentication mode, supported modes are:', INVALID_HEADERS_VALUE: 'advanced-http: header values must be strings', INVALID_TIMEOUT_VALUE: 'advanced-http: invalid timeout value, needs to be a positive numeric value', INVALID_PARAMS_VALUE: 'advanced-http: invalid params object, needs to be an object with strings' diff --git a/www/public-interface.js b/www/public-interface.js index 38fe2f2..0ee9058 100644 --- a/www/public-interface.js +++ b/www/public-interface.js @@ -13,6 +13,7 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf getRequestTimeout: getRequestTimeout, setRequestTimeout: setRequestTimeout, setSSLCertMode: setSSLCertMode, + setClientAuthMode: setClientAuthMode, disableRedirect: disableRedirect, sendRequest: sendRequest, post: post, @@ -92,6 +93,10 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf return exec(success, failure, 'CordovaHttpPlugin', 'setSSLCertMode', [helpers.checkSSLCertMode(mode)]); } + function setClientAuthMode(mode, success, failure) { + return exec(success, failure, 'CordovaHttpPlugin', 'setClientAuthMode', [helpers.checkClientAuthMode(mode)]); + } + function disableRedirect(disable, success, failure) { return exec(success, failure, 'CordovaHttpPlugin', 'disableRedirect', [!!disable]); }