From 405719e9b74c1aa127debec31ee95bc2fb889c27 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Tue, 7 Jul 2026 20:13:17 -0400 Subject: [PATCH 1/3] docs: troubleshoot 'QuickBase64 could not be found' (New Arch requirement) react-native-quick-base64 3.0.0+ is a pure C++ TurboModule that only registers under the New Architecture. As a required dependency of react-native-quick-crypto, it crashes the app on launch when the consuming app runs on the Old Architecture. Document the cause and the fixes (enable New Arch + clean rebuild, or pin quick-base64 2.2.2). Closes #1052 --- .docs/troubleshooting.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.docs/troubleshooting.md b/.docs/troubleshooting.md index 7d6428008..53b27c5a2 100644 --- a/.docs/troubleshooting.md +++ b/.docs/troubleshooting.md @@ -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: From 4c2741b0ee0cb3bc30ea9b9e3b9c950526d80bb4 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 8 Jul 2026 09:33:16 -0400 Subject: [PATCH 2/3] feat(hkdf): add hkdfExtract and hkdfExpand (RFC 5869, #1060) Expose the HKDF Extract and Expand steps independently for protocols that run them at different stages (TLS 1.3, Noise, Signal, MLS). The native HKDF path gains a `mode` param mapping to OpenSSL's EVP_KDF_HKDF_MODE_{EXTRACT_ONLY,EXPAND_ONLY,EXTRACT_AND_EXPAND}; existing full-HKDF callers pass 'full'. - hkdfExtract(digest, ikm[, salt]) -> PRK (HashLen bytes) - hkdfExpand(digest, prk, info, keylen) -> OKM - Tests: RFC 5869 A.1-A.7 PRK/OKM vectors, extract->expand recompose, default-salt, keylen ceiling - Docs: api/hkdf.mdx + both coverage tables (not-in-node / RNQC extension) --- .docs/implementation-coverage.md | 3 + docs/content/docs/api/hkdf.mdx | 25 ++++++++ docs/data/coverage.ts | 10 +++ example/ios/Podfile.lock | 2 +- example/src/tests/hkdf/hkdf_tests.ts | 63 ++++++++++++++++++- .../cpp/hkdf/HybridHkdf.cpp | 26 +++++--- .../cpp/hkdf/HybridHkdf.hpp | 5 +- .../generated/shared/c++/HybridHkdfSpec.hpp | 4 +- .../react-native-quick-crypto/src/hkdf.ts | 56 +++++++++++++++++ .../src/specs/hkdf.nitro.ts | 3 + 10 files changed, 183 insertions(+), 14 deletions(-) diff --git a/.docs/implementation-coverage.md b/.docs/implementation-coverage.md index 5d0cc4d9f..f1fe73ad4 100644 --- a/.docs/implementation-coverage.md +++ b/.docs/implementation-coverage.md @@ -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 @@ -145,6 +146,8 @@ 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])` (RFC 5869 Extract-only) + - ✨ `crypto.hkdfExpand(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)` diff --git a/docs/content/docs/api/hkdf.mdx b/docs/content/docs/api/hkdf.mdx index c789ce6f5..44ca5eb8b 100644 --- a/docs/content/docs/api/hkdf.mdx +++ b/docs/content/docs/api/hkdf.mdx @@ -73,3 +73,28 @@ const derived = QuickCrypto.hkdfSync( } }} /> + +### hkdfExtract(digest, ikm, salt?) + +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. + +```ts +import QuickCrypto from 'react-native-quick-crypto'; + +const prk = QuickCrypto.hkdfExtract('sha256', 'ikm-secret', 'salt'); +``` + +### hkdfExpand(digest, prk, info, keylen) + +Runs only the **Expand** step, deriving `keylen` bytes of output keying +material from an already-derived `prk`. `keylen` must not exceed +`255 * HashLen` (RFC 5869 §2.3). + +```ts +import QuickCrypto from 'react-native-quick-crypto'; + +const okm = QuickCrypto.hkdfExpand('sha256', prk, 'context-info', 64); +``` diff --git a/docs/data/coverage.ts b/docs/data/coverage.ts index 376be4dad..5dc8e8288 100644 --- a/docs/data/coverage.ts +++ b/docs/data/coverage.ts @@ -275,6 +275,16 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'getRandomValues', status: 'implemented' }, { name: 'hash', status: 'implemented' }, { name: 'hkdf', status: 'implemented' }, + { + name: 'hkdfExpand', + status: 'not-in-node', + note: 'RNQC extension: RFC 5869 Expand-only', + }, + { + name: 'hkdfExtract', + 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' }, diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index cadd36e07..40d396d47 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2841,7 +2841,7 @@ SPEC CHECKSUMS: NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 OpenSSL-Universal: ecee7b138fa75a74ecf00d7ffd248fb584739b9e - QuickCrypto: fd9ac356a20fa61425a09f5a6f7eff8d50ca3172 + QuickCrypto: 21eda4d230c62635279e316ea06abead4df1c662 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077 RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a diff --git a/example/src/tests/hkdf/hkdf_tests.ts b/example/src/tests/hkdf/hkdf_tests.ts index f757ec164..940d90a09 100644 --- a/example/src/tests/hkdf/hkdf_tests.ts +++ b/example/src/tests/hkdf/hkdf_tests.ts @@ -1,4 +1,10 @@ -import crypto, { Buffer, hkdf, hkdfSync } from 'react-native-quick-crypto'; +import crypto, { + Buffer, + hkdf, + hkdfSync, + hkdfExtract, + hkdfExpand, +} from 'react-native-quick-crypto'; import { expect } from 'chai'; import { test } from '../util'; @@ -17,6 +23,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 @@ -26,6 +33,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 @@ -35,6 +43,7 @@ const testVectors = [ info: '', len: 42, okm: '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8', + prk: '19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04', algo: 'sha256', }, // A.4 — Test Case 4: Basic test case with SHA-1 @@ -44,6 +53,7 @@ const testVectors = [ info: 'f0f1f2f3f4f5f6f7f8f9', len: 42, okm: '085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896', + prk: '9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243', algo: 'sha1', }, // A.5 — Test Case 5: SHA-1 with longer inputs/outputs @@ -53,6 +63,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 @@ -62,6 +73,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 @@ -71,6 +83,7 @@ const testVectors = [ info: '', len: 42, okm: '2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48', + prk: '2adccada18779e7c2077ad2eb19d3f3e731385dd', algo: 'sha1', }, ]; @@ -306,3 +319,51 @@ 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, `hkdfExtract (RFC 5869 Case ${i + 1})`, () => { + const ikm = Buffer.from(vec.ikm, 'hex'); + const salt = Buffer.from(vec.salt, 'hex'); + const prk = hkdfExtract(vec.algo, ikm, salt); + expect(prk.toString('hex')).to.equal(vec.prk); + }); + + test(SUITE, `hkdfExpand (RFC 5869 Case ${i + 1})`, () => { + const prk = Buffer.from(vec.prk, 'hex'); + const info = Buffer.from(vec.info, 'hex'); + const okm = hkdfExpand(vec.algo, prk, info, vec.len); + expect(okm.toString('hex')).to.equal(vec.okm); + }); + + test(SUITE, `hkdfExtract→hkdfExpand 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 = hkdfExtract(vec.algo, ikm, salt); + const okm = hkdfExpand(vec.algo, prk, info, vec.len); + expect(okm.toString('hex')).to.equal(vec.okm); + }); +} + +// 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, 'hkdfExtract: omitted salt defaults to HashLen zeros', () => { + const vec = testVectors[2]!; // Case 3, sha256, empty salt + const ikm = Buffer.from(vec.ikm, 'hex'); + const prk = hkdfExtract(vec.algo, ikm); + expect(prk.toString('hex')).to.equal(vec.prk); +}); + +// Expand inherits the RFC 5869 keylen ceiling (255 * HashLen). +test(SUITE, 'hkdfExpand: rejects keylen > 255 * HashLen', () => { + const prk = Buffer.from('00'.repeat(32), 'hex'); + expect(() => { + hkdfExpand('sha256', prk, Buffer.alloc(0), 8161); + }).to.throw(RangeError, /exceeds RFC 5869 ceiling/); +}); diff --git a/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp b/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp index 3b1b32cef..685b93158 100644 --- a/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp +++ b/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp @@ -12,23 +12,22 @@ namespace margelo::nitro::crypto { -std::shared_ptr>> HybridHkdf::deriveKey(const std::string& algorithm, - const std::shared_ptr& key, - const std::shared_ptr& salt, - const std::shared_ptr& info, double length) { +std::shared_ptr>> +HybridHkdf::deriveKey(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, + const std::shared_ptr& info, double length, const std::string& mode) { // get owned NativeArrayBuffers before passing to sync function auto nativeKey = ToNativeArrayBuffer(key); auto nativeSalt = ToNativeArrayBuffer(salt); auto nativeInfo = ToNativeArrayBuffer(info); - return Promise>::async([this, algorithm, nativeKey, nativeSalt, nativeInfo, length]() { - return this->deriveKeySync(algorithm, nativeKey, nativeSalt, nativeInfo, length); + return Promise>::async([this, algorithm, nativeKey, nativeSalt, nativeInfo, length, mode]() { + return this->deriveKeySync(algorithm, nativeKey, nativeSalt, nativeInfo, length, mode); }); } std::shared_ptr HybridHkdf::deriveKeySync(const std::string& algorithm, const std::shared_ptr& baseKey, const std::shared_ptr& salt, const std::shared_ptr& info, - double length) { + double length, const std::string& 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())); @@ -41,9 +40,20 @@ std::shared_ptr 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; + if (mode == "extract") { + hkdfMode = EVP_KDF_HKDF_MODE_EXTRACT_ONLY; + } else if (mode == "expand") { + hkdfMode = EVP_KDF_HKDF_MODE_EXPAND_ONLY; + } else { + hkdfMode = EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND; + } + params[paramIndex++] = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &hkdfMode); + params[paramIndex++] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, const_cast(algorithm.c_str()), 0); // Key (Input Keying Material) diff --git a/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.hpp b/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.hpp index 6e47ac703..4e3df5cfe 100644 --- a/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.hpp +++ b/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.hpp @@ -19,10 +19,11 @@ class HybridHkdf : public HybridHkdfSpec { // Methods std::shared_ptr deriveKeySync(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, - double length) override; + double length, const std::string& mode) override; std::shared_ptr>> deriveKey(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, - const std::shared_ptr& info, double length) override; + const std::shared_ptr& info, double length, + const std::string& mode) override; }; } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHkdfSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHkdfSpec.hpp index c60f19bc5..bcd418878 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHkdfSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHkdfSpec.hpp @@ -50,8 +50,8 @@ namespace margelo::nitro::crypto { public: // Methods - virtual std::shared_ptr deriveKeySync(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, double length) = 0; - virtual std::shared_ptr>> deriveKey(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, double length) = 0; + virtual std::shared_ptr deriveKeySync(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, double length, const std::string& mode) = 0; + virtual std::shared_ptr>> deriveKey(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, double length, const std::string& mode) = 0; protected: // Hybrid Setup diff --git a/packages/react-native-quick-crypto/src/hkdf.ts b/packages/react-native-quick-crypto/src/hkdf.ts index 5f12ae9bb..c339bc5cf 100644 --- a/packages/react-native-quick-crypto/src/hkdf.ts +++ b/packages/react-native-quick-crypto/src/hkdf.ts @@ -111,6 +111,7 @@ export function hkdf( sanitizedSalt, sanitizedInfo, keylen, + 'full', ) .then( res => { @@ -146,6 +147,60 @@ export function hkdfSync( sanitizedSalt, sanitizedInfo, keylen, + 'full', + ); + + return Buffer.from(result); +} + +// RFC 5869 §2.2 HKDF-Extract: PRK = HMAC(salt, IKM). Salt defaults to a +// string of HashLen zeros when omitted. Returns a PRK of HashLen bytes. +export function hkdfExtract( + digest: string, + ikm: KeyMaterial, + salt: Salt = new Uint8Array(0), +): Buffer { + const normalizedDigest = normalizeHashName(digest); + const hashLen = HKDF_HASH_BYTES[normalizedDigest.toLowerCase()]; + if (hashLen === undefined) { + throw new TypeError(`Unsupported HKDF digest: ${digest}`); + } + const sanitizedIkm = sanitizeInput(ikm, 'IKM'); + const sanitizedSalt = sanitizeInput(salt, 'Salt'); + + const result = getNative().deriveKeySync( + normalizedDigest, + sanitizedIkm, + sanitizedSalt, + new ArrayBuffer(0), + hashLen, + 'extract', + ); + + return Buffer.from(result); +} + +// RFC 5869 §2.3 HKDF-Expand: OKM = expand(PRK, info, L). `prk` must be at +// least HashLen bytes (a pseudorandom key, e.g. from hkdfExtract). +export function hkdfExpand( + digest: string, + prk: KeyMaterial, + info: Info, + keylen: number, +): Buffer { + const normalizedDigest = normalizeHashName(digest); + const sanitizedPrk = sanitizeInput(prk, 'PRK'); + const sanitizedInfo = sanitizeInput(info, 'Info'); + + validateHkdfKeylen(normalizedDigest, keylen); + + const result = getNative().deriveKeySync( + normalizedDigest, + sanitizedPrk, + new ArrayBuffer(0), + sanitizedInfo, + keylen, + 'expand', ); return Buffer.from(result); @@ -179,6 +234,7 @@ export function hkdfDeriveBits( binaryLikeToArrayBuffer(salt), binaryLikeToArrayBuffer(info), keylen, + 'full', ); return result; diff --git a/packages/react-native-quick-crypto/src/specs/hkdf.nitro.ts b/packages/react-native-quick-crypto/src/specs/hkdf.nitro.ts index ea6d3260f..b6fae0bf2 100644 --- a/packages/react-native-quick-crypto/src/specs/hkdf.nitro.ts +++ b/packages/react-native-quick-crypto/src/specs/hkdf.nitro.ts @@ -1,5 +1,6 @@ import type { HybridObject } from 'react-native-nitro-modules'; +// mode: 'full' (extract+expand), 'extract' (PRK only), or 'expand' (from PRK). export interface Hkdf extends HybridObject<{ ios: 'c++'; android: 'c++' }> { deriveKeySync( algorithm: string, @@ -7,6 +8,7 @@ export interface Hkdf extends HybridObject<{ ios: 'c++'; android: 'c++' }> { salt: ArrayBuffer, info: ArrayBuffer, length: number, + mode: string, ): ArrayBuffer; deriveKey( @@ -15,5 +17,6 @@ export interface Hkdf extends HybridObject<{ ios: 'c++'; android: 'c++' }> { salt: ArrayBuffer, info: ArrayBuffer, length: number, + mode: string, ): Promise; } From af5741e9cbde2767fb8d231c6c13585fa488c15b Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 8 Jul 2026 13:21:51 -0400 Subject: [PATCH 3/3] feat(hkdf): async extract/expand, typed mode, PRK guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups on the RFC 5869 Extract/Expand split (#1060): - Add callback-based hkdfExtract/hkdfExpand mirroring hkdf; move the synchronous forms to hkdfExtractSync/hkdfExpandSync. - Replace the stringly-typed native mode param with an HkdfMode union ('full' | 'extract' | 'expand'); native switch throws on unknown mode. - Reject PRK shorter than HashLen in Expand (RFC 5869 §2.3). - Docs + coverage updated; add async and PRK-guard tests. --- .docs/implementation-coverage.md | 6 +- docs/content/docs/api/hkdf.mdx | 23 ++-- docs/data/coverage.ts | 4 +- example/src/tests/hkdf/hkdf_tests.ts | 70 ++++++++++-- .../cpp/hkdf/HybridHkdf.cpp | 23 ++-- .../cpp/hkdf/HybridHkdf.hpp | 4 +- .../generated/shared/c++/HkdfMode.hpp | 80 +++++++++++++ .../generated/shared/c++/HybridHkdfSpec.hpp | 8 +- .../react-native-quick-crypto/src/hkdf.ts | 106 ++++++++++++++++-- .../src/specs/hkdf.nitro.ts | 9 +- 10 files changed, 284 insertions(+), 49 deletions(-) create mode 100644 packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HkdfMode.hpp diff --git a/.docs/implementation-coverage.md b/.docs/implementation-coverage.md index f1fe73ad4..a83fc3eb7 100644 --- a/.docs/implementation-coverage.md +++ b/.docs/implementation-coverage.md @@ -146,8 +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])` (RFC 5869 Extract-only) - - ✨ `crypto.hkdfExpand(digest, prk, info, keylen)` (RFC 5869 Expand-only) + - ✨ `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)` diff --git a/docs/content/docs/api/hkdf.mdx b/docs/content/docs/api/hkdf.mdx index 44ca5eb8b..657f549c4 100644 --- a/docs/content/docs/api/hkdf.mdx +++ b/docs/content/docs/api/hkdf.mdx @@ -74,27 +74,36 @@ const derived = QuickCrypto.hkdfSync( }} /> -### hkdfExtract(digest, ikm, salt?) +### 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. +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.hkdfExtract('sha256', 'ikm-secret', 'salt'); +const prk = QuickCrypto.hkdfExtractSync('sha256', 'ikm-secret', 'salt'); + +QuickCrypto.hkdfExtract('sha256', 'ikm-secret', 'salt', (err, prk) => { + // ... +}); ``` -### hkdfExpand(digest, prk, info, keylen) +### 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`. `keylen` must not exceed -`255 * HashLen` (RFC 5869 §2.3). +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.hkdfExpand('sha256', prk, 'context-info', 64); +const okm = QuickCrypto.hkdfExpandSync('sha256', prk, 'context-info', 64); + +QuickCrypto.hkdfExpand('sha256', prk, 'context-info', 64, (err, okm) => { + // ... +}); ``` diff --git a/docs/data/coverage.ts b/docs/data/coverage.ts index 5dc8e8288..dd4411598 100644 --- a/docs/data/coverage.ts +++ b/docs/data/coverage.ts @@ -276,12 +276,12 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'hash', status: 'implemented' }, { name: 'hkdf', status: 'implemented' }, { - name: 'hkdfExpand', + name: 'hkdfExpand / hkdfExpandSync', status: 'not-in-node', note: 'RNQC extension: RFC 5869 Expand-only', }, { - name: 'hkdfExtract', + name: 'hkdfExtract / hkdfExtractSync', status: 'not-in-node', note: 'RNQC extension: RFC 5869 Extract-only', }, diff --git a/example/src/tests/hkdf/hkdf_tests.ts b/example/src/tests/hkdf/hkdf_tests.ts index 940d90a09..dfcbf151c 100644 --- a/example/src/tests/hkdf/hkdf_tests.ts +++ b/example/src/tests/hkdf/hkdf_tests.ts @@ -3,7 +3,9 @@ import crypto, { hkdf, hkdfSync, hkdfExtract, + hkdfExtractSync, hkdfExpand, + hkdfExpandSync, } from 'react-native-quick-crypto'; import { expect } from 'chai'; import { test } from '../util'; @@ -327,43 +329,87 @@ test(SUITE, 'WebCrypto HKDF deriveKey (AES-GCM)', async () => { for (let i = 0; i < testVectors.length; i++) { const vec = testVectors[i]!; - test(SUITE, `hkdfExtract (RFC 5869 Case ${i + 1})`, () => { + test(SUITE, `hkdfExtractSync (RFC 5869 Case ${i + 1})`, () => { const ikm = Buffer.from(vec.ikm, 'hex'); const salt = Buffer.from(vec.salt, 'hex'); - const prk = hkdfExtract(vec.algo, ikm, salt); + const prk = hkdfExtractSync(vec.algo, ikm, salt); expect(prk.toString('hex')).to.equal(vec.prk); }); - test(SUITE, `hkdfExpand (RFC 5869 Case ${i + 1})`, () => { + test(SUITE, `hkdfExpandSync (RFC 5869 Case ${i + 1})`, () => { const prk = Buffer.from(vec.prk, 'hex'); const info = Buffer.from(vec.info, 'hex'); - const okm = hkdfExpand(vec.algo, prk, info, vec.len); + const okm = hkdfExpandSync(vec.algo, prk, info, vec.len); expect(okm.toString('hex')).to.equal(vec.okm); }); - test(SUITE, `hkdfExtract→hkdfExpand recomposes OKM (Case ${i + 1})`, () => { + 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((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'); - const prk = hkdfExtract(vec.algo, ikm, salt); - const okm = hkdfExpand(vec.algo, prk, info, vec.len); - expect(okm.toString('hex')).to.equal(vec.okm); + return new Promise((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, 'hkdfExtract: omitted salt defaults to HashLen zeros', () => { +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 = hkdfExtract(vec.algo, ikm); + 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, 'hkdfExpand: rejects keylen > 255 * HashLen', () => { +test(SUITE, 'hkdfExpandSync: rejects keylen > 255 * HashLen', () => { const prk = Buffer.from('00'.repeat(32), 'hex'); expect(() => { - hkdfExpand('sha256', prk, Buffer.alloc(0), 8161); + 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/); +}); diff --git a/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp b/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp index 685b93158..f74d23fac 100644 --- a/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp +++ b/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.cpp @@ -14,7 +14,7 @@ namespace margelo::nitro::crypto { std::shared_ptr>> HybridHkdf::deriveKey(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, - const std::shared_ptr& info, double length, const std::string& mode) { + const std::shared_ptr& info, double length, HkdfMode mode) { // get owned NativeArrayBuffers before passing to sync function auto nativeKey = ToNativeArrayBuffer(key); auto nativeSalt = ToNativeArrayBuffer(salt); @@ -27,7 +27,7 @@ HybridHkdf::deriveKey(const std::string& algorithm, const std::shared_ptr HybridHkdf::deriveKeySync(const std::string& algorithm, const std::shared_ptr& baseKey, const std::shared_ptr& salt, const std::shared_ptr& info, - double length, const std::string& mode) { + 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())); @@ -45,12 +45,19 @@ std::shared_ptr HybridHkdf::deriveKeySync(const std::string& algori // HKDF mode (RFC 5869): full extract+expand, extract-only (PRK), or expand-only. int hkdfMode; - if (mode == "extract") { - hkdfMode = EVP_KDF_HKDF_MODE_EXTRACT_ONLY; - } else if (mode == "expand") { - hkdfMode = EVP_KDF_HKDF_MODE_EXPAND_ONLY; - } else { - hkdfMode = EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND; + 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); diff --git a/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.hpp b/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.hpp index 4e3df5cfe..89079a556 100644 --- a/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.hpp +++ b/packages/react-native-quick-crypto/cpp/hkdf/HybridHkdf.hpp @@ -19,11 +19,11 @@ class HybridHkdf : public HybridHkdfSpec { // Methods std::shared_ptr deriveKeySync(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, - double length, const std::string& mode) override; + double length, HkdfMode mode) override; std::shared_ptr>> deriveKey(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, double length, - const std::string& mode) override; + HkdfMode mode) override; }; } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HkdfMode.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HkdfMode.hpp new file mode 100644 index 000000000..e159ca26a --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HkdfMode.hpp @@ -0,0 +1,80 @@ +/// +/// HkdfMode.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::crypto { + + /** + * An enum which can be represented as a JavaScript union (HkdfMode). + */ + enum class HkdfMode { + FULL SWIFT_NAME(full) = 0, + EXTRACT SWIFT_NAME(extract) = 1, + EXPAND SWIFT_NAME(expand) = 2, + } CLOSED_ENUM; + +} // namespace margelo::nitro::crypto + +namespace margelo::nitro { + + // C++ HkdfMode <> JS HkdfMode (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::crypto::HkdfMode fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("full"): return margelo::nitro::crypto::HkdfMode::FULL; + case hashString("extract"): return margelo::nitro::crypto::HkdfMode::EXTRACT; + case hashString("expand"): return margelo::nitro::crypto::HkdfMode::EXPAND; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum HkdfMode - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::crypto::HkdfMode arg) { + switch (arg) { + case margelo::nitro::crypto::HkdfMode::FULL: return JSIConverter::toJSI(runtime, "full"); + case margelo::nitro::crypto::HkdfMode::EXTRACT: return JSIConverter::toJSI(runtime, "extract"); + case margelo::nitro::crypto::HkdfMode::EXPAND: return JSIConverter::toJSI(runtime, "expand"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert HkdfMode to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("full"): + case hashString("extract"): + case hashString("expand"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHkdfSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHkdfSpec.hpp index bcd418878..4cd9ea9f2 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHkdfSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHkdfSpec.hpp @@ -13,10 +13,12 @@ #error NitroModules cannot be found! Are you sure you installed NitroModules properly? #endif - +// Forward declaration of `HkdfMode` to properly resolve imports. +namespace margelo::nitro::crypto { enum class HkdfMode; } #include #include +#include "HkdfMode.hpp" #include namespace margelo::nitro::crypto { @@ -50,8 +52,8 @@ namespace margelo::nitro::crypto { public: // Methods - virtual std::shared_ptr deriveKeySync(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, double length, const std::string& mode) = 0; - virtual std::shared_ptr>> deriveKey(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, double length, const std::string& mode) = 0; + virtual std::shared_ptr deriveKeySync(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, double length, HkdfMode mode) = 0; + virtual std::shared_ptr>> deriveKey(const std::string& algorithm, const std::shared_ptr& key, const std::shared_ptr& salt, const std::shared_ptr& info, double length, HkdfMode mode) = 0; protected: // Hybrid Setup diff --git a/packages/react-native-quick-crypto/src/hkdf.ts b/packages/react-native-quick-crypto/src/hkdf.ts index c339bc5cf..f20b0fe6d 100644 --- a/packages/react-native-quick-crypto/src/hkdf.ts +++ b/packages/react-native-quick-crypto/src/hkdf.ts @@ -62,6 +62,14 @@ const HKDF_HASH_BYTES: Readonly> = { ripemd160: 20, }; +function hkdfHashLen(digest: string): number { + const hashLen = HKDF_HASH_BYTES[digest.toLowerCase()]; + if (hashLen === undefined) { + throw new TypeError(`Unsupported HKDF digest: ${digest}`); + } + return hashLen; +} + function validateHkdfKeylen(digest: string, keylen: number): void { if ( typeof keylen !== 'number' || @@ -153,18 +161,15 @@ export function hkdfSync( return Buffer.from(result); } -// RFC 5869 §2.2 HKDF-Extract: PRK = HMAC(salt, IKM). Salt defaults to a -// string of HashLen zeros when omitted. Returns a PRK of HashLen bytes. -export function hkdfExtract( +// RFC 5869 §2.2 HKDF-Extract (sync): PRK = HMAC(salt, IKM). Salt defaults to +// a string of HashLen zeros when omitted. Returns a PRK of HashLen bytes. +export function hkdfExtractSync( digest: string, ikm: KeyMaterial, salt: Salt = new Uint8Array(0), ): Buffer { const normalizedDigest = normalizeHashName(digest); - const hashLen = HKDF_HASH_BYTES[normalizedDigest.toLowerCase()]; - if (hashLen === undefined) { - throw new TypeError(`Unsupported HKDF digest: ${digest}`); - } + const hashLen = hkdfHashLen(normalizedDigest); const sanitizedIkm = sanitizeInput(ikm, 'IKM'); const sanitizedSalt = sanitizeInput(salt, 'Salt'); @@ -180,16 +185,56 @@ export function hkdfExtract( return Buffer.from(result); } -// RFC 5869 §2.3 HKDF-Expand: OKM = expand(PRK, info, L). `prk` must be at -// least HashLen bytes (a pseudorandom key, e.g. from hkdfExtract). -export function hkdfExpand( +// Async HKDF-Extract, mirroring `hkdf`. Unlike the sync form, `salt` is +// required here because the callback occupies the trailing argument. +export function hkdfExtract( + digest: string, + ikm: KeyMaterial, + salt: Salt, + callback: HkdfCallback, +): void { + validateCallback(callback); + + try { + const normalizedDigest = normalizeHashName(digest); + const hashLen = hkdfHashLen(normalizedDigest); + const sanitizedIkm = sanitizeInput(ikm, 'IKM'); + const sanitizedSalt = sanitizeInput(salt, 'Salt'); + + getNative() + .deriveKey( + normalizedDigest, + sanitizedIkm, + sanitizedSalt, + new ArrayBuffer(0), + hashLen, + 'extract', + ) + .then( + res => callback(null, Buffer.from(res)), + err => callback(err), + ); + } catch (err) { + callback(err as Error); + } +} + +// RFC 5869 §2.3 HKDF-Expand (sync): OKM = expand(PRK, info, L). `prk` must be +// at least HashLen bytes (a pseudorandom key, e.g. from hkdfExtract). +export function hkdfExpandSync( digest: string, prk: KeyMaterial, info: Info, keylen: number, ): Buffer { const normalizedDigest = normalizeHashName(digest); + const hashLen = hkdfHashLen(normalizedDigest); const sanitizedPrk = sanitizeInput(prk, 'PRK'); + if (sanitizedPrk.byteLength < hashLen) { + throw new RangeError( + `HKDF-Expand PRK must be at least HashLen (${hashLen}) bytes for ${digest}`, + ); + } const sanitizedInfo = sanitizeInput(info, 'Info'); validateHkdfKeylen(normalizedDigest, keylen); @@ -206,6 +251,47 @@ export function hkdfExpand( return Buffer.from(result); } +// Async HKDF-Expand, mirroring `hkdf`. +export function hkdfExpand( + digest: string, + prk: KeyMaterial, + info: Info, + keylen: number, + callback: HkdfCallback, +): void { + validateCallback(callback); + + try { + const normalizedDigest = normalizeHashName(digest); + const hashLen = hkdfHashLen(normalizedDigest); + const sanitizedPrk = sanitizeInput(prk, 'PRK'); + if (sanitizedPrk.byteLength < hashLen) { + throw new RangeError( + `HKDF-Expand PRK must be at least HashLen (${hashLen}) bytes for ${digest}`, + ); + } + const sanitizedInfo = sanitizeInput(info, 'Info'); + + validateHkdfKeylen(normalizedDigest, keylen); + + getNative() + .deriveKey( + normalizedDigest, + sanitizedPrk, + new ArrayBuffer(0), + sanitizedInfo, + keylen, + 'expand', + ) + .then( + res => callback(null, Buffer.from(res)), + err => callback(err), + ); + } catch (err) { + callback(err as Error); + } +} + export function hkdfDeriveBits( algorithm: HkdfAlgorithm, baseKey: CryptoKey, diff --git a/packages/react-native-quick-crypto/src/specs/hkdf.nitro.ts b/packages/react-native-quick-crypto/src/specs/hkdf.nitro.ts index b6fae0bf2..c98ebe5f5 100644 --- a/packages/react-native-quick-crypto/src/specs/hkdf.nitro.ts +++ b/packages/react-native-quick-crypto/src/specs/hkdf.nitro.ts @@ -1,6 +1,9 @@ import type { HybridObject } from 'react-native-nitro-modules'; -// mode: 'full' (extract+expand), 'extract' (PRK only), or 'expand' (from PRK). +// RFC 5869 stage: 'full' (extract+expand), 'extract' (PRK only), or +// 'expand' (from an existing PRK). +export type HkdfMode = 'full' | 'extract' | 'expand'; + export interface Hkdf extends HybridObject<{ ios: 'c++'; android: 'c++' }> { deriveKeySync( algorithm: string, @@ -8,7 +11,7 @@ export interface Hkdf extends HybridObject<{ ios: 'c++'; android: 'c++' }> { salt: ArrayBuffer, info: ArrayBuffer, length: number, - mode: string, + mode: HkdfMode, ): ArrayBuffer; deriveKey( @@ -17,6 +20,6 @@ export interface Hkdf extends HybridObject<{ ios: 'c++'; android: 'c++' }> { salt: ArrayBuffer, info: ArrayBuffer, length: number, - mode: string, + mode: HkdfMode, ): Promise; }