diff --git a/CHANGELOG.md b/CHANGELOG.md index b54596e..ee7deef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.2.0 + +- Feature #420: implement blacklist feature to disable SSL/TLS versions on Android (thanks mobisys Mobile Informationssysteme GmbH) + ## 3.1.1 - Fixed #372: malformed empty multipart request on Android diff --git a/package.json b/package.json index 3fd6091..434751c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-advanced-http", - "version": "3.1.1", + "version": "3.2.0", "description": "Cordova / Phonegap plugin for communicating with HTTP servers using SSL pinning", "scripts": { "updatecert": "node ./scripts/update-e2e-server-cert.js && node ./scripts/update-e2e-client-cert.js", diff --git a/plugin.xml b/plugin.xml index 943bb70..c25c685 100644 --- a/plugin.xml +++ b/plugin.xml @@ -8,6 +8,7 @@ + diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java index bdec147..ce0c40c 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java @@ -47,6 +47,13 @@ public class CordovaHttpPlugin extends CordovaPlugin implements Observer { this.tlsConfiguration.setHostnameVerifier(null); this.tlsConfiguration.setTrustManagers(tmf.getTrustManagers()); + + if (this.preferences.contains("androidblacklisttlsprotocols")) { + this.tlsConfiguration.setBlacklistedProtocols( + this.preferences.getString("androidblacklisttlsprotocols", "").split(",") + ); + } + } catch (Exception e) { Log.e(TAG, "An error occured while loading system's CA certificates", e); } diff --git a/src/android/com/silkimen/http/TLSConfiguration.java b/src/android/com/silkimen/http/TLSConfiguration.java index c33df6c..ed6f0d0 100644 --- a/src/android/com/silkimen/http/TLSConfiguration.java +++ b/src/android/com/silkimen/http/TLSConfiguration.java @@ -13,9 +13,10 @@ import javax.net.ssl.TrustManager; import com.silkimen.http.TLSSocketFactory; public class TLSConfiguration { - private TrustManager[] trustManagers; - private KeyManager[] keyManagers; - private HostnameVerifier hostnameVerifier; + private TrustManager[] trustManagers = null; + private KeyManager[] keyManagers = null; + private HostnameVerifier hostnameVerifier = null; + private String[] blacklistedProtocols = {}; private SSLSocketFactory socketFactory; @@ -33,6 +34,11 @@ public class TLSConfiguration { this.socketFactory = null; } + public void setBlacklistedProtocols(String[] protocols) { + this.blacklistedProtocols = protocols; + this.socketFactory = null; + } + public HostnameVerifier getHostnameVerifier() { return this.hostnameVerifier; } @@ -46,12 +52,7 @@ public class TLSConfiguration { 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(); - } + this.socketFactory = new TLSSocketFactory(context, this.blacklistedProtocols); return this.socketFactory; } catch (GeneralSecurityException e) { diff --git a/src/android/com/silkimen/http/TLSSocketFactory.java b/src/android/com/silkimen/http/TLSSocketFactory.java index 9bc75b1..87d8f20 100644 --- a/src/android/com/silkimen/http/TLSSocketFactory.java +++ b/src/android/com/silkimen/http/TLSSocketFactory.java @@ -5,6 +5,9 @@ import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.stream.Stream; + import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; @@ -12,9 +15,11 @@ import javax.net.ssl.SSLSocketFactory; public class TLSSocketFactory extends SSLSocketFactory { private SSLSocketFactory delegate; + private String[] blacklistedProtocols; - public TLSSocketFactory(SSLContext context) { - delegate = context.getSocketFactory(); + public TLSSocketFactory(SSLContext context, String[] blacklistedProtocols) { + this.delegate = context.getSocketFactory(); + this.blacklistedProtocols = Arrays.stream(blacklistedProtocols).map(String::trim).toArray(String[]::new); } @Override @@ -55,9 +60,18 @@ public class TLSSocketFactory extends SSLSocketFactory { } private Socket enableTLSOnSocket(Socket socket) { - if (socket != null && (socket instanceof SSLSocket)) { - ((SSLSocket) socket).setEnabledProtocols(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }); + if (socket == null || !(socket instanceof SSLSocket)) { + return socket; } + + String[] supported = ((SSLSocket) socket).getSupportedProtocols(); + + String[] filtered = Arrays.stream(supported).filter( + val -> Arrays.stream(this.blacklistedProtocols).noneMatch(val::equals) + ).toArray(String[]::new); + + ((SSLSocket) socket).setEnabledProtocols(filtered); + return socket; } } diff --git a/test/e2e-app-template/config.xml b/test/e2e-app-template/config.xml index d910ef5..ae69196 100644 --- a/test/e2e-app-template/config.xml +++ b/test/e2e-app-template/config.xml @@ -27,4 +27,5 @@ + diff --git a/test/e2e-specs.js b/test/e2e-specs.js index 3f1eec8..717ac98 100644 --- a/test/e2e-specs.js +++ b/test/e2e-specs.js @@ -104,9 +104,17 @@ const helpers = { return buffer; }, + isTlsBlacklistSupported: function () { + if (window.cordova && window.cordova.platformId === 'android') { + return true; + } + + return false; + } }; const messageFactory = { + handshakeFailed: function() { return 'TLS connection could not be established: javax.net.ssl.SSLHandshakeException: Handshake failed' }, sslTrustAnchor: function () { return 'TLS connection could not be established: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.' }, invalidCertificate: function (domain) { return 'The certificate for this server is invalid. You might be connecting to a server that is pretending to be “' + domain + '” which could put your confidential information at risk.' } } @@ -1014,8 +1022,7 @@ const tests = [ before: helpers.setRawSerializer, func: function (resolve, reject, skip) { if (!helpers.isAbortSupported()) { - skip(); - return; + return skip(); } var targetUrl = 'http://httpbin.org/post'; @@ -1036,8 +1043,7 @@ const tests = [ expected: 'rejected: {"status":-8, "error": "Request ...}', func: function (resolve, reject, skip) { if (!helpers.isAbortSupported()) { - skip(); - return; + return skip(); } var url = 'https://httpbin.org/drip?duration=2&numbytes=10&code=200'; var options = { method: 'get', responseType: 'blob' }; @@ -1064,8 +1070,7 @@ const tests = [ expected: 'rejected: {"status":-8, "error": "Request ...}', func: function (resolve, reject, skip) { if (!helpers.isAbortSupported()) { - skip(); - return; + return skip(); } var sourceUrl = 'http://httpbin.org/xml'; var targetPath = cordova.file.cacheDirectory + 'test.xml'; @@ -1097,8 +1102,7 @@ const tests = [ expected: 'rejected: {"status":-8, "error": "Request ...}', func: function (resolve, reject, skip) { if (!helpers.isAbortSupported()) { - skip(); - return; + return skip(); } @@ -1148,6 +1152,21 @@ const tests = [ } } }, + { + description: 'should reject connecting to server with blacklisted SSL version #420', + expected: 'rejected: {"status":-2, ...', + func: function (resolve, reject, skip) { + if (!helpers.isTlsBlacklistSupported()) { + return skip(); + } + + cordova.plugin.http.get('https://tls-v1-0.badssl.com:1010/', {}, {}, resolve, reject); + }, + validationFunc: function (driver, result) { + result.type.should.be.equal('rejected'); + result.data.should.be.eql({ status: -2, error: messageFactory.handshakeFailed() }); + } + }, ]; if (typeof module !== 'undefined' && module.exports) {