diff --git a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java index f09f65ec..9b84da3f 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java +++ b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.realm.react; import android.content.res.AssetManager; diff --git a/react-native/android/src/main/java/io/realm/react/RealmReactPackage.java b/react-native/android/src/main/java/io/realm/react/RealmReactPackage.java index 9fb66f9d..a722b059 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmReactPackage.java +++ b/react-native/android/src/main/java/io/realm/react/RealmReactPackage.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.realm.react; import com.facebook.react.ReactPackage; diff --git a/react-native/android/src/main/java/io/realm/react/util/SSLHelper.java b/react-native/android/src/main/java/io/realm/react/util/SSLHelper.java new file mode 100644 index 00000000..df49dd72 --- /dev/null +++ b/react-native/android/src/main/java/io/realm/react/util/SSLHelper.java @@ -0,0 +1,152 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.react.util; + +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import okhttp3.internal.tls.OkHostnameVerifier; + +public class SSLHelper { + private final static String TAG = "REALM SSLHelper"; + // Holds the certificate chain (per hostname). We need to keep the order of each certificate + // according to it's depth in the chain. The depth of the last + // certificate is 0. The depth of the first certificate is chain + // length - 1. + private static HashMap> ROS_CERTIFICATES_CHAIN; + + // The default Android Trust Manager which uses the default KeyStore to + // validate the certificate chain. + private static X509TrustManager TRUST_MANAGER; + + // Help transform a String PEM representation of the certificate, into + // X509Certificate format. + private static CertificateFactory CERTIFICATE_FACTORY; + + // From Sync implementation: + // A recommended way of using the callback function is to return true + // if preverify_ok = 1 and depth > 0, + // always check the host name if depth = 0, + // and use an independent verification step if preverify_ok = 0. + // + // Another possible way of using the callback is to collect all the + // ROS_CERTIFICATES_CHAIN until depth = 0, and present the entire chain for + // independent verification. + // + // In this implementation we use the second method, since it's more suitable for + // the underlying Java API we need to call to validate the certificate chain. + + public synchronized static boolean certificateVerifier(String serverAddress, String pemData, int depth) { + try { + if (ROS_CERTIFICATES_CHAIN == null) { + ROS_CERTIFICATES_CHAIN = new HashMap<>(); + TRUST_MANAGER = systemDefaultTrustManager(); + CERTIFICATE_FACTORY = CertificateFactory.getInstance("X.509"); + } + + if (!ROS_CERTIFICATES_CHAIN.containsKey(serverAddress)) { + ROS_CERTIFICATES_CHAIN.put(serverAddress, new ArrayList()); + } + + ROS_CERTIFICATES_CHAIN.get(serverAddress).add(pemData); + + if (depth == 0) { + // transform all PEM ROS_CERTIFICATES_CHAIN into Java X509 + // with respecting the order/depth provided from Sync. + List pemChain = ROS_CERTIFICATES_CHAIN.get(serverAddress); + int n = pemChain.size(); + X509Certificate[] chain = new X509Certificate[n]; + for (String pem : pemChain) { + // The depth of the last certificate is 0. + // The depth of the first certificate is chain length - 1. + chain[--n] = buildCertificateFromPEM(pem); + } + + // verify the entire chain + try { + TRUST_MANAGER.checkServerTrusted(chain, "RSA"); + // verify the hostname + boolean isValid = OkHostnameVerifier.INSTANCE.verify(serverAddress, chain[0]); + if (isValid) { + return true; + } else { + Log.e(TAG, "Can not verify the hostname for the host: " + serverAddress); + return false; + } + } catch (CertificateException e) { + Log.e(TAG, "Can not validate SSL chain certificate for the host: " + serverAddress, e); + return false; + } finally { + // don't keep the certificate chain in memory + ROS_CERTIFICATES_CHAIN.remove(serverAddress); + } + } else { + // return true, since the verification will happen for the entire chain + // when receiving the depth == 0 (host certificate) + return true; + } + } catch (Exception e) { + Log.e(TAG, "Error during certificate validation for host: " + serverAddress, e); + return false; + } + } + + // Credit OkHttp https://github.com/square/okhttp/blob/e5c84e1aef9572adb493197c1b6c4e882aca085b/okhttp/src/main/java/okhttp3/OkHttpClient.java#L270 + private static X509TrustManager systemDefaultTrustManager() { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + return (X509TrustManager) trustManagers[0]; + } catch (GeneralSecurityException e) { + throw new AssertionError(); // The system has no TLS. Just give up. + } + } + + private static X509Certificate buildCertificateFromPEM(String pem) throws IOException, CertificateException { + InputStream stream = null; + try { + stream = new ByteArrayInputStream(pem.getBytes("UTF-8")); + return (X509Certificate) CERTIFICATE_FACTORY.generateCertificate(stream); + } finally { + if (stream != null) { + stream.close(); + } + } + } +} diff --git a/react-native/android/src/main/jni/Android.mk b/react-native/android/src/main/jni/Android.mk index ce0554ca..a3d727c3 100644 --- a/react-native/android/src/main/jni/Android.mk +++ b/react-native/android/src/main/jni/Android.mk @@ -45,6 +45,7 @@ LOCAL_SRC_FILES += src/rpc.cpp LOCAL_SRC_FILES += src/jsc/jsc_init.cpp LOCAL_SRC_FILES += src/jsc/jsc_value.cpp LOCAL_SRC_FILES += src/android/io_realm_react_RealmReactModule.cpp +LOCAL_SRC_FILES += src/android/jni_utils.cpp LOCAL_SRC_FILES += src/android/jsc_override.cpp LOCAL_SRC_FILES += src/android/platform.cpp LOCAL_SRC_FILES += src/object-store/src/impl/collection_change_builder.cpp diff --git a/src/android/io_realm_react_RealmReactModule.cpp b/src/android/io_realm_react_RealmReactModule.cpp index e776bca0..78c4e730 100644 --- a/src/android/io_realm_react_RealmReactModule.cpp +++ b/src/android/io_realm_react_RealmReactModule.cpp @@ -23,17 +23,49 @@ #include "io_realm_react_RealmReactModule.h" #include "rpc.hpp" #include "platform.hpp" +#include "jni_utils.hpp" using namespace realm::rpc; +using namespace realm::jni_util; static RPCServer *s_rpc_server; extern bool realmContextInjected; +jclass ssl_helper_class; namespace realm { // set the AssetManager used to access bundled files within the APK void set_asset_manager(AAssetManager* assetManager); } +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) +{ + JNIEnv* env; + if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + else { + JniUtils::initialize(vm, JNI_VERSION_1_6); + } + + // We do lookup the class in this Thread, since FindClass sometimes fails + // when issued from the sync client thread + ssl_helper_class = reinterpret_cast(env->NewGlobalRef(env->FindClass("io/realm/react/util/SSLHelper"))); + + return JNI_VERSION_1_6; +} + +JNIEXPORT void JNI_OnUnload(JavaVM* vm, void*) +{ + JNIEnv* env; + if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return; + } + else { + env->DeleteLocalRef(ssl_helper_class); + JniUtils::release(); + } +} + JNIEXPORT void JNICALL Java_io_realm_react_RealmReactModule_setDefaultRealmFileDirectory (JNIEnv *env, jclass, jstring fileDir, jobject javaAssetManager) { diff --git a/src/android/jni_utils.cpp b/src/android/jni_utils.cpp new file mode 100644 index 00000000..a9ea88da --- /dev/null +++ b/src/android/jni_utils.cpp @@ -0,0 +1,50 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "jni_utils.hpp" + +#include + +using namespace realm::jni_util; + +static std::unique_ptr s_instance; + +void JniUtils::initialize(JavaVM* vm, jint vm_version) noexcept +{ + s_instance = std::unique_ptr(new JniUtils(vm, vm_version)); +} + +void JniUtils::release() +{ + s_instance.release(); +} + +JNIEnv* JniUtils::get_env(bool attach_if_needed) +{ + JNIEnv* env; + if (s_instance->m_vm->GetEnv(reinterpret_cast(&env), s_instance->m_vm_version) != JNI_OK) { + if (attach_if_needed) { + jint ret = s_instance->m_vm->AttachCurrentThread(reinterpret_cast(&env), nullptr); + } + } + + return env; +} + +void JniUtils::detach_current_thread() +{ + s_instance->m_vm->DetachCurrentThread(); +} diff --git a/src/android/jni_utils.hpp b/src/android/jni_utils.hpp new file mode 100644 index 00000000..9c8869fc --- /dev/null +++ b/src/android/jni_utils.hpp @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef REALM_JNI_UTIL_JNI_UTILS_HPP +#define REALM_JNI_UTIL_JNI_UTILS_HPP + +#include + +#include + +namespace realm { +namespace jni_util { + +// Util functions for JNI. +class JniUtils { +public: + ~JniUtils() + { + } + + // Call this only once in JNI_OnLoad. + static void initialize(JavaVM* vm, jint vm_version) noexcept; + // Call this in JNI_OnUnload. + static void release(); + // When attach_if_needed is false, returns the JNIEnv if there is one attached to this thread. Assert if there is + // none. When attach_if_needed is true, try to attach and return a JNIEnv if necessary. + static JNIEnv* get_env(bool attach_if_needed = false); + // Detach the current thread from the JVM. Only required for C++ threads that where attached in the first place. + // Failing to do so is a resource leak. + static void detach_current_thread(); + +private: + JniUtils(JavaVM* vm, jint vm_version) noexcept + : m_vm(vm) + , m_vm_version(vm_version) + { + } + + JavaVM* m_vm; + jint m_vm_version; +}; + +} // namespace realm +} // namespace jni_util + +#endif // REALM_JNI_UTIL_JNI_UTILS_HPP diff --git a/src/js_sync.hpp b/src/js_sync.hpp index 36826965..501fc1a3 100644 --- a/src/js_sync.hpp +++ b/src/js_sync.hpp @@ -33,6 +33,14 @@ #include "realm/util/logger.hpp" #include "realm/util/uri.hpp" +#if REALM_ANDROID +#include +#include "./android/io_realm_react_RealmReactModule.h" +#include "./android/jni_utils.hpp" + +extern jclass ssl_helper_class; +#endif + namespace realm { namespace js { @@ -303,7 +311,6 @@ public: bool operator ()(const std::string& server_address, sync::Session::port_type server_port, const char* pem_data, size_t pem_size, int preverify_ok, int depth) { const std::string pem_certificate {pem_data, pem_size}; - { std::lock_guard lock {*m_mutex}; m_ssl_certificate_callback_done = false; @@ -824,7 +831,33 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr SSLVerifyCallbackSyncThreadFunctor ssl_verify_functor {ctx, Value::validated_to_function(ctx, ssl_verify_func)}; ssl_verify_callback = std::move(ssl_verify_functor); } - +#if REALM_ANDROID + // For React Native Android, if the user didn't define the ssl_verify_callback, we provide a default + // implementation for him, otherwise all SSL validation will fail, since the Sync client doesn't have + // access to the Android Keystore. + // This default implementation will perform a JNI call to invoke a Java method defined at the `SSLHelper` + // to perform the certificate verification. + else { + auto ssl_verify_functor = + [](const std::string server_address, realm::sync::Session::port_type server_port, + const char* pem_data, size_t pem_size, int preverify_ok, int depth) { + JNIEnv* env = realm::jni_util::JniUtils::get_env(true); + static jmethodID java_certificate_verifier = env->GetStaticMethodID(ssl_helper_class, "certificateVerifier", "(Ljava/lang/String;Ljava/lang/String;I)Z"); + jstring jserver_address = env->NewStringUTF(server_address.c_str()); + // deep copy the pem_data into a string so DeleteLocalRef delete the local reference not the original const char + std::string pem(pem_data, pem_size); + jstring jpem = env->NewStringUTF(pem.c_str()); + + bool isValid = env->CallStaticBooleanMethod(ssl_helper_class, java_certificate_verifier, + jserver_address, + jpem, depth) == JNI_TRUE; + env->DeleteLocalRef(jserver_address); + env->DeleteLocalRef(jpem); + return isValid; + }; + ssl_verify_callback = std::move(ssl_verify_functor); + } +#endif bool is_partial = false; ValueType partial_value = Object::get_property(ctx, sync_config_object, "partial"); if (!Value::is_undefined(ctx, partial_value)) {