Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
46 changes: 24 additions & 22 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,33 @@ updates:
schedule:
interval: "weekly"
ignore:
- "react"
- "react-native"
- "@react-native/eslint-plugin"
- "@react-native/metro-config"
- "@react-native/eslint-config"
- "@react-native/gradle-plugin"
- "@react-native-community/cli"
- "@react-native-community/cli-platform-android"
- "@react-native-community/cli-platform-ios"
- "@types/react"
- "chai"
- "@types/chai"
- dependency-name: "react"
- dependency-name: "react-native"
- dependency-name: "@react-native/eslint-plugin"
- dependency-name: "@react-native/metro-config"
- dependency-name: "@react-native/eslint-config"
- dependency-name: "@react-native/gradle-plugin"
- dependency-name: "@react-native-community/cli"
- dependency-name: "@react-native-community/cli-platform-android"
- dependency-name: "@react-native-community/cli-platform-ios"
- dependency-name: "@types/react"
- dependency-name: "chai"
versions: [ "<5.0.0" ]
- dependency-name: "@types/chai"
versions: [ "<5.0.0" ]
- package-ecosystem: "bun"
target-branch: "0.x"
directory: "/"
schedule:
interval: "weekly"
ignore:
- "react"
- "react-native"
- "@react-native/eslint-plugin"
- "@react-native/metro-config"
- "@react-native/eslint-config"
- "@react-native/gradle-plugin"
- "@react-native-community/cli"
- "@react-native-community/cli-platform-android"
- "@react-native-community/cli-platform-ios"
- "@types/react"
- dependency-name: "react"
- dependency-name: "react-native"
- dependency-name: "@react-native/eslint-plugin"
- dependency-name: "@react-native/metro-config"
- dependency-name: "@react-native/eslint-config"
- dependency-name: "@react-native/gradle-plugin"
- dependency-name: "@react-native-community/cli"
- dependency-name: "@react-native-community/cli-platform-android"
- dependency-name: "@react-native-community/cli-platform-ios"
- dependency-name: "@types/react"
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1942,7 +1942,7 @@ SPEC CHECKSUMS:
hermes-engine: 46f1ffbf0297f4298862068dd4c274d4ac17a1fd
NitroModules: 3a9c88afc1ca3dba01759ed410e8c2902a5d3dbb
OpenSSL-Universal: b60a3702c9fea8b3145549d421fdb018e53ab7b4
QuickCrypto: f52517b74ba4dece295584fdec852dbb807eecec
QuickCrypto: e457fb08347cd9807514cefad95337a7664aeabe
RCT-Folly: 84578c8756030547307e4572ab1947de1685c599
RCTDeprecation: fde92935b3caa6cb65cbff9fbb7d3a9867ffb259
RCTRequired: 75c6cee42d21c1530a6f204ba32ff57335d19007
Expand Down
9 changes: 9 additions & 0 deletions example/src/tests/cipher/cipher_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createCipheriv,
createDecipheriv,
randomFillSync,
xsalsa20,
type Cipher,
type Decipher,
} from 'react-native-quick-crypto';
Expand Down Expand Up @@ -198,3 +199,11 @@ allCiphers.forEach(cipherName => {
}
});
});

// libsodium cipher tests
test(SUITE, 'xsalsa20', () => {
const nonce = Buffer.from('0123456789abcdef', 'hex');
const ciphertext = xsalsa20(key, nonce, plaintextBuffer);
const decrypted = xsalsa20(key, nonce, ciphertext);
expect(decrypted).eql(plaintextBuffer);
});
2 changes: 1 addition & 1 deletion packages/react-native-quick-crypto/QuickCrypto.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Pod::Spec.new do |s|
s.macos.deployment_target = 10.13
s.tvos.deployment_target = 13.4

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

s.source_files = [
# implementation (Swift)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include "CCMCipher.hpp"
#include "HybridCipherFactorySpec.hpp"
#include "OCBCipher.hpp"
#include "Utils.hpp"
#include "XSalsa20Cipher.hpp"

namespace margelo::nitro::crypto {

Expand All @@ -20,40 +22,53 @@ class HybridCipherFactory : public HybridCipherFactorySpec {
public:
// Factory method exposed to JS
inline std::shared_ptr<HybridCipherSpec> createCipher(const CipherArgs& args) {
// Create a temporary cipher context to determine the mode
EVP_CIPHER* cipher = EVP_CIPHER_fetch(nullptr, args.cipherType.c_str(), nullptr);
if (!cipher) {
throw std::runtime_error("Invalid cipher type: " + args.cipherType);
}

int mode = EVP_CIPHER_get_mode(cipher);
EVP_CIPHER_free(cipher);

// Create the appropriate cipher instance based on mode
std::shared_ptr<HybridCipher> cipherInstance;
switch (mode) {
case EVP_CIPH_OCB_MODE: {
cipherInstance = std::make_shared<OCBCipher>();
cipherInstance->setArgs(args);
// Pass tag length (default 16 if not present)
size_t tag_len = args.authTagLen.has_value() ? static_cast<size_t>(args.authTagLen.value()) : 16;
std::static_pointer_cast<OCBCipher>(cipherInstance)->init(args.cipherKey, args.iv, tag_len);
return cipherInstance;
}
case EVP_CIPH_CCM_MODE: {
cipherInstance = std::make_shared<CCMCipher>();
cipherInstance->setArgs(args);
cipherInstance->init(args.cipherKey, args.iv);
return cipherInstance;
}
default: {
cipherInstance = std::make_shared<HybridCipher>();
cipherInstance->setArgs(args);
cipherInstance->init(args.cipherKey, args.iv);
return cipherInstance;

// OpenSSL
// temporary cipher context to determine the mode
EVP_CIPHER* cipher = EVP_CIPHER_fetch(nullptr, args.cipherType.c_str(), nullptr);
if (cipher) {
int mode = EVP_CIPHER_get_mode(cipher);

switch (mode) {
case EVP_CIPH_OCB_MODE: {
cipherInstance = std::make_shared<OCBCipher>();
cipherInstance->setArgs(args);
// Pass tag length (default 16 if not present)
size_t tag_len = args.authTagLen.has_value() ? static_cast<size_t>(args.authTagLen.value()) : 16;
std::static_pointer_cast<OCBCipher>(cipherInstance)->init(args.cipherKey, args.iv, tag_len);
return cipherInstance;
}
case EVP_CIPH_CCM_MODE: {
cipherInstance = std::make_shared<CCMCipher>();
cipherInstance->setArgs(args);
cipherInstance->init(args.cipherKey, args.iv);
return cipherInstance;
}
default: {
cipherInstance = std::make_shared<HybridCipher>();
cipherInstance->setArgs(args);
cipherInstance->init(args.cipherKey, args.iv);
return cipherInstance;
}
}
}
}
EVP_CIPHER_free(cipher);

// libsodium
std::string cipherName = toLower(args.cipherType);
if (cipherName == "xsalsa20") {
cipherInstance = std::make_shared<XSalsa20Cipher>();
cipherInstance->setArgs(args);
cipherInstance->init(args.cipherKey, args.iv);
return cipherInstance;
}

// Unsupported cipher type
throw std::runtime_error("Unsupported or unknown cipher type: " + args.cipherType);
};
};

} // namespace margelo::nitro::crypto
50 changes: 50 additions & 0 deletions packages/react-native-quick-crypto/cpp/cipher/XSalsa20Cipher.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#include "XSalsa20Cipher.hpp"
#include <cstring> // For std::memcpy
#include <stdexcept> // For std::runtime_error
#include <string> // For std::to_string

namespace margelo::nitro::crypto {

/**
* Initialize the cipher with a key and a nonce (using iv argument as nonce)
*/
void XSalsa20Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) {
auto native_key = ToNativeArrayBuffer(cipher_key);
auto native_iv = ToNativeArrayBuffer(iv);

// Validate key size
if (native_key->size() < crypto_stream_KEYBYTES) {
throw std::runtime_error("XSalsa20 key too short: expected " + std::to_string(crypto_stream_KEYBYTES) + " bytes, got " +
std::to_string(native_key->size()) + " bytes.");
}
// Validate nonce size
if (native_iv->size() < crypto_stream_NONCEBYTES) {
throw std::runtime_error("XSalsa20 nonce too short: expected " + std::to_string(crypto_stream_NONCEBYTES) + " bytes, got " +
std::to_string(native_iv->size()) + " bytes.");
}

// Copy key and nonce data
std::memcpy(key, native_key->data(), crypto_stream_KEYBYTES);
std::memcpy(nonce, native_iv->data(), crypto_stream_NONCEBYTES);
}

/**
* xsalsa20 call to sodium implementation
*/
std::shared_ptr<ArrayBuffer> XSalsa20Cipher::update(const std::shared_ptr<ArrayBuffer>& data) {
auto native_data = ToNativeArrayBuffer(data);
int result = crypto_stream(native_data->data(), native_data->size(), nonce, key);
if (result != 0) {
throw std::runtime_error("XSalsa20Cipher: Failed to update");
}
return std::make_shared<NativeArrayBuffer>(native_data->data(), native_data->size(), nullptr);
}

/**
* xsalsa20 does not have a final step, returns empty buffer
*/
std::shared_ptr<ArrayBuffer> XSalsa20Cipher::final() {
return std::make_shared<NativeArrayBuffer>(nullptr, 0, nullptr);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that deallocate fine? Not sure if this calls delete[] at some point but maybe it's fine.

}

} // namespace margelo::nitro::crypto
27 changes: 27 additions & 0 deletions packages/react-native-quick-crypto/cpp/cipher/XSalsa20Cipher.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

#include "sodium.h"

#include "HybridCipher.hpp"
#include "Utils.hpp"

namespace margelo::nitro::crypto {

class XSalsa20Cipher : public HybridCipher {
public:
XSalsa20Cipher() : HybridObject(TAG) {}
~XSalsa20Cipher() {
// Let parent destructor free the context
ctx = nullptr;
}

void init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) override;
std::shared_ptr<ArrayBuffer> update(const std::shared_ptr<ArrayBuffer>& data) override;
std::shared_ptr<ArrayBuffer> final() override;

private:
uint8_t key[crypto_stream_KEYBYTES];
uint8_t nonce[crypto_stream_NONCEBYTES];
};

} // namespace margelo::nitro::crypto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include "HybridRandom.hpp"
#include "Utils.hpp"

namespace margelo::nitro::crypto {

size_t checkSize(double size) {
if (!CheckIsUint32(size)) {
throw std::runtime_error("size must be uint32");
Expand All @@ -24,8 +26,6 @@ size_t checkOffset(double size, double offset) {
return static_cast<size_t>(offset);
}

namespace margelo::nitro::crypto {

std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> HybridRandom::randomFill(const std::shared_ptr<ArrayBuffer>& buffer, double dOffset,
double dSize) {
// get owned NativeArrayBuffer before passing to sync function
Expand Down
15 changes: 15 additions & 0 deletions packages/react-native-quick-crypto/cpp/utils/Utils.hpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
#pragma once

#include <algorithm>
#include <cctype>
#include <limits>
#include <string>

#include <NitroModules/ArrayBuffer.hpp>

namespace margelo::nitro::crypto {

// copy a JSArrayBuffer that we do not own into a NativeArrayBuffer that we do own
inline std::shared_ptr<margelo::nitro::NativeArrayBuffer> ToNativeArrayBuffer(const std::shared_ptr<margelo::nitro::ArrayBuffer>& buffer) {
size_t bufferSize = buffer.get()->size();
Expand All @@ -17,3 +24,11 @@ inline bool CheckIsUint32(double value) {
inline bool CheckIsInt32(double value) {
return (value >= std::numeric_limits<int32_t>::lowest() && value <= std::numeric_limits<int32_t>::max());
}

// Function to convert a string to lowercase
inline std::string toLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}

} // namespace margelo::nitro::crypto
Loading
Loading