Add a certificate validation using Android Keystore for RN (#1761)
* add a certificate validation using Android Keystore for RN
This commit is contained in:
parent
032ee3d178
commit
1c95abbff1
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String, List<String>> 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<String>());
|
||||
}
|
||||
|
||||
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<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<jclass>(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)
|
||||
{
|
||||
|
|
|
@ -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 <memory>
|
||||
|
||||
using namespace realm::jni_util;
|
||||
|
||||
static std::unique_ptr<JniUtils> s_instance;
|
||||
|
||||
void JniUtils::initialize(JavaVM* vm, jint vm_version) noexcept
|
||||
{
|
||||
s_instance = std::unique_ptr<JniUtils>(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<void**>(&env), s_instance->m_vm_version) != JNI_OK) {
|
||||
if (attach_if_needed) {
|
||||
jint ret = s_instance->m_vm->AttachCurrentThread(reinterpret_cast<void**>(&env), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
void JniUtils::detach_current_thread()
|
||||
{
|
||||
s_instance->m_vm->DetachCurrentThread();
|
||||
}
|
|
@ -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 <jni.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
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
|
|
@ -33,6 +33,14 @@
|
|||
#include "realm/util/logger.hpp"
|
||||
#include "realm/util/uri.hpp"
|
||||
|
||||
#if REALM_ANDROID
|
||||
#include <jni.h>
|
||||
#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<std::mutex> lock {*m_mutex};
|
||||
m_ssl_certificate_callback_done = false;
|
||||
|
@ -824,7 +831,33 @@ void SyncClass<T>::populate_sync_config(ContextType ctx, ObjectType realm_constr
|
|||
SSLVerifyCallbackSyncThreadFunctor<T> 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)) {
|
||||
|
|
Loading…
Reference in New Issue