diff --git a/.gitmodules b/.gitmodules index 06b6a239..5e87fa64 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "packages/react-native-quick-crypto/deps/ncrypto"] path = packages/react-native-quick-crypto/deps/ncrypto url = https://github.com/nodejs/ncrypto.git +[submodule "packages/react-native-quick-crypto/deps/simdutf"] + path = packages/react-native-quick-crypto/deps/simdutf + url = https://github.com/simdutf/simdutf.git diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 929004ce..6f382c88 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2815,7 +2815,7 @@ SPEC CHECKSUMS: NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 OpenSSL-Universal: 9110d21982bb7e8b22a962b6db56a8aa805afde7 - QuickCrypto: 2084e4b3fe8ec7c33d57c446927b90d7e266b5f0 + QuickCrypto: 706bcc29f0cf2b628721f705bd700ad906a43f91 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077 RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a diff --git a/example/src/tests/utils/encoding_tests.ts b/example/src/tests/utils/encoding_tests.ts index be68c9a5..3ee1815f 100644 --- a/example/src/tests/utils/encoding_tests.ts +++ b/example/src/tests/utils/encoding_tests.ts @@ -91,6 +91,25 @@ test(SUITE, 'base64 roundtrip binary data', () => { expect(toU8(stringToBuffer(b64, 'base64'))).to.deep.equal(bytes); }); +test(SUITE, 'base64 decode accepts URL-safe base64 input', () => { + expect(toU8(stringToBuffer('-_8', 'base64'))).to.deep.equal( + new Uint8Array([0xfb, 0xff]), + ); +}); + +test( + SUITE, + 'base64 decode stops at first padding and ignores trailing data', + () => { + expect(toU8(stringToBuffer('Zm9v=QUJD', 'base64'))).to.deep.equal( + new Uint8Array([0x66, 0x6f, 0x6f]), + ); + expect(toU8(stringToBuffer('AA==BB', 'base64'))).to.deep.equal( + new Uint8Array([0x00]), + ); + }, +); + // --- Base64url --- test(SUITE, 'base64url encode produces URL-safe characters', () => { @@ -110,6 +129,31 @@ test(SUITE, 'base64url roundtrip', () => { expect(toU8(stringToBuffer(encoded, 'base64url'))).to.deep.equal(bytes); }); +test(SUITE, 'base64url decode accepts standard base64 input', () => { + expect(toU8(stringToBuffer('+/8=', 'base64url'))).to.deep.equal( + new Uint8Array([0xfb, 0xff]), + ); +}); + +test( + SUITE, + 'base64url decode stops at first padding and ignores trailing data', + () => { + expect(toU8(stringToBuffer('Zm9v==QUJD', 'base64url'))).to.deep.equal( + new Uint8Array([0x66, 0x6f, 0x6f]), + ); + expect(toU8(stringToBuffer('TQ==QQ==', 'base64url'))).to.deep.equal( + new Uint8Array([0x4d]), + ); + }, +); + +test(SUITE, 'base64url decode accepts multiple trailing padding', () => { + expect(toU8(stringToBuffer('TQQQ==', 'base64url'))).to.deep.equal( + new Uint8Array([0x4d, 0x04, 0x10]), + ); +}); + // --- UTF-8 --- test(SUITE, 'utf8 encode/decode ASCII', () => { diff --git a/packages/react-native-quick-crypto/QuickCrypto.podspec b/packages/react-native-quick-crypto/QuickCrypto.podspec index b3d50fa3..70d234f9 100644 --- a/packages/react-native-quick-crypto/QuickCrypto.podspec +++ b/packages/react-native-quick-crypto/QuickCrypto.podspec @@ -82,6 +82,9 @@ Pod::Spec.new do |s| # dependencies (C++) - ncrypto "deps/ncrypto/include/**/*.{h}", "deps/ncrypto/src/*.{cpp}", + # dependencies (C++) - simdutf + "deps/simdutf/include/**/*.{h}", + "deps/simdutf/src/simdutf.cpp", # dependencies (C) - exclude BLAKE3 x86 SIMD files (only use portable + NEON for ARM) "deps/blake3/c/*.{h,c}", "deps/fastpbkdf2/*.{h,c}", @@ -151,6 +154,8 @@ Pod::Spec.new do |s| "\"$(PODS_TARGET_SRCROOT)/cpp/ecdh\"", "\"$(PODS_TARGET_SRCROOT)/nitrogen/generated/shared/c++\"", "\"$(PODS_TARGET_SRCROOT)/deps/ncrypto/include\"", + "\"$(PODS_TARGET_SRCROOT)/deps/simdutf/include\"", + "\"$(PODS_TARGET_SRCROOT)/deps/simdutf/src\"", "\"$(PODS_TARGET_SRCROOT)/deps/blake3/c\"", "\"$(PODS_TARGET_SRCROOT)/deps/fastpbkdf2\"" ] diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 6d9e91cb..f885b6a1 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10.0) +cmake_minimum_required(VERSION 3.15) project(QuickCrypto) set(PACKAGE_NAME QuickCrypto) @@ -139,3 +139,19 @@ else() ReactAndroid::turbomodulejsijni # <-- RN: TurboModules utils (e.g. CallInvokerHolder) ) endif() + +# simdutf +set(SIMDUTF_TESTS OFF CACHE BOOL "" FORCE) +set(SIMDUTF_TOOLS OFF CACHE BOOL "" FORCE) +set(SIMDUTF_ICONV OFF CACHE BOOL "" FORCE) + +add_subdirectory( + ${CMAKE_CURRENT_SOURCE_DIR}/../deps/simdutf + ${CMAKE_CURRENT_BINARY_DIR}/simdutf-build + EXCLUDE_FROM_ALL +) + +target_link_libraries( + ${PACKAGE_NAME} + simdutf::simdutf +) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index 7f9453ff..f3747efc 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -1,12 +1,12 @@ #include "HybridUtils.hpp" -#include #include -#include #include #include +#include #include "QuickCryptoUtils.hpp" +#include "simdutf.h" namespace margelo::nitro::crypto { @@ -52,67 +52,41 @@ namespace { } std::string encodeBase64(const uint8_t* data, size_t len) { - if (len > static_cast(std::numeric_limits::max())) { - throw std::runtime_error("Input too large for base64 encoding"); + if (len == 0) { + return {}; } - size_t encodedLen = ((len + 2) / 3) * 4; - std::string result(encodedLen + 1, '\0'); - int written = EVP_EncodeBlock(reinterpret_cast(result.data()), data, static_cast(len)); - if (written < 0) { - throw std::runtime_error("Base64 encoding failed"); - } - result.resize(static_cast(written)); + + size_t encodedLen = simdutf::base64_length_from_binary(len, simdutf::base64_default); + std::string result(encodedLen, '\0'); + simdutf::binary_to_base64(reinterpret_cast(data), len, result.data(), simdutf::base64_default); return result; } std::vector decodeBase64(const std::string& b64) { - if (b64.length() > static_cast(std::numeric_limits::max())) { - throw std::runtime_error("Input too large for base64 decoding"); + if (b64.empty()) { + return {}; } - size_t maxLen = ((b64.length() + 3) / 4) * 3; + + size_t maxLen = simdutf::maximal_binary_length_from_base64(b64.data(), b64.length()); std::vector result(maxLen); - int written = EVP_DecodeBlock(result.data(), reinterpret_cast(b64.data()), static_cast(b64.length())); - if (written < 0) { + auto decodeResult = simdutf::base64_to_binary(b64.data(), b64.size(), reinterpret_cast(result.data()), + simdutf::base64_default_or_url_accept_garbage); + if (decodeResult.error != simdutf::error_code::SUCCESS) { throw std::runtime_error("Base64 decoding failed"); } - // EVP_DecodeBlock doesn't account for padding — trim trailing zeros from padding - size_t padding = 0; - if (b64.length() >= 1 && b64[b64.length() - 1] == '=') - padding++; - if (b64.length() >= 2 && b64[b64.length() - 2] == '=') - padding++; - result.resize(static_cast(written) - padding); + result.resize(decodeResult.count); return result; } std::string encodeBase64Url(const uint8_t* data, size_t len) { - std::string b64 = encodeBase64(data, len); - for (auto& c : b64) { - if (c == '+') - c = '-'; - else if (c == '/') - c = '_'; + if (len == 0) { + return {}; } - // Remove trailing '=' padding - while (!b64.empty() && b64.back() == '=') { - b64.pop_back(); - } - return b64; - } - std::vector decodeBase64Url(const std::string& b64url) { - std::string b64 = b64url; - for (auto& c : b64) { - if (c == '-') - c = '+'; - else if (c == '_') - c = '/'; - } - // Add back padding - while (b64.length() % 4 != 0) { - b64.push_back('='); - } - return decodeBase64(b64); + size_t encodedLen = simdutf::base64_length_from_binary(len, simdutf::base64_url); + std::string result(encodedLen, '\0'); + simdutf::binary_to_base64(reinterpret_cast(data), len, result.data(), simdutf::base64_url); + return result; } std::vector decodeLatin1(const std::string& str) { @@ -145,17 +119,15 @@ namespace { } std::string encodeLatin1(const uint8_t* data, size_t len) { - std::string result; - result.reserve(len * 2); - for (size_t i = 0; i < len; i++) { - uint8_t byte = data[i]; - if (byte < 0x80) { - result.push_back(static_cast(byte)); - } else { - // Latin1 byte 0x80-0xFF → UTF-8 two-byte sequence - result.push_back(static_cast(0xC0 | (byte >> 6))); - result.push_back(static_cast(0x80 | (byte & 0x3F))); - } + if (len == 0) { + return {}; + } + + size_t utf8Len = simdutf::utf8_length_from_latin1(reinterpret_cast(data), len); + std::string result(utf8Len, '\0'); + size_t written = simdutf::convert_latin1_to_utf8(reinterpret_cast(data), len, result.data()); + if (written == 0) { + throw std::runtime_error("Latin1 encoding failed"); } return result; } @@ -205,29 +177,25 @@ std::string HybridUtils::bufferToString(const std::shared_ptr& buff std::shared_ptr HybridUtils::stringToBuffer(const std::string& str, const std::string& encoding) { if (encoding == "hex") { auto decoded = decodeHex(str); - return ToNativeArrayBuffer(decoded); + return ArrayBuffer::move(std::move(decoded)); } - if (encoding == "base64") { + if (encoding == "base64" || encoding == "base64url") { auto decoded = decodeBase64(str); - return ToNativeArrayBuffer(decoded); - } - if (encoding == "base64url") { - auto decoded = decodeBase64Url(str); - return ToNativeArrayBuffer(decoded); + return ArrayBuffer::move(std::move(decoded)); } if (encoding == "utf8" || encoding == "utf-8") { - return ToNativeArrayBuffer(str); + return ArrayBuffer::copy(reinterpret_cast(str.data()), str.size()); } if (encoding == "latin1" || encoding == "binary") { auto decoded = decodeLatin1(str); - return ToNativeArrayBuffer(decoded); + return ArrayBuffer::move(std::move(decoded)); } if (encoding == "ascii") { auto decoded = decodeLatin1(str); for (auto& b : decoded) { b &= 0x7F; } - return ToNativeArrayBuffer(decoded); + return ArrayBuffer::move(std::move(decoded)); } throw std::runtime_error("Unsupported encoding: " + encoding); } diff --git a/packages/react-native-quick-crypto/deps/simdutf b/packages/react-native-quick-crypto/deps/simdutf new file mode 160000 index 00000000..fd476229 --- /dev/null +++ b/packages/react-native-quick-crypto/deps/simdutf @@ -0,0 +1 @@ +Subproject commit fd476229424b40ae71a58dd5a205795c3d76b5f1