From c475c423c896b8918f33fa493b860b13b2c01bfb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 18 Feb 2026 12:05:02 +0100 Subject: [PATCH 1/2] crypto: rename CShakeParams and KmacParams length to outputLength PR-URL: https://github.com/nodejs/node/pull/61875 Refs: https://github.com/web-platform-tests/wpt/pull/57802 Reviewed-By: Yagiz Nizipli --- doc/api/webcrypto.md | 50 +++++++++++-------- lib/internal/crypto/hash.js | 2 +- lib/internal/crypto/mac.js | 2 +- lib/internal/crypto/webidl.js | 8 +-- test/fixtures/crypto/kmac.js | 12 ++--- .../webcrypto/supports-modern-algorithms.mjs | 24 ++++----- test/parallel/test-webcrypto-digest.js | 14 +++--- .../test-webcrypto-sign-verify-kmac.js | 10 ++-- test/parallel/test-webcrypto-sign-verify.js | 4 +- test/wpt/status/WebCryptoAPI.cjs | 49 ++++++++++++++++++ 10 files changed, 116 insertions(+), 59 deletions(-) diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index 178231647e76f8..71ab62ff8aa608 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -1868,19 +1868,27 @@ the message. -#### `cShakeParams.customization` +#### `cShakeParams.name` -* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} +* Type: {string} Must be `'cSHAKE128'`[^modern-algos] or `'cSHAKE256'`[^modern-algos] -The `customization` member represents the customization string. -The Node.js Web Crypto API implementation only supports zero-length customization -which is equivalent to not providing customization at all. +#### `cShakeParams.outputLength` + + + +* Type: {number} represents the requested output length in bits. #### `cShakeParams.functionName` @@ -1895,21 +1903,17 @@ functions based on cSHAKE. The Node.js Web Crypto API implementation only supports zero-length functionName which is equivalent to not providing functionName at all. -#### `cShakeParams.length` +#### `cShakeParams.customization` -* Type: {number} represents the requested output length in bits. - -#### `cShakeParams.name` - - +* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} -* Type: {string} Must be `'cSHAKE128'`[^modern-algos] or `'cSHAKE256'`[^modern-algos] +The `customization` member represents the customization string. +The Node.js Web Crypto API implementation only supports zero-length customization +which is equivalent to not providing customization at all. ### Class: `EcdhKeyDeriveParams` @@ -2386,6 +2390,10 @@ added: v24.8.0 #### `kmacParams.algorithm` @@ -2396,25 +2404,25 @@ added: v24.8.0 * Type: {string} Must be `'KMAC128'` or `'KMAC256'`. -#### `kmacParams.customization` +#### `kmacParams.outputLength` -* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} +* Type: {number} -The `customization` member represents the optional customization string. +The length of the output in bytes. This must be a positive integer. -#### `kmacParams.length` +#### `kmacParams.customization` -* Type: {number} +* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} -The length of the output in bytes. This must be a positive integer. +The `customization` member represents the optional customization string. ### Class: `Pbkdf2Params` diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 43417cf3544933..eecbe5213f568c 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -225,7 +225,7 @@ async function asyncDigest(algorithm, data) { kCryptoJobAsync, normalizeHashName(algorithm.name), data, - algorithm.length)); + algorithm.outputLength)); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index a31c3ddb0d9484..1ad4e27c6a8d39 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -234,7 +234,7 @@ function kmacSignVerify(key, data, algorithm, signature) { key[kKeyObject][kHandle], algorithm.name, algorithm.customization, - algorithm.length / 8, + algorithm.outputLength / 8, data, signature)); } diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index e72931e9c83cca..581f541847faea 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -589,14 +589,14 @@ converters.CShakeParams = createDictionaryConverter( 'CShakeParams', [ ...new SafeArrayIterator(dictAlgorithm), { - key: 'length', + key: 'outputLength', converter: (V, opts) => converters['unsigned long'](V, { ...opts, enforceRange: true }), validator: (V, opts) => { // The Web Crypto spec allows for SHAKE output length that are not multiples of // 8. We don't. if (V % 8) - throw lazyDOMException('Unsupported CShakeParams length', 'NotSupportedError'); + throw lazyDOMException('Unsupported CShakeParams outputLength', 'NotSupportedError'); }, required: true, }, @@ -879,13 +879,13 @@ converters.KmacParams = createDictionaryConverter( 'KmacParams', [ ...new SafeArrayIterator(dictAlgorithm), { - key: 'length', + key: 'outputLength', converter: (V, opts) => converters['unsigned long'](V, { ...opts, enforceRange: true }), validator: (V, opts) => { // The Web Crypto spec allows for KMAC output length that are not multiples of 8. We don't. if (V % 8) - throw lazyDOMException('Unsupported KmacParams length', 'NotSupportedError'); + throw lazyDOMException('Unsupported KmacParams outputLength', 'NotSupportedError'); }, required: true, }, diff --git a/test/fixtures/crypto/kmac.js b/test/fixtures/crypto/kmac.js index 86dec424bad9b1..cc1870af2bb04f 100644 --- a/test/fixtures/crypto/kmac.js +++ b/test/fixtures/crypto/kmac.js @@ -13,7 +13,7 @@ module.exports = function() { ]), data: Buffer.from([0x00, 0x01, 0x02, 0x03]), customization: undefined, - length: 256, + outputLength: 256, expected: Buffer.from([ 0xe5, 0x78, 0x0b, 0x0d, 0x3e, 0xa6, 0xf7, 0xd3, 0xa4, 0x29, 0xc5, 0x70, 0x6a, 0xa4, 0x3a, 0x00, 0xfa, 0xdb, 0xd7, 0xd4, 0x96, 0x28, 0x83, 0x9e, @@ -30,7 +30,7 @@ module.exports = function() { ]), data: Buffer.from([0x00, 0x01, 0x02, 0x03]), customization: Buffer.from('My Tagged Application'), - length: 256, + outputLength: 256, expected: Buffer.from([ 0x3b, 0x1f, 0xba, 0x96, 0x3c, 0xd8, 0xb0, 0xb5, 0x9e, 0x8c, 0x1a, 0x6d, 0x71, 0x88, 0x8b, 0x71, 0x43, 0x65, 0x1a, 0xf8, 0xba, 0x0a, 0x70, 0x70, @@ -47,7 +47,7 @@ module.exports = function() { ]), data: Buffer.from(Array.from({ length: 200 }, (_, i) => i)), // 0x00-0xC7 customization: Buffer.from('My Tagged Application'), - length: 256, + outputLength: 256, expected: Buffer.from([ 0x1f, 0x5b, 0x4e, 0x6c, 0xca, 0x02, 0x20, 0x9e, 0x0d, 0xcb, 0x5c, 0xa6, 0x35, 0xb8, 0x9a, 0x15, 0xe2, 0x71, 0xec, 0xc7, 0x60, 0x07, 0x1d, 0xfd, @@ -64,7 +64,7 @@ module.exports = function() { ]), data: Buffer.from([0x00, 0x01, 0x02, 0x03]), customization: Buffer.from('My Tagged Application'), - length: 512, + outputLength: 512, expected: Buffer.from([ 0x20, 0xc5, 0x70, 0xc3, 0x13, 0x46, 0xf7, 0x03, 0xc9, 0xac, 0x36, 0xc6, 0x1c, 0x03, 0xcb, 0x64, 0xc3, 0x97, 0x0d, 0x0c, 0xfc, 0x78, 0x7e, 0x9b, @@ -84,7 +84,7 @@ module.exports = function() { ]), data: Buffer.from(Array.from({ length: 200 }, (_, i) => i)), // 0x00-0xC7 customization: undefined, - length: 512, + outputLength: 512, expected: Buffer.from([ 0x75, 0x35, 0x8c, 0xf3, 0x9e, 0x41, 0x49, 0x4e, 0x94, 0x97, 0x07, 0x92, 0x7c, 0xee, 0x0a, 0xf2, 0x0a, 0x3f, 0xf5, 0x53, 0x90, 0x4c, 0x86, 0xb0, @@ -104,7 +104,7 @@ module.exports = function() { ]), data: Buffer.from(Array.from({ length: 200 }, (_, i) => i)), // 0x00-0xC7 customization: Buffer.from('My Tagged Application'), - length: 512, + outputLength: 512, expected: Buffer.from([ 0xb5, 0x86, 0x18, 0xf7, 0x1f, 0x92, 0xe1, 0xd5, 0x6c, 0x1b, 0x8c, 0x55, 0xdd, 0xd7, 0xcd, 0x18, 0x8b, 0x97, 0xb4, 0xca, 0x4d, 0x99, 0x83, 0x1e, diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 76d5e805cbc0e7..eafb95c559a0f7 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -17,17 +17,17 @@ const X25519 = await subtle.generateKey('X25519', false, ['deriveBits', 'deriveK export const vectors = { 'digest': [ [false, 'cSHAKE128'], - [shake128, { name: 'cSHAKE128', length: 128 }], - [shake128, { name: 'cSHAKE128', length: 128, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [false, { name: 'cSHAKE128', length: 128, functionName: Buffer.alloc(1) }], - [false, { name: 'cSHAKE128', length: 128, customization: Buffer.alloc(1) }], - [false, { name: 'cSHAKE128', length: 127 }], + [shake128, { name: 'cSHAKE128', outputLength: 128 }], + [shake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], + [false, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(1) }], + [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], + [false, { name: 'cSHAKE128', outputLength: 127 }], [false, 'cSHAKE256'], - [shake256, { name: 'cSHAKE256', length: 256 }], - [shake256, { name: 'cSHAKE256', length: 256, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [false, { name: 'cSHAKE256', length: 256, functionName: Buffer.alloc(1) }], - [false, { name: 'cSHAKE256', length: 256, customization: Buffer.alloc(1) }], - [false, { name: 'cSHAKE256', length: 255 }], + [shake256, { name: 'cSHAKE256', outputLength: 256 }], + [shake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], + [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(1) }], + [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], + [false, { name: 'cSHAKE256', outputLength: 255 }], ], 'sign': [ [pqc, 'ML-DSA-44'], @@ -44,8 +44,8 @@ export const vectors = { [false, 'Argon2id'], [false, 'KMAC128'], [false, 'KMAC256'], - [kmac, { name: 'KMAC128', length: 256 }], - [kmac, { name: 'KMAC256', length: 256 }], + [kmac, { name: 'KMAC128', outputLength: 256 }], + [kmac, { name: 'KMAC256', outputLength: 256 }], ], 'generateKey': [ [pqc, 'ML-DSA-44'], diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 4d22006937f8cb..4a28d88dcd72c3 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -19,8 +19,8 @@ const kTests = [ if (!process.features.openssl_is_boringssl) { kTests.push( - [{ name: 'cSHAKE128', length: 256 }, ['shake128', { outputLength: 256 >> 3 }], 256], - [{ name: 'cSHAKE256', length: 512 }, ['shake256', { outputLength: 512 >> 3 }], 512], + [{ name: 'cSHAKE128', outputLength: 256 }, ['shake128', { outputLength: 256 >> 3 }], 256], + [{ name: 'cSHAKE256', outputLength: 512 }, ['shake256', { outputLength: 512 >> 3 }], 512], ['SHA3-256', ['sha3-256'], 256], ['SHA3-384', ['sha3-384'], 384], ['SHA3-512', ['sha3-512'], 512], @@ -223,10 +223,10 @@ async function testDigest(size, alg) { function applyXOF(name) { if (name.match(/cshake128/i)) { - return { name, length: 256 }; + return { name, outputLength: 256 }; } if (name.match(/cshake256/i)) { - return { name, length: 512 }; + return { name, outputLength: 512 }; } return name; @@ -259,13 +259,13 @@ function applyXOF(name) { if (getHashes().includes('shake128')) { (async () => { assert.deepStrictEqual( - new Uint8Array(await subtle.digest({ name: 'cSHAKE128', length: 0 }, Buffer.alloc(1))), + new Uint8Array(await subtle.digest({ name: 'cSHAKE128', outputLength: 0 }, Buffer.alloc(1))), new Uint8Array(0), ); - await assert.rejects(subtle.digest({ name: 'cSHAKE128', length: 7 }, Buffer.alloc(1)), { + await assert.rejects(subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)), { name: 'NotSupportedError', - message: 'Unsupported CShakeParams length', + message: 'Unsupported CShakeParams outputLength', }); })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index c30196a94d9fb6..d41095e0893d97 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -19,7 +19,7 @@ async function testVerify({ algorithm, key, data, customization, - length, + outputLength, expected }) { const [ verifyKey, @@ -46,7 +46,7 @@ async function testVerify({ algorithm, const signParams = { name: algorithm, - length, + outputLength, customization, }; @@ -112,7 +112,7 @@ async function testVerify({ algorithm, { assert(!(await subtle.verify({ ...signParams, - length: length === 256 ? 512 : 256, + outputLength: outputLength === 256 ? 512 : 256, }, verifyKey, expected, data))); } } @@ -121,7 +121,7 @@ async function testSign({ algorithm, key, data, customization, - length, + outputLength, expected }) { const [ signKey, @@ -148,7 +148,7 @@ async function testSign({ algorithm, const signParams = { name: algorithm, - length, + outputLength, customization, }; diff --git a/test/parallel/test-webcrypto-sign-verify.js b/test/parallel/test-webcrypto-sign-verify.js index 8b034e005b29a4..26e66d9aa0fa8b 100644 --- a/test/parallel/test-webcrypto-sign-verify.js +++ b/test/parallel/test-webcrypto-sign-verify.js @@ -118,12 +118,12 @@ if (hasOpenSSL(3)) { const signature = await subtle.sign({ name, - length: 256, + outputLength: 256, }, key, ec.encode(data)); assert(await subtle.verify({ name, - length: 256, + outputLength: 256, }, key, signature, ec.encode(data))); } diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index d23fb1d40453fe..d14c617614af34 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -47,6 +47,43 @@ if (!hasOpenSSL(3, 5)) { 'sign_verify/mldsa.tentative.https.any.js'); } +const cshakeExpectedFailures = ['cSHAKE128', 'cSHAKE256'].flatMap((algorithm) => { + return [0, 256, 384, 512].flatMap((length) => { + return ['empty', 'short', 'medium'].flatMap((size) => { + const base = `${algorithm} with ${length} bit output and ${size} source data`; + return [ + base, + `${base} and altered buffer after call`, + ]; + }); + }); +}); + +const kmacVectorNames = [ + 'KMAC128 with no customization', + 'KMAC128 with customization', + 'KMAC128 with large data and customization', + 'KMAC256 with customization and 512-bit output', + 'KMAC256 with large data and no customization', + 'KMAC256 with large data and customization', +]; + +const kmacExpectedFailures = kmacVectorNames.flatMap((name) => { + return [ + `${name} verification`, + `${name} verification with altered signature after call`, + `${name} with altered plaintext after call`, + `${name} no verify usage`, + `${name} round trip`, + `${name} verification failure due to wrong plaintext`, + `${name} verification failure due to wrong signature`, + `${name} verification failure due to short signature`, + `${name} verification failure due to wrong length parameter`, + `${name} signing with wrong algorithm name`, + `${name} verifying with wrong algorithm name`, + ]; +}); + module.exports = { ...conditionalSkips, 'algorithm-discards-context.https.window.js': { @@ -86,4 +123,16 @@ module.exports = { ], }, }, + 'digest/cshake.tentative.https.any.js': { + 'fail': { + 'note': 'WPT still uses CShakeParams.length; implementation moved to CShakeParams.outputLength', + 'expected': cshakeExpectedFailures, + }, + }, + 'sign_verify/kmac.tentative.https.any.js': conditionalSkips['sign_verify/kmac.tentative.https.any.js'] ?? { + 'fail': { + 'note': 'WPT still uses KmacParams.length; implementation moved to KmacParams.outputLength', + 'expected': kmacExpectedFailures, + }, + }, }; From ee232026c6d2afc9b13018bb5ccb12b173512807 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 10 Mar 2026 14:11:56 +0100 Subject: [PATCH 2/2] crypto: add TurboSHAKE and KangarooTwelve Web Cryptography algorithms PR-URL: https://github.com/nodejs/node/pull/62183 Refs: https://wicg.github.io/webcrypto-modern-algos/#kangarootwelve Refs: https://wicg.github.io/webcrypto-modern-algos/#turboshake Refs: https://www.rfc-editor.org/rfc/rfc9861.html Refs: https://redirect.github.com/openssl/openssl/issues/30304 Reviewed-By: Anna Henningsen Reviewed-By: Yagiz Nizipli --- doc/api/webcrypto.md | 86 ++- lib/internal/crypto/hash.js | 20 + lib/internal/crypto/util.js | 12 + lib/internal/crypto/webidl.js | 46 ++ node.gyp | 2 + src/crypto/crypto_turboshake.cc | 642 ++++++++++++++++++ src/crypto/crypto_turboshake.h | 105 +++ src/node_crypto.cc | 5 +- src/node_crypto.h | 1 + .../webcrypto/supports-modern-algorithms.mjs | 24 + .../test-webcrypto-digest-turboshake-rfc.js | 399 +++++++++++ .../test-webcrypto-digest-turboshake.js | 181 +++++ tools/doc/type-parser.mjs | 2 + 13 files changed, 1523 insertions(+), 2 deletions(-) create mode 100644 src/crypto/crypto_turboshake.cc create mode 100644 src/crypto/crypto_turboshake.h create mode 100644 test/parallel/test-webcrypto-digest-turboshake-rfc.js create mode 100644 test/parallel/test-webcrypto-digest-turboshake.js diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index 71ab62ff8aa608..c72e15b3fd053e 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -2,6 +2,10 @@ -* `algorithm` {string|Algorithm|CShakeParams} +* `algorithm` {string|Algorithm|CShakeParams|TurboShakeParams|KangarooTwelveParams} * `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. @@ -1018,6 +1034,8 @@ If `algorithm` is provided as a {string}, it must be one of: * `'cSHAKE128'`[^modern-algos] * `'cSHAKE256'`[^modern-algos] +* `'KT128'`[^modern-algos] +* `'KT256'`[^modern-algos] * `'SHA-1'` * `'SHA-256'` * `'SHA-384'` @@ -1025,6 +1043,8 @@ If `algorithm` is provided as a {string}, it must be one of: * `'SHA3-256'`[^modern-algos] * `'SHA3-384'`[^modern-algos] * `'SHA3-512'`[^modern-algos] +* `'TurboSHAKE128'`[^modern-algos] +* `'TurboSHAKE256'`[^modern-algos] If `algorithm` is provided as an {Object}, it must have a `name` property whose value is one of the above. @@ -2311,6 +2331,38 @@ added: v15.0.0 * Type: {string} +### Class: `KangarooTwelveParams` + + + +#### `kangarooTwelveParams.customization` + + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} + +The optional customization string for KangarooTwelve. + +#### `kangarooTwelveParams.name` + + + +* Type: {string} Must be `'KT128'`[^modern-algos] or `'KT256'`[^modern-algos] + +#### `kangarooTwelveParams.outputLength` + + + +* Type: {number} represents the requested output length in bits. + ### Class: `KmacImportParams` + +#### `turboShakeParams.domainSeparation` + + + +* Type: {number|undefined} + +The optional domain separation byte (0x01-0x7f). Defaults to `0x1f`. + +#### `turboShakeParams.name` + + + +* Type: {string} Must be `'TurboSHAKE128'`[^modern-algos] or `'TurboSHAKE256'`[^modern-algos] + +#### `turboShakeParams.outputLength` + + + +* Type: {number} represents the requested output length in bits. + [^secure-curves]: See [Secure Curves in the Web Cryptography API][] [^modern-algos]: See [Modern Algorithms in the Web Cryptography API][] diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index eecbe5213f568c..3debc19eddbe0a 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -14,6 +14,8 @@ const { Hmac: _Hmac, kCryptoJobAsync, oneShotDigest, + TurboShakeJob, + KangarooTwelveJob, } = internalBinding('crypto'); const { @@ -226,6 +228,24 @@ async function asyncDigest(algorithm, data) { normalizeHashName(algorithm.name), data, algorithm.outputLength)); + case 'TurboSHAKE128': + // Fall through + case 'TurboSHAKE256': + return await jobPromise(() => new TurboShakeJob( + kCryptoJobAsync, + algorithm.name, + algorithm.domainSeparation ?? 0x1f, + algorithm.outputLength / 8, + data)); + case 'KT128': + // Fall through + case 'KT256': + return await jobPromise(() => new KangarooTwelveJob( + kCryptoJobAsync, + algorithm.name, + algorithm.customization, + algorithm.outputLength / 8, + data)); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index a91a60e09c04dd..3c1d0e6bc6e9f7 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -244,6 +244,10 @@ const kAlgorithmDefinitions = { }, 'cSHAKE128': { 'digest': 'CShakeParams' }, 'cSHAKE256': { 'digest': 'CShakeParams' }, + 'KT128': { 'digest': 'KangarooTwelveParams' }, + 'KT256': { 'digest': 'KangarooTwelveParams' }, + 'TurboSHAKE128': { 'digest': 'TurboShakeParams' }, + 'TurboSHAKE256': { 'digest': 'TurboShakeParams' }, 'ECDH': { 'generateKey': 'EcKeyGenParams', 'exportKey': null, @@ -441,6 +445,10 @@ const experimentalAlgorithms = [ 'SHA3-256', 'SHA3-384', 'SHA3-512', + 'TurboSHAKE128', + 'TurboSHAKE256', + 'KT128', + 'KT256', 'X448', ]; @@ -511,6 +519,10 @@ const simpleAlgorithmDictionaries = { KmacParams: { customization: 'BufferSource', }, + KangarooTwelveParams: { + customization: 'BufferSource', + }, + TurboShakeParams: {}, }; function validateMaxBufferLength(data, name) { diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 581f541847faea..8730bc0de94efb 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -895,6 +895,52 @@ converters.KmacParams = createDictionaryConverter( }, ]); +converters.KangarooTwelveParams = createDictionaryConverter( + 'KangarooTwelveParams', [ + ...new SafeArrayIterator(dictAlgorithm), + { + key: 'outputLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, opts) => { + if (V === 0 || V % 8) + throw lazyDOMException('Invalid KangarooTwelveParams outputLength', 'OperationError'); + }, + required: true, + }, + { + key: 'customization', + converter: converters.BufferSource, + }, + ]); + +converters.TurboShakeParams = createDictionaryConverter( + 'TurboShakeParams', [ + ...new SafeArrayIterator(dictAlgorithm), + { + key: 'outputLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, opts) => { + if (V === 0 || V % 8) + throw lazyDOMException('Invalid TurboShakeParams outputLength', 'OperationError'); + }, + required: true, + }, + { + key: 'domainSeparation', + converter: (V, opts) => + converters.octet(V, { ...opts, enforceRange: true }), + validator: (V) => { + if (V < 0x01 || V > 0x7F) { + throw lazyDOMException( + 'TurboShakeParams.domainSeparation must be in range 0x01-0x7f', + 'OperationError'); + } + }, + }, + ]); + module.exports = { converters, requiredArguments, diff --git a/node.gyp b/node.gyp index f5cd416b5fe7a5..206afe3b305ba3 100644 --- a/node.gyp +++ b/node.gyp @@ -344,6 +344,7 @@ 'src/crypto/crypto_kem.cc', 'src/crypto/crypto_hmac.cc', 'src/crypto/crypto_kmac.cc', + 'src/crypto/crypto_turboshake.cc', 'src/crypto/crypto_random.cc', 'src/crypto/crypto_rsa.cc', 'src/crypto/crypto_spkac.cc', @@ -362,6 +363,7 @@ 'src/crypto/crypto_dh.h', 'src/crypto/crypto_hmac.h', 'src/crypto/crypto_kmac.h', + 'src/crypto/crypto_turboshake.h', 'src/crypto/crypto_rsa.h', 'src/crypto/crypto_spkac.h', 'src/crypto/crypto_util.h', diff --git a/src/crypto/crypto_turboshake.cc b/src/crypto/crypto_turboshake.cc new file mode 100644 index 00000000000000..60ef7a8dae5e44 --- /dev/null +++ b/src/crypto/crypto_turboshake.cc @@ -0,0 +1,642 @@ +#include "crypto/crypto_turboshake.h" +#include "async_wrap-inl.h" +#include "node_internals.h" +#include "threadpoolwork-inl.h" + +#include +#include + +namespace node::crypto { + +using v8::FunctionCallbackInfo; +using v8::JustVoid; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::Uint32; +using v8::Value; + +// ============================================================================ +// Keccak-p[1600, n_r=12] permutation +// Reference: FIPS 202, Section 3.3 and 3.4; RFC 9861 Section 2.2 +// Adapted from OpenSSL's keccak1600.c (KECCAK_REF variant) +// ============================================================================ +namespace { + +inline uint64_t ROL64(uint64_t val, int offset) { + DCHECK(offset >= 0 && offset < 64); + if (offset == 0) return val; + return (val << offset) | (val >> (64 - offset)); +} + +// Load/store 64-bit lanes in little-endian byte order. +// The Keccak state uses LE lane encoding (FIPS 202 Section 1, B.1). +// These helpers ensure correctness on both LE and BE platforms. +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); +} + +static const unsigned char rhotates[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 the last 12 rounds (indices 12..23). +static const uint64_t iotas[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, +}; + +// Keccak-p[1600, 12]: the reduced-round permutation used by TurboSHAKE. +void KeccakP1600_12(uint64_t A[5][5]) { + for (size_t round = 12; round < 24; round++) { + // Theta + 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]; + } + } + + // Rho + for (size_t y = 0; y < 5; y++) { + for (size_t x = 0; x < 5; x++) { + A[y][x] = ROL64(A[y][x], rhotates[y][x]); + } + } + + // Pi + 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]; + } + } + + // Chi + 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)); + } + + // Iota + A[0][0] ^= iotas[round]; + } +} + +// ============================================================================ +// TurboSHAKE sponge construction +// RFC 9861 Section 2.2, Appendix A.2/A.3 +// ============================================================================ + +// TurboSHAKE128 rate = 168 bytes (1344 bits), capacity = 256 bits +// TurboSHAKE256 rate = 136 bytes (1088 bits), capacity = 512 bits +static constexpr size_t kTurboSHAKE128Rate = 168; +static 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] = {}; + // Both rates (168, 136) are multiples of 8 + size_t lane_count = rate / 8; + + size_t offset = 0; + + // Absorb complete blocks from input + 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; + } + + // Absorb last (partial) block: remaining input bytes + domain_sep + padding + size_t remaining = input_len - offset; + uint8_t pad[168] = {}; // sized for max rate (TurboSHAKE128) + 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); + + // Squeeze output + 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); + } + } +} + +// Convenience wrappers +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 Section 3) +// ============================================================================ + +static constexpr size_t kChunkSize = 8192; + +// length_encode(x): RFC 9861 Section 3.3 +// Returns byte string x_(n-1) || ... || x_0 || n +// where x = sum of 256^i * x_i, n is smallest such that x < 256^n +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; + } + + // Reverse to get big-endian: x_(n-1) || ... || x_0 + size_t n = result.size(); + for (size_t i = 0; i < n / 2; i++) { + std::swap(result[i], result[n - 1 - i]); + } + + // Append n (the length of the encoding) + 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) { + // Build S = M || C || length_encode(|C|) + auto len_enc = LengthEncode(custom_len); + size_t s_len = msg_len + custom_len + len_enc.size(); + + // Short message path: |S| <= 8192 + if (s_len <= kChunkSize) { + // Build S in a contiguous buffer + 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; + } + + // Long message path: tree hashing + // We need to process S in chunks, but S is virtual (M || C || length_encode) + // Build a helper to read from this virtual concatenation. + + // First chunk is S[0:8192], compute chaining values for rest + // FinalNode = S[0:8192] || 0x03 || 0x00^7 + + // We need to read from S = M || C || length_encode(|C|) + // Helper lambda to copy from virtual S + auto read_s = [&](size_t s_offset, uint8_t* buf, size_t len) { + size_t copied = 0; + // Part 1: message + 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; + } + // Part 2: customization + 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; + } + // Part 3: length_encode + 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; + } + }; + + // Start building FinalNode + // FinalNode = S_0 || 0x03 0x00^7 || CV_1 || CV_2 || ... || CV_(n-1) + // || length_encode(n-1) || 0xFF 0xFF + + // Read first chunk S_0 + std::vector first_chunk(kChunkSize); + read_s(0, first_chunk.data(), kChunkSize); + + // Start FinalNode with S_0 || 0x03 || 0x00^7 + 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); + + // Process remaining chunks + 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); + + // CV = TurboSHAKE(chunk, 0x0B, cv_len) + 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; + } + + // Append length_encode(num_blocks) || 0xFF 0xFF + 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); + + // Final hash + 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); +} + +} // anonymous namespace + +// ============================================================================ +// TurboShake bindings +// ============================================================================ + +TurboShakeConfig::TurboShakeConfig(TurboShakeConfig&& other) noexcept + : job_mode(other.job_mode), + variant(other.variant), + output_length(other.output_length), + domain_separation(other.domain_separation), + data(std::move(other.data)) {} + +TurboShakeConfig& TurboShakeConfig::operator=( + TurboShakeConfig&& other) noexcept { + if (&other == this) return *this; + this->~TurboShakeConfig(); + return *new (this) TurboShakeConfig(std::move(other)); +} + +void TurboShakeConfig::MemoryInfo(MemoryTracker* tracker) const { + if (job_mode == kCryptoJobAsync) { + // TODO(addaleax): Implement MemoryRetainer protocol for ByteSource + tracker->TrackFieldWithSize("data", data.size()); + } +} + +Maybe TurboShakeTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + TurboShakeConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->job_mode = mode; + + // args[offset + 0] = algorithm name (string) + CHECK(args[offset]->IsString()); + Utf8Value algorithm_name(env->isolate(), args[offset]); + std::string_view alg = algorithm_name.ToStringView(); + + if (alg == "TurboSHAKE128") { + params->variant = TurboShakeVariant::TurboSHAKE128; + } else if (alg == "TurboSHAKE256") { + params->variant = TurboShakeVariant::TurboSHAKE256; + } else { + UNREACHABLE(); + } + + // args[offset + 1] = domain separation byte (uint32) + CHECK(args[offset + 1]->IsUint32()); + params->domain_separation = + static_cast(args[offset + 1].As()->Value()); + CHECK_GE(params->domain_separation, 0x01); + CHECK_LE(params->domain_separation, 0x7F); + + // args[offset + 2] = output length in bytes (uint32) + CHECK(args[offset + 2]->IsUint32()); + params->output_length = args[offset + 2].As()->Value(); + + // args[offset + 3] = data (ArrayBuffer/View) + ArrayBufferOrViewContents data(args[offset + 3]); + if (!data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + return Nothing(); + } + params->data = mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource(); + + return JustVoid(); +} + +bool TurboShakeTraits::DeriveBits(Environment* env, + const TurboShakeConfig& params, + ByteSource* out, + CryptoJobMode mode) { + CHECK_GT(params.output_length, 0); + char* buf = MallocOpenSSL(params.output_length); + + const uint8_t* input = reinterpret_cast(params.data.data()); + size_t input_len = params.data.size(); + + switch (params.variant) { + case TurboShakeVariant::TurboSHAKE128: + TurboSHAKE128(input, + input_len, + params.domain_separation, + reinterpret_cast(buf), + params.output_length); + break; + case TurboShakeVariant::TurboSHAKE256: + TurboSHAKE256(input, + input_len, + params.domain_separation, + reinterpret_cast(buf), + params.output_length); + break; + } + + *out = ByteSource::Allocated(buf, params.output_length); + return true; +} + +MaybeLocal TurboShakeTraits::EncodeOutput(Environment* env, + const TurboShakeConfig& params, + ByteSource* out) { + return out->ToArrayBuffer(env); +} + +// ============================================================================ +// KangarooTwelve bindings +// ============================================================================ + +KangarooTwelveConfig::KangarooTwelveConfig( + KangarooTwelveConfig&& other) noexcept + : job_mode(other.job_mode), + variant(other.variant), + output_length(other.output_length), + data(std::move(other.data)), + customization(std::move(other.customization)) {} + +KangarooTwelveConfig& KangarooTwelveConfig::operator=( + KangarooTwelveConfig&& other) noexcept { + if (&other == this) return *this; + this->~KangarooTwelveConfig(); + return *new (this) KangarooTwelveConfig(std::move(other)); +} + +void KangarooTwelveConfig::MemoryInfo(MemoryTracker* tracker) const { + if (job_mode == kCryptoJobAsync) { + // TODO(addaleax): Implement MemoryRetainer protocol for ByteSource + tracker->TrackFieldWithSize("data", data.size()); + tracker->TrackFieldWithSize("customization", customization.size()); + } +} + +Maybe KangarooTwelveTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + KangarooTwelveConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->job_mode = mode; + + // args[offset + 0] = algorithm name (string) + CHECK(args[offset]->IsString()); + Utf8Value algorithm_name(env->isolate(), args[offset]); + std::string_view alg = algorithm_name.ToStringView(); + + if (alg == "KT128") { + params->variant = KangarooTwelveVariant::KT128; + } else if (alg == "KT256") { + params->variant = KangarooTwelveVariant::KT256; + } else { + UNREACHABLE(); + } + + // args[offset + 1] = customization (BufferSource or undefined) + if (!args[offset + 1]->IsUndefined()) { + ArrayBufferOrViewContents customization(args[offset + 1]); + if (!customization.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); + return Nothing(); + } + params->customization = mode == kCryptoJobAsync + ? customization.ToCopy() + : customization.ToByteSource(); + } + + // args[offset + 2] = output length in bytes (uint32) + CHECK(args[offset + 2]->IsUint32()); + params->output_length = args[offset + 2].As()->Value(); + + // args[offset + 3] = data (ArrayBuffer/View) + ArrayBufferOrViewContents data(args[offset + 3]); + if (!data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + return Nothing(); + } + params->data = mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource(); + + return JustVoid(); +} + +bool KangarooTwelveTraits::DeriveBits(Environment* env, + const KangarooTwelveConfig& params, + ByteSource* out, + CryptoJobMode mode) { + CHECK_GT(params.output_length, 0); + char* buf = MallocOpenSSL(params.output_length); + + const uint8_t* input = reinterpret_cast(params.data.data()); + size_t input_len = params.data.size(); + + const uint8_t* custom = + reinterpret_cast(params.customization.data()); + size_t custom_len = params.customization.size(); + + switch (params.variant) { + case KangarooTwelveVariant::KT128: + KT128(input, + input_len, + custom, + custom_len, + reinterpret_cast(buf), + params.output_length); + break; + case KangarooTwelveVariant::KT256: + KT256(input, + input_len, + custom, + custom_len, + reinterpret_cast(buf), + params.output_length); + break; + } + + *out = ByteSource::Allocated(buf, params.output_length); + return true; +} + +MaybeLocal KangarooTwelveTraits::EncodeOutput( + Environment* env, const KangarooTwelveConfig& params, ByteSource* out) { + return out->ToArrayBuffer(env); +} + +// ============================================================================ +// Registration +// ============================================================================ + +void TurboShake::Initialize(Environment* env, Local target) { + TurboShakeJob::Initialize(env, target); + KangarooTwelveJob::Initialize(env, target); +} + +void TurboShake::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + TurboShakeJob::RegisterExternalReferences(registry); + KangarooTwelveJob::RegisterExternalReferences(registry); +} + +} // namespace node::crypto diff --git a/src/crypto/crypto_turboshake.h b/src/crypto/crypto_turboshake.h new file mode 100644 index 00000000000000..53b01eec8bd7c8 --- /dev/null +++ b/src/crypto/crypto_turboshake.h @@ -0,0 +1,105 @@ +#ifndef SRC_CRYPTO_CRYPTO_TURBOSHAKE_H_ +#define SRC_CRYPTO_CRYPTO_TURBOSHAKE_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_util.h" + +namespace node::crypto { + +enum class TurboShakeVariant { TurboSHAKE128, TurboSHAKE256 }; + +struct TurboShakeConfig final : public MemoryRetainer { + CryptoJobMode job_mode; + TurboShakeVariant variant; + uint32_t output_length; // Output length in bytes + uint8_t domain_separation; // Domain separation byte (0x01–0x7F) + ByteSource data; + + TurboShakeConfig() = default; + + explicit TurboShakeConfig(TurboShakeConfig&& other) noexcept; + + TurboShakeConfig& operator=(TurboShakeConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(TurboShakeConfig) + SET_SELF_SIZE(TurboShakeConfig) +}; + +struct TurboShakeTraits final { + using AdditionalParameters = TurboShakeConfig; + static constexpr const char* JobName = "TurboShakeJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_DERIVEBITSREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + TurboShakeConfig* params); + + static bool DeriveBits(Environment* env, + const TurboShakeConfig& params, + ByteSource* out, + CryptoJobMode mode); + + static v8::MaybeLocal EncodeOutput(Environment* env, + const TurboShakeConfig& params, + ByteSource* out); +}; + +using TurboShakeJob = DeriveBitsJob; + +enum class KangarooTwelveVariant { KT128, KT256 }; + +struct KangarooTwelveConfig final : public MemoryRetainer { + CryptoJobMode job_mode; + KangarooTwelveVariant variant; + uint32_t output_length; // Output length in bytes + ByteSource data; + ByteSource customization; + + KangarooTwelveConfig() = default; + + explicit KangarooTwelveConfig(KangarooTwelveConfig&& other) noexcept; + + KangarooTwelveConfig& operator=(KangarooTwelveConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(KangarooTwelveConfig) + SET_SELF_SIZE(KangarooTwelveConfig) +}; + +struct KangarooTwelveTraits final { + using AdditionalParameters = KangarooTwelveConfig; + static constexpr const char* JobName = "KangarooTwelveJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_DERIVEBITSREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + KangarooTwelveConfig* params); + + static bool DeriveBits(Environment* env, + const KangarooTwelveConfig& params, + ByteSource* out, + CryptoJobMode mode); + + static v8::MaybeLocal EncodeOutput( + Environment* env, const KangarooTwelveConfig& params, ByteSource* out); +}; + +using KangarooTwelveJob = DeriveBitsJob; + +namespace TurboShake { +void Initialize(Environment* env, v8::Local target); +void RegisterExternalReferences(ExternalReferenceRegistry* registry); +} // namespace TurboShake + +} // namespace node::crypto + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_TURBOSHAKE_H_ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 991cbf95fbb786..84375f9a737675 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -75,6 +75,8 @@ namespace crypto { #define KMAC_NAMESPACE_LIST(V) #endif +#define TURBOSHAKE_NAMESPACE_LIST(V) V(TurboShake) + #ifdef OPENSSL_NO_SCRYPT #define SCRYPT_NAMESPACE_LIST(V) #else @@ -86,7 +88,8 @@ namespace crypto { ARGON2_NAMESPACE_LIST(V) \ KEM_NAMESPACE_LIST(V) \ KMAC_NAMESPACE_LIST(V) \ - SCRYPT_NAMESPACE_LIST(V) + SCRYPT_NAMESPACE_LIST(V) \ + TURBOSHAKE_NAMESPACE_LIST(V) void Initialize(Local target, Local unused, diff --git a/src/node_crypto.h b/src/node_crypto.h index e5e29544b57a81..cc8fc689f48438 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -55,6 +55,7 @@ #include "crypto/crypto_spkac.h" #include "crypto/crypto_timing.h" #include "crypto/crypto_tls.h" +#include "crypto/crypto_turboshake.h" #include "crypto/crypto_util.h" #include "crypto/crypto_x509.h" diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index eafb95c559a0f7..62b90daf7b0463 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -28,6 +28,30 @@ export const vectors = { [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(1) }], [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], [false, { name: 'cSHAKE256', outputLength: 255 }], + [false, 'TurboSHAKE128'], + [true, { name: 'TurboSHAKE128', outputLength: 128 }], + [true, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x07 }], + [false, { name: 'TurboSHAKE128', outputLength: 0 }], + [false, { name: 'TurboSHAKE128', outputLength: 127 }], + [false, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x00 }], + [false, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x80 }], + [false, 'TurboSHAKE256'], + [true, { name: 'TurboSHAKE256', outputLength: 256 }], + [true, { name: 'TurboSHAKE256', outputLength: 256, domainSeparation: 0x07 }], + [false, { name: 'TurboSHAKE256', outputLength: 0 }], + [false, { name: 'TurboSHAKE256', outputLength: 255 }], + [false, { name: 'TurboSHAKE256', outputLength: 256, domainSeparation: 0x00 }], + [false, { name: 'TurboSHAKE256', outputLength: 256, domainSeparation: 0x80 }], + [false, 'KT128'], + [true, { name: 'KT128', outputLength: 128 }], + [true, { name: 'KT128', outputLength: 128, customization: Buffer.alloc(0) }], + [false, { name: 'KT128', outputLength: 0 }], + [false, { name: 'KT128', outputLength: 127 }], + [false, 'KT256'], + [true, { name: 'KT256', outputLength: 256 }], + [true, { name: 'KT256', outputLength: 256, customization: Buffer.alloc(0) }], + [false, { name: 'KT256', outputLength: 0 }], + [false, { name: 'KT256', outputLength: 255 }], ], 'sign': [ [pqc, 'ML-DSA-44'], diff --git a/test/parallel/test-webcrypto-digest-turboshake-rfc.js b/test/parallel/test-webcrypto-digest-turboshake-rfc.js new file mode 100644 index 00000000000000..43762fecc2c41e --- /dev/null +++ b/test/parallel/test-webcrypto-digest-turboshake-rfc.js @@ -0,0 +1,399 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +// RFC 9861 Section 5 test vectors + +// Generates a Buffer of length n by repeating the pattern 00 01 02 .. F9 FA. +function ptn(n) { + const buf = Buffer.allocUnsafe(n); + for (let i = 0; i < n; i++) + buf[i] = i % 251; + return buf; +} + +assert.deepStrictEqual( + ptn(17 ** 2).toString('hex'), + '000102030405060708090a0b0c0d0e0f' + + '101112131415161718191a1b1c1d1e1f' + + '202122232425262728292a2b2c2d2e2f' + + '303132333435363738393a3b3c3d3e3f' + + '404142434445464748494a4b4c4d4e4f' + + '505152535455565758595a5b5c5d5e5f' + + '606162636465666768696a6b6c6d6e6f' + + '707172737475767778797a7b7c7d7e7f' + + '808182838485868788898a8b8c8d8e8f' + + '909192939495969798999a9b9c9d9e9f' + + 'a0a1a2a3a4a5a6a7a8a9aaabacadaeaf' + + 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebf' + + 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf' + + 'd0d1d2d3d4d5d6d7d8d9dadbdcdddedf' + + 'e0e1e2e3e4e5e6e7e8e9eaebecedeeef' + + 'f0f1f2f3f4f5f6f7f8f9fa0001020304' + + '05060708090a0b0c0d0e0f1011121314' + + '15161718191a1b1c1d1e1f2021222324' + + '25', +); + +const turboSHAKE128Vectors = [ + // [input, outputLengthBytes, expected(, domainSeparation)] + [new Uint8Array(0), 32, + '1e415f1c5983aff2169217277d17bb53' + + '8cd945a397ddec541f1ce41af2c1b74c'], + [new Uint8Array(0), 64, + '1e415f1c5983aff2169217277d17bb53' + + '8cd945a397ddec541f1ce41af2c1b74c' + + '3e8ccae2a4dae56c84a04c2385c03c15' + + 'e8193bdf58737363321691c05462c8df'], + [ptn(17 ** 0), 32, + '55cedd6f60af7bb29a4042ae832ef3f5' + + '8db7299f893ebb9247247d856958daa9'], + [ptn(17 ** 1), 32, + '9c97d036a3bac819db70ede0ca554ec6' + + 'e4c2a1a4ffbfd9ec269ca6a111161233'], + [ptn(17 ** 2), 32, + '96c77c279e0126f7fc07c9b07f5cdae1' + + 'e0be60bdbe10620040e75d7223a624d2'], + [ptn(17 ** 3), 32, + 'd4976eb56bcf118520582b709f73e1d6' + + '853e001fdaf80e1b13e0d0599d5fb372'], + [ptn(17 ** 4), 32, + 'da67c7039e98bf530cf7a37830c6664e' + + '14cbab7f540f58403b1b82951318ee5c'], + [ptn(17 ** 5), 32, + 'b97a906fbf83ef7c812517abf3b2d0ae' + + 'a0c4f60318ce11cf103925127f59eecd'], + [ptn(17 ** 6), 32, + '35cd494adeded2f25239af09a7b8ef0c' + + '4d1ca4fe2d1ac370fa63216fe7b4c2b1'], + [Buffer.from('ffffff', 'hex'), 32, + 'bf323f940494e88ee1c540fe660be8a0' + + 'c93f43d15ec006998462fa994eed5dab', 0x01], + [Buffer.from('ff', 'hex'), 32, + '8ec9c66465ed0d4a6c35d13506718d68' + + '7a25cb05c74cca1e42501abd83874a67', 0x06], + [Buffer.from('ffffff', 'hex'), 32, + 'b658576001cad9b1e5f399a9f77723bb' + + 'a05458042d68206f7252682dba3663ed', 0x07], + [Buffer.from('ffffffffffffff', 'hex'), 32, + '8deeaa1aec47ccee569f659c21dfa8e1' + + '12db3cee37b18178b2acd805b799cc37', 0x0b], + [Buffer.from('ff', 'hex'), 32, + '553122e2135e363c3292bed2c6421fa2' + + '32bab03daa07c7d6636603286506325b', 0x30], + [Buffer.from('ffffff', 'hex'), 32, + '16274cc656d44cefd422395d0f9053bd' + + 'a6d28e122aba15c765e5ad0e6eaf26f9', 0x7f], +]; + +const turboSHAKE256Vectors = [ + // [input, outputLengthBytes, expected(, domainSeparation)] + [new Uint8Array(0), 64, + '367a329dafea871c7802ec67f905ae13' + + 'c57695dc2c6663c61035f59a18f8e7db' + + '11edc0e12e91ea60eb6b32df06dd7f00' + + '2fbafabb6e13ec1cc20d995547600db0'], + [ptn(17 ** 0), 64, + '3e1712f928f8eaf1054632b2aa0a246e' + + 'd8b0c378728f60bc970410155c28820e' + + '90cc90d8a3006aa2372c5c5ea176b068' + + '2bf22bae7467ac94f74d43d39b0482e2'], + [ptn(17 ** 1), 64, + 'b3bab0300e6a191fbe61379398359235' + + '78794ea54843f5011090fa2f3780a9e5' + + 'cb22c59d78b40a0fbff9e672c0fbe097' + + '0bd2c845091c6044d687054da5d8e9c7'], + [ptn(17 ** 2), 64, + '66b810db8e90780424c0847372fdc957' + + '10882fde31c6df75beb9d4cd9305cfca' + + 'e35e7b83e8b7e6eb4b78605880116316' + + 'fe2c078a09b94ad7b8213c0a738b65c0'], + [ptn(17 ** 3), 64, + 'c74ebc919a5b3b0dd1228185ba02d29e' + + 'f442d69d3d4276a93efe0bf9a16a7dc0' + + 'cd4eabadab8cd7a5edd96695f5d360ab' + + 'e09e2c6511a3ec397da3b76b9e1674fb'], + [ptn(17 ** 4), 64, + '02cc3a8897e6f4f6ccb6fd46631b1f52' + + '07b66c6de9c7b55b2d1a23134a170afd' + + 'ac234eaba9a77cff88c1f020b7372461' + + '8c5687b362c430b248cd38647f848a1d'], + [ptn(17 ** 5), 64, + 'add53b06543e584b5823f626996aee50' + + 'fe45ed15f20243a7165485acb4aa76b4' + + 'ffda75cedf6d8cdc95c332bd56f4b986' + + 'b58bb17d1778bfc1b1a97545cdf4ec9f'], + [ptn(17 ** 6), 64, + '9e11bc59c24e73993c1484ec66358ef7' + + '1db74aefd84e123f7800ba9c4853e02c' + + 'fe701d9e6bb765a304f0dc34a4ee3ba8' + + '2c410f0da70e86bfbd90ea877c2d6104'], + [Buffer.from('ffffff', 'hex'), 64, + 'd21c6fbbf587fa2282f29aea620175fb' + + '0257413af78a0b1b2a87419ce031d933' + + 'ae7a4d383327a8a17641a34f8a1d1003' + + 'ad7da6b72dba84bb62fef28f62f12424', 0x01], + [Buffer.from('ff', 'hex'), 64, + '738d7b4e37d18b7f22ad1b5313e357e3' + + 'dd7d07056a26a303c433fa3533455280' + + 'f4f5a7d4f700efb437fe6d281405e07b' + + 'e32a0a972e22e63adc1b090daefe004b', 0x06], + [Buffer.from('ffffff', 'hex'), 64, + '18b3b5b7061c2e67c1753a00e6ad7ed7' + + 'ba1c906cf93efb7092eaf27fbeebb755' + + 'ae6e292493c110e48d260028492b8e09' + + 'b5500612b8f2578985ded5357d00ec67', 0x07], + [Buffer.from('ffffffffffffff', 'hex'), 64, + 'bb36764951ec97e9d85f7ee9a67a7718' + + 'fc005cf42556be79ce12c0bde50e5736' + + 'd6632b0d0dfb202d1bbb8ffe3dd74cb0' + + '0834fa756cb03471bab13a1e2c16b3c0', 0x0b], + [Buffer.from('ff', 'hex'), 64, + 'f3fe12873d34bcbb2e608779d6b70e7f' + + '86bec7e90bf113cbd4fdd0c4e2f4625e' + + '148dd7ee1a52776cf77f240514d9ccfc' + + '3b5ddab8ee255e39ee389072962c111a', 0x30], + [Buffer.from('ffffff', 'hex'), 64, + 'abe569c1f77ec340f02705e7d37c9ab7' + + 'e155516e4a6a150021d70b6fac0bb40c' + + '069f9a9828a0d575cd99f9bae435ab1a' + + 'cf7ed9110ba97ce0388d074bac768776', 0x7f], +]; + +const kt128Vectors = [ + // [input, outputLengthBytes, expected(, customization)] + [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'], + [ptn(17 ** 3), 32, + 'cb552e2ec77d9910701d578b457ddf77' + + '2c12e322e4ee7fe417f92c758f0d59d0'], + [ptn(17 ** 4), 32, + '8701045e22205345ff4dda05555cbb5c' + + '3af1a771c2b89baef37db43d9998b9fe'], + [ptn(17 ** 5), 32, + '844d610933b1b9963cbdeb5ae3b6b05c' + + 'c7cbd67ceedf883eb678a0a8e0371682'], + [ptn(17 ** 6), 32, + '3c390782a8a4e89fa6367f72feaaf132' + + '55c8d95878481d3cd8ce85f58e880af8'], + [new Uint8Array(0), 32, + 'fab658db63e94a246188bf7af69a1330' + + '45f46ee984c56e3c3328caaf1aa1a583', ptn(1)], + [Buffer.from('ff', 'hex'), 32, + 'd848c5068ced736f4462159b9867fd4c' + + '20b808acc3d5bc48e0b06ba0a3762ec4', ptn(41)], + [Buffer.from('ffffff', 'hex'), 32, + 'c389e5009ae57120854c2e8c64670ac0' + + '1358cf4c1baf89447a724234dc7ced74', ptn(41 ** 2)], + [Buffer.from('ffffffffffffff', 'hex'), 32, + '75d2f86a2e644566726b4fbcfc5657b9' + + 'dbcf070c7b0dca06450ab291d7443bcf', ptn(41 ** 3)], + [ptn(8191), 32, + '1b577636f723643e990cc7d6a6598374' + + '36fd6a103626600eb8301cd1dbe553d6'], + [ptn(8192), 32, + '48f256f6772f9edfb6a8b661ec92dc93' + + 'b95ebd05a08a17b39ae3490870c926c3'], + [ptn(8192), 32, + '3ed12f70fb05ddb58689510ab3e4d23c' + + '6c6033849aa01e1d8c220a297fedcd0b', ptn(8189)], + [ptn(8192), 32, + '6a7c1b6a5cd0d8c9ca943a4a216cc646' + + '04559a2ea45f78570a15253d67ba00ae', ptn(8190)], +]; + +const kt256Vectors = [ + // [input, outputLengthBytes, expected(, customization)] + [new Uint8Array(0), 64, + 'b23d2e9cea9f4904e02bec06817fc10c' + + 'e38ce8e93ef4c89e6537076af8646404' + + 'e3e8b68107b8833a5d30490aa3348235' + + '3fd4adc7148ecb782855003aaebde4a9'], + [new Uint8Array(0), 128, + 'b23d2e9cea9f4904e02bec06817fc10c' + + 'e38ce8e93ef4c89e6537076af8646404' + + 'e3e8b68107b8833a5d30490aa3348235' + + '3fd4adc7148ecb782855003aaebde4a9' + + 'b0925319d8ea1e121a609821ec19efea' + + '89e6d08daee1662b69c840289f188ba8' + + '60f55760b61f82114c030c97e5178449' + + '608ccd2cd2d919fc7829ff69931ac4d0'], + [ptn(1), 64, + '0d005a194085360217128cf17f91e1f7' + + '1314efa5564539d444912e3437efa17f' + + '82db6f6ffe76e781eaa068bce01f2bbf' + + '81eacb983d7230f2fb02834a21b1ddd0'], + [ptn(17), 64, + '1ba3c02b1fc514474f06c8979978a905' + + '6c8483f4a1b63d0dccefe3a28a2f323e' + + '1cdcca40ebf006ac76ef039715234683' + + '7b1277d3e7faa9c9653b19075098527b'], + [ptn(17 ** 2), 64, + 'de8ccbc63e0f133ebb4416814d4c66f6' + + '91bbf8b6a61ec0a7700f836b086cb029' + + 'd54f12ac7159472c72db118c35b4e6aa' + + '213c6562caaa9dcc518959e69b10f3ba'], + [ptn(17 ** 3), 64, + '647efb49fe9d717500171b41e7f11bd4' + + '91544443209997ce1c2530d15eb1ffbb' + + '598935ef954528ffc152b1e4d731ee26' + + '83680674365cd191d562bae753b84aa5'], + [ptn(17 ** 4), 64, + 'b06275d284cd1cf205bcbe57dccd3ec1' + + 'ff6686e3ed15776383e1f2fa3c6ac8f0' + + '8bf8a162829db1a44b2a43ff83dd89c3' + + 'cf1ceb61ede659766d5ccf817a62ba8d'], + [ptn(17 ** 5), 64, + '9473831d76a4c7bf77ace45b59f1458b' + + '1673d64bcd877a7c66b2664aa6dd149e' + + '60eab71b5c2bab858c074ded81ddce2b' + + '4022b5215935c0d4d19bf511aeeb0772'], + [ptn(17 ** 6), 64, + '0652b740d78c5e1f7c8dcc1777097382' + + '768b7ff38f9a7a20f29f413bb1b3045b' + + '31a5578f568f911e09cf44746da84224' + + 'a5266e96a4a535e871324e4f9c7004da'], + [new Uint8Array(0), 64, + '9280f5cc39b54a5a594ec63de0bb9937' + + '1e4609d44bf845c2f5b8c316d72b1598' + + '11f748f23e3fabbe5c3226ec96c62186' + + 'df2d33e9df74c5069ceecbb4dd10eff6', ptn(1)], + [Buffer.from('ff', 'hex'), 64, + '47ef96dd616f200937aa7847e34ec2fe' + + 'ae8087e3761dc0f8c1a154f51dc9ccf8' + + '45d7adbce57ff64b639722c6a1672e3b' + + 'f5372d87e00aff89be97240756998853', ptn(41)], + [Buffer.from('ffffff', 'hex'), 64, + '3b48667a5051c5966c53c5d42b95de45' + + '1e05584e7806e2fb765eda959074172c' + + 'b438a9e91dde337c98e9c41bed94c4e0' + + 'aef431d0b64ef2324f7932caa6f54969', ptn(41 ** 2)], + [Buffer.from('ffffffffffffff', 'hex'), 64, + 'e0911cc00025e1540831e266d94add9b' + + '98712142b80d2629e643aac4efaf5a3a' + + '30a88cbf4ac2a91a2432743054fbcc98' + + '97670e86ba8cec2fc2ace9c966369724', ptn(41 ** 3)], + [ptn(8191), 64, + '3081434d93a4108d8d8a3305b89682ce' + + 'bedc7ca4ea8a3ce869fbb73cbe4a58ee' + + 'f6f24de38ffc170514c70e7ab2d01f03' + + '812616e863d769afb3753193ba045b20'], + [ptn(8192), 64, + 'c6ee8e2ad3200c018ac87aaa031cdac2' + + '2121b412d07dc6e0dccbb53423747e9a' + + '1c18834d99df596cf0cf4b8dfafb7bf0' + + '2d139d0c9035725adc1a01b7230a41fa'], + [ptn(8192), 64, + '74e47879f10a9c5d11bd2da7e194fe57' + + 'e86378bf3c3f7448eff3c576a0f18c5c' + + 'aae0999979512090a7f348af4260d4de' + + '3c37f1ecaf8d2c2c96c1d16c64b12496', ptn(8189)], + [ptn(8192), 64, + 'f4b5908b929ffe01e0f79ec2f21243d4' + + '1a396b2e7303a6af1d6399cd6c7a0a2d' + + 'd7c4f607e8277f9c9b1cb4ab9ddc59d4' + + 'b92d1fc7558441f1832c3279a4241b8b', ptn(8190)], +]; + +async function checkDigest(name, vectors) { + const isKT = name.startsWith('KT'); + for (const [input, outputLength, expected, ...rest] of vectors) { + const algorithm = { name, outputLength: outputLength * 8 }; + if (rest.length) { + if (isKT) + algorithm.customization = rest[0]; + else + algorithm.domainSeparation = rest[0]; + } + const result = await subtle.digest(algorithm, input); + assert.deepStrictEqual( + Buffer.from(result).toString('hex'), + expected, + ); + } +} + +(async () => { + await checkDigest('TurboSHAKE128', turboSHAKE128Vectors); + + // TurboSHAKE128(M=00^0, D=1F, 10032), last 32 bytes + { + const result = await subtle.digest({ + name: 'TurboSHAKE128', + outputLength: 10032 * 8, + }, new Uint8Array(0)); + assert.deepStrictEqual( + Buffer.from(result).subarray(-32).toString('hex'), + 'a3b9b0385900ce761f22aed548e754da' + + '10a5242d62e8c658e3f3a923a7555607', + ); + } + + await checkDigest('TurboSHAKE256', turboSHAKE256Vectors); + + // TurboSHAKE256(M=00^0, D=1F, 10032), last 32 bytes + { + const result = await subtle.digest({ + name: 'TurboSHAKE256', + outputLength: 10032 * 8, + }, new Uint8Array(0)); + assert.deepStrictEqual( + Buffer.from(result).subarray(-32).toString('hex'), + 'abefa11630c661269249742685ec082f' + + '207265dccf2f43534e9c61ba0c9d1d75', + ); + } + + await checkDigest('KT128', kt128Vectors); + + // KT128(M=00^0, C=00^0, 10032), last 32 bytes + { + const result = await subtle.digest({ + name: 'KT128', + outputLength: 10032 * 8, + }, new Uint8Array(0)); + assert.deepStrictEqual( + Buffer.from(result).subarray(-32).toString('hex'), + 'e8dc563642f7228c84684c898405d3a8' + + '34799158c079b12880277a1d28e2ff6d', + ); + } + + await checkDigest('KT256', kt256Vectors); + + // KT256(M=00^0, C=00^0, 10064), last 64 bytes + { + const result = await subtle.digest({ + name: 'KT256', + outputLength: 10064 * 8, + }, new Uint8Array(0)); + assert.deepStrictEqual( + Buffer.from(result).subarray(-64).toString('hex'), + 'ad4a1d718cf950506709a4c33396139b' + + '4449041fc79a05d68da35f1e453522e0' + + '56c64fe94958e7085f2964888259b993' + + '2752f3ccd855288efee5fcbb8b563069', + ); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-digest-turboshake.js b/test/parallel/test-webcrypto-digest-turboshake.js new file mode 100644 index 00000000000000..0b5586b19286be --- /dev/null +++ b/test/parallel/test-webcrypto-digest-turboshake.js @@ -0,0 +1,181 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const kSourceData = { + empty: '', + short: '156eea7cc14c56cb94db030a4a9d95ff', + medium: 'b6c8f9df648cd088b70f38e74197b18cb81e1e435' + + '0d50bccb8fb5a7379c87bb2e3d6ed5461ed1e9f36' + + 'f340a3962a446b815b794b4bd43a4403502077b22' + + '56cc807837f3aacd118eb4b9c2baeb897068625ab' + + 'aca193', + long: null +}; + +kSourceData.long = kSourceData.medium.repeat(1024); + +// Test vectors generated with PyCryptodome as a reference implementation +const kDigestedData = { + 'turboshake128': { + empty: '1e415f1c5983aff2169217277d17bb538cd945a397ddec541f1ce41af2c1b74c', + short: 'f8d1ebf3b48b71b0514686090eb25f1de322a00149be9b4dc5f09ac9077cd8a8', + medium: '0d0e7eceb4ae58c3c48f6c2bad56d0f8ff3f887468d3ea55a138aedf395233c0', + long: '5747c06f02ffd9d6c911b6453cc8b717083ab6417319a6ec5c3bb39ed0baf331', + }, + 'turboshake256': { + empty: '367a329dafea871c7802ec67f905ae13c57695dc2c6663c61035f59a18f8e7db' + + '11edc0e12e91ea60eb6b32df06dd7f002fbafabb6e13ec1cc20d995547600db0', + short: 'b47aa0a5b76caf9b10cfaeff036df0cdb86362d2bd036a2fee0cd0d74e79279c' + + 'b9c57a70da1e3dd9e126a469857ba4c82b0efb3ae06d1a3781a6f102c3eb3a1d', + medium: '7fa19fd828762d2dba6eea8407d1fb04302b5a4f1ca3d00b3672c1e3b3331d18' + + '925b7ec380f3f04673a164dab04d2a0c5c12818046284c38d286645741a8aa3e', + long: '12d0b90c08f588710733cc07f0a2d6ab0795a4a24904c111062226fcd9d5dcb2' + + '1d6b5b848c9aebbcab221f031e9b4ea71e099ec785e822b1b83e73d0750ca1a7', + }, + 'kt128': { + empty: '1ac2d450fc3b4205d19da7bfca1b37513c0803577ac7167f06fe2ce1f0ef39e5', + short: '4719a2ac1dc1c592521cf201df3f476ea496fe461abe9a2604527f6bec047579', + medium: '00f3add71679681720b925416953897ac62cfae97060dd5f2e1641a076580cc9', + long: 'c05805c2736deb4be3fca6e3717b9af0aa18ceeaaeeab66b328a3ffebf0a814d', + }, + 'kt256': { + empty: 'b23d2e9cea9f4904e02bec06817fc10ce38ce8e93ef4c89e6537076af8646404' + + 'e3e8b68107b8833a5d30490aa33482353fd4adc7148ecb782855003aaebde4a9', + short: '6709e5e312f2dee4547ecb0ab7d42728ba57985983731afbd6c2a0676c522274' + + 'cf9153064ee07982129d3f58d4dbe00050eb28b392559bdb020aca302b7a28cb', + medium: '9078b6ff78e9c4b3c8ff49e5b9f337b36cc6d6749d23985035886d993db69f7e' + + '05fea97125e0889130da09fc5837761f7793e3e44d85be1ee1f6af7f4a1f50cb', + long: '41f83b7c7d02fc6d98f1fa1474d765caff4673f90cd7204894d7da72d97403b6' + + '2fe5c4bae2bf0ce3dcd51e80c98bd25ce5fe54040259d9466b67f1517dac0712', + }, +}; + +function buildAlg(name) { + const lower = name.toLowerCase(); + if (lower.startsWith('turboshake')) { + const outputLength = lower === 'turboshake128' ? 256 : 512; + return { name, outputLength }; + } + if (lower.startsWith('kt')) { + const outputLength = lower === 'kt128' ? 256 : 512; + return { name, outputLength }; + } + return name; +} + +async function testDigest(size, alg) { + const digest = await subtle.digest( + alg, + Buffer.from(kSourceData[size], 'hex')); + + assert.strictEqual( + Buffer.from(digest).toString('hex'), + kDigestedData[(alg.name || alg).toLowerCase()][size]); +} + +// Known-answer tests +(async function() { + const variations = []; + Object.keys(kSourceData).forEach((size) => { + Object.keys(kDigestedData).forEach((alg) => { + const upCase = alg.toUpperCase(); + const downCase = alg.toLowerCase(); + const mixedCase = upCase.slice(0, 1) + downCase.slice(1); + + variations.push(testDigest(size, buildAlg(upCase))); + variations.push(testDigest(size, buildAlg(downCase))); + variations.push(testDigest(size, buildAlg(mixedCase))); + }); + }); + + await Promise.all(variations); +})().then(common.mustCall()); + +// Edge cases: zero-length output rejects +(async () => { + await assert.rejects( + subtle.digest({ name: 'TurboSHAKE128', outputLength: 0 }, Buffer.alloc(1)), + { + name: 'OperationError', + message: 'Invalid TurboShakeParams outputLength', + }); + + await assert.rejects( + subtle.digest({ name: 'KT128', outputLength: 0 }, Buffer.alloc(1)), + { + name: 'OperationError', + message: 'Invalid KangarooTwelveParams outputLength', + }); +})().then(common.mustCall()); + +// Edge case: non-byte-aligned outputLength rejects +(async () => { + await assert.rejects( + subtle.digest({ name: 'TurboSHAKE128', outputLength: 7 }, Buffer.alloc(1)), + { + name: 'OperationError', + message: 'Invalid TurboShakeParams outputLength', + }); + + await assert.rejects( + subtle.digest({ name: 'KT128', outputLength: 7 }, Buffer.alloc(1)), + { + name: 'OperationError', + message: 'Invalid KangarooTwelveParams outputLength', + }); +})().then(common.mustCall()); + +// TurboSHAKE domain separation byte +(async () => { + // Domain separation 0x07 should produce different output than default 0x1F + const [d07, d1f] = await Promise.all([ + subtle.digest( + { name: 'TurboSHAKE128', outputLength: 256, domainSeparation: 0x07 }, + Buffer.alloc(0)), + subtle.digest( + { name: 'TurboSHAKE128', outputLength: 256 }, + Buffer.alloc(0)), + ]); + assert.notDeepStrictEqual( + new Uint8Array(d07), + new Uint8Array(d1f)); + + // Verify D=0x07 against known vector + assert.strictEqual( + Buffer.from(d07).toString('hex'), + '5a223ad30b3b8c66a243048cfced430f54e7529287d15150b973133adfac6a2f'); +})().then(common.mustCall()); + +// KT128 with customization string +(async () => { + const digest = await subtle.digest( + { name: 'KT128', outputLength: 256, customization: Buffer.from('test') }, + Buffer.from('hello')); + assert(digest instanceof ArrayBuffer); + assert.strictEqual(digest.byteLength, 32); +})().then(common.mustCall()); + +// TurboSHAKE domain separation out of range +(async () => { + await assert.rejects( + subtle.digest( + { name: 'TurboSHAKE128', outputLength: 256, domainSeparation: 0x00 }, + Buffer.alloc(0)), + { + name: 'OperationError', + }); + await assert.rejects( + subtle.digest( + { name: 'TurboSHAKE128', outputLength: 256, domainSeparation: 0x80 }, + Buffer.alloc(0)), + { + name: 'OperationError', + }); +})().then(common.mustCall()); diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 3f79c7e441b767..e3b97fe4971209 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -124,6 +124,8 @@ const customTypesMap = { 'RsaPssParams': 'webcrypto.html#class-rsapssparams', 'ContextParams': 'webcrypto.html#class-contextparams', 'CShakeParams': 'webcrypto.html#class-cshakeparams', + 'TurboShakeParams': 'webcrypto.html#class-turboshakeparams', + 'KangarooTwelveParams': 'webcrypto.html#class-kangarootwelveparams', 'KmacImportParams': 'webcrypto.html#class-kmacimportparams', 'KmacKeyAlgorithm': 'webcrypto.html#class-kmackeyalgorithm', 'KmacKeyGenParams': 'webcrypto.html#class-kmackeygenparams',