diff --git a/src/android/com/github/kevinsawicki/http/HttpRequest.java b/src/android/com/github/kevinsawicki/http/HttpRequest.java index bf53277..6171c2c 100644 --- a/src/android/com/github/kevinsawicki/http/HttpRequest.java +++ b/src/android/com/github/kevinsawicki/http/HttpRequest.java @@ -263,6 +263,12 @@ public class HttpRequest { */ public static final String PARAM_CHARSET = "charset"; + public static final String CERT_MODE_DEFAULT = "default"; + + public static final String CERT_MODE_PINNED = "pinned"; + + public static final String CERT_MODE_TRUSTALL = "trustall"; + private static final String BOUNDARY = "00content0boundary00"; private static final String CONTENT_TYPE_MULTIPART = "multipart/form-data; boundary=" @@ -272,13 +278,13 @@ public class HttpRequest { private static final String[] EMPTY_STRINGS = new String[0]; - private static SSLSocketFactory PINNED_FACTORY; + private static SSLSocketFactory SOCKET_FACTORY; - private static SSLSocketFactory TRUSTED_FACTORY; + private static String CURRENT_CERT_MODE = CERT_MODE_DEFAULT; private static ArrayList PINNED_CERTS; - private static HostnameVerifier TRUSTED_VERIFIER; + private static HostnameVerifier HOSTNAME_VERIFIER; private static String getValidCharset(final String charset) { if (charset != null && charset.length() > 0) @@ -287,63 +293,107 @@ public class HttpRequest { return CHARSET_UTF8; } - private static SSLSocketFactory getPinnedFactory() - throws HttpRequestException { - if (PINNED_FACTORY != null) { - return PINNED_FACTORY; - } else { - IOException e = new IOException("You must add at least 1 certificate in order to pin to certificates"); - throw new HttpRequestException(e); + /** + * Configure SSL cert handling for all future HTTPS connections + * + * @param mode + */ + public static void setSSLCertMode(String mode) { + try { + if (mode == CERT_MODE_TRUSTALL) { + SOCKET_FACTORY = createSocketFactory(getNoopTrustManagers()); + } else if (mode == CERT_MODE_PINNED) { + SOCKET_FACTORY = createSocketFactory(getPinnedTrustManagers()); + } else { + SOCKET_FACTORY = null; + } + + CURRENT_CERT_MODE = mode; + } catch(IOException e) { + throw new HttpRequestException(e); } } - private static SSLSocketFactory getTrustedFactory() - throws HttpRequestException { - if (TRUSTED_FACTORY == null) { - final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { + /** + * Configure host name verification for all future HTTPS connections + * + * @param enabled + */ + public static void setHostnameVerification(boolean enabled) { + if (enabled) { + HOSTNAME_VERIFIER = null; + } else { + HOSTNAME_VERIFIER = getTrustedVerifier(); + } + } - 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 - } - } }; - try { - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, trustAllCerts, new SecureRandom()); - - if (android.os.Build.VERSION.SDK_INT < 20) { - TRUSTED_FACTORY = new TLSSocketFactory(context); - } else { - TRUSTED_FACTORY = context.getSocketFactory(); - } - } catch (GeneralSecurityException e) { - IOException ioException = new IOException( - "Security exception configuring SSL context"); - ioException.initCause(e); - throw new HttpRequestException(ioException); - } + private static TrustManager[] getPinnedTrustManagers() throws IOException { + if (PINNED_CERTS == null) { + throw new IOException("You must add at least 1 certificate in order to pin to certificates"); } - return TRUSTED_FACTORY; + try { + String keyStoreType = KeyStore.getDefaultType(); + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(null, null); + + for (int i = 0; i < PINNED_CERTS.size(); i++) { + keyStore.setCertificateEntry("CA" + i, PINNED_CERTS.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 new HttpRequestException(ioException); + } + } + + private static TrustManager[] getNoopTrustManagers() { + return 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 + } + }}; + } + + private static SSLSocketFactory createSocketFactory(TrustManager[] trustManagers) + throws HttpRequestException { + try { + SSLContext context = SSLContext.getInstance("TLS"); + 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 configuring SSL context"); + ioException.initCause(e); + throw new HttpRequestException(ioException); + } } private static HostnameVerifier getTrustedVerifier() { - if (TRUSTED_VERIFIER == null) - TRUSTED_VERIFIER = new HostnameVerifier() { - - public boolean verify(String hostname, SSLSession session) { - return true; - } - }; - - return TRUSTED_VERIFIER; + return new HostnameVerifier() { + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; } private static StringBuilder addPathSeparator(final String baseUrl, @@ -453,32 +503,15 @@ public class HttpRequest { * @throws IOException */ public static void addCert(Certificate ca) throws GeneralSecurityException, IOException { - if (PINNED_CERTS == null) { - PINNED_CERTS = new ArrayList(); - } - PINNED_CERTS.add(ca); - String keyStoreType = KeyStore.getDefaultType(); - KeyStore keyStore = KeyStore.getInstance(keyStoreType); - keyStore.load(null, null); + if (PINNED_CERTS == null) { + PINNED_CERTS = new ArrayList(); + } - for (int i = 0; i < PINNED_CERTS.size(); i++) { - keyStore.setCertificateEntry("CA" + i, PINNED_CERTS.get(i)); - } + PINNED_CERTS.add(ca); - // Create a TrustManager that trusts the CAs in our KeyStore - String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); - tmf.init(keyStore); - - // Create an SSLContext that uses our TrustManager - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, tmf.getTrustManagers(), null); - - if (android.os.Build.VERSION.SDK_INT < 20) { - PINNED_FACTORY = new TLSSocketFactory(sslContext); - } else { - PINNED_FACTORY = sslContext.getSocketFactory(); - } + if (CURRENT_CERT_MODE == CERT_MODE_PINNED) { + SOCKET_FACTORY = createSocketFactory(getPinnedTrustManagers()); + } } /** @@ -1632,6 +1665,7 @@ public class HttpRequest { throw new HttpRequestException(e); } this.requestMethod = method; + this.setupSecurity(); } /** @@ -1645,6 +1679,23 @@ public class HttpRequest { throws HttpRequestException { this.url = url; this.requestMethod = method; + this.setupSecurity(); + } + + private void setupSecurity() { + final HttpURLConnection connection = getConnection(); + + if (!(connection instanceof HttpsURLConnection)) { + return; + } + + if (SOCKET_FACTORY != null) { + ((HttpsURLConnection) connection).setSSLSocketFactory(SOCKET_FACTORY); + } + + if (HOSTNAME_VERIFIER != null) { + ((HttpsURLConnection) connection).setHostnameVerifier(HOSTNAME_VERIFIER); + } } private Proxy createProxy() { @@ -3351,58 +3402,6 @@ public class HttpRequest { return this; } - /** - * Configure HTTPS connection to trust only certain certificates - *

- * This method throws an exception if the current request is not a HTTPS request - * - * @return this request - * @throws HttpRequestException - */ - public HttpRequest pinToCerts() throws HttpRequestException { - final HttpURLConnection connection = getConnection(); - if (connection instanceof HttpsURLConnection) { - ((HttpsURLConnection) connection).setSSLSocketFactory(getPinnedFactory()); - } else { - IOException e = new IOException("You must use a https url to use ssl pinning"); - throw new HttpRequestException(e); - } - return this; - } - - /** - * Configure HTTPS connection to trust all certificates - *

- * This method does nothing if the current request is not a HTTPS request - * - * @return this request - * @throws HttpRequestException - */ - public HttpRequest trustAllCerts() throws HttpRequestException { - final HttpURLConnection connection = getConnection(); - if (connection instanceof HttpsURLConnection) - ((HttpsURLConnection) connection) - .setSSLSocketFactory(getTrustedFactory()); - return this; - } - - /** - * Configure HTTPS connection to trust all hosts using a custom - * {@link HostnameVerifier} that always returns true for each - * host verified - *

- * This method does nothing if the current request is not a HTTPS request - * - * @return this request - */ - public HttpRequest trustAllHosts() { - final HttpURLConnection connection = getConnection(); - if (connection instanceof HttpsURLConnection) - ((HttpsURLConnection) connection) - .setHostnameVerifier(getTrustedVerifier()); - return this; - } - /** * Get the {@link URL} of this request's connection * diff --git a/src/android/com/synconset/cordovahttp/CordovaHttp.java b/src/android/com/synconset/cordovahttp/CordovaHttp.java index 6033383..9bf8314 100644 --- a/src/android/com/synconset/cordovahttp/CordovaHttp.java +++ b/src/android/com/synconset/cordovahttp/CordovaHttp.java @@ -38,10 +38,6 @@ import com.github.kevinsawicki.http.HttpRequest.HttpRequestException; abstract class CordovaHttp { protected static final String TAG = "CordovaHTTP"; protected static final String[] ACCEPTED_CHARSETS = new String[] { HttpRequest.CHARSET_UTF8, HttpRequest.CHARSET_LATIN1 }; - - private static AtomicBoolean sslPinning = new AtomicBoolean(false); - private static AtomicBoolean acceptAllCerts = new AtomicBoolean(false); - private static AtomicBoolean validateDomainName = new AtomicBoolean(true); private static AtomicBoolean disableRedirect = new AtomicBoolean(false); private String urlString; @@ -64,24 +60,6 @@ abstract class CordovaHttp { this.callbackContext = callbackContext; } - public static void enableSSLPinning(boolean enable) { - sslPinning.set(enable); - if (enable) { - acceptAllCerts.set(false); - } - } - - public static void acceptAllCerts(boolean accept) { - acceptAllCerts.set(accept); - if (accept) { - sslPinning.set(false); - } - } - - public static void validateDomainName(boolean accept) { - validateDomainName.set(accept); - } - public static void disableRedirect(boolean disable) { disableRedirect.set(disable); } @@ -122,20 +100,6 @@ abstract class CordovaHttp { return this.callbackContext; } - protected HttpRequest setupSecurity(HttpRequest request) { - if (acceptAllCerts.get()) { - request.trustAllCerts(); - } - if (!validateDomainName.get()) { - request.trustAllHosts(); - } - if (sslPinning.get()) { - request.pinToCerts(); - } - - return request; - } - protected HttpRequest setupRedirect(HttpRequest request) { if (disableRedirect.get()) { request.followRedirects(false); @@ -222,7 +186,6 @@ abstract class CordovaHttp { protected void prepareRequest(HttpRequest request) throws HttpRequestException, JSONException { this.setupRedirect(request); - this.setupSecurity(request); request.readTimeout(this.getRequestTimeout()); request.acceptCharset(ACCEPTED_CHARSETS); diff --git a/src/android/com/synconset/cordovahttp/CordovaHttpPlugin.java b/src/android/com/synconset/cordovahttp/CordovaHttpPlugin.java index 2cd8ded..250b180 100644 --- a/src/android/com/synconset/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/synconset/cordovahttp/CordovaHttpPlugin.java @@ -98,8 +98,14 @@ public class CordovaHttpPlugin extends CordovaPlugin { } else if (action.equals("acceptAllCerts")) { boolean accept = args.getBoolean(0); - CordovaHttp.acceptAllCerts(accept); - CordovaHttp.validateDomainName(!accept); + if (accept) { + HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_TRUSTALL); + HttpRequest.setHostnameVerification(false); + } else { + HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_DEFAULT); + HttpRequest.setHostnameVerification(true); + } + callbackContext.success(); } else if (action.equals("uploadFile")) { String urlString = args.getString(0); @@ -161,9 +167,12 @@ public class CordovaHttpPlugin extends CordovaPlugin { InputStream caInput = new BufferedInputStream(in); HttpRequest.addCert(caInput); } - CordovaHttp.enableSSLPinning(true); + + HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_PINNED); + HttpRequest.setHostnameVerification(true); } else { - CordovaHttp.enableSSLPinning(false); + HttpRequest.setSSLCertMode(HttpRequest.CERT_MODE_DEFAULT); + HttpRequest.setHostnameVerification(true); } } } diff --git a/test/app-test-definitions.js b/test/app-test-definitions.js index 89874cb..d6493f2 100644 --- a/test/app-test-definitions.js +++ b/test/app-test-definitions.js @@ -437,6 +437,17 @@ const tests = [ validationFunc: function(driver, result) { result.type.should.be.equal('resolved'); } + },{ + description: 'should reject when pinned cert does not match received server cert (GET)', + expected: 'rejected: {"status": -1 ...', + before: helpers.enableSSLPinning, + func: function(resolve, reject) { + cordova.plugin.http.get('https://sha512.badssl.com/', {}, {}, resolve, reject); + }, + validationFunc: function(driver, result, targetInfo) { + result.type.should.be.equal('rejected'); + result.data.should.be.eql({ status: -1, error: targetInfo.isAndroid ? 'SSL handshake failed' : 'cancelled' }); + } },{ description: 'should send deeply structured JSON object correctly (POST) #65', expected: 'resolved: {"status": 200, "data": "{\\"data\\": \\"{\\\\"outerObj\\\\":{\\\\"innerStr\\\\":\\\\"testString\\\\",\\\\"innerArr\\\\":[1,2,3]}}\\" ...',