Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .docs/implementation-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This document attempts to describe the implementation status of Crypto APIs/Inte
- ` ` - not implemented in Node
- ❌ - implemented in Node, not RNQC
- ✅ - implemented in Node and RNQC
- ✨ - implemented in RNQC, not in Node (RNQC extension)
- 🚧 - work in progress
- `-` - not applicable to React Native

Expand Down Expand Up @@ -145,6 +146,10 @@ These algorithms provide quantum-resistant cryptography.
- ✅ `crypto.hash(algorithm, data[, outputEncoding])`
- ✅ `crypto.hkdf(digest, ikm, salt, info, keylen, callback)`
- ✅ `crypto.hkdfSync(digest, ikm, salt, info, keylen)`
- ✨ `crypto.hkdfExtract(digest, ikm, salt, callback)` (RFC 5869 Extract-only)
- ✨ `crypto.hkdfExtractSync(digest, ikm[, salt])` (RFC 5869 Extract-only)
- ✨ `crypto.hkdfExpand(digest, prk, info, keylen, callback)` (RFC 5869 Expand-only)
- ✨ `crypto.hkdfExpandSync(digest, prk, info, keylen)` (RFC 5869 Expand-only)
- ✅ `crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)`
- ✅ `crypto.pbkdf2Sync(password, salt, iterations, keylen, digest)`
- ✅ `crypto.privateDecrypt(privateKey, buffer)`
Expand Down
19 changes: 19 additions & 0 deletions .docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@ Cannot read property 'install' of undefined

Then you need to install `react-native-quick-crypto` as a dependency in your `package.json` file. Make sure to install pods (ios).

## `QuickBase64` could not be found

If your app crashes on launch with:

```
Invariant Violation: TurboModuleRegistry.getEnforcing(...): 'QuickBase64' could not be found. Verify that a module by this name is registered in the native binary.
```

This comes from `react-native-quick-base64`, which is a required dependency of `react-native-quick-crypto`. Since `1.x` of `react-native-quick-crypto` targets the New Architecture, `react-native-quick-base64` `3.0.0`+ is a pure C++ TurboModule that only registers when your app runs with the New Architecture enabled. On the Old Architecture there is no registration for `QuickBase64`, so the lookup fails and takes `react-native-quick-crypto` down with it at startup.

Fix it one of these ways:

- **Enable the New Architecture, then rebuild.**
- Bare React Native: set `newArchEnabled=true` in `android/gradle.properties`, then `cd android && ./gradlew clean` and rebuild. (New Architecture is the default on RN `0.76`+ and required on `0.85`+.)
- Expo: use SDK 54+, where the New Architecture is on by default, or enable it explicitly on SDK 53.
- **Stay on the Old Architecture** by pinning `react-native-quick-base64@2.2.2`, whose `2.x` line still ships the legacy Old Architecture module.

> 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

If you get an error similar to this:
Expand Down
34 changes: 34 additions & 0 deletions docs/content/docs/api/hkdf.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,37 @@ const derived = QuickCrypto.hkdfSync(
}
}}
/>

### hkdfExtractSync(digest, ikm, salt?) / hkdfExtract(digest, ikm, salt, callback)

Runs only the **Extract** step of RFC 5869, returning the pseudorandom key
($PRK$) as a `Buffer` of `HashLen` bytes. Use this when a protocol performs
Extract and Expand at different stages (TLS 1.3, Noise, Signal, MLS). `salt`
defaults to a string of `HashLen` zero bytes when omitted from the sync form;
the async form requires `salt` (the callback takes the trailing argument).

```ts
import QuickCrypto from 'react-native-quick-crypto';

const prk = QuickCrypto.hkdfExtractSync('sha256', 'ikm-secret', 'salt');

QuickCrypto.hkdfExtract('sha256', 'ikm-secret', 'salt', (err, prk) => {
// ...
});
```

### hkdfExpandSync(digest, prk, info, keylen) / hkdfExpand(digest, prk, info, keylen, callback)

Runs only the **Expand** step, deriving `keylen` bytes of output keying
material from an already-derived `prk`. `prk` must be at least `HashLen` bytes,
and `keylen` must not exceed `255 * HashLen` (RFC 5869 §2.3).

```ts
import QuickCrypto from 'react-native-quick-crypto';

const okm = QuickCrypto.hkdfExpandSync('sha256', prk, 'context-info', 64);

QuickCrypto.hkdfExpand('sha256', prk, 'context-info', 64, (err, okm) => {
// ...
});
```
10 changes: 10 additions & 0 deletions docs/data/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,16 @@ export const COVERAGE_DATA: CoverageCategory[] = [
{ name: 'getRandomValues', status: 'implemented' },
{ name: 'hash', status: 'implemented' },
{ name: 'hkdf', status: 'implemented' },
{
name: 'hkdfExpand / hkdfExpandSync',
status: 'not-in-node',
note: 'RNQC extension: RFC 5869 Expand-only',
},
{
name: 'hkdfExtract / hkdfExtractSync',
status: 'not-in-node',
note: 'RNQC extension: RFC 5869 Extract-only',
},
{ name: 'pbkdf2', status: 'implemented' },
{ name: 'privateDecrypt / privateEncrypt', status: 'implemented' },
{ name: 'publicDecrypt / publicEncrypt', status: 'implemented' },
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2841,7 +2841,7 @@ SPEC CHECKSUMS:
NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e
NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3
OpenSSL-Universal: ecee7b138fa75a74ecf00d7ffd248fb584739b9e
QuickCrypto: fd9ac356a20fa61425a09f5a6f7eff8d50ca3172
QuickCrypto: 21eda4d230c62635279e316ea06abead4df1c662
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
Expand Down
109 changes: 108 additions & 1 deletion example/src/tests/hkdf/hkdf_tests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import crypto, { Buffer, hkdf, hkdfSync } from 'react-native-quick-crypto';
import crypto, {
Buffer,
hkdf,
hkdfSync,
hkdfExtract,
hkdfExtractSync,
hkdfExpand,
hkdfExpandSync,
} from 'react-native-quick-crypto';
import { expect } from 'chai';
import { test } from '../util';

Expand All @@ -17,6 +25,7 @@ const testVectors = [
info: 'f0f1f2f3f4f5f6f7f8f9',
len: 42,
okm: '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865',
prk: '077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5',
algo: 'sha256',
},
// A.2 — Test Case 2: Test with SHA-256 and longer inputs/outputs
Expand All @@ -26,6 +35,7 @@ const testVectors = [
info: 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff',
len: 82,
okm: 'b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87',
prk: '06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244',
algo: 'sha256',
},
// A.3 — Test Case 3: SHA-256 with zero-length salt and zero-length info
Expand All @@ -35,6 +45,7 @@ const testVectors = [
info: '',
len: 42,
okm: '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8',
prk: '19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04',
algo: 'sha256',
},
// A.4 — Test Case 4: Basic test case with SHA-1
Expand All @@ -44,6 +55,7 @@ const testVectors = [
info: 'f0f1f2f3f4f5f6f7f8f9',
len: 42,
okm: '085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896',
prk: '9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243',
algo: 'sha1',
},
// A.5 — Test Case 5: SHA-1 with longer inputs/outputs
Expand All @@ -53,6 +65,7 @@ const testVectors = [
info: 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff',
len: 82,
okm: '0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4',
prk: '8adae09a2a307059478d309b26c4115a224cfaf6',
algo: 'sha1',
},
// A.6 — Test Case 6: SHA-1 with zero-length salt and zero-length info
Expand All @@ -62,6 +75,7 @@ const testVectors = [
info: '',
len: 42,
okm: '0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918',
prk: 'da8c8a73c7fa77288ec6f5e7c297786aa0d32d01',
algo: 'sha1',
},
// A.7 — Test Case 7: SHA-1, salt == NULL (treated as HashLen zeros), zero info
Expand All @@ -71,6 +85,7 @@ const testVectors = [
info: '',
len: 42,
okm: '2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48',
prk: '2adccada18779e7c2077ad2eb19d3f3e731385dd',
algo: 'sha1',
},
];
Expand Down Expand Up @@ -306,3 +321,95 @@ test(SUITE, 'WebCrypto HKDF deriveKey (AES-GCM)', async () => {
const expected = vec.okm.slice(0, 64); // 32 bytes * 2 hex chars
expect(Buffer.from(rawDerived).toString('hex')).to.equal(expected);
});

// --- HKDF-Extract / HKDF-Expand split (RFC 5869 §2.2–2.3, issue #1060) ---
//
// Validates the two steps independently against the published intermediate
// PRK, and that Extract→Expand recomposes the full-HKDF OKM.
for (let i = 0; i < testVectors.length; i++) {
const vec = testVectors[i]!;

test(SUITE, `hkdfExtractSync (RFC 5869 Case ${i + 1})`, () => {
const ikm = Buffer.from(vec.ikm, 'hex');
const salt = Buffer.from(vec.salt, 'hex');
const prk = hkdfExtractSync(vec.algo, ikm, salt);
expect(prk.toString('hex')).to.equal(vec.prk);
});

test(SUITE, `hkdfExpandSync (RFC 5869 Case ${i + 1})`, () => {
const prk = Buffer.from(vec.prk, 'hex');
const info = Buffer.from(vec.info, 'hex');
const okm = hkdfExpandSync(vec.algo, prk, info, vec.len);
expect(okm.toString('hex')).to.equal(vec.okm);
});

test(
SUITE,
`hkdfExtractSync→hkdfExpandSync recomposes OKM (Case ${i + 1})`,
() => {
const ikm = Buffer.from(vec.ikm, 'hex');
const salt = Buffer.from(vec.salt, 'hex');
const info = Buffer.from(vec.info, 'hex');
const prk = hkdfExtractSync(vec.algo, ikm, salt);
const okm = hkdfExpandSync(vec.algo, prk, info, vec.len);
expect(okm.toString('hex')).to.equal(vec.okm);
},
);

test(SUITE, `hkdfExtract async (RFC 5869 Case ${i + 1})`, async () => {
const ikm = Buffer.from(vec.ikm, 'hex');
const salt = Buffer.from(vec.salt, 'hex');
return new Promise<void>((resolve, reject) => {
hkdfExtract(vec.algo, ikm, salt, (err, prk) => {
try {
expect(err).to.equal(null);
expect(prk?.toString('hex')).to.equal(vec.prk);
resolve();
} catch (e) {
reject(e);
}
});
});
});

test(SUITE, `hkdfExpand async (RFC 5869 Case ${i + 1})`, async () => {
const prk = Buffer.from(vec.prk, 'hex');
const info = Buffer.from(vec.info, 'hex');
return new Promise<void>((resolve, reject) => {
hkdfExpand(vec.algo, prk, info, vec.len, (err, okm) => {
try {
expect(err).to.equal(null);
expect(okm?.toString('hex')).to.equal(vec.okm);
resolve();
} catch (e) {
reject(e);
}
});
});
});
}

// Extract with omitted salt defaults to HashLen zero bytes (RFC 5869 §2.2).
// Case 3 uses an all-zero salt path, so an empty explicit salt must match.
test(SUITE, 'hkdfExtractSync: omitted salt defaults to HashLen zeros', () => {
const vec = testVectors[2]!; // Case 3, sha256, empty salt
const ikm = Buffer.from(vec.ikm, 'hex');
const prk = hkdfExtractSync(vec.algo, ikm);
expect(prk.toString('hex')).to.equal(vec.prk);
});

// Expand inherits the RFC 5869 keylen ceiling (255 * HashLen).
test(SUITE, 'hkdfExpandSync: rejects keylen > 255 * HashLen', () => {
const prk = Buffer.from('00'.repeat(32), 'hex');
expect(() => {
hkdfExpandSync('sha256', prk, Buffer.alloc(0), 8161);
}).to.throw(RangeError, /exceeds RFC 5869 ceiling/);
});

// Expand requires PRK >= HashLen (RFC 5869 §2.3).
test(SUITE, 'hkdfExpandSync: rejects PRK shorter than HashLen', () => {
const shortPrk = Buffer.from('00'.repeat(16), 'hex'); // 16 < sha256 HashLen 32
expect(() => {
hkdfExpandSync('sha256', shortPrk, Buffer.alloc(0), 32);
}).to.throw(RangeError, /at least HashLen/);
});
33 changes: 25 additions & 8 deletions packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,22 @@

namespace margelo::nitro::crypto {

std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> HybridHkdf::deriveKey(const std::string& algorithm,
const std::shared_ptr<ArrayBuffer>& key,
const std::shared_ptr<ArrayBuffer>& salt,
const std::shared_ptr<ArrayBuffer>& info, double length) {
std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>>
HybridHkdf::deriveKey(const std::string& algorithm, const std::shared_ptr<ArrayBuffer>& key, const std::shared_ptr<ArrayBuffer>& salt,
const std::shared_ptr<ArrayBuffer>& info, double length, HkdfMode mode) {
// get owned NativeArrayBuffers before passing to sync function
auto nativeKey = ToNativeArrayBuffer(key);
auto nativeSalt = ToNativeArrayBuffer(salt);
auto nativeInfo = ToNativeArrayBuffer(info);

return Promise<std::shared_ptr<ArrayBuffer>>::async([this, algorithm, nativeKey, nativeSalt, nativeInfo, length]() {
return this->deriveKeySync(algorithm, nativeKey, nativeSalt, nativeInfo, length);
return Promise<std::shared_ptr<ArrayBuffer>>::async([this, algorithm, nativeKey, nativeSalt, nativeInfo, length, mode]() {
return this->deriveKeySync(algorithm, nativeKey, nativeSalt, nativeInfo, length, mode);
});
}

std::shared_ptr<ArrayBuffer> HybridHkdf::deriveKeySync(const std::string& algorithm, const std::shared_ptr<ArrayBuffer>& baseKey,
const std::shared_ptr<ArrayBuffer>& salt, const std::shared_ptr<ArrayBuffer>& info,
double length) {
double length, HkdfMode mode) {
EVP_KDF* kdf = EVP_KDF_fetch(nullptr, "HKDF", nullptr);
if (kdf == nullptr) {
throw std::runtime_error("Failed to fetch HKDF implementation: " + std::to_string(ERR_get_error()));
Expand All @@ -41,9 +40,27 @@ std::shared_ptr<ArrayBuffer> HybridHkdf::deriveKeySync(const std::string& algori
}

// Set up parameters
OSSL_PARAM params[5];
OSSL_PARAM params[6];
size_t paramIndex = 0;

// HKDF mode (RFC 5869): full extract+expand, extract-only (PRK), or expand-only.
int hkdfMode;
switch (mode) {
case HkdfMode::EXTRACT:
hkdfMode = EVP_KDF_HKDF_MODE_EXTRACT_ONLY;
break;
case HkdfMode::EXPAND:
hkdfMode = EVP_KDF_HKDF_MODE_EXPAND_ONLY;
break;
case HkdfMode::FULL:
hkdfMode = EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND;
break;
default:
EVP_KDF_CTX_free(ctx);
throw std::runtime_error("Unknown HKDF mode");
}
params[paramIndex++] = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &hkdfMode);

params[paramIndex++] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, const_cast<char*>(algorithm.c_str()), 0);

// Key (Input Keying Material)
Expand Down
5 changes: 3 additions & 2 deletions packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ class HybridHkdf : public HybridHkdfSpec {
// Methods
std::shared_ptr<ArrayBuffer> deriveKeySync(const std::string& algorithm, const std::shared_ptr<ArrayBuffer>& key,
const std::shared_ptr<ArrayBuffer>& salt, const std::shared_ptr<ArrayBuffer>& info,
double length) override;
double length, HkdfMode mode) override;
std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> deriveKey(const std::string& algorithm, const std::shared_ptr<ArrayBuffer>& key,
const std::shared_ptr<ArrayBuffer>& salt,
const std::shared_ptr<ArrayBuffer>& info, double length) override;
const std::shared_ptr<ArrayBuffer>& info, double length,
HkdfMode mode) override;
};

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