Skip to content
Merged
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
79 changes: 43 additions & 36 deletions .docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,56 +29,63 @@ Fix it one of these ways:

> Note: the `ndkVersion` patch that circulates for this error targets the old `react-native-quick-base64` `2.2.2` `android/build.gradle`. Version `3.0.0`+ has no `build.gradle` (it is CMake only), so that patch does not apply.

## Android build errors
## `libcrypto.so` / `libssl.so` collision on Android

If you get an error similar to this:
If a build fails like this:

```
Execution failed for task ':app:mergeDebugNativeLibs'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.MergeNativeLibsTask$MergeNativeLibsTaskWorkAction
> 2 files found with path 'lib/arm64-v8a/libcrypto.so' from inputs:
- /Users/osp/Developer/mac_test/node_modules/react-native-quick-crypto/android/build/intermediates/library_jni/debug/jni/arm64-v8a/libcrypto.so
- /Users/osp/.gradle/caches/transforms-3/e13f88164840fe641a466d05cd8edac7/transformed/jetified-flipper-0.182.0/jni/arm64-v8a/libcrypto.so
> 2 files found with path 'lib/arm64-v8a/libcrypto.so'
```

It means you have a transitive dependency where two libraries depend on OpenSSL and are generating a `libcrypto.so` file. You can get around this issue by adding the following in your `app/build.gradle`:
two libraries in your app are each shipping their own OpenSSL. The usual second
one is `@op-engineering/op-sqlite` with `sqlcipher: true`.

Since `1.1.7`, `react-native-quick-crypto` links OpenSSL **statically** into
`libQuickCrypto.so` and keeps its symbols off the global symbol table, so it no
longer ships `libcrypto.so` or `libssl.so` at all. Upgrading is the fix.

<h4>
React Native  <a href="#"><img src="./img/react-native.png" height="15" /></a>
</h4>
### On older versions

`android/app/build.gradle` file
Do **not** reach for `pickFirst` on its own:

```groovy
packagingOptions {
// Should prevent clashes with other libraries that use OpenSSL
pickFirst '**/libcrypto.so'
pickFirst '**/libcrypto.so' // builds, then crashes at runtime
}
```

<h4>
Expo  <a href="#"><img src="./img/expo.png" height="12" /></a>
</h4>

`app.json` file

```diff
...
plugins: [
...
+ [
+ 'expo-build-properties',
+ {
+ android: {
+ packagingOptions: {
+ pickFirst: ['**/libcrypto.so'],
+ },
+ },
+ },
+ ],
],
It makes the build succeed by dropping one of the two OpenSSL builds, leaving
whichever library lost the coin toss bound to a version it was not compiled
against. Expect corruption or `SIGSEGV` inside OpenSSL rather than a clean
error.

If you cannot upgrade, force both libraries onto a single OpenSSL build first,
so that whichever copy `pickFirst` keeps is the one both sides compiled against.
Both `react-native-quick-crypto` and `@op-engineering/op-sqlite` consume the
same artifact, just at different versions:

```groovy
// android/build.gradle
allprojects {
configurations.all {
resolutionStrategy.force 'io.github.ronickg:openssl:3.6.2-1'
}
}
```

> This caused by flipper which also depends on OpenSSL
Pick the newest version any of your dependencies asks for — OpenSSL keeps ABI
compatibility across `3.x`, so the older consumer keeps working against the
newer build, but not the reverse.

## `libcrypto` symbol collision on iOS

The same problem has an iOS form: another dependency statically links its own
OpenSSL into your app binary, `ld` resolves each OpenSSL symbol first-wins, and
`react-native-quick-crypto` ends up calling into a foreign OpenSSL with
different struct layouts. It typically surfaces as `EXC_BAD_ACCESS` on the first
`subtle.*` call.

This just tells Gradle to grab whatever OpenSSL version it finds first and link against that, but as you can imagine this is not correct if the packages depend on different OpenSSL versions (quick-crypto depends on `com.android.ndk.thirdparty:openssl:1.1.1q-beta-1`). You should make sure all the OpenSSL versions match and you have no conflicts or errors.
Since `1.1.7` this cannot happen: the bundled OpenSSL's symbols are renamed to
`rnqc_*` and the original names are local to the archive, so neither copy can
see the other. Upgrade to fix it.
6 changes: 1 addition & 5 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- OpenSSL-Universal (3.6.2000)
- QuickCrypto (1.1.6):
- boost
- DoubleConversion
Expand All @@ -78,7 +77,6 @@ PODS:
- glog
- hermes-engine
- NitroModules
- OpenSSL-Universal (~> 3.6.2000)
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
Expand Down Expand Up @@ -2665,7 +2663,6 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- MMKVCore
- OpenSSL-Universal
- SocketRocket

EXTERNAL SOURCES:
Expand Down Expand Up @@ -2840,8 +2837,7 @@ SPEC CHECKSUMS:
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e
NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3
OpenSSL-Universal: ecee7b138fa75a74ecf00d7ffd248fb584739b9e
QuickCrypto: 149336b1a5257bad6ee521e0de3d79cd17dcc116
QuickCrypto: 736e5af30a4e875009b62f08139d38a19c01493b
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
Expand Down
60 changes: 59 additions & 1 deletion packages/react-native-quick-crypto/QuickCrypto.podspec
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require "json"
require "digest"
require "shellwords"

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

Expand All @@ -19,6 +21,44 @@ Pod::Spec.new do |s|

s.source = { :git => "https://github.com/margelo/react-native-quick-crypto.git", :tag => "#{s.version}" }

# Prebuilt static OpenSSL whose every global symbol is renamed to rnqc_*, with
# the original names demoted to non-external. QuickCrypto is source-built, so
# its OpenSSL references are resolved when the *app* links; without the rename,
# any other library that statically embeds OpenSSL wins the first-come symbol
# resolution and QuickCrypto silently runs on a foreign OpenSSL. See
# scripts/build-openssl-apple.sh and issue #1059.
#
# Downloaded during podspec evaluation rather than in prepare_command, because
# prepare_command is skipped for :path pods.
openssl_version = "3.6.2"
openssl_sha256 = "a50e3c8473b0526b159ad8d105e97a90d1153a4a230388cc731ee23a7d0ad3a4"
openssl_dir = File.join(__dir__, "ios", "openssl")
openssl_prefix_header = File.join(openssl_dir, "quickcrypto_openssl_prefix.h")

unless File.exist?(openssl_prefix_header)
if openssl_sha256 == "REPLACE_WITH_RELEASE_SHA256"
raise "[QuickCrypto] openssl_sha256 is unset — publish the openssl-apple-#{openssl_version} release and paste its checksum into QuickCrypto.podspec"
end

archive = File.join(__dir__, "ios", "QuickCryptoOpenSSL.zip")
url = "https://github.com/margelo/react-native-quick-crypto/releases/download/openssl-apple-#{openssl_version}/QuickCryptoOpenSSL-#{openssl_version}.zip"

Pod::UI.puts "[QuickCrypto] ⬇️ Downloading static OpenSSL #{openssl_version}..."
FileUtils.mkdir_p(File.join(__dir__, "ios"))
FileUtils.rm_rf(openssl_dir)
system("curl -sSfL --connect-timeout 30 --max-time 600 -o #{archive.shellescape} #{url.shellescape}") || raise("[QuickCrypto] Failed to download static OpenSSL")

actual = Digest::SHA256.file(archive).hexdigest
unless actual == openssl_sha256
File.delete(archive)
raise "[QuickCrypto] Static OpenSSL checksum mismatch: expected #{openssl_sha256}, got #{actual}"
end

system("unzip -q #{archive.shellescape} -d #{openssl_dir.shellescape}") || raise("[QuickCrypto] Failed to extract static OpenSSL")
File.delete(archive)
Pod::UI.puts "[QuickCrypto] ✅ Static OpenSSL ready"
end

sodium_enabled = ENV['SODIUM_ENABLED'] == '1'
Pod::UI.puts("[QuickCrypto] 🧂 has libsodium #{sodium_enabled ? "enabled" : "disabled"}!")

Expand Down Expand Up @@ -94,6 +134,12 @@ Pod::Spec.new do |s|
# These use Intel intrinsics that don't compile on ARM
# Also exclude example files, TBB files, test files, and non-C directories
s.exclude_files = [
# Prebuilt OpenSSL headers — reached through HEADER_SEARCH_PATHS, never
# compiled as sources. Must not glob the .xcframework alongside them:
# CocoaPods applies exclude_files to vendored_frameworks too, so a broader
# pattern here silently unlinks the library.
"ios/openssl/include/**/*",
"ios/openssl/quickcrypto_openssl_prefix.h",
"deps/blake3/c/blake3_sse2.c",
"deps/blake3/c/blake3_sse41.c",
"deps/blake3/c/blake3_avx2.c",
Expand Down Expand Up @@ -146,8 +192,19 @@ Pod::Spec.new do |s|
"GCC_PREPROCESSOR_DEFINITIONS[sdk=iphonesimulator*][arch=x86_64]" => "$(inherited) BLAKE3_NO_AVX512 BLAKE3_NO_AVX2 BLAKE3_NO_SSE41 BLAKE3_NO_SSE2"
}

# Every translation unit calls the renamed OpenSSL symbols. Missing a rename
# is a link error, never a silent bind to a foreign OpenSSL.
openssl_include = File.join(openssl_dir, "include")
force_include = "-include \"#{openssl_prefix_header}\""
xcconfig["OTHER_CFLAGS"] = "$(inherited) #{force_include}"
xcconfig["OTHER_CPLUSPLUSFLAGS"] = "$(inherited) #{force_include}"

# Add cpp subdirectories to header search paths
cpp_headers = [
# Absolute path as well as the pod-relative one: in monorepos the podspec is
# reached through a symlink but sources resolve to their real paths.
"\"$(PODS_TARGET_SRCROOT)/ios/openssl/include\"",
"\"#{openssl_include}\"",
"\"$(PODS_TARGET_SRCROOT)/cpp/utils\"",
"\"$(PODS_TARGET_SRCROOT)/cpp/hkdf\"",
"\"$(PODS_TARGET_SRCROOT)/cpp/dh\"",
Expand Down Expand Up @@ -184,7 +241,8 @@ Pod::Spec.new do |s|
load "nitrogen/generated/ios/QuickCrypto+autolinking.rb"
add_nitrogen_files(s)

s.dependency "OpenSSL-Universal", "~> 3.6.2000"
s.vendored_frameworks = "ios/openssl/QuickCryptoOpenSSL.xcframework"

s.dependency "React-jsi"
s.dependency "React-callinvoker"

Expand Down
13 changes: 10 additions & 3 deletions packages/react-native-quick-crypto/android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,24 @@ include_directories(
# Third party libraries (Prefabs)
find_library(LOG_LIB log)

find_package(openssl REQUIRED CONFIG)
find_package(opensslstatic REQUIRED CONFIG)

# Link all libraries together
target_link_libraries(
${PACKAGE_NAME}
${LOG_LIB} # <-- Logcat logger
android # <-- Android core
openssl::crypto # <-- OpenSSL (Crypto)
openssl::ssl # <-- OpenSSL (SSL)
opensslstatic::ssl # <-- OpenSSL (SSL)
opensslstatic::crypto # <-- OpenSSL (Crypto)
)

# Keep every symbol we pull in from a static archive — i.e. all of OpenSSL — out
# of this .so's dynamic symbol table, so our OpenSSL and another library's
# OpenSSL cannot interpose on each other at runtime. Symbols from our own
# sources are unaffected, so the Nitro/JNI entry points stay exported.
# See issue #1059.
target_link_options(${PACKAGE_NAME} PRIVATE "-Wl,--exclude-libs,ALL")

if(SODIUM_ENABLED)
add_definitions(-DBLSALLOC_SODIUM)
find_package(sodium REQUIRED CONFIG)
Expand Down
6 changes: 4 additions & 2 deletions packages/react-native-quick-crypto/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,10 @@ dependencies {
// Add a dependency on NitroModules
implementation project(":react-native-nitro-modules")

// Add a dependency on OpenSSL
implementation 'io.github.ronickg:openssl:3.6.2-1'
// Static OpenSSL: linked into libQuickCrypto.so with its symbols localized, so we
// neither ship a libcrypto.so that collides with another library's nor export
// OpenSSL symbols that could interpose. See CMakeLists.txt and issue #1059.
implementation 'io.github.ronickg:openssl-static:3.6.2-2'

if (sodiumEnabled) {
// Add a dependency on libsodium
Expand Down
18 changes: 14 additions & 4 deletions scripts/build-openssl-apple.sh
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,23 @@ for slice in "${SLICES[@]}"; do
done

# The header QuickCrypto force-includes so its sources call the renamed symbols.
# Only symbols that appear in OpenSSL's public headers get a #define: the macros
# apply to every token in every QuickCrypto translation unit, so renaming an
# internal name like `sha256_block_data_order` would risk clobbering an
# Only symbols reachable from OpenSSL's public headers get a #define: these
# macros apply to every token in every QuickCrypto translation unit, so renaming
# an internal name like `sha256_block_data_order` would risk clobbering an
# unrelated identifier of ours for no gain — internal symbols are only reached
# from inside OpenSSL, where the link-level alias already covers them.
sort -u "$WORK/all-symbols.txt" | sed 's/^_//' | sort -u >"$WORK/symbols.all"
grep -ohE '\b[A-Za-z_][A-Za-z0-9_]*\b' "$DIST"/include/openssl/*.h | sort -u >"$WORK/header.idents"
# Identifiers are collected from the *preprocessed* headers, not their text. A
# large part of OpenSSL's public API is generated by token pasting —
# DECLARE_ASN1_FUNCTIONS(X509) produces X509_free/d2i_X509/i2d_X509,
# DECLARE_PEM_write_bio produces PEM_write_bio_X509 — so those names never
# appear literally in a header and reading the text alone silently drops the
# entire ASN.1 and PEM surface.
for h in "$DIST"/include/openssl/*.h; do
echo "#include <openssl/$(basename "$h")>"
done >"$WORK/all-headers.h"
xcrun clang -E -I"$DIST/include" -x c "$WORK/all-headers.h" -o "$WORK/headers.pp"
grep -ohE '\b[A-Za-z_][A-Za-z0-9_]*\b' "$WORK/headers.pp" | sort -u >"$WORK/header.idents"
# Names OpenSSL's own headers #define (EVP_des_cfb -> EVP_des_cfb64) are skipped:
# that macro already redirects to a symbol we do rename, and defining both just
# collides.
Expand Down
Loading