Skip to content
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2815,7 +2815,7 @@ SPEC CHECKSUMS:
NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e
NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3
OpenSSL-Universal: 9110d21982bb7e8b22a962b6db56a8aa805afde7
QuickCrypto: 2084e4b3fe8ec7c33d57c446927b90d7e266b5f0
QuickCrypto: 706bcc29f0cf2b628721f705bd700ad906a43f91
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
Expand Down
44 changes: 44 additions & 0 deletions example/src/tests/utils/encoding_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/react-native-quick-crypto/QuickCrypto.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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\""
]
Expand Down
18 changes: 17 additions & 1 deletion packages/react-native-quick-crypto/android/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.10.0)
cmake_minimum_required(VERSION 3.15)
project(QuickCrypto)

set(PACKAGE_NAME QuickCrypto)
Expand Down Expand Up @@ -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
)
106 changes: 37 additions & 69 deletions packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#include "HybridUtils.hpp"

#include <limits>
#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <stdexcept>
#include <string>
#include <vector>

#include "QuickCryptoUtils.hpp"
#include "simdutf.h"

namespace margelo::nitro::crypto {

Expand Down Expand Up @@ -52,67 +52,41 @@ namespace {
}

std::string encodeBase64(const uint8_t* data, size_t len) {
if (len > static_cast<size_t>(std::numeric_limits<int>::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<unsigned char*>(result.data()), data, static_cast<int>(len));
if (written < 0) {
throw std::runtime_error("Base64 encoding failed");
}
result.resize(static_cast<size_t>(written));

size_t encodedLen = simdutf::base64_length_from_binary(len, simdutf::base64_default);
std::string result(encodedLen, '\0');
simdutf::binary_to_base64(reinterpret_cast<const char*>(data), len, result.data(), simdutf::base64_default);
return result;
}

std::vector<uint8_t> decodeBase64(const std::string& b64) {
if (b64.length() > static_cast<size_t>(std::numeric_limits<int>::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<uint8_t> result(maxLen);
int written = EVP_DecodeBlock(result.data(), reinterpret_cast<const unsigned char*>(b64.data()), static_cast<int>(b64.length()));
if (written < 0) {
auto decodeResult = simdutf::base64_to_binary(b64.data(), b64.size(), reinterpret_cast<char*>(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<size_t>(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<uint8_t> 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<const char*>(data), len, result.data(), simdutf::base64_url);
return result;
}

std::vector<uint8_t> decodeLatin1(const std::string& str) {
Expand Down Expand Up @@ -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<char>(byte));
} else {
// Latin1 byte 0x80-0xFF → UTF-8 two-byte sequence
result.push_back(static_cast<char>(0xC0 | (byte >> 6)));
result.push_back(static_cast<char>(0x80 | (byte & 0x3F)));
}
if (len == 0) {
return {};
}

size_t utf8Len = simdutf::utf8_length_from_latin1(reinterpret_cast<const char*>(data), len);
std::string result(utf8Len, '\0');
size_t written = simdutf::convert_latin1_to_utf8(reinterpret_cast<const char*>(data), len, result.data());
if (written == 0) {
throw std::runtime_error("Latin1 encoding failed");
}
return result;
}
Expand Down Expand Up @@ -205,29 +177,25 @@ std::string HybridUtils::bufferToString(const std::shared_ptr<ArrayBuffer>& buff
std::shared_ptr<ArrayBuffer> 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<const uint8_t*>(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);
}
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-quick-crypto/deps/simdutf
Submodule simdutf added at fd4762
Loading