diff --git a/.docs/troubleshooting.md b/.docs/troubleshooting.md index 53b27c5a2..cfe10ad4a 100644 --- a/.docs/troubleshooting.md +++ b/.docs/troubleshooting.md @@ -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. -

- React Native   -

+### 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 } ``` -

- Expo   -

- -`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. diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 265d9c7a6..b3a8e8604 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -69,7 +69,6 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - OpenSSL-Universal (3.6.2000) - QuickCrypto (1.1.6): - boost - DoubleConversion @@ -78,7 +77,6 @@ PODS: - glog - hermes-engine - NitroModules - - OpenSSL-Universal (~> 3.6.2000) - RCT-Folly - RCT-Folly/Fabric - RCTRequired @@ -2665,7 +2663,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - MMKVCore - - OpenSSL-Universal - SocketRocket EXTERNAL SOURCES: @@ -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 diff --git a/packages/react-native-quick-crypto/QuickCrypto.podspec b/packages/react-native-quick-crypto/QuickCrypto.podspec index aa4306928..957934551 100644 --- a/packages/react-native-quick-crypto/QuickCrypto.podspec +++ b/packages/react-native-quick-crypto/QuickCrypto.podspec @@ -1,4 +1,6 @@ require "json" +require "digest" +require "shellwords" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) @@ -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"}!") @@ -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", @@ -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\"", @@ -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" diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 19ff94b77..2bb708e66 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -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) diff --git a/packages/react-native-quick-crypto/android/build.gradle b/packages/react-native-quick-crypto/android/build.gradle index e9ce64dd4..4e32c5af4 100644 --- a/packages/react-native-quick-crypto/android/build.gradle +++ b/packages/react-native-quick-crypto/android/build.gradle @@ -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 diff --git a/scripts/build-openssl-apple.sh b/scripts/build-openssl-apple.sh index 1b54d8c67..4aac83e9b 100755 --- a/scripts/build-openssl-apple.sh +++ b/scripts/build-openssl-apple.sh @@ -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 " +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.