Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add encrypted SQLCipher WatermelonDB JSI #1635

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions WatermelonDB.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "json"

package = JSON.parse(File.read(File.join(__dir__, 'package.json')))

isEncryptedDB = $isEncryptedDB || false
Pod::Spec.new do |s|
s.name = "WatermelonDB"
s.version = package["version"]
Expand All @@ -26,13 +26,26 @@ Pod::Spec.new do |s|
# I don't think this is a correct fix, but… seems to work?
# 'OTHER_SWIFT_FLAGS' => '-Xcc -Wno-error=non-modular-include-in-framework-module'
}


s.requires_arc = true
# simdjson is annoyingly slow without compiler optimization, disable for debugging
s.compiler_flags = '-Os'

s.dependency "React"

s.libraries = 'sqlite3'
# s.libraries = 'sqlite3'
if isEncryptedDB
print "Using encrypted DB\n"
s.xcconfig = {
'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SQLITE_HAS_CODEC=1',
'OTHER_CFLAGS' => '$(inherited) -DSQLITE_HAS_CODEC=1 -DSQLITE_TEMP_STORE=2',
}
s.dependency "SQLCipher"
else
s.libraries = "sqlite3"
end


# NOTE: This dependency doesn't seem to be needed anymore (tested on RN 0.66, 0.71), file an issue
# if this causes issues for you
Expand Down
45 changes: 45 additions & 0 deletions docs-website/docs/docs/Installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,48 @@ backtrace:

</details>

## Using Encrypted Database (With SQLCipher)

Note that this is only supported on JSI, and not on the legacy bridge.
We recommend you to enable JSI for both platforms, but if you choose to enable only for one, note that the other one will not support encrypted databases.

Steps:

1. Go to your `Podfile` and add the following line:

```ruby
$isEncryptedDB = true
```

At the top of the file.

2. Go to your `build.gradle` and add the following line:

```gradle
ext {
isEncryptedDB = true
}
```

3. Run `pod install`
4. Go to Android Studio and sync gradle files

Great now you installed SQLCipher, but you still need to set a password for your database.

in your `index.native.js` file, add the following line:

```js
const adapter = new SQLiteAdapter({
...,
jsi: true, // will only work when JSI is enabled.
passphrase: ... // your password
});
```

Thats All!

Note that you CAN NOT change the password of an existing database, you will need to create a new one, and that you can not encrypt an existing DB as well.

## Web setup

This guide assumes you use Webpack as your bundler.
Expand All @@ -247,6 +289,8 @@ This guide assumes you use Webpack as your bundler.
npm install -D @babel/plugin-transform-runtime
```

````

2. Add ES7 support to your `.babelrc` file:
```json
{
Expand Down Expand Up @@ -315,3 +359,4 @@ You only need this if you want to use WatermelonDB in NodeJS with SQLite (e.g. f
## Next steps

➡️ After Watermelon is installed, [**set it up**](./Setup.md)
````
17 changes: 15 additions & 2 deletions native/android-jsi/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3"
def DEFAULT_MIN_SDK_VERSION = 16
def DEFAULT_TARGET_SDK_VERSION = 28
def DEFAULT_NDK_VERSION = "20.1.5948944"

def isUsingEncryptedDB = rootProject.hasProperty('encryptedDB') && rootProject.encryptedDB
android {
compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION
buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION
ndkVersion rootProject.hasProperty('ndkVersion') ? rootProject.ndkVersion : DEFAULT_NDK_VERSION

buildFeatures {
prefab isUsingEncryptedDB
}

defaultConfig {
minSdkVersion rootProject.hasProperty('minSdkVersion') ? rootProject.minSdkVersion : DEFAULT_MIN_SDK_VERSION
targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION
Expand All @@ -32,6 +36,10 @@ android {
// libwatermelondb-jsi.so (std::__ndk1::basic_ostream<char, std::__ndk1::char_traits<char>>::operator<<(long long)+124)
// libwatermelondb-jsi.so (std::__ndk1::basic_string<char, watermelondb::to_json_string<simdjson::fallback::ondemand::value&>::char_traits<char>, watermelondb::to_json_string<simdjson::fallback::ondemand::value&>::allocator<char>> watermelondb::to_json_string<simdjson::fallback::ondemand::value&>(simdjson::fallback::ondemand::value&&&)+3486)
// arguments "-DANDROID_STL=c++_shared"
if(isUsingEncryptedDB){
arguments '-DENCRYPTED_DB=1', '-DANDROID_STL=c++_shared'
cFlags '-DSQLITE_HAS_CODEC', '-DSQLITE_TEMP_STORE=2'
}
}
}
}
Expand All @@ -44,11 +52,16 @@ android {

packagingOptions {
// TODO: This only seems necessary if c++_shared is enabled in cmake
// pickFirst '**/libc++_shared.so'
if(isUsingEncryptedDB){
pickFirst '**/libc++_shared.so'
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
if (isUsingEncryptedDB) {
implementation 'com.android.ndk.thirdparty:openssl:1.1.1l-beta-1'
}
implementation 'com.facebook.react:react-native:+'
}
36 changes: 25 additions & 11 deletions native/android-jsi/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ endif()
# -------------------------------------------------
# Header search paths
# FIXME: <simdjson/simdjson.h> should work…

set(SQLITE_VERSION sqlite-amalgamation-3400100)
if(${ENCRYPTED_DB})
set(SQLITE_PATH ${NODE_MODULES_PATH_WM}@nozbe/watermelondb/native/sqlite-cipher-amalgamation/)
else()
set(SQLITE_PATH ${NODE_MODULES_PATH_WM}@nozbe/sqlite/sqlite-amalgamation-3400100/)
endif()

include_directories(
../../../../shared
${NODE_MODULES_PATH_WM}/@nozbe/sqlite/${SQLITE_VERSION}/
${SQLITE_PATH}
${NODE_MODULES_PATH_WM}/@nozbe/simdjson/src/
${NODE_MODULES_PATH_RN}/react-native/React
${NODE_MODULES_PATH_RN}/react-native/React/Base
Expand All @@ -52,12 +55,12 @@ include_directories(

# -------------------------------------------------
# Build configuration

#add_definitions(
# -DFOLLY_USE_LIBCPP=1
# -DFOLLY_NO_CONFIG=1
# -DFOLLY_HAVE_MEMRCHR=1
#)
if(${ENCRYPTED_DB})
add_definitions(
-DSQLITE_HAS_CODEC
-DSQLITE_TEMP_STORE=2
)
endif()

# simdjson is slow without optimization
set(CMAKE_CXX_FLAGS_DEBUG "-Os") # comment out for JSI debugging
Expand All @@ -71,10 +74,12 @@ set(CMAKE_CXX_FLAGS_RELEASE "-Os")

file(GLOB ANDROID_JSI_SRC_FILES ./*.cpp)
file(GLOB SHARED_SRC_FILES ../../../../shared/*.cpp)
# -------------------------------------------------

add_library(watermelondb-jsi SHARED
# vendor files
${NODE_MODULES_PATH_WM}/@nozbe/sqlite/${SQLITE_VERSION}/sqlite3.c
${SQLITE_PATH}/sqlite3.c
${SQLITE_PATH}/sqlite3.h
${NODE_MODULES_PATH_WM}/@nozbe/simdjson/src/simdjson.cpp
# our sources
${ANDROID_JSI_SRC_FILES}
Expand All @@ -83,7 +88,16 @@ add_library(watermelondb-jsi SHARED
# seems wrong to compile a file that's already getting compiled as part of the app, but ¯\_(ツ)_/¯
${NODE_MODULES_PATH_RN}/react-native/ReactCommon/jsi/jsi/jsi.cpp)

if(${ENCRYPTED_DB})
find_package(openssl REQUIRED CONFIG)
set(openSSLLib openssl::crypto openssl::ssl)
endif()



target_link_libraries(watermelondb-jsi
# link with these libraries:
android
log)
log
${openSSLLib}
)
25 changes: 13 additions & 12 deletions native/shared/Database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,33 @@ namespace watermelondb {
using platform::consoleError;
using platform::consoleLog;

Database::Database(jsi::Runtime *runtime, std::string path, bool usesExclusiveLocking) : runtime_(runtime), mutex_() {
db_ = std::make_unique<SqliteDb>(path);
Database::Database(jsi::Runtime *runtime, std::string path, std::string password, bool usesExclusiveLocking)
: runtime_(runtime), mutex_() {
db_ = std::make_unique<SqliteDb>(path, password.c_str());

std::string initSql = "";

// FIXME: On Android, Watermelon often errors out on large batches with an IO error, because it
// can't find a temp store... I tried setting sqlite3_temp_directory to /tmp/something, but that
// didn't work. Setting temp_store to memory seems to fix the issue, but causes a significant
// slowdown, at least on iOS (not confirmed on Android). Worth investigating if the slowdown is
// also present on Android, and if so, investigate the root cause. Perhaps we need to set the temp
// directory by interacting with JNI and finding a path within the app's sandbox?
#ifdef ANDROID
// FIXME: On Android, Watermelon often errors out on large batches with an IO error, because it
// can't find a temp store... I tried setting sqlite3_temp_directory to /tmp/something, but that
// didn't work. Setting temp_store to memory seems to fix the issue, but causes a significant
// slowdown, at least on iOS (not confirmed on Android). Worth investigating if the slowdown is
// also present on Android, and if so, investigate the root cause. Perhaps we need to set the temp
// directory by interacting with JNI and finding a path within the app's sandbox?
#ifdef ANDROID
initSql += "pragma temp_store = memory;";
#endif
#endif

initSql += "pragma journal_mode = WAL;";

// set timeout before SQLITE_BUSY error is returned
initSql += "pragma busy_timeout = 5000;";

#ifdef ANDROID
#ifdef ANDROID
// NOTE: This was added in an attempt to fix mysterious `database disk image is malformed` issue when using
// headless JS services
// NOTE: This slows things down
initSql += "pragma synchronous = FULL;";
#endif
#endif
if (usesExclusiveLocking) {
// this seems to fix the headless JS service issue but breaks if you have multiple readers
initSql += "pragma locking_mode = EXCLUSIVE;";
Expand Down
22 changes: 11 additions & 11 deletions native/shared/Database.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#pragma once

#include <jsi/jsi.h>
#include <unordered_map>
#include <unordered_set>
#include <mutex>
#include <sqlite3.h>
#import <jsi/jsi.h>
#import <mutex>
#import <sqlite3.h>
#import <unordered_map>
#import <unordered_set>

// FIXME: Make these paths consistent across platforms
#if __ANDROID__
Expand All @@ -16,17 +16,17 @@
#include <simdjson/simdjson.h>
#endif

#include "Sqlite.h"
#include "DatabasePlatform.h"
#import "DatabasePlatform.h"
#import "Sqlite.h"

using namespace facebook;

namespace watermelondb {

class Database : public jsi::HostObject {
public:
public:
static void install(jsi::Runtime *runtime);
Database(jsi::Runtime *runtime, std::string path, bool usesExclusiveLocking);
Database(jsi::Runtime *runtime, std::string path, std::string password, bool usesExclusiveLocking);
~Database();
void destroy();

Expand All @@ -43,7 +43,7 @@ class Database : public jsi::HostObject {
jsi::Value getLocal(jsi::String &key);
void executeMultiple(std::string sql);

private:
private:
bool initialized_;
bool isDestroyed_;
std::mutex mutex_;
Expand All @@ -55,7 +55,7 @@ class Database : public jsi::HostObject {
jsi::Runtime &getRt();
jsi::JSError dbError(std::string description);

sqlite3_stmt* prepareQuery(std::string sql);
sqlite3_stmt *prepareQuery(std::string sql);
void bindArgs(sqlite3_stmt *statement, jsi::Array &arguments);
std::string bindArgsAndReturnId(sqlite3_stmt *statement, simdjson::ondemand::array &args);
SqliteStatement executeQuery(std::string sql, jsi::Array &arguments);
Expand Down
10 changes: 5 additions & 5 deletions native/shared/DatabaseBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ using platform::consoleLog;
void Database::install(jsi::Runtime *runtime) {
jsi::Runtime &rt = *runtime;
auto globalObject = rt.global();
createMethod(rt, globalObject, "nativeWatermelonCreateAdapter", 2, [runtime](jsi::Runtime &rt, const jsi::Value *args) {
createMethod(rt, globalObject, "nativeWatermelonCreateAdapter", 3, [runtime](jsi::Runtime &rt, const jsi::Value *args) {
std::string dbPath = args[0].getString(rt).utf8(rt);
bool usesExclusiveLocking = args[1].getBool();
std::string password = args[1].getString(rt).utf8(rt);
bool usesExclusiveLocking = args[2].getBool();

jsi::Object adapter(rt);

std::shared_ptr<Database> database = std::make_shared<Database>(runtime, dbPath, usesExclusiveLocking);
std::shared_ptr<Database> database = std::make_shared<Database>(runtime, dbPath, password, usesExclusiveLocking);
adapter.setProperty(rt, "database", jsi::Object::createFromHostObject(rt, database));

// FIXME: Important hack!
Expand Down Expand Up @@ -187,7 +188,7 @@ void Database::install(jsi::Runtime *runtime) {
});
createMethod(rt, adapter, "unsafeLoadFromSync", 4, [database](jsi::Runtime &rt, const jsi::Value *args) {
assert(database->initialized_);
auto jsonId = (int) args[0].getNumber();
auto jsonId = (int)args[0].getNumber();
auto schema = args[1].getObject(rt);
auto preamble = args[2].getString(rt).utf8(rt);
auto postamble = args[3].getString(rt).utf8(rt);
Expand Down Expand Up @@ -228,4 +229,3 @@ void Database::install(jsi::Runtime *runtime) {


} // namespace watermelondb

22 changes: 18 additions & 4 deletions native/shared/Sqlite.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ std::string resolveDatabasePath(std::string path) {
}
}

SqliteDb::SqliteDb(std::string path) {
SqliteDb::SqliteDb(std::string path, const char *password) {
consoleLog("Will open database...");
platform::initializeSqlite();
#ifndef ANDROID
#ifndef ANDROID
assert(sqlite3_threadsafe());
#endif
#endif

auto resolvedPath = resolveDatabasePath(path);
int openResult = sqlite3_open(resolvedPath.c_str(), &sqlite);
Expand All @@ -37,7 +37,21 @@ SqliteDb::SqliteDb(std::string path) {
}
}
assert(sqlite != nullptr);

#ifdef SQLITE_HAS_CODEC
if (password != nullptr && strlen(password) > 0) {
consoleLog("##### Will set key...");
sqlite3_key(sqlite, password, (int)strlen(password));
int rc = sqlite3_exec(sqlite, "SELECT count(*) FROM sqlite_master;", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
consoleError("Failed to open encrypted database - " + std::string(sqlite3_errmsg(sqlite)));
sqlite3_close(sqlite);
sqlite = nullptr;
throw new std::runtime_error("Failed to open encrypted database - " + std::string(sqlite3_errmsg(sqlite)));
}
consoleLog("##### Key set!");
}
#endif
assert(sqlite != nullptr);
consoleLog("Opened database at " + resolvedPath);
}

Expand Down
Loading