Skip to content

Commit 2f972cf

Browse files
committed
feat: add xsalsa20 cipher from libsodium
1 parent 83d2aed commit 2f972cf

File tree

18 files changed

+233
-65
lines changed

18 files changed

+233
-65
lines changed

.rules

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Every time you choose to apply a rule(s), explicitly state the rule(s) in the ou
2727
- Use modern C++ features.
2828
- Attempt to reduce the amount of code rather than add more.
2929
- Prefer iteration and modularization over code duplication.
30+
- Do not add comments unless explicitly told to do so.
3031

3132
## TypeScript Best Practices
3233

bun.lock

Lines changed: 3 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1942,7 +1942,7 @@ SPEC CHECKSUMS:
19421942
hermes-engine: 46f1ffbf0297f4298862068dd4c274d4ac17a1fd
19431943
NitroModules: 3a9c88afc1ca3dba01759ed410e8c2902a5d3dbb
19441944
OpenSSL-Universal: b60a3702c9fea8b3145549d421fdb018e53ab7b4
1945-
QuickCrypto: e457fb08347cd9807514cefad95337a7664aeabe
1945+
QuickCrypto: 39358cf38783f7f26bd750cf3a2609feb845fa3d
19461946
RCT-Folly: 84578c8756030547307e4572ab1947de1685c599
19471947
RCTDeprecation: fde92935b3caa6cb65cbff9fbb7d3a9867ffb259
19481948
RCTRequired: 75c6cee42d21c1530a6f204ba32ff57335d19007

example/src/tests/cipher/cipher_tests.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
createCipheriv,
55
createDecipheriv,
66
randomFillSync,
7+
xsalsa20,
78
type Cipher,
89
type Decipher,
910
} from 'react-native-quick-crypto';
@@ -198,3 +199,11 @@ allCiphers.forEach(cipherName => {
198199
}
199200
});
200201
});
202+
203+
// libsodium cipher tests
204+
test(SUITE, 'xsalsa20', () => {
205+
const nonce = Buffer.from('0123456789abcdef', 'hex');
206+
const ciphertext = xsalsa20(key, nonce, plaintextBuffer);
207+
const decrypted = xsalsa20(key, nonce, ciphertext);
208+
expect(decrypted).eql(plaintextBuffer);
209+
});

example/src/tests/pbkdf2/pbkdf2_tests.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { expect } from 'chai';
44
import { test } from '../util';
55
import { fixtures, type Fixture } from './fixtures';
66

7-
import crypto, { ab2str } from 'react-native-quick-crypto';
7+
import crypto from 'react-native-quick-crypto';
88
import type { BinaryLike, HashAlgorithm } from 'react-native-quick-crypto';
99

1010
type TestFixture = [string, string, number, number, string];
@@ -36,7 +36,7 @@ const SUITE = 'pbkdf2';
3636
function (err, result) {
3737
expect(err).to.be.null;
3838
expect(result).not.to.be.null;
39-
expect(ab2str(result as ArrayBuffer)).to.equal(expected);
39+
expect(result?.toString('hex')).to.equal(expected);
4040
},
4141
);
4242
};
@@ -76,7 +76,7 @@ const SUITE = 'pbkdf2';
7676

7777
test(SUITE, 'handles buffers', () => {
7878
const resultSync = crypto.pbkdf2Sync('password', 'salt', 1, 32);
79-
expect(ab2str(resultSync)).to.equal(
79+
expect(resultSync.toString('hex')).to.equal(
8080
'0c60c80f961f0e71f3a9b524af6012062fe037a6e0f0eb94fe8fc46bdc637164',
8181
);
8282

@@ -186,7 +186,7 @@ algos.forEach(function (algorithm) {
186186
function (err, result) {
187187
expect(err).to.be.null;
188188
expect(result).not.to.be.null;
189-
expect(ab2str(result as ArrayBuffer)).to.equal(expected);
189+
expect(result?.toString('hex')).to.equal(expected);
190190
},
191191
);
192192
});
@@ -199,7 +199,7 @@ algos.forEach(function (algorithm) {
199199
f.dkLen as number,
200200
algorithm as HashAlgorithm,
201201
);
202-
expect(ab2str(result)).to.equal(expected);
202+
expect(result?.toString('hex')).to.equal(expected);
203203
});
204204
});
205205

example/tsconfig.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
"include": [
77
"index.ts",
88
"app.json",
9-
"src",
10-
"**.*.ts",
11-
"**.*.tsx",
9+
"src",
10+
"./**/*.ts",
11+
"./**/*.tsx",
12+
"../packages/react-native-quick-crypto/src/**/*.ts"
1213
],
1314
"compilerOptions": {
1415
"jsx": "react",

packages/react-native-quick-crypto/QuickCrypto.podspec

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ Pod::Spec.new do |s|
1717
s.macos.deployment_target = 10.13
1818
s.tvos.deployment_target = 13.4
1919

20-
s.source = { :git => "https://github.com/margelo/react-native-quick-crypto.git", :tag => "#{s.version}" }
20+
s.source = { :git => "https://github.com/margelo/react-native-quick-crypto.git", :tag => "#{s.version}" }
21+
22+
# TODO: handle libsodium
23+
s.source = { :http => "file:///Users/brad/dev/rnqc/lib/libsodium-1.0.20/libsodium-apple/Clibsodium.xcframework.tar.gz" }
24+
s.vendored_frameworks = "Clibsodium.xcframework"
2125

2226
s.source_files = [
2327
# implementation (Swift)
@@ -44,5 +48,6 @@ Pod::Spec.new do |s|
4448
s.dependency 'React-jsi'
4549
s.dependency 'React-callinvoker'
4650
s.dependency "OpenSSL-Universal"
51+
# s.dependency "libsodium"
4752
install_modules_dependencies(s)
4853
end

packages/react-native-quick-crypto/cpp/cipher/HybridCipherFactory.hpp

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#include "CCMCipher.hpp"
88
#include "HybridCipherFactorySpec.hpp"
99
#include "OCBCipher.hpp"
10+
#include "Utils.hpp"
11+
#include "XSalsa20Cipher.hpp"
1012

1113
namespace margelo::nitro::crypto {
1214

@@ -20,40 +22,54 @@ class HybridCipherFactory : public HybridCipherFactorySpec {
2022
public:
2123
// Factory method exposed to JS
2224
inline std::shared_ptr<HybridCipherSpec> createCipher(const CipherArgs& args) {
23-
// Create a temporary cipher context to determine the mode
24-
EVP_CIPHER* cipher = EVP_CIPHER_fetch(nullptr, args.cipherType.c_str(), nullptr);
25-
if (!cipher) {
26-
throw std::runtime_error("Invalid cipher type: " + args.cipherType);
27-
}
28-
29-
int mode = EVP_CIPHER_get_mode(cipher);
30-
EVP_CIPHER_free(cipher);
3125

3226
// Create the appropriate cipher instance based on mode
3327
std::shared_ptr<HybridCipher> cipherInstance;
34-
switch (mode) {
35-
case EVP_CIPH_OCB_MODE: {
36-
cipherInstance = std::make_shared<OCBCipher>();
37-
cipherInstance->setArgs(args);
38-
// Pass tag length (default 16 if not present)
39-
size_t tag_len = args.authTagLen.has_value() ? static_cast<size_t>(args.authTagLen.value()) : 16;
40-
std::static_pointer_cast<OCBCipher>(cipherInstance)->init(args.cipherKey, args.iv, tag_len);
41-
return cipherInstance;
42-
}
43-
case EVP_CIPH_CCM_MODE: {
44-
cipherInstance = std::make_shared<CCMCipher>();
45-
cipherInstance->setArgs(args);
46-
cipherInstance->init(args.cipherKey, args.iv);
47-
return cipherInstance;
48-
}
49-
default: {
50-
cipherInstance = std::make_shared<HybridCipher>();
51-
cipherInstance->setArgs(args);
52-
cipherInstance->init(args.cipherKey, args.iv);
53-
return cipherInstance;
28+
29+
// OpenSSL
30+
// temporary cipher context to determine the mode
31+
EVP_CIPHER* cipher = EVP_CIPHER_fetch(nullptr, args.cipherType.c_str(), nullptr);
32+
if (cipher) {
33+
int mode = EVP_CIPHER_get_mode(cipher);
34+
35+
switch (mode) {
36+
case EVP_CIPH_OCB_MODE: {
37+
cipherInstance = std::make_shared<OCBCipher>();
38+
cipherInstance->setArgs(args);
39+
// Pass tag length (default 16 if not present)
40+
size_t tag_len = args.authTagLen.has_value() ? static_cast<size_t>(args.authTagLen.value()) : 16;
41+
std::static_pointer_cast<OCBCipher>(cipherInstance)->init(args.cipherKey, args.iv, tag_len);
42+
return cipherInstance;
43+
}
44+
case EVP_CIPH_CCM_MODE: {
45+
cipherInstance = std::make_shared<CCMCipher>();
46+
cipherInstance->setArgs(args);
47+
cipherInstance->init(args.cipherKey, args.iv);
48+
return cipherInstance;
49+
}
50+
default: {
51+
cipherInstance = std::make_shared<HybridCipher>();
52+
cipherInstance->setArgs(args);
53+
cipherInstance->init(args.cipherKey, args.iv);
54+
return cipherInstance;
55+
}
5456
}
5557
}
56-
}
58+
EVP_CIPHER_free(cipher);
59+
60+
// libsodium
61+
std::string cipherName = toLower(args.cipherType);
62+
if (cipherName == "xsalsa20") {
63+
cipherInstance = std::make_shared<XSalsa20Cipher>();
64+
cipherInstance->setArgs(args);
65+
cipherInstance->init(args.cipherKey, args.iv);
66+
return cipherInstance;
67+
}
68+
69+
// Unsupported cipher type
70+
throw std::runtime_error("Unsupported or unknown cipher type: " + args.cipherType);
71+
};
72+
5773
};
5874

5975
} // namespace margelo::nitro::crypto
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include "XSalsa20Cipher.hpp"
2+
#include <cstring> // For std::memcpy
3+
#include <stdexcept> // For std::runtime_error
4+
#include <string> // For std::to_string
5+
6+
namespace margelo::nitro::crypto {
7+
8+
/**
9+
* Initialize the cipher with a key and a nonce (using iv argument as nonce)
10+
*/
11+
void XSalsa20Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) {
12+
auto native_key = ToNativeArrayBuffer(cipher_key);
13+
auto native_iv = ToNativeArrayBuffer(iv);
14+
15+
// Validate key size
16+
if (native_key->size() < crypto_stream_KEYBYTES) {
17+
throw std::runtime_error("XSalsa20 key too short: expected " +
18+
std::to_string(crypto_stream_KEYBYTES) + " bytes, got " +
19+
std::to_string(native_key->size()) + " bytes.");
20+
}
21+
// Validate nonce size
22+
if (native_iv->size() < crypto_stream_NONCEBYTES) {
23+
throw std::runtime_error("XSalsa20 nonce too short: expected " +
24+
std::to_string(crypto_stream_NONCEBYTES) + " bytes, got " +
25+
std::to_string(native_iv->size()) + " bytes.");
26+
}
27+
28+
// Copy key and nonce data
29+
std::memcpy(key, native_key->data(), crypto_stream_KEYBYTES);
30+
std::memcpy(nonce, native_iv->data(), crypto_stream_NONCEBYTES);
31+
}
32+
33+
/**
34+
* xsalsa20 call to sodium implementation
35+
*/
36+
std::shared_ptr<ArrayBuffer> XSalsa20Cipher::update(const std::shared_ptr<ArrayBuffer>& data) {
37+
auto native_data = ToNativeArrayBuffer(data);
38+
int result = crypto_stream(native_data->data(), native_data->size(), nonce, key);
39+
if (result != 0) {
40+
throw std::runtime_error("XSalsa20Cipher: Failed to update");
41+
}
42+
return std::make_shared<NativeArrayBuffer>(native_data->data(), native_data->size(), nullptr);
43+
}
44+
45+
/**
46+
* xsalsa20 does not have a final step, returns empty buffer
47+
*/
48+
std::shared_ptr<ArrayBuffer> XSalsa20Cipher::final() {
49+
return std::make_shared<NativeArrayBuffer>(nullptr, 0, nullptr);
50+
}
51+
52+
} // namespace margelo::nitro::crypto
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#pragma once
2+
3+
#include "sodium.h"
4+
5+
#include "HybridCipher.hpp"
6+
#include "Utils.hpp"
7+
8+
namespace margelo::nitro::crypto {
9+
10+
class XSalsa20Cipher : public HybridCipher {
11+
public:
12+
XSalsa20Cipher() : HybridObject(TAG) {}
13+
~XSalsa20Cipher() {
14+
// Let parent destructor free the context
15+
ctx = nullptr;
16+
}
17+
18+
void init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) override;
19+
std::shared_ptr<ArrayBuffer> update(const std::shared_ptr<ArrayBuffer>& data) override;
20+
std::shared_ptr<ArrayBuffer> final() override;
21+
22+
private:
23+
uint8_t key[crypto_stream_KEYBYTES];
24+
uint8_t nonce[crypto_stream_NONCEBYTES];
25+
};
26+
27+
} // namespace margelo::nitro::crypto

0 commit comments

Comments
 (0)