diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java index a5c68b689..3928905d4 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java @@ -19,6 +19,11 @@ class RNFetchBlobConfig { public Boolean followRedirect = true; public ReadableArray binaryContentTypes = null; + public Boolean nqmTrustSystem; + public ReadableArray nqmCACertificate; + public String nqmClientCertificate; + public String nqmClientCertificatePassword; + RNFetchBlobConfig(ReadableMap options) { if(options == null) return; @@ -46,6 +51,13 @@ class RNFetchBlobConfig { if(options.hasKey("timeout")) { this.timeout = options.getInt("timeout"); } + + // Default to not using OS trust store. + this.nqmTrustSystem = options.hasKey("nqmTrustSystem") ? options.getBoolean("nqmTrustSystem") : false; + // Cache the passed certificate data. + this.nqmCACertificate = options.hasKey("nqmCACertificate") ? options.getArray("nqmCACertificate") : null; + this.nqmClientCertificate = options.hasKey("nqmClientCertificate") ? options.getString("nqmClientCertificate") : null; + this.nqmClientCertificatePassword = options.hasKey("nqmClientCertificatePassword") ? options.getString("nqmClientCertificatePassword") : null; } } diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java index 060538b48..ff88c542e 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java @@ -224,8 +224,10 @@ else if(this.options.fileCache) OkHttpClient.Builder clientBuilder; try { - // use trusty SSL socket - if (this.options.trusty) { + if (this.options.nqmClientCertificate != null) { + clientBuilder = RNFetchBlobUtils.getClientCertCAOkHttpClient(this.options.nqmTrustSystem, this.options.nqmCACertificate, this.options.nqmClientCertificate, this.options.nqmClientCertificatePassword, client); + } else if (this.options.trusty) { + // use trusty SSL socket clientBuilder = RNFetchBlobUtils.getUnsafeOkHttpClient(client); } else { clientBuilder = client.newBuilder(); diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobTrustManager.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobTrustManager.java new file mode 100644 index 000000000..0fbbd1a76 --- /dev/null +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobTrustManager.java @@ -0,0 +1,95 @@ +package com.RNFetchBlob; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.cert.*; +import java.util.Arrays; +import java.util.List; + +/** + * A custom X509TrustManager implementation that trusts a specified server certificate in addition + * to those that are in the system TrustStore. + */ +public class RNFetchBlobTrustManager implements X509TrustManager { + + private final X509TrustManager originalX509TrustManager; + private Boolean trustSystem; + private final KeyStore trustStore; + + /** + * @param trustStore A KeyStore containing the server certificate that should be trusted + * @throws NoSuchAlgorithmException + * @throws KeyStoreException + */ + public RNFetchBlobTrustManager(boolean trustSystem, KeyStore trustStore) throws NoSuchAlgorithmException, KeyStoreException { + this.trustSystem = trustSystem; + this.trustStore = trustStore; + + TrustManagerFactory originalTrustManagerFactory = TrustManagerFactory.getInstance("X509"); + originalTrustManagerFactory.init((KeyStore) null); + + TrustManager[] originalTrustManagers = originalTrustManagerFactory.getTrustManagers(); + this.originalX509TrustManager = (X509TrustManager) originalTrustManagers[0]; + } + + /** + * No-op. Never invoked by client, only used in server-side implementations + * @return + */ + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + /** + * No-op. Never invoked by client, only used in server-side implementations + * @return + */ + public void checkClientTrusted(X509Certificate[] chain, String authType) throws java.security.cert.CertificateException { + } + + private void doCustomCheck(X509Certificate[] chain, String authType) throws java.security.cert.CertificateException { + try { + CertPathValidator validator = CertPathValidator.getInstance("PKIX"); + CertificateFactory factory = CertificateFactory.getInstance("X509"); + CertPath certPath = factory.generateCertPath(Arrays.asList(chain)); + PKIXParameters params = new PKIXParameters(this.trustStore); + params.setRevocationEnabled(false); + validator.validate(certPath, params); + } catch(Exception ex) { + throw new java.security.cert.CertificateException(ex.getMessage()); + } + } + + /** + * Given the partial or complete certificate chain provided by the peer, + * build a certificate path to a trusted root and return if it can be validated and is trusted + * for client SSL authentication based on the authentication type. The authentication type is + * determined by the actual certificate used. For instance, if RSAPublicKey is used, the authType should be "RSA". + * Checking is case-sensitive. + * If `trustSystem` is set, defers to the default trust manager first, checks the cert supplied in the ctor if + * that fails. + * @param chain the server's certificate chain + * @param authType the authentication type based on the client certificate + * @throws java.security.cert.CertificateException + */ + public void checkServerTrusted(X509Certificate[] chain, String authType) throws java.security.cert.CertificateException { + if (this.trustSystem) { + try { + this.originalX509TrustManager.checkServerTrusted(chain, authType); + } catch(CertificateException originalException) { + try { + this.doCustomCheck(chain, authType); + } catch(Exception ex) { + throw originalException; + } + } + } else { + this.doCustomCheck(chain, authType); + } + } +} diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java index 6dbcbe7c7..ca2f854fa 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java @@ -1,11 +1,17 @@ package com.RNFetchBlob; +import android.content.res.AssetManager; +import android.content.Context; +import android.util.Log; + import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; +import java.security.KeyStore; import java.security.MessageDigest; import java.security.cert.CertificateException; +import java.io.*; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; @@ -13,9 +19,16 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; + +import android.util.Base64; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import okhttp3.OkHttpClient; +import com.facebook.react.bridge.ReadableArray; public class RNFetchBlobUtils { @@ -93,4 +106,128 @@ public boolean verify(String hostname, SSLSession session) { throw new RuntimeException(e); } } + + /** + * Produces a KeyStore from a String containing a PEM certificate (typically, the server's CA certificate) + * @param certificateString A String containing the PEM-encoded certificate + * @return a KeyStore (to be used as a trust store) that contains the certificate + * @throws Exception + */ + private static KeyStore loadPEMTrustStore(ReadableArray caCertificates) throws Exception { + + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null); + + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + for(int i = 0; i< caCertificates.size();i++) { + String certificateString = caCertificates.getString(i); + byte[] der = loadPemCertificate(new ByteArrayInputStream(certificateString.getBytes())); + ByteArrayInputStream derInputStream = new ByteArrayInputStream(der); + X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream); + String alias = cert.getSubjectX500Principal().getName(); + + trustStore.setCertificateEntry(alias, cert); + } + + return trustStore; + } + + /** + * Reads and decodes a base-64 encoded DER certificate (a .pem certificate), typically the server's CA cert. + * @param certificateStream an InputStream from which to read the cert + * @return a byte[] containing the decoded certificate + * @throws IOException + */ + private static byte[] loadPemCertificate(InputStream certificateStream) throws IOException { + + byte[] der = null; + BufferedReader br = null; + + try { + StringBuilder buf = new StringBuilder(); + br = new BufferedReader(new InputStreamReader(certificateStream)); + + String line = br.readLine(); + while(line != null) { + if(!line.startsWith("--")){ + buf.append(line); + } + line = br.readLine(); + } + + String pem = buf.toString(); + der = Base64.decode(pem, Base64.DEFAULT); + + } finally { + if(br != null) { + br.close(); + } + } + + return der; + } + + /** + * Produces a KeyStore from a PKCS12 (.p12) certificate file, typically the client certificate + * @param p12Base64 The base64-encoded p12 file. + * @param clientCertPassword Password for the certificate + * @return A KeyStore containing the certificate from the certificateFile + * @throws Exception + */ + private static KeyStore loadPKCS12KeyStore(String p12Base64, String clientCertPassword) throws Exception { + KeyStore keyStore = null; + byte[] p12Decoded = Base64.decode(p12Base64, Base64.DEFAULT); + InputStream fis = new ByteArrayInputStream(p12Decoded); + try { + keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(fis, clientCertPassword.toCharArray()); + } finally { + try { + if(fis != null) { + fis.close(); + } + } catch(IOException ex) { + // ignore + } + } + return keyStore; + } + + public static OkHttpClient.Builder getClientCertCAOkHttpClient(boolean trustSystem, ReadableArray caCertificates, String p12Base64ClientCertificate, String clientCertificatePassword, OkHttpClient client) { + try { + Log.i("TOBY", "in getClientCertCAOkHttpClient"); + + // Create a trust store from the CA certificates. + KeyStore trustStore = loadPEMTrustStore(caCertificates); + TrustManager[] trustManagers = {new RNFetchBlobTrustManager(trustSystem, trustStore)}; + + // Load the client certificate. + KeyStore keyStore = loadPKCS12KeyStore(p12Base64ClientCertificate, clientCertificatePassword); + KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509"); + kmf.init(keyStore, clientCertificatePassword.toCharArray()); + KeyManager[] keyManagers = kmf.getKeyManagers(); + + // Create a context using the custom key and trust managers. + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagers, trustManagers, null); + + // Create an ssl socket factory with our all-trusting manager + final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + OkHttpClient.Builder builder = client.newBuilder(); + builder.sslSocketFactory(sslSocketFactory); + + // builder.hostnameVerifier(new HostnameVerifier() { + // @Override + // public boolean verify(String hostname, SSLSession session) { + // return true; + // } + // }); + + return builder; + } catch (Exception e) { + throw new RuntimeException(e); + } + } }