diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 20a1b687..e7abbfab 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2815,7 +2815,7 @@ SPEC CHECKSUMS: NitroMmkv: afbc5b2fbf963be567c6c545aa1efcf6a9cec68e NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 OpenSSL-Universal: 9110d21982bb7e8b22a962b6db56a8aa805afde7 - QuickCrypto: 30f0d4eceadf1dbb9df5c17605e9f4279a0041ea + QuickCrypto: 71ccdcd47ae5f72d4817a4914966b8c34b3eb48d RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077 RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts index 1228d918..e3ee74d5 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -36,6 +36,7 @@ import '../tests/subtle/argon2_deriveBits'; import '../tests/subtle/deriveBits'; import '../tests/subtle/derive_key'; import '../tests/subtle/digest'; +import '../tests/subtle/digest_turboshake'; import '../tests/subtle/encap_decap'; import '../tests/subtle/encrypt_decrypt'; import '../tests/subtle/generateKey'; diff --git a/example/src/tests/subtle/digest_turboshake.ts b/example/src/tests/subtle/digest_turboshake.ts new file mode 100644 index 00000000..22aa6b30 --- /dev/null +++ b/example/src/tests/subtle/digest_turboshake.ts @@ -0,0 +1,363 @@ +import { expect } from 'chai'; +import { Buffer, subtle } from 'react-native-quick-crypto'; +import { test } from '../util'; + +// RFC 9861 §5 test vectors for TurboSHAKE128/256 and KangarooTwelve KT128/256. +// Vectors mirror Node's test/parallel/test-webcrypto-digest-turboshake-rfc.js. + +const SUITE = 'subtle.digest.turboshake'; + +// ptn(n): RFC 9861 helper — n bytes following the pattern 00, 01, ..., F9, FA. +function ptn(n: number): Uint8Array { + const out = new Uint8Array(n); + for (let i = 0; i < n; i++) out[i] = i % 251; + return out; +} + +const fromHex = (hex: string): Uint8Array => { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + return bytes; +}; + +type ShakeVec = [Uint8Array, number, string, number?]; // [input, outBytes, hex, domainSeparation?] +type KtVec = [Uint8Array, number, string, Uint8Array?]; // [input, outBytes, hex, customization?] + +const turboSHAKE128Vectors: ShakeVec[] = [ + [ + new Uint8Array(0), + 32, + '1e415f1c5983aff2169217277d17bb53' + '8cd945a397ddec541f1ce41af2c1b74c', + ], + [ + new Uint8Array(0), + 64, + '1e415f1c5983aff2169217277d17bb53' + + '8cd945a397ddec541f1ce41af2c1b74c' + + '3e8ccae2a4dae56c84a04c2385c03c15' + + 'e8193bdf58737363321691c05462c8df', + ], + [ + ptn(1), + 32, + '55cedd6f60af7bb29a4042ae832ef3f5' + '8db7299f893ebb9247247d856958daa9', + ], + [ + ptn(17), + 32, + '9c97d036a3bac819db70ede0ca554ec6' + 'e4c2a1a4ffbfd9ec269ca6a111161233', + ], + [ + ptn(17 ** 2), + 32, + '96c77c279e0126f7fc07c9b07f5cdae1' + 'e0be60bdbe10620040e75d7223a624d2', + ], + [ + ptn(17 ** 3), + 32, + 'd4976eb56bcf118520582b709f73e1d6' + '853e001fdaf80e1b13e0d0599d5fb372', + ], + [ + fromHex('ffffff'), + 32, + 'bf323f940494e88ee1c540fe660be8a0' + 'c93f43d15ec006998462fa994eed5dab', + 0x01, + ], + [ + fromHex('ff'), + 32, + '8ec9c66465ed0d4a6c35d13506718d68' + '7a25cb05c74cca1e42501abd83874a67', + 0x06, + ], + [ + fromHex('ffffff'), + 32, + 'b658576001cad9b1e5f399a9f77723bb' + 'a05458042d68206f7252682dba3663ed', + 0x07, + ], + [ + fromHex('ffffffffffffff'), + 32, + '8deeaa1aec47ccee569f659c21dfa8e1' + '12db3cee37b18178b2acd805b799cc37', + 0x0b, + ], + [ + fromHex('ff'), + 32, + '553122e2135e363c3292bed2c6421fa2' + '32bab03daa07c7d6636603286506325b', + 0x30, + ], + [ + fromHex('ffffff'), + 32, + '16274cc656d44cefd422395d0f9053bd' + 'a6d28e122aba15c765e5ad0e6eaf26f9', + 0x7f, + ], +]; + +const turboSHAKE256Vectors: ShakeVec[] = [ + [ + new Uint8Array(0), + 64, + '367a329dafea871c7802ec67f905ae13' + + 'c57695dc2c6663c61035f59a18f8e7db' + + '11edc0e12e91ea60eb6b32df06dd7f00' + + '2fbafabb6e13ec1cc20d995547600db0', + ], + [ + ptn(1), + 64, + '3e1712f928f8eaf1054632b2aa0a246e' + + 'd8b0c378728f60bc970410155c28820e' + + '90cc90d8a3006aa2372c5c5ea176b068' + + '2bf22bae7467ac94f74d43d39b0482e2', + ], + [ + ptn(17 ** 2), + 64, + '66b810db8e90780424c0847372fdc957' + + '10882fde31c6df75beb9d4cd9305cfca' + + 'e35e7b83e8b7e6eb4b78605880116316' + + 'fe2c078a09b94ad7b8213c0a738b65c0', + ], + [ + fromHex('ffffff'), + 64, + 'd21c6fbbf587fa2282f29aea620175fb' + + '0257413af78a0b1b2a87419ce031d933' + + 'ae7a4d383327a8a17641a34f8a1d1003' + + 'ad7da6b72dba84bb62fef28f62f12424', + 0x01, + ], + [ + fromHex('ffffffffffffff'), + 64, + 'bb36764951ec97e9d85f7ee9a67a7718' + + 'fc005cf42556be79ce12c0bde50e5736' + + 'd6632b0d0dfb202d1bbb8ffe3dd74cb0' + + '0834fa756cb03471bab13a1e2c16b3c0', + 0x0b, + ], +]; + +const kt128Vectors: KtVec[] = [ + [ + new Uint8Array(0), + 32, + '1ac2d450fc3b4205d19da7bfca1b3751' + '3c0803577ac7167f06fe2ce1f0ef39e5', + ], + [ + new Uint8Array(0), + 64, + '1ac2d450fc3b4205d19da7bfca1b3751' + + '3c0803577ac7167f06fe2ce1f0ef39e5' + + '4269c056b8c82e48276038b6d292966c' + + 'c07a3d4645272e31ff38508139eb0a71', + ], + [ + ptn(1), + 32, + '2bda92450e8b147f8a7cb629e784a058' + 'efca7cf7d8218e02d345dfaa65244a1f', + ], + [ + ptn(17), + 32, + '6bf75fa2239198db4772e36478f8e19b' + '0f371205f6a9a93a273f51df37122888', + ], + [ + ptn(17 ** 2), + 32, + '0c315ebcdedbf61426de7dcf8fb725d1' + 'e74675d7f5327a5067f367b108ecb67c', + ], + [ + new Uint8Array(0), + 32, + 'fab658db63e94a246188bf7af69a1330' + '45f46ee984c56e3c3328caaf1aa1a583', + ptn(1), + ], + [ + fromHex('ff'), + 32, + 'd848c5068ced736f4462159b9867fd4c' + '20b808acc3d5bc48e0b06ba0a3762ec4', + ptn(41), + ], + // tree-hashing path: |S| > 8192, exercises the multi-chunk branch. + [ + ptn(8192), + 32, + '48f256f6772f9edfb6a8b661ec92dc93' + 'b95ebd05a08a17b39ae3490870c926c3', + ], + [ + ptn(8192), + 32, + '6a7c1b6a5cd0d8c9ca943a4a216cc646' + '04559a2ea45f78570a15253d67ba00ae', + ptn(8190), + ], +]; + +const kt256Vectors: KtVec[] = [ + [ + new Uint8Array(0), + 64, + 'b23d2e9cea9f4904e02bec06817fc10c' + + 'e38ce8e93ef4c89e6537076af8646404' + + 'e3e8b68107b8833a5d30490aa3348235' + + '3fd4adc7148ecb782855003aaebde4a9', + ], + [ + ptn(1), + 64, + '0d005a194085360217128cf17f91e1f7' + + '1314efa5564539d444912e3437efa17f' + + '82db6f6ffe76e781eaa068bce01f2bbf' + + '81eacb983d7230f2fb02834a21b1ddd0', + ], + [ + ptn(17 ** 2), + 64, + 'de8ccbc63e0f133ebb4416814d4c66f6' + + '91bbf8b6a61ec0a7700f836b086cb029' + + 'd54f12ac7159472c72db118c35b4e6aa' + + '213c6562caaa9dcc518959e69b10f3ba', + ], + [ + new Uint8Array(0), + 64, + '9280f5cc39b54a5a594ec63de0bb9937' + + '1e4609d44bf845c2f5b8c316d72b1598' + + '11f748f23e3fabbe5c3226ec96c62186' + + 'df2d33e9df74c5069ceecbb4dd10eff6', + ptn(1), + ], + [ + ptn(8192), + 64, + 'c6ee8e2ad3200c018ac87aaa031cdac2' + + '2121b412d07dc6e0dccbb53423747e9a' + + '1c18834d99df596cf0cf4b8dfafb7bf0' + + '2d139d0c9035725adc1a01b7230a41fa', + ], +]; + +const toHex = (buf: ArrayBuffer): string => Buffer.from(buf).toString('hex'); + +turboSHAKE128Vectors.forEach(([input, outBytes, expected, ds], i) => { + test(SUITE, `TurboSHAKE128 RFC 9861 vector ${i}`, async () => { + const algorithm: { + name: 'TurboSHAKE128'; + outputLength: number; + domainSeparation?: number; + } = { name: 'TurboSHAKE128', outputLength: outBytes * 8 }; + if (ds !== undefined) algorithm.domainSeparation = ds; + const result = await subtle.digest(algorithm, input); + expect(toHex(result)).to.equal(expected); + }); +}); + +turboSHAKE256Vectors.forEach(([input, outBytes, expected, ds], i) => { + test(SUITE, `TurboSHAKE256 RFC 9861 vector ${i}`, async () => { + const algorithm: { + name: 'TurboSHAKE256'; + outputLength: number; + domainSeparation?: number; + } = { name: 'TurboSHAKE256', outputLength: outBytes * 8 }; + if (ds !== undefined) algorithm.domainSeparation = ds; + const result = await subtle.digest(algorithm, input); + expect(toHex(result)).to.equal(expected); + }); +}); + +kt128Vectors.forEach(([input, outBytes, expected, customization], i) => { + test(SUITE, `KT128 RFC 9861 vector ${i}`, async () => { + const algorithm: { + name: 'KT128'; + outputLength: number; + customization?: Uint8Array; + } = { name: 'KT128', outputLength: outBytes * 8 }; + if (customization !== undefined) algorithm.customization = customization; + const result = await subtle.digest(algorithm, input); + expect(toHex(result)).to.equal(expected); + }); +}); + +kt256Vectors.forEach(([input, outBytes, expected, customization], i) => { + test(SUITE, `KT256 RFC 9861 vector ${i}`, async () => { + const algorithm: { + name: 'KT256'; + outputLength: number; + customization?: Uint8Array; + } = { name: 'KT256', outputLength: outBytes * 8 }; + if (customization !== undefined) algorithm.customization = customization; + const result = await subtle.digest(algorithm, input); + expect(toHex(result)).to.equal(expected); + }); +}); + +// Long-output vectors that exercise the squeeze loop across multiple rate-blocks. +test(SUITE, 'TurboSHAKE128 long squeeze (10032 bytes, last 32)', async () => { + const result = await subtle.digest( + { name: 'TurboSHAKE128', outputLength: 10032 * 8 }, + new Uint8Array(0), + ); + expect(Buffer.from(result).subarray(-32).toString('hex')).to.equal( + 'a3b9b0385900ce761f22aed548e754da' + '10a5242d62e8c658e3f3a923a7555607', + ); +}); + +test(SUITE, 'TurboSHAKE256 long squeeze (10032 bytes, last 32)', async () => { + const result = await subtle.digest( + { name: 'TurboSHAKE256', outputLength: 10032 * 8 }, + new Uint8Array(0), + ); + expect(Buffer.from(result).subarray(-32).toString('hex')).to.equal( + 'abefa11630c661269249742685ec082f' + '207265dccf2f43534e9c61ba0c9d1d75', + ); +}); + +// Validation: WICG WebCrypto Modern Algos requires outputLength multiple of 8. +test(SUITE, 'TurboSHAKE rejects non-byte-aligned outputLength', async () => { + let threw = false; + try { + await subtle.digest( + { name: 'TurboSHAKE128', outputLength: 17 }, + new Uint8Array(0), + ); + } catch (err) { + threw = true; + expect((err as Error).name).to.equal('OperationError'); + } + expect(threw).to.equal(true); +}); + +test( + SUITE, + 'TurboSHAKE rejects domainSeparation outside 0x01..0x7F', + async () => { + let threw = false; + try { + await subtle.digest( + { name: 'TurboSHAKE128', outputLength: 256, domainSeparation: 0x80 }, + new Uint8Array(0), + ); + } catch (err) { + threw = true; + expect((err as Error).name).to.equal('OperationError'); + } + expect(threw).to.equal(true); + }, +); + +test(SUITE, 'KangarooTwelve rejects missing outputLength', async () => { + let threw = false; + try { + // outputLength deliberately omitted — required by WICG WebCrypto Modern + // Algos draft and Node webidl.js:880-897. + await subtle.digest({ name: 'KT128' }, new Uint8Array(0)); + } catch (err) { + threw = true; + expect((err as Error).name).to.equal('OperationError'); + } + expect(threw).to.equal(true); +}); diff --git a/packages/react-native-quick-crypto/QuickCrypto.podspec b/packages/react-native-quick-crypto/QuickCrypto.podspec index 70d234f9..487b4c64 100644 --- a/packages/react-native-quick-crypto/QuickCrypto.podspec +++ b/packages/react-native-quick-crypto/QuickCrypto.podspec @@ -152,6 +152,7 @@ Pod::Spec.new do |s| "\"$(PODS_TARGET_SRCROOT)/cpp/hkdf\"", "\"$(PODS_TARGET_SRCROOT)/cpp/dh\"", "\"$(PODS_TARGET_SRCROOT)/cpp/ecdh\"", + "\"$(PODS_TARGET_SRCROOT)/cpp/turboshake\"", "\"$(PODS_TARGET_SRCROOT)/nitrogen/generated/shared/c++\"", "\"$(PODS_TARGET_SRCROOT)/deps/ncrypto/include\"", "\"$(PODS_TARGET_SRCROOT)/deps/simdutf/include\"", diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index fe293980..19ff94b7 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -60,6 +60,7 @@ add_library( ../cpp/scrypt/HybridScrypt.cpp ../cpp/sign/HybridSignHandle.cpp ../cpp/sign/HybridVerifyHandle.cpp + ../cpp/turboshake/HybridTurboShake.cpp ../cpp/x509/HybridX509Certificate.cpp ../cpp/utils/HybridUtils.cpp ../cpp/utils/QuickCryptoUtils.cpp @@ -100,6 +101,7 @@ include_directories( "../cpp/rsa" "../cpp/sign" "../cpp/scrypt" + "../cpp/turboshake" "../cpp/utils" "../cpp/x509" "../deps/blake3/c" diff --git a/packages/react-native-quick-crypto/cpp/turboshake/HybridTurboShake.cpp b/packages/react-native-quick-crypto/cpp/turboshake/HybridTurboShake.cpp new file mode 100644 index 00000000..2e7aa754 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/turboshake/HybridTurboShake.cpp @@ -0,0 +1,379 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "HybridTurboShake.hpp" +#include "QuickCryptoUtils.hpp" + +// TurboSHAKE128/256 and KangarooTwelve KT128/256 (RFC 9861). +// Implementation adapted from Node.js src/crypto/crypto_turboshake.cc +// (commit e0cab9dcf75), which itself adapts the OpenSSL keccak1600.c +// reference variant. OpenSSL does not yet expose these algorithms via EVP, +// so the Keccak-p[1600, n_r=12] permutation and sponge are provided here. + +namespace margelo::nitro::crypto { + +namespace { + + // --------------------------------------------------------------------------- + // Keccak-p[1600, n_r=12] permutation (FIPS 202 §3.3-3.4, RFC 9861 §2.2). + // --------------------------------------------------------------------------- + + inline uint64_t ROL64(uint64_t val, int offset) { + if (offset == 0) + return val; + return (val << offset) | (val >> (64 - offset)); + } + + inline uint64_t LoadLE64(const uint8_t* src) { + return static_cast(src[0]) | (static_cast(src[1]) << 8) | (static_cast(src[2]) << 16) | + (static_cast(src[3]) << 24) | (static_cast(src[4]) << 32) | (static_cast(src[5]) << 40) | + (static_cast(src[6]) << 48) | (static_cast(src[7]) << 56); + } + + inline void StoreLE64(uint8_t* dst, uint64_t val) { + dst[0] = static_cast(val); + dst[1] = static_cast(val >> 8); + dst[2] = static_cast(val >> 16); + dst[3] = static_cast(val >> 24); + dst[4] = static_cast(val >> 32); + dst[5] = static_cast(val >> 40); + dst[6] = static_cast(val >> 48); + dst[7] = static_cast(val >> 56); + } + + constexpr unsigned char kRhoTates[5][5] = { + {0, 1, 62, 28, 27}, {36, 44, 6, 55, 20}, {3, 10, 43, 25, 39}, {41, 45, 15, 21, 8}, {18, 2, 61, 56, 14}, + }; + + // Round constants for Keccak-f[1600]; TurboSHAKE uses indices 12..23. + constexpr uint64_t kIotas[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, 0x8000000080008000ULL, 0x000000000000808bULL, + 0x0000000080000001ULL, 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, 0x0000000000000088ULL, + 0x0000000080008009ULL, 0x000000008000000aULL, 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, 0x000000000000800aULL, 0x800000008000000aULL, + 0x8000000080008081ULL, 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL, + }; + + void KeccakP1600_12(uint64_t A[5][5]) { + for (size_t round = 12; round < 24; round++) { + uint64_t C[5]; + for (size_t x = 0; x < 5; x++) { + C[x] = A[0][x] ^ A[1][x] ^ A[2][x] ^ A[3][x] ^ A[4][x]; + } + uint64_t D[5]; + for (size_t x = 0; x < 5; x++) { + D[x] = C[(x + 4) % 5] ^ ROL64(C[(x + 1) % 5], 1); + } + for (size_t y = 0; y < 5; y++) { + for (size_t x = 0; x < 5; x++) { + A[y][x] ^= D[x]; + } + } + for (size_t y = 0; y < 5; y++) { + for (size_t x = 0; x < 5; x++) { + A[y][x] = ROL64(A[y][x], kRhoTates[y][x]); + } + } + uint64_t T[5][5]; + memcpy(T, A, sizeof(T)); + for (size_t y = 0; y < 5; y++) { + for (size_t x = 0; x < 5; x++) { + A[y][x] = T[x][(3 * y + x) % 5]; + } + } + for (size_t y = 0; y < 5; y++) { + uint64_t row[5]; + for (size_t x = 0; x < 5; x++) { + row[x] = A[y][x] ^ (~A[y][(x + 1) % 5] & A[y][(x + 2) % 5]); + } + memcpy(A[y], row, sizeof(row)); + } + A[0][0] ^= kIotas[round]; + } + } + + // --------------------------------------------------------------------------- + // TurboSHAKE sponge (RFC 9861 §2.2, App. A.2/A.3). + // TurboSHAKE128: rate = 168 bytes (capacity 256 bits). + // TurboSHAKE256: rate = 136 bytes (capacity 512 bits). + // --------------------------------------------------------------------------- + + constexpr size_t kTurboSHAKE128Rate = 168; + constexpr size_t kTurboSHAKE256Rate = 136; + + void TurboSHAKE(const uint8_t* input, size_t input_len, size_t rate, uint8_t domain_sep, uint8_t* output, size_t output_len) { + uint64_t A[5][5] = {}; + size_t lane_count = rate / 8; + + size_t offset = 0; + while (offset + rate <= input_len) { + for (size_t i = 0; i < lane_count; i++) { + A[i / 5][i % 5] ^= LoadLE64(input + offset + i * 8); + } + KeccakP1600_12(A); + offset += rate; + } + + size_t remaining = input_len - offset; + // Sized for the larger TurboSHAKE128 rate (168); also fits the 136-byte TurboSHAKE256 rate. + uint8_t pad[kTurboSHAKE128Rate] = {}; + if (remaining > 0) { + memcpy(pad, input + offset, remaining); + } + pad[remaining] ^= domain_sep; + pad[rate - 1] ^= 0x80; + + for (size_t i = 0; i < lane_count; i++) { + A[i / 5][i % 5] ^= LoadLE64(pad + i * 8); + } + KeccakP1600_12(A); + + size_t out_offset = 0; + while (out_offset < output_len) { + size_t block = output_len - out_offset; + if (block > rate) + block = rate; + size_t full_lanes = block / 8; + for (size_t i = 0; i < full_lanes; i++) { + StoreLE64(output + out_offset + i * 8, A[i / 5][i % 5]); + } + size_t rem = block % 8; + if (rem > 0) { + uint8_t tmp[8]; + StoreLE64(tmp, A[full_lanes / 5][full_lanes % 5]); + memcpy(output + out_offset + full_lanes * 8, tmp, rem); + } + out_offset += block; + if (out_offset < output_len) { + KeccakP1600_12(A); + } + } + } + + void TurboSHAKE128(const uint8_t* input, size_t input_len, uint8_t domain_sep, uint8_t* output, size_t output_len) { + TurboSHAKE(input, input_len, kTurboSHAKE128Rate, domain_sep, output, output_len); + } + + void TurboSHAKE256(const uint8_t* input, size_t input_len, uint8_t domain_sep, uint8_t* output, size_t output_len) { + TurboSHAKE(input, input_len, kTurboSHAKE256Rate, domain_sep, output, output_len); + } + + // --------------------------------------------------------------------------- + // KangarooTwelve tree hashing (RFC 9861 §3). + // --------------------------------------------------------------------------- + + constexpr size_t kChunkSize = 8192; + + // length_encode(x) per RFC 9861 §3.3. + std::vector LengthEncode(size_t x) { + if (x == 0) { + return {0x00}; + } + std::vector result; + size_t val = x; + while (val > 0) { + result.push_back(static_cast(val & 0xFF)); + val >>= 8; + } + size_t n = result.size(); + for (size_t i = 0; i < n / 2; i++) { + std::swap(result[i], result[n - 1 - i]); + } + result.push_back(static_cast(n)); + return result; + } + + using TurboSHAKEFn = void (*)(const uint8_t* input, size_t input_len, uint8_t domain_sep, uint8_t* output, size_t output_len); + + void KangarooTwelve(const uint8_t* message, size_t msg_len, const uint8_t* customization, size_t custom_len, uint8_t* output, + size_t output_len, TurboSHAKEFn turboshake, size_t cv_len) { + auto len_enc = LengthEncode(custom_len); + size_t s_len = msg_len + custom_len + len_enc.size(); + + // Short message: |S| <= 8192. + if (s_len <= kChunkSize) { + std::vector s(s_len); + size_t pos = 0; + if (msg_len > 0) { + memcpy(s.data() + pos, message, msg_len); + pos += msg_len; + } + if (custom_len > 0) { + memcpy(s.data() + pos, customization, custom_len); + pos += custom_len; + } + memcpy(s.data() + pos, len_enc.data(), len_enc.size()); + + turboshake(s.data(), s_len, 0x07, output, output_len); + return; + } + + // S is virtual: M || C || length_encode(|C|). Read on demand. + auto read_s = [&](size_t s_offset, uint8_t* buf, size_t len) { + size_t copied = 0; + if (s_offset < msg_len && copied < len) { + size_t avail = msg_len - s_offset; + size_t to_copy = avail < (len - copied) ? avail : (len - copied); + memcpy(buf + copied, message + s_offset, to_copy); + copied += to_copy; + s_offset += to_copy; + } + size_t custom_start = msg_len; + if (s_offset < custom_start + custom_len && copied < len) { + size_t off_in_custom = s_offset - custom_start; + size_t avail = custom_len - off_in_custom; + size_t to_copy = avail < (len - copied) ? avail : (len - copied); + memcpy(buf + copied, customization + off_in_custom, to_copy); + copied += to_copy; + s_offset += to_copy; + } + size_t le_start = msg_len + custom_len; + if (s_offset < le_start + len_enc.size() && copied < len) { + size_t off_in_le = s_offset - le_start; + size_t avail = len_enc.size() - off_in_le; + size_t to_copy = avail < (len - copied) ? avail : (len - copied); + memcpy(buf + copied, len_enc.data() + off_in_le, to_copy); + copied += to_copy; + } + }; + + std::vector first_chunk(kChunkSize); + read_s(0, first_chunk.data(), kChunkSize); + + std::vector final_node; + final_node.reserve(kChunkSize + 8 + ((s_len / kChunkSize) * cv_len) + 16); + final_node.insert(final_node.end(), first_chunk.begin(), first_chunk.end()); + final_node.push_back(0x03); + final_node.insert(final_node.end(), 7, 0x00); + + size_t offset = kChunkSize; + size_t num_blocks = 0; + std::vector chunk(kChunkSize); + std::vector cv(cv_len); + + while (offset < s_len) { + size_t block_size = s_len - offset; + if (block_size > kChunkSize) + block_size = kChunkSize; + + chunk.resize(block_size); + read_s(offset, chunk.data(), block_size); + + turboshake(chunk.data(), block_size, 0x0B, cv.data(), cv_len); + final_node.insert(final_node.end(), cv.begin(), cv.end()); + num_blocks++; + offset += block_size; + } + + auto num_blocks_enc = LengthEncode(num_blocks); + final_node.insert(final_node.end(), num_blocks_enc.begin(), num_blocks_enc.end()); + final_node.push_back(0xFF); + final_node.push_back(0xFF); + + turboshake(final_node.data(), final_node.size(), 0x06, output, output_len); + } + + void KT128(const uint8_t* message, size_t msg_len, const uint8_t* customization, size_t custom_len, uint8_t* output, size_t output_len) { + KangarooTwelve(message, msg_len, customization, custom_len, output, output_len, TurboSHAKE128, 32); + } + + void KT256(const uint8_t* message, size_t msg_len, const uint8_t* customization, size_t custom_len, uint8_t* output, size_t output_len) { + KangarooTwelve(message, msg_len, customization, custom_len, output, output_len, TurboSHAKE256, 64); + } + + uint8_t parseDomainSeparation(double value) { + if (!(value >= 0x01 && value <= 0x7F) || value != static_cast(static_cast(value))) { + throw std::runtime_error("TurboSHAKE domainSeparation must be an integer in 0x01..0x7F"); + } + return static_cast(value); + } + + uint32_t parseOutputLength(double value) { + if (!(value > 0)) { + throw std::runtime_error("outputLength must be > 0"); + } + // 16 MiB upper bound matches HybridHash::setParams to keep memory bounded. + constexpr double kMaxOutputBytes = 16.0 * 1024.0 * 1024.0; + if (value > kMaxOutputBytes) { + throw std::runtime_error("outputLength exceeds maximum allowed size"); + } + if (value != static_cast(static_cast(value))) { + throw std::runtime_error("outputLength must be an integer"); + } + return static_cast(value); + } + +} // namespace + +std::shared_ptr>> HybridTurboShake::turboShake(TurboShakeVariant variant, double domainSeparation, + double outputLength, + const std::shared_ptr& data) { + uint8_t ds = parseDomainSeparation(domainSeparation); + uint32_t outLen = parseOutputLength(outputLength); + + bool is128 = variant == TurboShakeVariant::TURBOSHAKE128; + + auto nativeData = ToNativeArrayBuffer(data); + + return Promise>::async([is128, ds, outLen, nativeData]() -> std::shared_ptr { + auto outBuf = std::make_unique(outLen); + const uint8_t* in = reinterpret_cast(nativeData->data()); + size_t inLen = nativeData->size(); + if (is128) { + TurboSHAKE128(in, inLen, ds, outBuf.get(), outLen); + } else { + TurboSHAKE256(in, inLen, ds, outBuf.get(), outLen); + } + uint8_t* raw = outBuf.get(); + return std::make_shared(outBuf.release(), outLen, [raw]() { delete[] raw; }); + }); +} + +std::shared_ptr>> +HybridTurboShake::kangarooTwelve(KangarooTwelveVariant variant, double outputLength, const std::shared_ptr& data, + const std::optional>& customization) { + uint32_t outLen = parseOutputLength(outputLength); + + bool is128 = variant == KangarooTwelveVariant::KT128; + + auto nativeData = ToNativeArrayBuffer(data); + std::optional> nativeCustom; + if (customization.has_value()) { + nativeCustom = ToNativeArrayBuffer(customization.value()); + } + + return Promise>::async( + [is128, outLen, nativeData, nativeCustom = std::move(nativeCustom)]() -> std::shared_ptr { + const uint8_t* in = reinterpret_cast(nativeData->data()); + size_t inLen = nativeData->size(); + const uint8_t* custom = nullptr; + size_t customLen = 0; + if (nativeCustom.has_value() && nativeCustom.value()->size() > 0) { + custom = reinterpret_cast(nativeCustom.value()->data()); + customLen = nativeCustom.value()->size(); + } + + // Mirror Node's overflow guard for s_len = msg + custom + length_encode(|custom|). + // length_encode produces at most sizeof(size_t) + 1 bytes. + constexpr size_t kMaxLengthEncodeSize = sizeof(size_t) + 1; + if (inLen > SIZE_MAX - customLen || inLen + customLen > SIZE_MAX - kMaxLengthEncodeSize) { + throw std::runtime_error("KangarooTwelve input length overflow"); + } + + auto outBuf = std::make_unique(outLen); + if (is128) { + KT128(in, inLen, custom, customLen, outBuf.get(), outLen); + } else { + KT256(in, inLen, custom, customLen, outBuf.get(), outLen); + } + uint8_t* raw = outBuf.get(); + return std::make_shared(outBuf.release(), outLen, [raw]() { delete[] raw; }); + }); +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/turboshake/HybridTurboShake.hpp b/packages/react-native-quick-crypto/cpp/turboshake/HybridTurboShake.hpp new file mode 100644 index 00000000..48aa5e69 --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/turboshake/HybridTurboShake.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "HybridTurboShakeSpec.hpp" + +namespace margelo::nitro::crypto { + +using namespace facebook; + +class HybridTurboShake : public HybridTurboShakeSpec { + public: + HybridTurboShake() : HybridObject(TAG) {} + + public: + std::shared_ptr>> turboShake(TurboShakeVariant variant, double domainSeparation, double outputLength, + const std::shared_ptr& data) override; + + std::shared_ptr>> + kangarooTwelve(KangarooTwelveVariant variant, double outputLength, const std::shared_ptr& data, + const std::optional>& customization) override; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitro.json b/packages/react-native-quick-crypto/nitro.json index 91c49343..3d04ea7d 100644 --- a/packages/react-native-quick-crypto/nitro.json +++ b/packages/react-native-quick-crypto/nitro.json @@ -86,6 +86,9 @@ "SignHandle": { "cpp": "HybridSignHandle" }, + "TurboShake": { + "cpp": "HybridTurboShake" + }, "Utils": { "cpp": "HybridUtils" }, diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake index f15a6a85..6a9cb11a 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake @@ -60,6 +60,7 @@ target_sources( ../nitrogen/generated/shared/c++/HybridSignHandleSpec.cpp ../nitrogen/generated/shared/c++/HybridVerifyHandleSpec.cpp ../nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.cpp + ../nitrogen/generated/shared/c++/HybridTurboShakeSpec.cpp ../nitrogen/generated/shared/c++/HybridUtilsSpec.cpp ../nitrogen/generated/shared/c++/HybridX509CertificateHandleSpec.cpp # Android-specific Nitrogen C++ sources diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp index 5483c34b..a3494770 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp @@ -41,6 +41,7 @@ #include "HybridRsaKeyPair.hpp" #include "HybridScrypt.hpp" #include "HybridSignHandle.hpp" +#include "HybridTurboShake.hpp" #include "HybridUtils.hpp" #include "HybridVerifyHandle.hpp" #include "HybridX509Certificate.hpp" @@ -291,6 +292,15 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "TurboShake", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridTurboShake\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); HybridObjectRegistry::registerHybridObjectConstructor( "Utils", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm index f8f8a507..f38afc9e 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm @@ -36,6 +36,7 @@ #include "HybridRsaKeyPair.hpp" #include "HybridScrypt.hpp" #include "HybridSignHandle.hpp" +#include "HybridTurboShake.hpp" #include "HybridUtils.hpp" #include "HybridVerifyHandle.hpp" #include "HybridX509Certificate.hpp" @@ -283,6 +284,15 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "TurboShake", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridTurboShake\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); HybridObjectRegistry::registerHybridObjectConstructor( "Utils", []() -> std::shared_ptr { diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridTurboShakeSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridTurboShakeSpec.cpp new file mode 100644 index 00000000..da6b1a09 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridTurboShakeSpec.cpp @@ -0,0 +1,22 @@ +/// +/// HybridTurboShakeSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridTurboShakeSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridTurboShakeSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("turboShake", &HybridTurboShakeSpec::turboShake); + prototype.registerHybridMethod("kangarooTwelve", &HybridTurboShakeSpec::kangarooTwelve); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridTurboShakeSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridTurboShakeSpec.hpp new file mode 100644 index 00000000..614216db --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridTurboShakeSpec.hpp @@ -0,0 +1,70 @@ +/// +/// HybridTurboShakeSpec.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 + +// Forward declaration of `TurboShakeVariant` to properly resolve imports. +namespace margelo::nitro::crypto { enum class TurboShakeVariant; } +// Forward declaration of `KangarooTwelveVariant` to properly resolve imports. +namespace margelo::nitro::crypto { enum class KangarooTwelveVariant; } + +#include +#include +#include "TurboShakeVariant.hpp" +#include "KangarooTwelveVariant.hpp" +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `TurboShake` + * Inherit this class to create instances of `HybridTurboShakeSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridTurboShake: public HybridTurboShakeSpec { + * public: + * HybridTurboShake(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridTurboShakeSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridTurboShakeSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridTurboShakeSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr>> turboShake(TurboShakeVariant variant, double domainSeparation, double outputLength, const std::shared_ptr& data) = 0; + virtual std::shared_ptr>> kangarooTwelve(KangarooTwelveVariant variant, double outputLength, const std::shared_ptr& data, const std::optional>& customization) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "TurboShake"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KangarooTwelveVariant.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KangarooTwelveVariant.hpp new file mode 100644 index 00000000..7757e220 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/KangarooTwelveVariant.hpp @@ -0,0 +1,76 @@ +/// +/// KangarooTwelveVariant.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 (KangarooTwelveVariant). + */ + enum class KangarooTwelveVariant { + KT128 SWIFT_NAME(kt128) = 0, + KT256 SWIFT_NAME(kt256) = 1, + } CLOSED_ENUM; + +} // namespace margelo::nitro::crypto + +namespace margelo::nitro { + + // C++ KangarooTwelveVariant <> JS KangarooTwelveVariant (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::crypto::KangarooTwelveVariant fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("KT128"): return margelo::nitro::crypto::KangarooTwelveVariant::KT128; + case hashString("KT256"): return margelo::nitro::crypto::KangarooTwelveVariant::KT256; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum KangarooTwelveVariant - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::crypto::KangarooTwelveVariant arg) { + switch (arg) { + case margelo::nitro::crypto::KangarooTwelveVariant::KT128: return JSIConverter::toJSI(runtime, "KT128"); + case margelo::nitro::crypto::KangarooTwelveVariant::KT256: return JSIConverter::toJSI(runtime, "KT256"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert KangarooTwelveVariant 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("KT128"): + case hashString("KT256"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/TurboShakeVariant.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/TurboShakeVariant.hpp new file mode 100644 index 00000000..09491f25 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/TurboShakeVariant.hpp @@ -0,0 +1,76 @@ +/// +/// TurboShakeVariant.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 (TurboShakeVariant). + */ + enum class TurboShakeVariant { + TURBOSHAKE128 SWIFT_NAME(turboshake128) = 0, + TURBOSHAKE256 SWIFT_NAME(turboshake256) = 1, + } CLOSED_ENUM; + +} // namespace margelo::nitro::crypto + +namespace margelo::nitro { + + // C++ TurboShakeVariant <> JS TurboShakeVariant (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::crypto::TurboShakeVariant fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("TurboSHAKE128"): return margelo::nitro::crypto::TurboShakeVariant::TURBOSHAKE128; + case hashString("TurboSHAKE256"): return margelo::nitro::crypto::TurboShakeVariant::TURBOSHAKE256; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum TurboShakeVariant - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::crypto::TurboShakeVariant arg) { + switch (arg) { + case margelo::nitro::crypto::TurboShakeVariant::TURBOSHAKE128: return JSIConverter::toJSI(runtime, "TurboSHAKE128"); + case margelo::nitro::crypto::TurboShakeVariant::TURBOSHAKE256: return JSIConverter::toJSI(runtime, "TurboSHAKE256"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert TurboShakeVariant 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("TurboSHAKE128"): + case hashString("TurboSHAKE256"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-quick-crypto/src/hash.ts b/packages/react-native-quick-crypto/src/hash.ts index d5640e23..d09a0f7a 100644 --- a/packages/react-native-quick-crypto/src/hash.ts +++ b/packages/react-native-quick-crypto/src/hash.ts @@ -3,6 +3,7 @@ import { NitroModules } from 'react-native-nitro-modules'; import type { TransformOptions } from 'readable-stream'; import { Buffer } from '@craftzdog/react-native-buffer'; import type { Hash as NativeHash } from './specs/hash.nitro'; +import type { TurboShake as NativeTurboShake } from './specs/turboshake.nitro'; import type { BinaryLike, Encoding, @@ -288,12 +289,108 @@ export const asyncDigest = async ( return internalDigest(algorithm, data, algorithm.outputLength / 8); } + if (name === 'TurboSHAKE128' || name === 'TurboSHAKE256') { + return turboShakeDigest(name, algorithm, data); + } + + if (name === 'KT128' || name === 'KT256') { + return kangarooTwelveDigest(name, algorithm, data); + } + throw lazyDOMException( `Unrecognized algorithm name: ${name}`, 'NotSupportedError', ); }; +// TurboSHAKE / KangarooTwelve are not exposed by OpenSSL EVP, so we route +// them to a dedicated Nitro module (cpp/turboshake) that ports the Node.js +// reference implementation. Lazy-load to keep the module out of the hot path +// for callers that only use SHA-2/SHA-3. +let nativeTurboShake: NativeTurboShake | undefined; +const getTurboShake = (): NativeTurboShake => { + if (!nativeTurboShake) { + nativeTurboShake = + NitroModules.createHybridObject('TurboShake'); + } + return nativeTurboShake; +}; + +// RFC 9861 §2.2 default per the WICG Modern Algos draft for TurboSHAKE. +const kDefaultTurboShakeDomainSeparation = 0x1f; + +const turboShakeDigest = async ( + name: 'TurboSHAKE128' | 'TurboSHAKE256', + algorithm: SubtleAlgorithm, + data: BufferLike, +): Promise => { + if ( + typeof algorithm.outputLength !== 'number' || + algorithm.outputLength <= 0 + ) { + throw lazyDOMException( + 'TurboShakeParams.outputLength is required', + 'OperationError', + ); + } + if (algorithm.outputLength % 8) { + throw lazyDOMException( + 'Invalid TurboShakeParams outputLength', + 'OperationError', + ); + } + const ds = algorithm.domainSeparation ?? kDefaultTurboShakeDomainSeparation; + if ( + typeof ds !== 'number' || + !Number.isInteger(ds) || + ds < 0x01 || + ds > 0x7f + ) { + throw lazyDOMException( + 'TurboShakeParams.domainSeparation must be in range 0x01-0x7f', + 'OperationError', + ); + } + return getTurboShake().turboShake( + name, + ds, + algorithm.outputLength / 8, + bufferLikeToArrayBuffer(data), + ); +}; + +const kangarooTwelveDigest = async ( + name: 'KT128' | 'KT256', + algorithm: SubtleAlgorithm, + data: BufferLike, +): Promise => { + if ( + typeof algorithm.outputLength !== 'number' || + algorithm.outputLength <= 0 + ) { + throw lazyDOMException( + 'KangarooTwelveParams.outputLength is required', + 'OperationError', + ); + } + if (algorithm.outputLength % 8) { + throw lazyDOMException( + 'Invalid KangarooTwelveParams outputLength', + 'OperationError', + ); + } + const customization = + algorithm.customization !== undefined + ? bufferLikeToArrayBuffer(algorithm.customization) + : undefined; + return getTurboShake().kangarooTwelve( + name, + algorithm.outputLength / 8, + bufferLikeToArrayBuffer(data), + customization, + ); +}; + const internalDigest = ( algorithm: SubtleAlgorithm, data: BufferLike, diff --git a/packages/react-native-quick-crypto/src/specs/turboshake.nitro.ts b/packages/react-native-quick-crypto/src/specs/turboshake.nitro.ts new file mode 100644 index 00000000..b0602162 --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/turboshake.nitro.ts @@ -0,0 +1,21 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export type TurboShakeVariant = 'TurboSHAKE128' | 'TurboSHAKE256'; +export type KangarooTwelveVariant = 'KT128' | 'KT256'; + +export interface TurboShake + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + turboShake( + variant: TurboShakeVariant, + domainSeparation: number, + outputLength: number, + data: ArrayBuffer, + ): Promise; + + kangarooTwelve( + variant: KangarooTwelveVariant, + outputLength: number, + data: ArrayBuffer, + customization?: ArrayBuffer, + ): Promise; +} diff --git a/packages/react-native-quick-crypto/src/subtle.ts b/packages/react-native-quick-crypto/src/subtle.ts index 39d36a3c..3d878dc7 100644 --- a/packages/react-native-quick-crypto/src/subtle.ts +++ b/packages/react-native-quick-crypto/src/subtle.ts @@ -2485,6 +2485,10 @@ const SUPPORTED_ALGORITHMS: Record> = { 'SHA3-512', 'cSHAKE128', 'cSHAKE256', + 'TurboSHAKE128', + 'TurboSHAKE256', + 'KT128', + 'KT256', ]), generateKey: new Set([ 'RSASSA-PKCS1-v1_5', diff --git a/packages/react-native-quick-crypto/src/utils/types.ts b/packages/react-native-quick-crypto/src/utils/types.ts index f739a08c..8ef6bfa8 100644 --- a/packages/react-native-quick-crypto/src/utils/types.ts +++ b/packages/react-native-quick-crypto/src/utils/types.ts @@ -49,7 +49,11 @@ export type DigestAlgorithm = | 'SHA3-384' | 'SHA3-512' | 'cSHAKE128' - | 'cSHAKE256'; + | 'cSHAKE256' + | 'TurboSHAKE128' + | 'TurboSHAKE256' + | 'KT128' + | 'KT256'; export type HashAlgorithm = DigestAlgorithm | 'SHA-224' | 'RIPEMD-160'; @@ -276,9 +280,13 @@ export type SubtleAlgorithm = { secretValue?: BufferLike; associatedData?: BufferLike; version?: number; - // KMAC / cSHAKE parameters + // KMAC / cSHAKE / KangarooTwelve parameters customization?: BufferLike; outputLength?: number; + // TurboSHAKE parameter (RFC 9861 §2.2): single-byte domain separator in + // [0x01, 0x7F]. Defaults to 0x1F per the WICG WebCrypto Modern Algorithms + // draft when omitted. + domainSeparation?: number; }; export type KeyPairType =