From 52ab64ec3a964b1b12d4f77864ab8e1b120904a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 23:40:13 +0000 Subject: [PATCH 001/111] tools: bump @eslint/plugin-kit from 0.3.3 to 0.3.4 in /tools/eslint Bumps [@eslint/plugin-kit](https://github.com/eslint/rewrite/tree/HEAD/packages/plugin-kit) from 0.3.3 to 0.3.4. - [Release notes](https://github.com/eslint/rewrite/releases) - [Changelog](https://github.com/eslint/rewrite/blob/main/packages/plugin-kit/CHANGELOG.md) - [Commits](https://github.com/eslint/rewrite/commits/plugin-kit-v0.3.4/packages/plugin-kit) --- updated-dependencies: - dependency-name: "@eslint/plugin-kit" dependency-version: 0.3.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] PR-URL: https://github.com/nodejs/node/pull/59271 Reviewed-By: Rafael Gonzaga --- tools/eslint/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/eslint/package-lock.json b/tools/eslint/package-lock.json index 5e090c1887846f..2e33138a2109a8 100644 --- a/tools/eslint/package-lock.json +++ b/tools/eslint/package-lock.json @@ -481,9 +481,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.15.1", From 9a12f71ad933ee95e33ad87bd0d20c34a30bee85 Mon Sep 17 00:00:00 2001 From: Krishnadas Date: Wed, 6 Aug 2025 12:52:13 +0530 Subject: [PATCH 002/111] lib: simplify IPv6 checks in isLoopback() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checks for '[::1]' and '[0:0:0:0:0:0:0:1]' in isLoopback were using startsWith, which is unnecessary as these are canonical loopback addresses with no valid prefixes. Switching to strict equality improves clarity and improves performance. PR-URL: https://github.com/nodejs/node/pull/59375 Reviewed-By: Tim Perry Reviewed-By: theanarkh Reviewed-By: Ethan Arrowood Reviewed-By: Luigi Pinca Reviewed-By: Ulises Gascón Reviewed-By: Matteo Collina Reviewed-By: Stefan Stojanovic --- lib/internal/net.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/net.js b/lib/internal/net.js index 4e6accb57a4297..d380d8a41982e2 100644 --- a/lib/internal/net.js +++ b/lib/internal/net.js @@ -93,8 +93,8 @@ function isLoopback(host) { return ( hostLower === 'localhost' || hostLower.startsWith('127.') || - hostLower.startsWith('[::1]') || - hostLower.startsWith('[0:0:0:0:0:0:0:1]') + hostLower === '[::1]' || + hostLower === '[0:0:0:0:0:0:0:1]' ); } From b35041c7dccc44792cb086cadc769145ea9afc94 Mon Sep 17 00:00:00 2001 From: Shima Ryuhei <65934663+islandryu@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:14:18 +0900 Subject: [PATCH 003/111] inspector: prevent propagation of promise hooks to noPromise hooks PR-URL: https://github.com/nodejs/node/pull/58841 Reviewed-By: Chengzhong Wu --- lib/internal/async_hooks.js | 28 +++++++++++------ .../test-inspector-debug-async-hook.js | 31 +++++++++++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 test/parallel/test-inspector-debug-async-hook.js diff --git a/lib/internal/async_hooks.js b/lib/internal/async_hooks.js index f4a45c99858c55..5f28ea6a0e8193 100644 --- a/lib/internal/async_hooks.js +++ b/lib/internal/async_hooks.js @@ -189,7 +189,7 @@ function lookupPublicResource(resource) { // Used by C++ to call all init() callbacks. Because some state can be setup // from C++ there's no need to perform all the same operations as in // emitInitScript. -function emitInitNative(asyncId, type, triggerAsyncId, resource) { +function emitInitNative(asyncId, type, triggerAsyncId, resource, isPromiseHook) { active_hooks.call_depth += 1; resource = lookupPublicResource(resource); // Use a single try/catch for all hooks to avoid setting up one per iteration. @@ -199,6 +199,10 @@ function emitInitNative(asyncId, type, triggerAsyncId, resource) { // eslint-disable-next-line no-var for (var i = 0; i < active_hooks.array.length; i++) { if (typeof active_hooks.array[i][init_symbol] === 'function') { + if (isPromiseHook && + active_hooks.array[i][kNoPromiseHook]) { + continue; + } active_hooks.array[i][init_symbol]( asyncId, type, triggerAsyncId, resource, @@ -222,7 +226,7 @@ function emitInitNative(asyncId, type, triggerAsyncId, resource) { // Called from native. The asyncId stack handling is taken care of there // before this is called. -function emitHook(symbol, asyncId) { +function emitHook(symbol, asyncId, isPromiseHook) { active_hooks.call_depth += 1; // Use a single try/catch for all hook to avoid setting up one per // iteration. @@ -232,6 +236,10 @@ function emitHook(symbol, asyncId) { // eslint-disable-next-line no-var for (var i = 0; i < active_hooks.array.length; i++) { if (typeof active_hooks.array[i][symbol] === 'function') { + if (isPromiseHook && + active_hooks.array[i][kNoPromiseHook]) { + continue; + } active_hooks.array[i][symbol](asyncId); } } @@ -321,7 +329,7 @@ function promiseInitHook(promise, parent) { trackPromise(promise, parent); const asyncId = promise[async_id_symbol]; const triggerAsyncId = promise[trigger_async_id_symbol]; - emitInitScript(asyncId, 'PROMISE', triggerAsyncId, promise); + emitInitScript(asyncId, 'PROMISE', triggerAsyncId, promise, true); } function promiseInitHookWithDestroyTracking(promise, parent) { @@ -339,14 +347,14 @@ function promiseBeforeHook(promise) { trackPromise(promise); const asyncId = promise[async_id_symbol]; const triggerId = promise[trigger_async_id_symbol]; - emitBeforeScript(asyncId, triggerId, promise); + emitBeforeScript(asyncId, triggerId, promise, true); } function promiseAfterHook(promise) { trackPromise(promise); const asyncId = promise[async_id_symbol]; if (hasHooks(kAfter)) { - emitAfterNative(asyncId); + emitAfterNative(asyncId, true); } if (asyncId === executionAsyncId()) { // This condition might not be true if async_hooks was enabled during @@ -361,7 +369,7 @@ function promiseAfterHook(promise) { function promiseResolveHook(promise) { trackPromise(promise); const asyncId = promise[async_id_symbol]; - emitPromiseResolveNative(asyncId); + emitPromiseResolveNative(asyncId, true); } let wantPromiseHook = false; @@ -492,7 +500,7 @@ function promiseResolveHooksExist() { } -function emitInitScript(asyncId, type, triggerAsyncId, resource) { +function emitInitScript(asyncId, type, triggerAsyncId, resource, isPromiseHook = false) { // Short circuit all checks for the common case. Which is that no hooks have // been set. Do this to remove performance impact for embedders (and core). if (!hasHooks(kInit)) @@ -502,15 +510,15 @@ function emitInitScript(asyncId, type, triggerAsyncId, resource) { triggerAsyncId = getDefaultTriggerAsyncId(); } - emitInitNative(asyncId, type, triggerAsyncId, resource); + emitInitNative(asyncId, type, triggerAsyncId, resource, isPromiseHook); } -function emitBeforeScript(asyncId, triggerAsyncId, resource) { +function emitBeforeScript(asyncId, triggerAsyncId, resource, isPromiseHook = false) { pushAsyncContext(asyncId, triggerAsyncId, resource); if (hasHooks(kBefore)) - emitBeforeNative(asyncId); + emitBeforeNative(asyncId, isPromiseHook); } diff --git a/test/parallel/test-inspector-debug-async-hook.js b/test/parallel/test-inspector-debug-async-hook.js new file mode 100644 index 00000000000000..e8f13b40c7e43a --- /dev/null +++ b/test/parallel/test-inspector-debug-async-hook.js @@ -0,0 +1,31 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +const test = require('node:test'); +const { NodeInstance } = require('../common/inspector-helper'); + +const script = ` +import { createHook } from "async_hooks" +import fs from "fs" + +const hook = createHook({ + after() { + } +}); +hook.enable(true); +console.log('Async hook enabled'); +`; + +test('inspector async hooks should not crash in debug build', async () => { + const instance = new NodeInstance([ + '--inspect-brk=0', + ], script); + const session = await instance.connectInspectorSession(); + await session.send({ method: 'NodeRuntime.enable' }); + await session.waitForNotification('NodeRuntime.waitingForDebugger'); + await session.send({ method: 'Runtime.enable' }); + await session.send({ method: 'Debugger.enable' }); + await session.send({ id: 6, method: 'Debugger.setAsyncCallStackDepth', params: { maxDepth: 32 } }); + await session.send({ method: 'Runtime.runIfWaitingForDebugger' }); + await session.waitForDisconnect(); +}); From f54ace694a257921d8a625982816004dc9dfdc10 Mon Sep 17 00:00:00 2001 From: theanarkh Date: Tue, 12 Aug 2025 18:25:12 +0800 Subject: [PATCH 004/111] worker: add worker name to report PR-URL: https://github.com/nodejs/node/pull/58935 Reviewed-By: Chengzhong Wu Reviewed-By: Luigi Pinca Reviewed-By: Anna Henningsen Reviewed-By: James M Snell Reviewed-By: Stefan Stojanovic --- src/node_report.cc | 8 ++++---- src/node_worker.h | 1 + test/report/test-report-worker.js | 6 ++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/node_report.cc b/src/node_report.cc index 2dcdfa8b139cf7..1b5fd64726add2 100644 --- a/src/node_report.cc +++ b/src/node_report.cc @@ -233,11 +233,11 @@ static void WriteNodeReport(Isolate* isolate, size_t expected_results = 0; env->ForEachWorker([&](Worker* w) { - expected_results += w->RequestInterrupt([&](Environment* env) { + expected_results += w->RequestInterrupt([&, w = w](Environment* env) { std::ostringstream os; - - GetNodeReport( - env, "Worker thread subreport", trigger, Local(), os); + std::string name = + "Worker thread subreport [" + std::string(w->name()) + "]"; + GetNodeReport(env, name.c_str(), trigger, Local(), os); Mutex::ScopedLock lock(workers_mutex); worker_infos.emplace_back(os.str()); diff --git a/src/node_worker.h b/src/node_worker.h index 9e80a764a8bd71..54e76e5f98b26b 100644 --- a/src/node_worker.h +++ b/src/node_worker.h @@ -62,6 +62,7 @@ class Worker : public AsyncWrap { bool is_stopped() const; const SnapshotData* snapshot_data() const { return snapshot_data_; } bool is_internal() const { return is_internal_; } + std::string_view name() const { return name_; } static void New(const v8::FunctionCallbackInfo& args); static void CloneParentEnvVars( diff --git a/test/report/test-report-worker.js b/test/report/test-report-worker.js index 26b28219c84088..f17e3986e97813 100644 --- a/test/report/test-report-worker.js +++ b/test/report/test-report-worker.js @@ -13,7 +13,7 @@ async function basic() { parentPort.once('message', () => { /* Wait for message to stop the Worker */ }); - `, { eval: true }); + `, { eval: true, name: 'hello' }); await once(w, 'online'); @@ -22,7 +22,9 @@ async function basic() { assert.strictEqual(report.workers.length, 1); helper.validateContent(report.workers[0]); assert.strictEqual(report.workers[0].header.threadId, w.threadId); - + assert.strictEqual(report.workers[0].header.event, + 'Worker thread subreport [hello]', + report.workers[0].header.event); w.postMessage({}); await once(w, 'exit'); From 823dce32ecca28a69fd31c0ea7a2394a21c25c1c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 12 Aug 2025 23:15:25 +0200 Subject: [PATCH 005/111] src: update OpenSSL pqc checks PR-URL: https://github.com/nodejs/node/pull/59436 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli Reviewed-By: Luigi Pinca Reviewed-By: Richard Lau --- deps/ncrypto/ncrypto.cc | 8 ++++---- deps/ncrypto/ncrypto.h | 13 ++++++++----- src/crypto/crypto_keys.cc | 14 +++++++------- src/crypto/crypto_keys.h | 2 +- src/crypto/crypto_ml_dsa.cc | 2 +- src/crypto/crypto_ml_dsa.h | 2 +- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index a2bfe874650fd5..741c3510e199ac 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -1897,7 +1897,7 @@ EVPKeyPointer EVPKeyPointer::NewRawPrivate( EVP_PKEY_new_raw_private_key(id, nullptr, data.data, data.len)); } -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC EVPKeyPointer EVPKeyPointer::NewRawSeed( int id, const Buffer& data) { if (id == 0) return {}; @@ -1968,7 +1968,7 @@ EVP_PKEY* EVPKeyPointer::release() { int EVPKeyPointer::id(const EVP_PKEY* key) { if (key == nullptr) return 0; int type = EVP_PKEY_id(key); -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC // https://github.com/openssl/openssl/issues/27738#issuecomment-3013215870 if (type == -1) { if (EVP_PKEY_is_a(key, "ML-DSA-44")) return EVP_PKEY_ML_DSA_44; @@ -2032,7 +2032,7 @@ DataPointer EVPKeyPointer::rawPublicKey() const { return {}; } -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC DataPointer EVPKeyPointer::rawSeed() const { if (!pkey_) return {}; switch (id()) { @@ -2515,7 +2515,7 @@ bool EVPKeyPointer::isOneShotVariant() const { switch (type) { case EVP_PKEY_ED25519: case EVP_PKEY_ED448: -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 82af70798f3171..f8f634111759fe 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -28,11 +28,14 @@ #include #endif // OPENSSL_FIPS -#if OPENSSL_VERSION_MAJOR >= 3 -#define OSSL3_CONST const -#if OPENSSL_VERSION_MINOR >= 5 +// Define OPENSSL_WITH_PQC for post-quantum cryptography support +#if OPENSSL_VERSION_NUMBER >= 0x30500000L +#define OPENSSL_WITH_PQC 1 #include #endif + +#if OPENSSL_VERSION_MAJOR >= 3 +#define OSSL3_CONST const #else #define OSSL3_CONST #endif @@ -820,7 +823,7 @@ class EVPKeyPointer final { const Buffer& data); static EVPKeyPointer NewRawPrivate(int id, const Buffer& data); -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC static EVPKeyPointer NewRawSeed(int id, const Buffer& data); #endif @@ -917,7 +920,7 @@ class EVPKeyPointer final { DataPointer rawPrivateKey() const; BIOPointer derPublicKey() const; -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC DataPointer rawSeed() const; #endif diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 167018d7d208c6..91044a94dc6259 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -177,7 +177,7 @@ bool ExportJWKAsymmetricKey(Environment* env, // Fall through case EVP_PKEY_X448: return ExportJWKEdKey(env, key, target); -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC case EVP_PKEY_ML_DSA_44: // Fall through case EVP_PKEY_ML_DSA_65: @@ -280,7 +280,7 @@ int GetNidFromName(const char* name) { nid = EVP_PKEY_X25519; } else if (strcmp(name, "X448") == 0) { nid = EVP_PKEY_X448; -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC } else if (strcmp(name, "ML-DSA-44") == 0) { nid = EVP_PKEY_ML_DSA_44; } else if (strcmp(name, "ML-DSA-65") == 0) { @@ -620,7 +620,7 @@ Local KeyObjectHandle::Initialize(Environment* env) { SetProtoMethod(isolate, templ, "exportJwk", ExportJWK); SetProtoMethod(isolate, templ, "initECRaw", InitECRaw); SetProtoMethod(isolate, templ, "initEDRaw", InitEDRaw); -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC SetProtoMethod(isolate, templ, "initMlDsaRaw", InitMlDsaRaw); #endif SetProtoMethod(isolate, templ, "initJwk", InitJWK); @@ -643,7 +643,7 @@ void KeyObjectHandle::RegisterExternalReferences( registry->Register(ExportJWK); registry->Register(InitECRaw); registry->Register(InitEDRaw); -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC registry->Register(InitMlDsaRaw); #endif registry->Register(InitJWK); @@ -838,7 +838,7 @@ void KeyObjectHandle::InitEDRaw(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(true); } -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC void KeyObjectHandle::InitMlDsaRaw(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); KeyObjectHandle* key; @@ -971,7 +971,7 @@ Local KeyObjectHandle::GetAsymmetricKeyType() const { return env()->crypto_x25519_string(); case EVP_PKEY_X448: return env()->crypto_x448_string(); -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC case EVP_PKEY_ML_DSA_44: return env()->crypto_ml_dsa_44_string(); case EVP_PKEY_ML_DSA_65: @@ -1254,7 +1254,7 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatJWK); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED25519); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED448); -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_44); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_65); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_87); diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index 2a2a38ebe0e3cb..f18e3b023c0d34 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -152,7 +152,7 @@ class KeyObjectHandle : public BaseObject { static void Init(const v8::FunctionCallbackInfo& args); static void InitECRaw(const v8::FunctionCallbackInfo& args); static void InitEDRaw(const v8::FunctionCallbackInfo& args); -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC static void InitMlDsaRaw(const v8::FunctionCallbackInfo& args); #endif static void InitJWK(const v8::FunctionCallbackInfo& args); diff --git a/src/crypto/crypto_ml_dsa.cc b/src/crypto/crypto_ml_dsa.cc index 119597c634cc11..65f7053cc1fa1d 100644 --- a/src/crypto/crypto_ml_dsa.cc +++ b/src/crypto/crypto_ml_dsa.cc @@ -14,7 +14,7 @@ using v8::Value; namespace crypto { -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC constexpr const char* GetMlDsaAlgorithmName(int id) { switch (id) { case EVP_PKEY_ML_DSA_44: diff --git a/src/crypto/crypto_ml_dsa.h b/src/crypto/crypto_ml_dsa.h index 6ecdedaee12629..e4739fcdd7fda7 100644 --- a/src/crypto/crypto_ml_dsa.h +++ b/src/crypto/crypto_ml_dsa.h @@ -9,7 +9,7 @@ namespace node { namespace crypto { -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC bool ExportJwkMlDsaKey(Environment* env, const KeyObjectData& key, v8::Local target); From 64ffde608f5d47603ad1767e82b3e3cfb92b3a7e Mon Sep 17 00:00:00 2001 From: tjuhaszrh Date: Wed, 13 Aug 2025 03:52:07 +0200 Subject: [PATCH 006/111] src: add Intel CET properties to large_pages.S Add note indicating support of Intel CET for large_pages.S file based on annocheck guide: https://sourceware.org/annobin/annobin.html/Test-cf-protection.html PR-URL: https://github.com/nodejs/node/pull/59363 Refs: https://github.com/nodejs/node/issues/59084 Reviewed-By: Anna Henningsen Reviewed-By: Richard Lau --- src/large_pages/node_text_start.S | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/large_pages/node_text_start.S b/src/large_pages/node_text_start.S index d27dd39cc236f0..2a643457b57254 100644 --- a/src/large_pages/node_text_start.S +++ b/src/large_pages/node_text_start.S @@ -1,6 +1,27 @@ #if defined(__ELF__) .section .note.GNU-stack,"",%progbits #endif +// Add .note.gnu.property note for x86_64 to enable Intel CET +// Based on: https://sourceware.org/annobin/annobin.html/Test-cf-protection.html +// Refs: https://github.com/nodejs/node/issues/59084 +#if defined(__x86_64__) || defined(_M_X64) +.section .note.gnu.property,"a" +.align 8 +.long 1f - 0f +.long 4f - 1f +.long 5 +0: +.string "GNU" +1: +.align 8 +.long 0xc0000002 +.long 3f - 2f +2: +.long 0x3 +3: +.align 8 +4: +#endif .text .align 0x2000 .global __node_text_start From 6ae202fcdfd9d4baaa274700ab56dafa058756c9 Mon Sep 17 00:00:00 2001 From: Haram Jeong <91401364+haramj@users.noreply.github.com> Date: Wed, 13 Aug 2025 11:32:30 +0900 Subject: [PATCH 007/111] http: add Agent.agentKeepAliveTimeoutBuffer option PR-URL: https://github.com/nodejs/node/pull/59315 Reviewed-By: Jason Zhang --- doc/api/http.md | 10 +++++ lib/_http_agent.js | 13 ++++-- ...st-http-agent-keep-alive-timeout-buffer.js | 44 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-http-agent-keep-alive-timeout-buffer.js diff --git a/doc/api/http.md b/doc/api/http.md index 64bd5a519a75f1..57820902429096 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -116,6 +116,10 @@ http.get({ * Type: {string} The default value of the `ciphers` option of From e7809d6ddb32804937968115aa1ffa69ae937bd2 Mon Sep 17 00:00:00 2001 From: BCD1me <152237136+Amemome@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:08:01 +0900 Subject: [PATCH 042/111] test: make test-debug-process locale-independent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test `test/parallel/test-debug-process.js` fails on non-English Windows systems due to a locale-dependent error message string. The test asserts that a call to `process._debugProcess()` on a terminated process throws an error with the message `'The system cannot find the file specified.'`. While this holds true on an English Windows system, the test fails on systems with a different display language. The underlying `WinapiErrnoException` function correctly retrieves the localized error message from the OS. For example, on a Korean system, the message is "지정된 파일을 찾을 수 없습니다.". This mismatch causes an `AssertionError`. This behavior can be verified directly in PowerShell: # On Windows with English (US) display language PS> (New-Object System.ComponentModel.Win32Exception 2).Message The system cannot find the file specified. # On Windows with Korean display language PS> (New-Object System.ComponentModel.Win32Exception 2).Message 지정된 파일을 찾을 수 없습니다. To make the test robust and environment-agnostic, this commit changes the assertion to check the language-independent `error.errno` property, which is consistently `2` for this type of error, instead of the localized `error.message`. PR-URL: https://github.com/nodejs/node/pull/59254 Reviewed-By: Luigi Pinca Reviewed-By: Joyee Cheung Reviewed-By: Stefan Stojanovic --- test/parallel/test-debug-process.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-debug-process.js b/test/parallel/test-debug-process.js index 0d10a15e2eefa0..4eb5ad7b4d5db8 100644 --- a/test/parallel/test-debug-process.js +++ b/test/parallel/test-debug-process.js @@ -16,6 +16,6 @@ cp.on('exit', common.mustCall(function() { try { process._debugProcess(cp.pid); } catch (error) { - assert.strictEqual(error.message, 'The system cannot find the file specified.'); + assert.strictEqual(error.errno, 2); } })); From 2b7a7a525ea6047daa1ebab435a82153d447eebb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 19 Aug 2025 06:40:36 +0200 Subject: [PATCH 043/111] doc,crypto: add supported asymmetric key types section PR-URL: https://github.com/nodejs/node/pull/59492 Reviewed-By: Luigi Pinca Reviewed-By: Yagiz Nizipli --- doc/api/crypto.md | 59 +++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 51e3282fce6afb..a95a2a4173f32f 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -73,6 +73,28 @@ try { } ``` +## Asymmetric key types + +The following table lists the asymmetric key types recognized by the [`KeyObject`][] API: + +| Key Type | Description | OID | +| --------------------------- | -------------- | ----------------------- | +| `'dh'` | Diffie-Hellman | 1.2.840.113549.1.3.1 | +| `'dsa'` | DSA | 1.2.840.10040.4.1 | +| `'ec'` | Elliptic curve | 1.2.840.10045.2.1 | +| `'ed25519'` | Ed25519 | 1.3.101.112 | +| `'ed448'` | Ed448 | 1.3.101.113 | +| `'ml-dsa-44'`[^openssl35] | ML-DSA-44 | 2.16.840.1.101.3.4.3.17 | +| `'ml-dsa-65'`[^openssl35] | ML-DSA-65 | 2.16.840.1.101.3.4.3.18 | +| `'ml-dsa-87'`[^openssl35] | ML-DSA-87 | 2.16.840.1.101.3.4.3.19 | +| `'ml-kem-1024'`[^openssl35] | ML-KEM-1024 | 2.16.840.1.101.3.4.4.3 | +| `'ml-kem-512'`[^openssl35] | ML-KEM-512 | 2.16.840.1.101.3.4.4.1 | +| `'ml-kem-768'`[^openssl35] | ML-KEM-768 | 2.16.840.1.101.3.4.4.2 | +| `'rsa-pss'` | RSA PSS | 1.2.840.113549.1.1.10 | +| `'rsa'` | RSA | 1.2.840.113549.1.1.1 | +| `'x25519'` | X25519 | 1.3.101.110 | +| `'x448'` | X448 | 1.3.101.111 | + ## Class: `Certificate` -* `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, - `'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35], - `'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35]. +* `type` {string} The asymmetric key type to generate. See the + supported [asymmetric key types][]. * `options` {Object} * `modulusLength` {number} Key size in bits (RSA, DSA). * `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`. @@ -3825,9 +3830,8 @@ changes: produce key objects if no encoding was specified. --> -* `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, - `'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35], - `'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35]. +* `type` {string} The asymmetric key type to generate. See the + supported [asymmetric key types][]. * `options` {Object} * `modulusLength` {number} Key size in bits (RSA, DSA). * `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`. @@ -6280,6 +6284,7 @@ See the [list of SSL OP Flags][] for details. [`verify.verify()`]: #verifyverifyobject-signature-signatureencoding [`x509.fingerprint256`]: #x509fingerprint256 [`x509.verify(publicKey)`]: #x509verifypublickey +[asymmetric key types]: #asymmetric-key-types [caveats when using strings as inputs to cryptographic APIs]: #using-strings-as-inputs-to-cryptographic-apis [certificate object]: tls.md#certificate-object [encoding]: buffer.md#buffers-and-character-encodings From e076f7857c6f1738acc7d91ed32a0635de4c8d40 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Tue, 19 Aug 2025 10:42:00 +0300 Subject: [PATCH 044/111] test_runner: add option to rerun only failed tests PR-URL: https://github.com/nodejs/node/pull/59443 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Pietro Marchini Reviewed-By: Chemi Atlow --- doc/api/cli.md | 16 +++ doc/api/test.md | 54 ++++++++ doc/node-config-schema.json | 6 + doc/node.1 | 4 + lib/internal/test_runner/harness.js | 26 ++++ lib/internal/test_runner/reporter/rerun.js | 40 ++++++ lib/internal/test_runner/runner.js | 11 ++ lib/internal/test_runner/test.js | 31 +++++ lib/internal/test_runner/utils.js | 25 +++- src/node_options.cc | 5 + src/node_options.h | 1 + test/fixtures/test-runner/rerun.js | 25 ++++ .../test-runner-test-rerun-failures.js | 117 ++++++++++++++++++ 13 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 lib/internal/test_runner/reporter/rerun.js create mode 100644 test/fixtures/test-runner/rerun.js create mode 100644 test/parallel/test-runner-test-rerun-failures.js diff --git a/doc/api/cli.md b/doc/api/cli.md index ba1356650c2383..b9b04c34cb24a2 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -2601,6 +2601,20 @@ changes: The destination for the corresponding test reporter. See the documentation on [test reporters][] for more details. +### `--test-rerun-failures` + + + +A path to a file allowing the test runner to persist the state of the test +suite between runs. The test runner will use this file to determine which tests +have already succeeded or failed, allowing for re-running of failed tests +without having to re-run the entire test suite. The test runner will create this +file if it does not exist. +See the documentation on [test reruns][] for more details. + ### `--test-shard` + +* `params` {Object} + +This feature is only available with the `--experimental-network-inspection` flag enabled. + +Broadcasts the `Network.webSocketCreated` event to connected frontends. This event indicates that +a WebSocket connection has been initiated. + +### `inspector.Network.webSocketHandshakeResponseReceived([params])` + + + +* `params` {Object} + +This feature is only available with the `--experimental-network-inspection` flag enabled. + +Broadcasts the `Network.webSocketHandshakeResponseReceived` event to connected frontends. +This event indicates that the WebSocket handshake response has been received. + +### `inspector.Network.webSocketClosed([params])` + + + +* `params` {Object} + +This feature is only available with the `--experimental-network-inspection` flag enabled. + +Broadcasts the `Network.webSocketClosed` event to connected frontends. +This event indicates that a WebSocket connection has been closed. + ### `inspector.NetworkResources.put` -* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. +* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, `'jwk'`, `'raw-secret'`[^modern-algos], + `'raw-public'`[^modern-algos], or `'raw-seed'`[^modern-algos]. * `key` {CryptoKey} * Returns: {Promise} Fulfills with an {ArrayBuffer|Object} upon success. @@ -738,25 +784,32 @@ When `format` is `'jwk'` and the export is successful, the returned promise will be resolved with a JavaScript object conforming to the [JSON Web Key][] specification. -| Supported Key Algorithm | `'spki'` | `'pkcs8'` | `'jwk'` | `'raw'` | -| ------------------------------------------------------- | -------- | --------- | ------- | ------- | -| `'AES-CBC'` | | | ✔ | ✔ | -| `'AES-CTR'` | | | ✔ | ✔ | -| `'AES-GCM'` | | | ✔ | ✔ | -| `'AES-KW'` | | | ✔ | ✔ | -| `'ECDH'` | ✔ | ✔ | ✔ | ✔ | -| `'ECDSA'` | ✔ | ✔ | ✔ | ✔ | -| `'Ed25519'` | ✔ | ✔ | ✔ | ✔ | -| `'Ed448'` [^1] | ✔ | ✔ | ✔ | ✔ | -| `'HMAC'` | | | ✔ | ✔ | -| `'RSA-OAEP'` | ✔ | ✔ | ✔ | | -| `'RSA-PSS'` | ✔ | ✔ | ✔ | | -| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | +| Supported Key Algorithm | `'spki'` | `'pkcs8'` | `'jwk'` | `'raw'` | `'raw-secret'` | `'raw-public'` | `'raw-seed'` | +| ---------------------------- | -------- | --------- | ------- | ------- | -------------- | -------------- | ------------ | +| `'AES-CBC'` | | | ✔ | ✔ | ✔ | | | +| `'AES-CTR'` | | | ✔ | ✔ | ✔ | | | +| `'AES-GCM'` | | | ✔ | ✔ | ✔ | | | +| `'AES-KW'` | | | ✔ | ✔ | ✔ | | | +| `'ECDH'` | ✔ | ✔ | ✔ | ✔ | | ✔ | | +| `'ECDSA'` | ✔ | ✔ | ✔ | ✔ | | ✔ | | +| `'Ed25519'` | ✔ | ✔ | ✔ | ✔ | | ✔ | | +| `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | ✔ | | ✔ | | +| `'HMAC'` | | | ✔ | ✔ | ✔ | | | +| `'ML-DSA-44'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | +| `'ML-DSA-65'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | +| `'ML-DSA-87'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | +| `'RSA-OAEP'` | ✔ | ✔ | ✔ | | | | | +| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | +| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | ### `subtle.generateKey(algorithm, extractable, keyUsages)` @@ -776,29 +829,35 @@ may generate either a single {CryptoKey} or a {CryptoKeyPair}. The {CryptoKeyPair} (public and private key) generating algorithms supported include: -* `'RSASSA-PKCS1-v1_5'` -* `'RSA-PSS'` -* `'RSA-OAEP'` +* `'ECDH'` * `'ECDSA'` * `'Ed25519'` -* `'Ed448'` [^1] -* `'ECDH'` +* `'Ed448'`[^secure-curves] +* `'ML-DSA-44'`[^modern-algos] +* `'ML-DSA-65'`[^modern-algos] +* `'ML-DSA-87'`[^modern-algos] +* `'RSA-OAEP'` +* `'RSA-PSS'` +* `'RSASSA-PKCS1-v1_5'` * `'X25519'` -* `'X448'` [^1] +* `'X448'`[^secure-curves] The {CryptoKey} (secret key) generating algorithms supported include: -* `'HMAC'` -* `'AES-CTR'` * `'AES-CBC'` +* `'AES-CTR'` * `'AES-GCM'` * `'AES-KW'` +* `'HMAC'` ### `subtle.importKey(format, keyData, algorithm, extractable, keyUsages)` -* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. +* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, `'jwk'`, `'raw-secret'`[^modern-algos], + `'raw-public'`[^modern-algos], or `'raw-seed'`[^modern-algos]. * `keyData` {ArrayBuffer|TypedArray|DataView|Buffer|Object} @@ -832,30 +892,36 @@ If importing a `'PBKDF2'` key, `extractable` must be `false`. The algorithms currently supported include: -| Supported Key Algorithm | `'spki'` | `'pkcs8'` | `'jwk'` | `'raw'` | -| ------------------------------------------------------- | -------- | --------- | ------- | ------- | -| `'AES-CBC'` | | | ✔ | ✔ | -| `'AES-CTR'` | | | ✔ | ✔ | -| `'AES-GCM'` | | | ✔ | ✔ | -| `'AES-KW'` | | | ✔ | ✔ | -| `'ECDH'` | ✔ | ✔ | ✔ | ✔ | -| `'X25519'` | ✔ | ✔ | ✔ | ✔ | -| `'X448'` [^1] | ✔ | ✔ | ✔ | ✔ | -| `'ECDSA'` | ✔ | ✔ | ✔ | ✔ | -| `'Ed25519'` | ✔ | ✔ | ✔ | ✔ | -| `'Ed448'` [^1] | ✔ | ✔ | ✔ | ✔ | -| `'HDKF'` | | | | ✔ | -| `'HMAC'` | | | ✔ | ✔ | -| `'PBKDF2'` | | | | ✔ | -| `'RSA-OAEP'` | ✔ | ✔ | ✔ | | -| `'RSA-PSS'` | ✔ | ✔ | ✔ | | -| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | +| Supported Key Algorithm | `'spki'` | `'pkcs8'` | `'jwk'` | `'raw'` | `'raw-secret'` | `'raw-public'` | `'raw-seed'` | +| ---------------------------- | -------- | --------- | ------- | ------- | -------------- | -------------- | ------------ | +| `'AES-CBC'` | | | ✔ | ✔ | ✔ | | | +| `'AES-CTR'` | | | ✔ | ✔ | ✔ | | | +| `'AES-GCM'` | | | ✔ | ✔ | ✔ | | | +| `'AES-KW'` | | | ✔ | ✔ | ✔ | | | +| `'ECDH'` | ✔ | ✔ | ✔ | ✔ | | ✔ | | +| `'ECDSA'` | ✔ | ✔ | ✔ | ✔ | | ✔ | | +| `'Ed25519'` | ✔ | ✔ | ✔ | ✔ | | ✔ | | +| `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | ✔ | | ✔ | | +| `'HDKF'` | | | | ✔ | ✔ | | | +| `'HMAC'` | | | ✔ | ✔ | ✔ | | | +| `'ML-DSA-44'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | +| `'ML-DSA-65'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | +| `'ML-DSA-87'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | +| `'PBKDF2'` | | | | ✔ | ✔ | | | +| `'RSA-OAEP'` | ✔ | ✔ | ✔ | | | | | +| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | +| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | +| `'X25519'` | ✔ | ✔ | ✔ | ✔ | | ✔ | | +| `'X448'`[^secure-curves] | ✔ | ✔ | ✔ | ✔ | | ✔ | | ### `subtle.sign(algorithm, key, data)` -* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params} +* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params|ContextParams} * `key` {CryptoKey} * `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. @@ -879,12 +945,15 @@ an {ArrayBuffer} containing the generated signature. The algorithms currently supported include: -* `'RSASSA-PKCS1-v1_5'` -* `'RSA-PSS'` * `'ECDSA'` * `'Ed25519'` -* `'Ed448'` [^1] +* `'Ed448'`[^secure-curves] * `'HMAC'` +* `'ML-DSA-44'`[^modern-algos] +* `'ML-DSA-65'`[^modern-algos] +* `'ML-DSA-87'`[^modern-algos] +* `'RSA-PSS'` +* `'RSASSA-PKCS1-v1_5'` ### `subtle.unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgo, unwrappedKeyAlgo, extractable, keyUsages)` @@ -892,7 +961,8 @@ The algorithms currently supported include: added: v15.0.0 --> -* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. +* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, `'jwk'`, `'raw-secret'`[^modern-algos], + `'raw-public'`[^modern-algos], or `'raw-seed'`[^modern-algos]. * `wrappedKey` {ArrayBuffer|TypedArray|DataView|Buffer} * `unwrappingKey` {CryptoKey} @@ -918,34 +988,40 @@ promise is resolved with a {CryptoKey} object. The wrapping algorithms currently supported include: -* `'RSA-OAEP'` -* `'AES-CTR'` * `'AES-CBC'` +* `'AES-CTR'` * `'AES-GCM'` * `'AES-KW'` +* `'RSA-OAEP'` The unwrapped key algorithms supported include: -* `'RSASSA-PKCS1-v1_5'` -* `'RSA-PSS'` -* `'RSA-OAEP'` -* `'ECDSA'` -* `'Ed25519'` -* `'Ed448'` [^1] -* `'ECDH'` -* `'X25519'` -* `'X448'` [^1] -* `'HMAC'` -* `'AES-CTR'` * `'AES-CBC'` +* `'AES-CTR'` * `'AES-GCM'` * `'AES-KW'` +* `'ECDH'` +* `'ECDSA'` +* `'Ed25519'` +* `'Ed448'`[^secure-curves] +* `'HMAC'` +* `'ML-DSA-44'`[^modern-algos] +* `'ML-DSA-65'`[^modern-algos] +* `'ML-DSA-87'`[^modern-algos] +* `'RSA-OAEP'` +* `'RSA-PSS'` +* `'RSASSA-PKCS1-v1_5'` +* `'X25519'` +* `'X448'`[^secure-curves] ### `subtle.verify(algorithm, key, signature, data)` -* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params} +* `algorithm` {string|Algorithm|RsaPssParams|EcdsaParams|Ed448Params|ContextParams} * `key` {CryptoKey} * `signature` {ArrayBuffer|TypedArray|DataView|Buffer} * `data` {ArrayBuffer|TypedArray|DataView|Buffer} @@ -970,12 +1046,15 @@ with either `true` or `false`. The algorithms currently supported include: -* `'RSASSA-PKCS1-v1_5'` -* `'RSA-PSS'` * `'ECDSA'` * `'Ed25519'` -* `'Ed448'` [^1] +* `'Ed448'`[^secure-curves] * `'HMAC'` +* `'ML-DSA-44'`[^modern-algos] +* `'ML-DSA-65'`[^modern-algos] +* `'ML-DSA-87'`[^modern-algos] +* `'RSA-PSS'` +* `'RSASSA-PKCS1-v1_5'` ### `subtle.wrapKey(format, key, wrappingKey, wrapAlgo)` @@ -985,7 +1064,8 @@ added: v15.0.0 -* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, or `'jwk'`. +* `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, `'jwk'`, `'raw-secret'`[^modern-algos], + `'raw-public'`[^modern-algos], or `'raw-seed'`[^modern-algos]. * `key` {CryptoKey} * `wrappingKey` {CryptoKey} * `wrapAlgo` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} @@ -1005,11 +1085,11 @@ containing the encrypted key data. The wrapping algorithms currently supported include: -* `'RSA-OAEP'` -* `'AES-CTR'` * `'AES-CBC'` +* `'AES-CTR'` * `'AES-GCM'` * `'AES-KW'` +* `'RSA-OAEP'` ## Algorithm parameters @@ -1221,6 +1301,33 @@ added: v15.0.0 * Type: {string} Must be one of `'AES-CBC'`, `'AES-CTR'`, `'AES-GCM'`, or `'AES-KW'` +### Class: `ContextParams` + + + +#### `contextParams.name` + + + +* Type: {string} Must be `'ML-DSA-44'`[^modern-algos], `'ML-DSA-65'`[^modern-algos], or `'ML-DSA-87'`[^modern-algos]. + +#### `contextParams.context` + + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} + +The `context` member represents the optional context data to associate with +the message. +The Node.js Web Crypto API implementation only supports zero-length context +which is equivalent to not providing context at all. + ### Class: `EcdhKeyDeriveParams` -* Type: {string} Must be `'ECDH'`, `'X25519'`, or `'X448'`. +* Type: {string} Must be `'ECDH'`, `'X25519'`, or `'X448'`[^secure-curves]. #### `ecdhKeyDeriveParams.public` @@ -1360,7 +1467,7 @@ added: - v16.17.0 --> -* Type: {string} Must be `'Ed448'`. +* Type: {string} Must be `'Ed448'`[^secure-curves]. #### `ed448Params.context` @@ -1802,12 +1909,16 @@ added: v15.0.0 The length (in bytes) of the random salt to use. -[^1]: An experimental implementation of Ed448 and X448 algorithms from - [Secure Curves in the Web Cryptography API][] as of 21 October 2024 +[^secure-curves]: See [Secure Curves in the Web Cryptography API][] + +[^modern-algos]: See [Modern Algorithms in the Web Cryptography API][] + +[^openssl35]: Requires OpenSSL >= 3.5 [JSON Web Key]: https://tools.ietf.org/html/rfc7517 [Key usages]: #cryptokeyusages +[Modern Algorithms in the Web Cryptography API]: #modern-algorithms-in-the-web-cryptography-api [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf [RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt -[Secure Curves in the Web Cryptography API]: https://wicg.github.io/webcrypto-secure-curves/ +[Secure Curves in the Web Cryptography API]: #secure-curves-in-the-web-cryptography-api [Web Crypto API]: https://www.w3.org/TR/WebCryptoAPI/ diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 60b4d26d35e967..33c84c86a24581 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -293,6 +293,14 @@ const { result = require('internal/crypto/cfrg') .cfrgImportKey('KeyObject', this, algorithm, extractable, keyUsages); break; + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': + result = require('internal/crypto/ml_dsa') + .mlDsaImportKey('KeyObject', this, algorithm, extractable, keyUsages); + break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js new file mode 100644 index 00000000000000..a8f3e7d5a1d3e9 --- /dev/null +++ b/lib/internal/crypto/ml_dsa.js @@ -0,0 +1,252 @@ +'use strict'; + +const { + SafeSet, +} = primordials; + +const { Buffer } = require('buffer'); + +const { + KeyObjectHandle, + SignJob, + kCryptoJobAsync, + kKeyTypePrivate, + kKeyTypePublic, + kSignJobModeSign, + kSignJobModeVerify, +} = internalBinding('crypto'); + +const { + codes: { + ERR_CRYPTO_INVALID_JWK, + }, +} = require('internal/errors'); + +const { + getUsagesUnion, + hasAnyNotIn, + jobPromise, + validateKeyOps, + kHandle, + kKeyObject, +} = require('internal/crypto/util'); + +const { + lazyDOMException, + promisify, +} = require('internal/util'); + +const { + generateKeyPair: _generateKeyPair, +} = require('internal/crypto/keygen'); + +const { + InternalCryptoKey, + PrivateKeyObject, + PublicKeyObject, + createPublicKey, +} = require('internal/crypto/keys'); + +const generateKeyPair = promisify(_generateKeyPair); + +function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) { + const checkSet = isPublic ? ['verify'] : ['sign']; + if (hasAnyNotIn(usages, checkSet)) { + throw lazyDOMException( + `Unsupported key usage for a ${name} key`, + 'SyntaxError'); + } +} + +function createMlDsaRawKey(name, keyData, isPublic) { + const handle = new KeyObjectHandle(); + const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; + if (!handle.initMlDsaRaw(name, keyData, keyType)) { + throw lazyDOMException('Invalid keyData', 'DataError'); + } + + return isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle); +} + +async function mlDsaGenerateKey(algorithm, extractable, keyUsages) { + const { name } = algorithm; + + const usageSet = new SafeSet(keyUsages); + if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { + throw lazyDOMException( + `Unsupported key usage for an ${name} key`, + 'SyntaxError'); + } + + const keyPair = await generateKeyPair(name.toLowerCase()).catch((err) => { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); + }); + + const publicUsages = getUsagesUnion(usageSet, 'verify'); + const privateUsages = getUsagesUnion(usageSet, 'sign'); + + const keyAlgorithm = { name }; + + const publicKey = + new InternalCryptoKey( + keyPair.publicKey, + keyAlgorithm, + publicUsages, + true); + + const privateKey = + new InternalCryptoKey( + keyPair.privateKey, + keyAlgorithm, + privateUsages, + extractable); + + return { __proto__: null, privateKey, publicKey }; +} + +function mlDsaExportKey(key) { + try { + if (key.type === 'private') { + const { priv } = key[kKeyObject][kHandle].exportJwk({}, false); + return Buffer.alloc(32, priv, 'base64url').buffer; + } + + const { pub } = key[kKeyObject][kHandle].exportJwk({}, false); + return Buffer.alloc(Buffer.byteLength(pub, 'base64url'), pub, 'base64url').buffer; + } catch (err) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); + } +} + +function mlDsaImportKey( + format, + keyData, + algorithm, + extractable, + keyUsages) { + + const { name } = algorithm; + let keyObject; + const usagesSet = new SafeSet(keyUsages); + switch (format) { + case 'KeyObject': { + verifyAcceptableMlDsaKeyUse(name, keyData.type === 'public', usagesSet); + keyObject = keyData; + break; + } + case 'jwk': { + if (!keyData.kty) + throw lazyDOMException('Invalid keyData', 'DataError'); + if (keyData.kty !== 'AKP') + throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); + if (keyData.alg !== name) + throw lazyDOMException( + 'JWK "alg" Parameter and algorithm name mismatch', 'DataError'); + const isPublic = keyData.priv === undefined; + + if (usagesSet.size > 0 && keyData.use !== undefined) { + if (keyData.use !== 'sig') + throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); + } + + validateKeyOps(keyData.key_ops, usagesSet); + + if (keyData.ext !== undefined && + keyData.ext === false && + extractable === true) { + throw lazyDOMException( + 'JWK "ext" Parameter and extractable mismatch', + 'DataError'); + } + + if (!isPublic && typeof keyData.pub !== 'string') { + throw lazyDOMException('Invalid JWK', 'DataError'); + } + + verifyAcceptableMlDsaKeyUse( + name, + isPublic, + usagesSet); + + try { + const publicKeyObject = createMlDsaRawKey( + name, + Buffer.from(keyData.pub, 'base64url'), + true); + + if (isPublic) { + keyObject = publicKeyObject; + } else { + keyObject = createMlDsaRawKey( + name, + Buffer.from(keyData.priv, 'base64url'), + false); + + if (!createPublicKey(keyObject).equals(publicKeyObject)) { + throw new ERR_CRYPTO_INVALID_JWK(); + } + } + } catch (err) { + throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err }); + } + break; + } + case 'raw-public': + case 'raw-seed': { + const isPublic = format === 'raw-public'; + verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet); + + try { + keyObject = createMlDsaRawKey(name, keyData, isPublic); + } catch (err) { + throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err }); + } + break; + } + default: + return undefined; + } + + if (keyObject.asymmetricKeyType !== name.toLowerCase()) { + throw lazyDOMException('Invalid key type', 'DataError'); + } + + return new InternalCryptoKey( + keyObject, + { name }, + keyUsages, + extractable); +} + +function mlDsaSignVerify(key, data, algorithm, signature) { + const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; + const type = mode === kSignJobModeSign ? 'private' : 'public'; + + if (key.type !== type) + throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + + return jobPromise(() => new SignJob( + kCryptoJobAsync, + mode, + key[kKeyObject][kHandle], + undefined, + undefined, + undefined, + data, + undefined, + undefined, + undefined, + undefined, + signature)); +} + +module.exports = { + mlDsaExportKey, + mlDsaImportKey, + mlDsaGenerateKey, + mlDsaSignVerify, +}; diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index c7e0c31a4cc609..8052020605684c 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -33,6 +33,9 @@ const { secureHeapUsed: _secureHeapUsed, getCachedAliases, getOpenSSLSecLevelCrypto: getOpenSSLSecLevel, + EVP_PKEY_ML_DSA_44, + EVP_PKEY_ML_DSA_65, + EVP_PKEY_ML_DSA_87, } = internalBinding('crypto'); const { getOptionValue } = require('internal/options'); @@ -284,6 +287,22 @@ const experimentalAlgorithms = ObjectEntries({ }, }); +for (const { 0: algorithm, 1: nid } of [ + ['ML-DSA-44', EVP_PKEY_ML_DSA_44], + ['ML-DSA-65', EVP_PKEY_ML_DSA_65], + ['ML-DSA-87', EVP_PKEY_ML_DSA_87], +]) { + if (nid) { + ArrayPrototypePush(experimentalAlgorithms, [algorithm, { + generateKey: null, + sign: 'ContextParams', + verify: 'ContextParams', + importKey: null, + exportKey: null, + }]); + } +} + for (let i = 0; i < experimentalAlgorithms.length; i++) { const name = experimentalAlgorithms[i][0]; const ops = ObjectEntries(experimentalAlgorithms[i][1]); @@ -314,6 +333,7 @@ const simpleAlgorithmDictionaries = { info: 'BufferSource', }, Ed448Params: { context: 'BufferSource' }, + ContextParams: { context: 'BufferSource' }, Pbkdf2Params: { hash: 'HashAlgorithmIdentifier', salt: 'BufferSource' }, RsaOaepParams: { label: 'BufferSource' }, RsaHashedImportParams: { hash: 'HashAlgorithmIdentifier' }, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 82bdc29f50a4d4..7d23f62d3c88cd 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -152,6 +152,15 @@ async function generateKey( result = await require('internal/crypto/aes') .aesGenerateKey(algorithm, extractable, keyUsages); break; + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': + resultType = 'CryptoKeyPair'; + result = await require('internal/crypto/ml_dsa') + .mlDsaGenerateKey(algorithm, extractable, keyUsages); + break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } @@ -311,7 +320,7 @@ async function deriveKey( return ReflectApply( importKey, this, - ['raw', bits, derivedKeyAlgorithm, extractable, keyUsages], + ['raw-secret', bits, derivedKeyAlgorithm, extractable, keyUsages], ); } @@ -371,7 +380,7 @@ async function exportKeyPkcs8(key) { } } -async function exportKeyRawPublic(key) { +async function exportKeyRawPublic(key, format) { switch (key.algorithm.name) { case 'ECDSA': // Fall through @@ -387,6 +396,33 @@ async function exportKeyRawPublic(key) { case 'X448': return require('internal/crypto/cfrg') .cfrgExportKey(key, kWebCryptoKeyFormatRaw); + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': { + // ML-DSA keys don't recognize "raw" + if (format !== 'raw-public') { + return undefined; + } + return require('internal/crypto/ml_dsa') + .mlDsaExportKey(key, kWebCryptoKeyFormatRaw); + } + default: + return undefined; + } +} + +async function exportKeyRawSeed(key) { + switch (key.algorithm.name) { + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': { + return require('internal/crypto/ml_dsa') + .mlDsaExportKey(key, kWebCryptoKeyFormatRaw); + } default: return undefined; } @@ -437,6 +473,12 @@ async function exportKeyJWK(key) { case 'X25519': // Fall through case 'X448': + // Fall through + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': break; case 'Ed25519': // Fall through @@ -508,15 +550,29 @@ async function exportKey(format, key) { result = await exportKeyJWK(key); break; } - case 'raw': { + case 'raw-secret': { if (key.type === 'secret') { result = await exportKeyRawSecret(key); - break; } - + break; + } + case 'raw-public': { if (key.type === 'public') { - result = await exportKeyRawPublic(key); - break; + result = await exportKeyRawPublic(key, format); + } + break; + } + case 'raw-seed': { + if (key.type === 'private') { + result = await exportKeyRawSeed(key); + } + break; + } + case 'raw': { + if (key.type === 'secret') { + result = await exportKeyRawSecret(key); + } else if (key.type === 'public') { + result = await exportKeyRawPublic(key, format); } break; } @@ -531,6 +587,16 @@ async function exportKey(format, key) { return result; } +function aliasKeyFormat(format) { + switch (format) { + case 'raw-public': + case 'raw-secret': + return 'raw'; + default: + return format; + } +} + async function importKey( format, keyData, @@ -572,12 +638,14 @@ async function importKey( case 'RSA-PSS': // Fall through case 'RSA-OAEP': + format = aliasKeyFormat(format); result = require('internal/crypto/rsa') .rsaImportKey(format, keyData, algorithm, extractable, keyUsages); break; case 'ECDSA': // Fall through case 'ECDH': + format = aliasKeyFormat(format); result = require('internal/crypto/ec') .ecImportKey(format, keyData, algorithm, extractable, keyUsages); break; @@ -588,10 +656,12 @@ async function importKey( case 'X25519': // Fall through case 'X448': + format = aliasKeyFormat(format); result = require('internal/crypto/cfrg') .cfrgImportKey(format, keyData, algorithm, extractable, keyUsages); break; case 'HMAC': + format = aliasKeyFormat(format); result = require('internal/crypto/mac') .hmacImportKey(format, keyData, algorithm, extractable, keyUsages); break; @@ -602,12 +672,14 @@ async function importKey( case 'AES-GCM': // Fall through case 'AES-KW': + format = aliasKeyFormat(format); result = require('internal/crypto/aes') .aesImportKey(algorithm, format, keyData, extractable, keyUsages); break; case 'HKDF': // Fall through case 'PBKDF2': + format = aliasKeyFormat(format); result = importGenericSecretKey( algorithm, format, @@ -615,6 +687,14 @@ async function importKey( extractable, keyUsages); break; + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': + result = require('internal/crypto/ml_dsa') + .mlDsaImportKey(format, keyData, algorithm, extractable, keyUsages); + break; } if (!result) { @@ -796,6 +876,13 @@ function signVerify(algorithm, key, data, signature) { case 'HMAC': return require('internal/crypto/mac') .hmacSignVerify(key, data, algorithm, signature); + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': + return require('internal/crypto/ml_dsa') + .mlDsaSignVerify(key, data, algorithm, signature); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index cba02279977e4b..b6f76f8d90662e 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -332,6 +332,10 @@ converters.AlgorithmIdentifier = (V, opts) => { converters.KeyFormat = createEnumConverter('KeyFormat', [ 'raw', + 'raw-public', + 'raw-seed', + 'raw-secret', + 'raw-private', 'pkcs8', 'spki', 'jwk', @@ -464,6 +468,15 @@ function validateHmacKeyAlgorithm(length) { throw lazyDOMException('Unsupported algorithm.length', 'NotSupportedError'); } +function validateZeroLength(parameterName) { + return (V, dict) => { + if (V.byteLength) { + throw lazyDOMException( + `Non zero-length ${parameterName} is not supported.`, 'NotSupportedError'); + } + }; +} + converters.RsaPssParams = createDictionaryConverter( 'RsaPssParams', [ ...new SafeArrayIterator(dictAlgorithm), @@ -545,6 +558,8 @@ converters.JsonWebKey = createDictionaryConverter( simpleDomStringKey('dp'), simpleDomStringKey('dq'), simpleDomStringKey('qi'), + simpleDomStringKey('pub'), + simpleDomStringKey('priv'), { key: 'oth', converter: converters['sequence'], @@ -694,20 +709,17 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter( }, ]); -converters.Ed448Params = createDictionaryConverter( - 'Ed448Params', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'context', - converter: converters.BufferSource, - validator: (V, dict) => { - if (V.byteLength) - throw lazyDOMException( - 'Non zero-length context is not supported.', 'NotSupportedError'); +for (const name of ['Ed448Params', 'ContextParams']) { + converters[name] = createDictionaryConverter( + name, [ + ...new SafeArrayIterator(dictAlgorithm), + { + key: 'context', + converter: converters.BufferSource, + validator: validateZeroLength(`${name}.context`), }, - required: false, - }, - ]); + ]); +} module.exports = { converters, diff --git a/test/fixtures/crypto/ml-dsa.js b/test/fixtures/crypto/ml-dsa.js new file mode 100644 index 00000000000000..26327ddf1ad1e8 --- /dev/null +++ b/test/fixtures/crypto/ml-dsa.js @@ -0,0 +1,47 @@ +'use strict'; + +const fixtures = require('../../common/fixtures'); + +function getKeyFileName(type, suffix) { + return `${type.replaceAll('-', '_')}_${suffix}.pem`; +} + +module.exports = function() { + const pkcs8 = { + 'ML-DSA-44': fixtures.readKey(getKeyFileName('ml-dsa-44', 'private_seed_only'), 'ascii'), + 'ML-DSA-65': fixtures.readKey(getKeyFileName('ml-dsa-65', 'private_seed_only'), 'ascii'), + 'ML-DSA-87': fixtures.readKey(getKeyFileName('ml-dsa-87', 'private_seed_only'), 'ascii'), + } + + const spki = { + 'ML-DSA-44': fixtures.readKey(getKeyFileName('ml-dsa-44', 'public'), 'ascii'), + 'ML-DSA-65': fixtures.readKey(getKeyFileName('ml-dsa-65', 'public'), 'ascii'), + 'ML-DSA-87': fixtures.readKey(getKeyFileName('ml-dsa-87', 'public'), 'ascii'), + } + + const data = Buffer.from( + '2b7ed0bc7795694ab4acd35903fe8cd7d80f6a1c8688a6c3414409457514a1457855bb' + + 'b219e30a1beea8fe869082d99fc8282f9050d024e59eaf0730ba9db70a', 'hex'); + + // For verification tests. + const signatures = { + // eslint-disable-next-line @stylistic/js/max-len + 'ML-DSA-44': Buffer.from('f4d00cfbf585bc0178c62de0a37a3e3a2593a8fb4ea7147ae35c3020e0084b0a1ec21f6523695ca7e14a267083b95f56e9423b3487fb09be59c84f1ea80c8ec573e8e8d2eaa9a52dd7739295f6614fc705a2dc9b0122f9d32b731b3743d8bb44f47c5a4488cbaabbd44bc3aad0bbad9fa9ca6d29c4c3cf6a87f1c8654b2fd7c955f2721d0da7e39a01a7fa39b1b1a0c603da985a4b02e6951bb7b5ab0943fdb199defbdea65f64b619ab3334337a67b4547efa7e6e132f18b8abbe66875fc3f9fa95d9d4613f55356dfdb2357c7b7556560695aae35dbfa69d2c259ba7966dad1640fa6fefdfdcdee71aaf2a35784b19f8f6118ecb5a8f174bda4589f13dbfd21b5d90a68bca5c31a270ab3aecf9ac549568396396b40b2fba1e5e1500a9f127eb5d08685be93a4298f03351715a917e1cbedd4db605389b69448c06607d7f38a25746f9d1acae6c7827b620ea6e8526fce50e6ec4cdef1a7a154fb52db77cd3b0c4de8e54f43426fc9342aa4b9077dbc62b30a71af3eab48ccd83dd84122bdae87173bd9520c6caba46d8184340cb34da8a97803a21a670714096efc925f6920df3dd8b29aea13788040328192c44245275c562ffec15cbdca5bfd5a3514080353872b8d88bf4ecc58b6db8d16d00d16e13d536eb91df584c8aa8ac560f752c1d5b0c7247d628f4a569a6a3810562d1364055f3cef73f1b2f51a0f2595f50a3a09840c59a497f3cd837aa29f64f28b635fd0500b4b62766987c99cc5d3c79b01178a7fd2f84d588858905a74da8b725d094a7c255a12be113cd7907d44ebbabdae92ae21d9affb25dbd08524fa5fffa513d772df135bdd8bc99e48af30ea46567f8336cdb546cb2db23817b23c0915b5da6e56a83cf19ab742faf7eefc9f95e22ca95d0587b3dcc580cd2a932b884e0d7b6e72124cdcdd6f49caa63e4f85aac04825ffefcf223f4537ab04cb718dc18ba72993aa51d4d712e9a42fe3e1834e6c70b19762a5895ee4fe1e2329dadd053d296bc18084bac81431280433f8b73d3d7a63f9a9ce7a09b90698e467517382bd0d3e305e212381065bd2e01d6ac4b30a9ddb95f9af7b9724fe3e38d97b6324d90dae256fe389b3b835b63a47d80da3e9389b8970d58ff5b8bdc62c6984c86ae7629b9b6e728e87341515d2420275d00a5a65d9bd512af1b1d47fa84e422d6757a9a70244d0aa64ca818d5259f9efb80e5d251a6e1c4e1768ec9c77c6822246d2cbe18ed72b7877cf95002ebf4aefd93caacc5e130dedd3b35c782fb75a57ed29bf6dce66041e13ebeb54e98ea35eeaa6b32e943f6a7b668d5ecd563fa03be46151d863e9747cb1c355c2e3cb0b7627172e1ecaa50d295ed93ac0529d606b8c4c8285d9dce0c9b061ad93a12dc1832710bf1e4c5799fc0b7383c8055ea14a9a039d3f1ed8334741b7ec8bf2d8f7b1d6ad9db22b44a170aed2d358e5a20fc19e1ada04c390ec7647c49bde13924a9650dcc356d45acdbbcad1b2ec21a5eb835689dc958078e3c41898bad8989c5d816bbf403babe96661215e68e59f0ba747e433a3073afb957ba3ae3a435666dc287c2def5aba859674b181c522a6a54ed613faadcd26729725fffaed54692634b13908dd52db47d441306d8f31aafe6b1146cc736290ab4f8e01d54f10525a1454b5a49d389451d071076d8cac0b8d09ddbe44e2f5648a617cc075da438014eeb1c3bb3de26a2d1a332f82b1cdf11544e9f459334cd61e94d9ba2ba57b2db7e9d22b86950e2a2ef5bc5ecee6efb60d5123a7507b6fb73f2ffeab60c9a70de6098427aabaf8fd29f32335e2d14964b56d5bc86361fe01ab0ea6278579b4e7c3866c9e79ce006997a74d7964be9a632b376df797fb593ded91c802959792b3c38fc92186ae2eeb38dae94181f15c8a4abb5e1740c8eb3460fc88de5ca6a558a81381b29e2fafd2bca4dcabdaa4e45c9cd334614757c6a7be7788c93e27b6fe8a4135db82312274c57e98f06f8114e11ba5a4354cae97d44ae4f5b4682e34f955d5535b859a6cb59028feb0309470177163da3a7c1a76fb37f021f16678cdcb07bd2b49b0c77211560e02bc08446e0af3d1c7f736cf620a0a79d2a849117cf1af585e26b4984c157f5012557f96d41dabf98d1533c31df28bb8a43183e592d890bc111c0a94db3d081c34bd6c003cca0abef79bb8c95c62c6647de01e47962874a68b671042e315e6f192175ace030aa9b76ffa9f4e199c6a4f56453a2b312477eafa4c383ad2429fa453ac15bfde030532fa2e26cb200e3c7b7061f076fe28dda68cc07e7c58ccdc22fd164cf5643238d9a39945da27372f600b80f88d641734a1f8f5e4e47d0cdfc083139198f42908d766f308a3409f56b853dda9c4eeef87f9e772bc8a1a3d5f7c4ebf865a722d5181da636297161f8a7e23d54b8523e2b52c17ddcf8f388032e4b8780a5698dda7fcca8fb6fdeb5e4328d6a886da5015bd58087a0cc43a72f5b88a78a7a12b0daaf470ed049e170b8b03ef8620b654e744859bba5d5543f1e823eacb59511230aff547900091a37227c9021549e4eb954c2b7b587764fc88dcd224067d9639cbc8c3fcbef71651d4273f21f5813626c710c12f16cd0757b3d8b8ed19b474e53072207502b66faaf756fa8846304fbb20ece8ce87cb71dd8344d7b10c7e516fc3eec7854e6048a893bd456421402994612a52fef16a97ab642edd69c30b1cbbe0f8e73b8f12db6574f7c9c7de94cbd698688060de7a63c8247aa3b95bbabbfdbdab7fa637e5404123ebafba52383c459621c154a2a23eec97382eb531b2ca83cdf075ee5bd54ba3bc3e19dc3ccfff08e5e61e8b49b080809a57db3548845ae9f5e15fa10024e2b86b283259aa11f3620d5104561fc32afffe54526c5f26e1c941928fed2fc4ad247ae0f590484265589cf0ead253f0a9fd50ae2d8cf02deab6a0f715148f905274186f0c30743e3542bd6ee93563f596c43baa6e0b99be127d722fe17299af2716365a7e72ebc20eb811291ce489b739ad2d3f79a4e81a2090cf81b7a45ff782ba3655bc717e0f80e4a276c0c3db48036c67d1e95797e2c7c9e183a861e28c9604f3e2187e470cd0b37a7d2e78b3415f6391b7fa04c492edc03c5a728507f234fb6fa885ab1939f64291a2058538bf83cc7cc3ea785774332d92b816c76d8d99b94fb8c237113a24b90768b2314aeba2846a645e623834a3a947856e7c85eddd63a1865700d8ad5e36521fa1f90478601a557ce111bc7eed0b7542ff6e8e887e2253db793abed45581fbe9ff0a3100308102026616a7a858f9e9fa1b0e10e2254607f84b1b3b9c2cce4e7fe08162b31384354627b8296a5b4c6e3e6f1f6f7020a191c213b686f8491929c9faae3fb000000000000000000000000000000000f1d3040', 'hex'), + // eslint-disable-next-line @stylistic/js/max-len + 'ML-DSA-65': Buffer.from('0d7f54a113bc87e6be6abc34282a0abf0dd12e8b3ef64f338d62be9986eff721776a3ba2992b069c8c2d15e0b948f2615a7a71ba42650abcddf147f50e6ff7de01aea560ead3b540ca1754b4749ad345f750d75c379804b7893cdfd059345297c8776fca5378ebbe1ee1fceadae9bcd01baf39c8f01e8791730e7e1e5d0efdfae2acdb34e6be09a258b83eb29da9e48506c5d1029d1b7157b103fa4fd77c8b305a0e4f2dc82b68936254bf3a345143920f67bada15d3038f76c160281085a666aba741ee44b12ed4018e9a41d77c642bfefd658255ef560958575c338635d140bf73c2daf923f26898fdf45fd48ec24c4ecb8adbef15bcec7ef5302fb58ce6a16628bae20a55c497c0897f86f0ec22323b96e1bce5e0cd7c608aed5706a2c73d5d4d05cc637f86320a8eac06b8e26a13874bcaa9131db6c9e237841df21789a47fe48a13895575b77e141e3285197b5a16f19a00bccb1d384ae026a17d64a55152188b88a682562340f667a19dc3bd6b5592d39e8b7b408c1ecbf768628038fa4333a475a72c03f3cd8aef346b1910724a47ed915ff1b2bc71ae17f127b903c679b103026c2aeda967b0d6497a30790e2507cc79bb78e9326f66c8d015f20baab3311a7b436c357356f20cb03209bc82863004597439d0905d73eb6213d14ef4505879e5ac3f4988956d5483bd0665d0d9b3e9353de68e7108dd0a6af6d23422fe193294419c3887571b7470d206f23cd059895ed5a53cdaa6ec94969b43eb9c98ae53b909a4162d6230717b3105dd4d511a40b9817a71e037df02d95c508df1a392aa40d9516811bd520442c00b8377023cce3f1462e50636813ba1e4350942c435743939cb540d4688d0db69885ec2d2e3d37a408030737dc8fbd42cf19df941fa5f8b532cbf839ab13b940b5b37200b900d27681ad97a4f92c256c29d63888302b076665d5b9c2ce219fcb12f80dce2b94e6001dcf3f662e01f905694a78ca5e2afdf8ab7cc1fb72e04521d9998662d423936d5ad359ab44091b53467406b7c306c0a0ad2e480c968f2d9e4f392ecf983fd2a4ad198bd701d09a1e2efc5c313999ceb83695d99a5ef25daf159d26f4c0ffbe3c2ae03cde0962cbf1a4e6a98dfc660922c2a399190abcf933a73f68d491c4ad8806cc450bf23d4e1c1377f6168a3c4df5b6bf8e1cb4be11bda24b6d9c113ed4d40fc2dba9f541d97739b5e40e915be1e10eb01c30ef609b6eaaf5d570f886d031df36fb7614080dfc088e821d1ac4fc50a48c5dd7ca6f08c7ac280bfcca640cf738f08f7da29639341e396a46d7204cc514d9c8dbc213b967657f4a67a29458947687b55ecf26d833239a7c0915f10334befa61b74372a8e1f4adf44906ea6395e15dda5e4b2d2005d574a943abd9d224c7f904298e2b1e249ab33d93089f239de6ce6b0c2e1b856940b7b08ccf1f98a65dfb3fc615457e2ba436bc63cce72510757380a5c07d56b47c93430b6bf9a89e5f6e1721d7d50b7417432471dc2ee0b02430fe71f4122c0e3363db446e4f7ad21cee944f3e6072fc1fa57d175329707f52c51f97eb287d7af70ef91c0632ad5b89929507a2d01609993c2ff8bb991e232b554430cbc0ae4f6b3225013e1fd0f8e0a485f7b55c1f96dc0fb6b395ce104d9c3d780567cc9f7bcda296f665a5eca11a94fd1d0f35d10e0b017880e16f31de961f7b5fabafadd1c99c80cf7535e7df7b807fde38b530ee527234983e25eace30c4be8fe18edc58ae53ad79a145ee0700be63b636a18a4eedfab826383a9e5d9f2b7e490c6bbcc3ec0f37ee44b8e301091c6636cf212e3519b706919af41b872d6b4778cca8ca3373f62dbd8f68b59ca29467c63e10b886a18d2a32864ebca2ebb2f602b736ac8587d544d74d9aacbe6e436646ffccf2ca3da2121af8b3431b523e09a538bdd07ad3cd4c71a76cfcb67273f277e18094ca9730d06af60c354824abc84f623e1727a4f43c7021ca18d463c5aa1c53cd4adb77f13156103ac0907f6487689bf5eb2202a6ace8c9f55b5a285311d2e243090ce07e9cd2bcd253edc4ca57c1c42383184ef08d3f7b4ddc05fee25e8d72a6fa0dbe545dd3240edfc8081fa49542bc23d426ad286010920ad92b0fbae5833523a94ae59b87d90e1ae4cb504f06512b9c0c47d854750b45bb01a0a2788c51d808748f6cfab177fdd943965187fabc8419292f45f57718f709228a9d51dc43e21ed3af2bc5cf8ba75a4d22203a77662a71675b15bf91d3c7074657948b43e87837449bfc5eab1fb1fa9f8f91df5669b4957eceb02d013b0c04b493444ef673a103f630923c9ecc5c2cfb9fe40c1877fb9a67e54f7f0ea0c49d0a624220eea68bbe0e23d20092ae73a8a0f7749cf85499dbf4846308a190bf9ce68fb712a20db6910923eba88f42e93e76230bdbfa9890b6bce07a5ecffc396cbb314d3152bf1677a26a0245fd8373a1288d97e713d2326194e2e26990ef413a1076035e7e04be3d07cbf9403ce9f8b7f10c70badf62efd144324c1d37ead93ea94edff937b0d62820baeca5f56cdf30487097f5058ed3d74639543b1c160d6e876021c96feceff84bb2b1588c72f5f9a6a843b85d9b78da39a085f258296538467ae9568d23d9006e768445f9752ee754dc5ddf9b44998e90dae1a5c720d0abd3e81b750ff8a689547998a45b5a6155b462e8ed38c7b6c44bb81e64630449191a38ddff8eacb3f0cc675bd84aeb6acba135a65d2e7233e6b3d59f57d5a1bdf215a3a847a96306a203ff4a564524e03526fe3c35906fac3316cd19cbf39fa9b3a37bf2624c4f7eab0b98d43b967598349e7b0e69ae7a110aa5614e68afde1ac453bd8b02a39307c6423339d49df4a1d7c3042d0bd34ab9c36894eacaf1872ac70f0691dfec0ace938d17f1f61c7a0b6a12542f0d5e0438d2f0c69dc4f0a2eb5eebc56155954d97855a5e95d133663f29402279d39e41750c0667a721cee486847913d383940419dc9fbc118d1f608abf2f2ca244a7486d1ca2e883958a0d36e290edeea0e5dc8b44f22a6ae7efacec2ee642e8417d5b6338d58ef540b5438a2de2589e3c911d93570d1f7043da7f04f6a97c64af34f25a99c57662ddc27264d5fd880d6538a81661b8e83ce0f00e41a09dedc2dcf0935842b46c7b0560f0fef6c7e45d20de5566cd68c4bea75af56f68ec6f8061b59bad47d346c02d4ba89ed92ebe9276090a70f119faf4038cac4d655682285dd53707d00063f89f97dcda0e370a2b83bb6c8d9a4db2dc85efbf93d76b007de3566ae416a817d1fe9d7383451b8789d4892a4144befbf784a837dda4963cd20cca292273121b370cd1d0dbdc9e42aee99ea7c833779cd1fcd570f2c502c791ce06b082ca2f820eb75fa2b5ca072c915e3bdbc6eaa3cce642e1001fcca516c6635ee21c4a921af590d50d5127da0acb8ea48d11ea777192a540e588855acfe2ab0aa23e7743da7dddf11cb5fd7c3b0536b1dde5156b2968420b58d502d546781c2c85d39c80e9182474bf36970b19a721f01d103b99b7074e2fcd3bafe90609e6c3764e12bbdda9544cf0715aea922b34f33fbcbe0d3a878520f8fcb63a68302c704053ca1f4ec2ba20935561863d4a4d7c442b82f7e8d4c12a987ce5c1aca012fcbce39b96af6c34525eb197c82385771dda017e0d88007e944cce84336c08505f39138caa9f9b2106ad5aab0cb178d3361eeeae7b2124729afd40064c9dc3c211ddb5228791f888dfdebf9c17df3470f6c3ecd400ad150e2da5acebec9e0707916e481733b40eb728bf4530f634e0834e14d6e5024d5e00278f503cd0b308284016c829bab2239429eed5dd8b99b3295e27e4968a59f6e411abce882d1b4282f3cddad447ae8011740985ca9e8de7862f80ce1713a2e7261217864671567728fd0aca1812804a99264fba96e6b6ffb2aefcc1c8f95c990710f1e422175c8d26cb3968522c4216806d83982948d4a9a99af261c85cf713c70b4552079f9ec7e1563f4f3604cf3a8f258a63774a2c98071fde26a4b462ff64d3a829d53274ebc8306fbcdf90f244333a217196a744dc3bb67d998d6af560bd9e328aff3865d02621b4203caf986a0f6216c827eff118b28ed08ab42e70be5568d41a5e9ebfd4ece177670f5373797c0b9f99d07e25cf8a1615fbe0331f952685c6ff68626cd01551778bb6efc6e8912acfe7028254b20f3a8f6fa11d0ca336c73cfa4f754c16cbeebd89d11b91b5b59f422e14b5d419d19669af331427ec4949ebefd83ca4b973c53655a38d189c06764d3e7230fe2327d7d98e4e6f82b15d4ea0a4d3c07214d00b4bd00a6198010f13415d43076e0d6af24e94a242f70ae947bd0550f391516a9223bef09862bf5e13787561539fafe4c07e4db618239e6fd69c0ab130eea98e864f76ef0eb273af8eb7228016b495aa3b717ef057a7e28ebc1fc8e28d8549936f511382bdec65af3c7f1e149df5b51ad0480ce282fc605bccc8b0c4f7688351947b396643cb1f7f61dbcb123f2230175cf5fc6ecaf9ca6a58ec493cd6379095f6afc4e0f4cf0e6dc710d3e2dbbc307e4b80db6855bc26c47334d4e5a7d8691a3afea043bb9e5f818406f0e5da4c0f7fd57c9cad8e277b1b4e7000000000000000000000000000000000000000000000a0f12181d21', 'hex'), + // eslint-disable-next-line @stylistic/js/max-len + 'ML-DSA-87': Buffer.from('737897d83f7fb82c704f4660c16c544212f3d3d280b3390c43d356c281aca7a3c90637a5dea83bf3504e593b178b17ec38a8175507a6c28b80c21cee28146af2161af60fb123420578e0576fbb9d579631efda9a4304c0ed67572d393d813155024ceab089c1cf1d0a5e9716948312f70b4a7deff76853f5cdf29737160901c14ac2f24ea4c7f819d86fa9758b8ca6aa6ffcfcd6d60f90ce31d31cd9d88586b9a38a22402be7c89c9d57c2b194179011c4fd185a5bd737032c1e54da0108f355130ffecae3f39a47148dacafdf25f9590bf6c8fdd9d9b7cdfe30607a235993c59631f29fe01453e2fd61cd4e8e3be21ddffa054c5e91fec88ecd531090ce21d68b7b11888ae9372fe2dabdb00ea311bfa564f86de46c61f284a246e420dd8056f021448a2fa6dfa2391e5beb6ef0a5a28180dcdf20e00dff4f9628a99054ce72061b0359cc67d4329545faea8943d90bb0f85bcdc88ef2acfd72056d2b5fbfed40034f2d344a395434d604a336c1e418e2598d07e8611f9c28c23bd2771c4d4dd5f820f197b50e58bfea502f6a53893d7fe6a7460a62b8ee892d17dedf606cf61fd5f27e002230bd15f0e3d5d951df3c54228c20d58fe5e5d072a1869bdeba1d90cf019c6bc3fb0df24a11894bb3a2d279c67d3a6d4342e0e2913861bd3f684bce8ffccbd6358dc0778c02896b719d675b584621edceabec3a8e679686363825e6bcdf6579ec67ecafdf23d6e23b747e340fc0982422865b8002590f9e35c8332ac47be5c4be6eeac3ee4ba79574f7c0aee0fac0e5cc7a9b7288814c36b10e2dbc900acee6841c386fec11c69580a87244fc9f995f6adec68b308b28eaa17129c1edfcbb8592b4bfdf6604afbf1f206010b47e92d08d830052d109f0265213fffd6ecc47aef5bd6f4d5fc19d2b06924cb96309945168f382977e9f6a2f59237b4a91bdde2d00453df0e7c516b7a53b0b8582aaa7ad8af9f40723d09a247b653ee48999148b5c160a25f5e9cbbdb53642429909a5af11c6f36db3fe8f8916f36f565fdbcd863565a6dd63b974304b6aaeddf91decc981d21cc93c11d8f76dfeca8444b6b882ff59be214fb1ae9fd90108f65d4f533031c35bbc552950b1a96f4be7ea9cd167231d7a7fbbb203d34bd764dab3ed2eeaba3419ae3674f7f01347427b84ddb5b86969e57e6bbf86b61bbda8fe830f357d12d15eea0df7d5fa2d641b03077f058719bf11819f0eb1dd921b783b9c648b5b3cad3d22e9442e514e9eef96d53c1de328dbb79eb801974b88e179d98d28bcc75469a2be46ae9fcafbfdb22d302ad2b535d7c09d06cc5ce1a50a569dbb13debeee5cbdd3341d06d8be28681c7716b466284efb4bb1b316b72f50c85c2a260c68231e2c516265f156e5f5ac735b9a420878db52950cdfcf21dcd9da62e41a4e117386b624f638ed51b4d2e156573b6a327698bd41a3d036414b201b0a31d818fcdce9c1e4d55def388fff5fcf5110b680e5ba3808f244dccf7fb01e8197ba3c3a3a2be74f44551f5bc03f77ca5010d1c9d8020fcc7c354e3ce9fa25d4900943f564695da56a6e4388d1bc69a68d413fc80e5913d22d2414b081052c216afc67ecce4ed126a8e0762191558cbbdd8d4150af7bae41f4941ed42fdae4fed9a40d70a0159d4a8ae48006022dccb6b4ae05dd484c065dbcce9b253b40d1eb542073fac64dca11b6e1196c5b50fb33d6bb831b9977602bc9d43c80713a5d714116e62fda8e9aa2c790e8d5adbf8025dbfa5f80bb92b661d45fb972eafa25303f1db7839efab7271ae41ce9f7c28a7cb8349a3eb2734c6b13caab678d53eb742fbe84f53b8ec89e6c155ddb0a4908799372123023f4c8d6f97d30fb70ff2d192be0928fdfabb9aedd04f944b2c7e0ef143390a20a5d5ea93d3a8f425470666dbf8733a2e2564f27e1878b2805d71069e55b225e3d133a7cccbe0db3f2b14ee77d717bcef348df09259efe12bfef08cbb0e8d58f08bf10c6a7b0c2820b442c06209e9b8134aa20d05e8e2c763a8b88b5d4ec544aeb1ccd9468901f6ccfc20eb632a75d051a0295590e02f5ded6efb33e54479b7e1104062ccb73bb5bcea1da9d4d659e1ed40a42ec13a6f27606a002097565264954625103e921886df888740b7166640132b428a97062241d083cf8b4892c159902a9cf35921df2966b50f6f31209773d1bd10c61d14409cf1e90a7d06d3fa226a865fb241299a859b8dcc898b4bb1dcf65ea17353a130e71ad184e9e89f0676e666352ff4614d8a7046588d97038a449ec426b67c9ee3101b8091efdd10867366cdcdff3581c0e973ab6ed71c984b33a58c2e3a865ab1bd055e629a21fb07a8a23e998f2db958ccbc89fd1b3512330f2cc69c3d74e16f8848556011881701b213dd39e123aca241c7231052a7bfd4738fd69bbfcd0ab74276907c1be3f75311e395a485d15b592d16d22c33ebf91d64e81d500ea23d53f130804ea1cf51c4bf2b0474f4f62264ac7bd90e6e9dda4d2b627b1e8043dfae64c6920a4f9a14ef15826996035af30cfa59a6a2411c61d16938004e2ae2ec72c32825a44c3200853fce28e36620fd0c71121346aff28e19218899f8e894fd6e477571f2a5c6c850104495a89cdc0998496b979b038f65ebd15ffc477ef22e943358de58612bc734d06bf8b77fac755b64f4c71966c699c43c5c38b7fd647a8b82cf354b1da4d31c9e918e146a997f2b4fc612e2ec73ab8e017728308e589efe326d4a14eeb55de34849938446911b9f3c62ee51992da1ea284650e0fd6f6078301a46948abd7d6ec2ca9ee202f26b9aab99fc29271697d8565a5147631ddbc9295c44812262a9c39cf0df4e08c52749823bd51d117d6cc106d74fe329c658c1034dd7d9a2f073fdf8bec519aa4a2f9caf8d0d719cb37138a0383b263c1c28a8b15c1d18a63e366a9892299b507da6bc9926672289a4f9f679e645db6c85e1cc1f4e6f9581984b2f72283a8efa471614e3e3d4a58921bd0d5feb85dc9cccfe4a7eda2be3d8118b2f4ca35ef6824e1a2d09162fc81791c2e30fba9368022ce8e01e1601944f05a0771b2d3c970f88841389b11eff76c853570c66022c3d35c4e94714277cad948c1a2b3273c6b3d675f9606b8b1a5d1848a8fbaf0f6e108b0029b6fe2ca230cf0e5a24049616db1b5bbaa0116308fe5948c45a4569fb5e3aa0bc5ebfeeffec460935477ce1c06dab0a1e7012859435e9887052293ad68a92df2b498b07dc991f16d04dd96bff885bb49a381b27c7e3701aefedaac8472f9d547da1aa8e226db691fe48c0593c8824803e564f57bf6b1c927742c909666b07f92f77236a2268a6148b517c1d2dea60321ff1baa5f47c5dfc000c8abc1c78384e1d6ad67dd343ad5ca3577b254d0ff85f12953f2100e6ec07d3c87d8cb79538cc75aa60fb936823009a55a1e7642a18efb94ec2eb44724f5b492263070a3e5481d091820459691507ccca318ffdfa4a0f5bc079e4c6a0fd41e715d0a7aae8d1916b44edaeace00053ddd45aea137ec0ca8a9afd497fce05b6fee9d5eb8365f3bbde783e51e29bd399c4611b1bf67487b1a30c1e3097c599bc74a4531ef9f1cb3b9eee69ab195601bf3a75f4abd8cd2475211ae9de36b42c1cea349195a7758dcd6f0b0667012e271143ee765d48348b7ccc47a77ff7780fc071505b5a10c5734abcc07de9016e6efd1839eeb13d050bf1bbe28a119c81578516250eda43ee91effae2b3bde7128a6d0bd2426afbc1feb1f98d64de06d5d6899122013c467c7970e18edbe11a3c4900a182cb03cc149384ecff2833a3eed9e0071b7b3da23fb91302b76d8d386f60ac16e814be0114d16a85f757abd78e63b3b75e7b29b222aefbbb545e1286f10fcec514b9e8071de9861688b6c481e670a6d22a1d4f91f8d690df9e7d4e94b5846203d2fb865fe1a3fe7bfec8fc72c5e42a56cdfcbe6f7d3aedf685450d44f6143d1d64de4b093abd10d298accc5292109f67c4634c2dd973d38e2a4e48154e17f29a8aad0b8e6cf740437557e13c0b6d7fed4252d07e61db7f65e28e968a6dd47c1765973f13ef5d3c47801dd0cca810fe34ba111e3ffe18d3fea4814705c797cea0c53ad14f857e1e16a77eb649cbb3e3eadc613cf25f125f0c6a096b0d3b85b5a7bb53976a817235df8a384f62bf5afc08ae27e52863ac99ce1da62e592d41f5bc13550c6a77923c363cd3f5350a2781182b3469f34d6009a4417cb2bea309a40f5b6ad917c48adb6f9a2a3836d7491a96cea1328b1e0481a00a7b187d27fefceab3a09a2dc4741b699bee6ad6247a67b977dc329a54c05b2ba7dd3558125e73439570427f78e5f74576743e8649a9be6bdbd36c2b6cc070369b706e253ca2fcf2d82689b997873caddc13d1c0d62a98818d7525c11488dcca621cc7603de6be5ad7ea8b708e75361a357aeec7026d3ed9e92ec06057883a8d9afdf8701aa5074d2cd7414fce63bfd807d818fc11cbd31fef12bb398b286ddf68d8cabbc10d90f4ec191030c2fc0846ec8ed207b793be8e71c9d046cb6f39aff164d524386f89efdda65b48607f6b9ff82eb0b6ec7e606c732778c80edead9760b5aacc0eb0f8f9345b8d9b4e539818a7083a366ed5142f4dd8ff427833538545ee93e811e777db349ad85492f6c119491dba95ee5d3d681f8e2602808d366295aa90ae780e87a25408b6bba38fd74573d6e167ca96d29c8c21b648f92e0469b960c7320281dc9cd8aa03d5203e1ff47f9dc7beea4f49fc14b8047c0c40bba27637da755e0f14eea3a899e2c346483e26403ab7d119434c8c9ee35e3819cf578995cd50dcaf108af736e1bb769a64d64152b5dd760c51819b69379f1bf80bc291da1072e0d75b88ecf11e15b3a43d4bb9403359ed87530be3b949c1f11dfbe1f7d04936048039f1cf9b036e056bb451bd31b871b1632d4cc0c01db0caa48625a6a2fc33e94f9afd8afa4710c944b6b6dbcf1f0fdb687575d2f4d334985744a45f0b33ebd57c5557836996c58976850e8fbd6fec193d6d8d9d7b72af74d2db3d0183db111237612e376a2695b55643926c2a0043c40acd9584a36f79656ec1e3ffe48c10a311dff01572e34d74561e3be0cbbe6ed7dcdcb41cc406921130f8dcd49d71300fffcb896623a615120040f5eaf75a2f849d8bd7f1cd10036ffa400f4582f407b09f6cb466fa65d237afae541c378fe972a0829c5081236b2578bfc7e2daff68cbf87f73ba08de3040068984e5b3262160e5fa65450342af94ebd454d9c246de45d7b556207ce35b54661de74cd6bf12edfbabb9d4a67f34d6481712e2ffa08a2b5747cfefc5771148af9c8793b57ecb1d559ea6121ce7c8dcfa4fe5fc86417ade50a38f059719aadbf2546d4831443dbeb8d389c83bbb71be9936dc9f884ef5eeb69a04dcab8ef98f7ff3c57b9411f4b3535c99736fafcaa6f12255329839e34788671572087832ae5b1c65d553b7ca511eff57ebe30b857fbbb42ac2958f09aabe7e7372d406dfe920cbbbda4b3443940db9fa812aac43930b95eab060cfc89fc04c34a93306a5fa51e16bb7a1c29e144428e5a196733cffe4ebe2c72d0343cf40a1a1b103912ff47d699cc688baecb29b774c4611318038c47e3788a82b8f29541f423b31aaac2925d7f1251baef27e73f13cd6895598fe6411f7655ae2474c86c17ef48ae7151938328f2bdcee388efa792a01a2f59cd1bcf17787a093a4fef6f0d37a5693e934b9b869c272ce6e140780268358d6f7a6cec82e48915a46f4bf3368cb6c6ae3ce3d63a4ab497fd20babd3fd51c359daaef77ce8ebe578e9e108c8a251f8f3c7b5412a81aeb806532a02c6880783ac9ef01b2b9ded2bcd81de8da008d0c8303597e826bb64da7b1e59b31009680e09edb2b5e75129192d6e99b745fad01c554492b33bec31a2a10b52441f09ddf130a7b9d305081bc2b0db23cd70fdd91a472bab7804489aa612d8bf003a64b5383191ffa9cb97630dc81d80a616682463836005273f12b357002068d71a04c00739f5007978254db5a560e878697cddb4f6ff641ec98e64f67dcf83d7c1f063874fcbccbf30d9098eda3dc12fd75a9d7585360c9196fd1f6ad7d9607fa4761913be523fcb09d7af1c921065e8b8fa479b15477100219bee47c5e9f00b3e82657b5cd2f125792ac9f78e5a02f3c1c4c43823278c4358e8956569a3f53ff2e5c41f364a7f0d4a489ff59603cfac5680f1e0813ad4e0cd0e1fcdec1768823d8f021bfe6c82b0b8c238c5363820a79378f76f48828879b855ae6e9d6116cbe15c87594b210be86583b5dd0eea790fb4b8be58efb2d14ada79a1fb8a05838f367098ca73cc81dbc78d02de145849c31f22a3a2aa05f68b8435c28f13d3df5020b288c1e72985910bc7b5b6d1d5c14294e4ff20b197aa5afb1c2c9fa12295a6048585a5c68bdd43b3c476a7e80adb8cdde3742445c747994c6c9db4f5cabd6fc0f12385191c300000000000000000000000000000000000000050e1219232d3238', 'hex'), + } + + const algorithms = ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']; + + const vectors = algorithms.map((algorithm) => ({ + publicKeyPem: spki[algorithm], + privateKeyPem: pkcs8[algorithm], + name: algorithm, + data, + signature: signatures[algorithm], + })); + + return vectors; +} diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index 1656f37a3c58b5..8488647b3ed204 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -4,6 +4,8 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasOpenSSL } = require('../common/crypto'); + const assert = require('assert'); const { createSecretKey, @@ -180,3 +182,23 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { } } } + +if (hasOpenSSL(3, 5)) { + for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { + const { publicKey, privateKey } = generateKeyPairSync(name.toLowerCase()); + assert.throws(() => { + privateKey.toCryptoKey(name, true, []); + }, { + name: 'SyntaxError', + message: 'Usages cannot be empty when importing a private key.' + }); + for (const key of [publicKey, privateKey]) { + const usages = key.type === 'public' ? ['verify'] : ['sign']; + for (const extractable of [true, false]) { + const cryptoKey = key.toCryptoKey({ name }, extractable, usages); + assertCryptoKey(cryptoKey, key, name, extractable, usages); + assert.strictEqual(cryptoKey.algorithm.name, name); + } + } + } +} diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js new file mode 100644 index 00000000000000..942be715f89534 --- /dev/null +++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js @@ -0,0 +1,335 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasOpenSSL } = require('../common/crypto'); + +if (!hasOpenSSL(3, 5)) + common.skip('requires OpenSSL >= 3.5'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const keyData = { + 'ML-DSA-44': { + jwk: { + kty: 'AKP', + alg: 'ML-DSA-44', + // eslint-disable-next-line @stylistic/js/max-len + pub: 'gtP6UVGz71mY6VgeI-VIcelf_Dr6FrqOpyU_uU3Vm6NsWHuLd5RhxYdMdi-yZLV2auTSvjL-vaHIdnAatD1bw_ggrvPSIGFdEL4MCM6XTvEovOP_vk9KUBf9w44YZhV5VYezSpJxlT7eYPmCp_yHu2Se6bIqmCBgibi5-ZBhwBM6W4Aojq2a6G-a4M5aH4tBNT7nemoOEiqgteqDWx-xaduFjRShqTzn5OT6TXn1hNIeavnUuTDgxnveA5z_SrIQs_IVMlzgN2a1nnPr0rjO4WLhpNk3vMZ4zqeqQ2iIXMauh0yAM9zI9kR3AIshIg6oWXF7_bq7RXdPobaIWzAhVdhqllyhKnwKhY-mPbYvHz7h1G7lhye1QOwC63gtBxlZ64KAi73nTnxKlaWRFm96gSKBwNZuZVxNLUkpNDmGWZVBjXlnPCCaZ8CYSM2kiuRUJcZaQnS9JJmvf-ZYsZdQvG-crEVGq56MwjUSJtuSPFYdwD5k7Pz1BVJADmI9ushzFVFo8pgfGGkxR9fStg_gVpD4rwnB3-DmiXeRtIcsxkFGzhjUorLauAP1ZQSYqCHZc3OCtLV77bBaGc0I8Xbu-QbF82Cm1xepulXF1MqbroGUqH-sWZn6_7dy8pg1gAXvED-rTZVACr90HnxIcfzRMWpUpbbUk7H1ywA6qHMIXJXKsNjo2gqkae5PVRCskf1NLZ4z8xyDiCC3vkw9_Hd4dUGUhhLwifFOoHuffZg3a5Sxinc4DHW2qyClZ7iN-QfJ00Uz0wZPts1Acwl1dfo2B7UAFpYeL9YlVehQgJvAcfsrffwvSVIWt9XHBmydABVJctrwz-p4s4EJNSK-tXrpgGS6w4uo7_6gDoZyukLYLJOy-YBhMSUrdJg4RhnjezTekV8huCk6qq4oj1JNAzOjKjc-X-CHnhIsyNjxfjcrO2SVntJLMDJWr52eyxcyhKmrmcjJaL39dE21WxGIcAdFAvD9IoAL6fuaSpOwxi2XTDBmXBJwWmdHY2HXGUma2RlfMNe0el-XcPxgAyv1nbypBW1pcK3k9e82mrE0ldNYwPifaZZp9ebQpYJ5Snj6m59twNCGFmiKUn3N67seCEkxzUeLnxcHTGmpQxIt5y53VHY535npd-H0dxRMnISTSrQ2tVtf-vQPU5QW_ldcqiYHRBSNv2Art8NDjYJZQU4qvvRyox5mhH94g3Xoiq5N3lFBtUtBIrMCNFxGHye8sNaOjJcimWBqZdoM1bScDflqnR1W_tOnIlLHJzrNtKHDObdtHbWf0K30Y93g239CZ_pSz7JsKmu2h5CnXRYMKxYY8tzwje222rlvqh8XcDHFBOHGDFT3etQvD7hD1_XaglYwNm_UIl1XRjQuYTNIvekUHFoA7GDOBfE17Txdbir1Ne_QUt_tufBuU5p8CuWbJMWKq50Dt0Oj6GT6eK7rfELsJWrqezcKzXQzm4xTXLKdfx1Lz_INsekVZUu4963VPubqNir0zBGFScznG_zUeGlMIYyLUGxqk7BPXLql_xaaeLblx8HfeMSRi6k4Cw0OFHX6Vy6sPIk5r00D6_ROD6DN8bcExC_3bF8K2qCrg914EY4pZ9BjgVqKVGvp2HWPpgQqvQCq6M5C_FKVhtfsd3f1flO3_xu08R2edvfL-1bSqCy9zEUpEc5vGBfKLY3DnTU5dG5V-xZNwqjWrZKDNQVf5wOyl-PZlWlp3DI36PH5NNlktUy0rMXkT-X-Odjy5xGbFQ', + priv: '1Z0s934w8QPUceWNuRN5dfrhQb5_4GxQYICWnBFxnOE', + }, + }, + 'ML-DSA-65': { + jwk: { + kty: 'AKP', + alg: 'ML-DSA-65', + // eslint-disable-next-line @stylistic/js/max-len + pub: 'gMe-7J_WYur_NsxzG0iYWqh0LHtK8rbArEnvuNwNHYVmB1gLqnMjYMeK7_ScIGigoMddrW0Rjy7iv1srvbIpsx75K0ifzIqtf8z-gjguZCKWLihbt58KuhPmwPmTUmYtW-oztmVGpAMk5oH6dhxAyPJQO8hP5ZIh78e4bcRjYeyA3yCvQtT4tpDRtCDBnXA7mvdDLmitQI0-rvHtA3638EHZmbho1FxAf_BrmdLdUZR3d9-KlEN-7NWRkn52VYGitaG14SXcCorKvaxfo01v0v-BqAeLxBFzwa93NZKVQKrjHx0m8wXKJGkZ7qQPC1HSm40HGps7kxRNjSDL0T7pOwEZorCqL5n1W6GWzMY0NCm5B_t7hQdZenBkSarWJjHKDiRTWpKygDOQLwqKXUjX42Y5kKh4N2H2iXqsCyb5b2p3D8CzXn5OMkTFHARpP4lGr88m22zq_r6f_L-Q_cW0gWhFILxyUQdzwQYtDfVIJVvdrkCEwCyN66nPKQR9b393j1uunJh4iSfV9SVgL3Ok1P5UT7h65cufsaikMNEnsN4XAtUXtFivUkxLFJwSJZMnZpZnVpzgIJWYnmqrHoRhPiHK1JtH9o1XxQcnGpYWDiW6_-WLycpfSbo4pRNkMjr-fGv7xmdXejQELByZMDxcM-QY3B_BRK7gTgmhr-_hjwg6B4x5MUQXKFeON2qmqYqcelkLWi3q-8UuGm5WIRF3u0ODHxqo896wVnq6nTbgycrLfvLUT_AM2qAAxR-w4SfQ6KZxGeGidNe6dFN4TacW8-NbsMzGTuyhzWsZxLIG8kTyveyNMFwqOYMj2mpPTcg52tRu2izczeFQqXiFhRVGATt-KXGMjmfaKavGkf2SUMcTSJqnTHJaZv7QmMNosgXYinkQAz4XyOfwsi8fgXVyfbh_05o2roM6AiEDff3aqD-m0Vp1d-AFHLxYDkRFvuW9BxbQe24sTopODF6hn42Hh_ZQF2EU1cIqFGzecPV1XGeGfP6Df0rO78701LOw1PemGXCsU30Mn1sIK1fmbqK8gHyZwP9mwS6WjxX5ByIMS_iyqQwWvzcFI3d4h5vYCKC9o0uetorwpvcv8SRTR7959qvPF6eYFO8OgvckB1Ydq6KREMpphL7-EXb1Ot-wxm8v9X2ZhUvTtV-LPxx-pq17qmcAM_ifwinktBrbvGifxGJ7Ku4ZFO7RtuCCB7FOWucGXGsHfs7LMwHdF8XRmTeci-XsG983zAKGYDxyulLD81TGrO2o9ongsG4XIAH9y85ebnfCG9QKfOSdPBxtk8nSJXVb4gSOpioeyt0WSnNBxiUyqOUKTLsrzmK4FZPvEQgZhJSbamoE6_MNkuJ40FOmH28Xb56WETJNJjibd9Mj0d6_ozq0ykpAdHLTDO1EHGIVImEqDQlCyp2zQRun4SVH7MDP8zZpSI2g6GlofDT2A5Yft4OPX6VTX3uaePxIOPmZr1MrBiRNrP9NboJQ_uw6X9pWFCu93jts7XQWnVubfKPcDDHFzZhUMy1y7GaZxqxuW0AlI60XrVElqR_RPa6o2LVd1_CAMWPPj3UAIOoZ4tfkDUo_Ss9HStkDV6gswh9whvTCOW-Nt7C4z3dunvtiT4chxq_0d3_xHzqY7xAnbENIdIIVDWhRUQI53_ThwBVGGpKzijNj_IZxFfz0R4pDyp4msYII8jIe4K87bZ0SuKf2MTLL_sI7E0Rqv-kGasONULQGkVoEMqprOS7BY4P7VrADU_riVLBeasJ3gZ0rh9YaN7GkUDsYa0Lp2Ts_NKIOjH3pZOY8MYyuv2bIfgWE4zYVlmmxyACC2K8EwQQe0QArx2DZAKQ6k-MwrwidGRQfe4dJ0-XCYD5947Zx44sTLmYri00awyuCk_o9NG_KLOZU8mpLUmFFQazh2wEsyY0fYgtK95hZfGLsVcci5LPQPl_B8WhgFoKNXxqGAqht7-8YXwGEdUxT68EvAPFXFuQKyOZu4_q64ApPxe1YWTDntePR1_bhOLlK8as0zZB8iJEeZDTVRmTA5CknGDfIt5rSA1_jcSfteXecNk4xTH_iU4CPSpePUES79kQGVG82cD6sQ03N8Y_buP0GDpViZJOB187hh3NhosghozMoqFnFUgcQaYpMxqMYyVEZczaJdQbWnEWdb1ygfmosfRxceHFF9Sb7-SwE8jRXa_5QMTOhu1nohV27Zownoq0Bd9bqBLMNYsuUOa-s6frJkOZVVvr97v6qJMaq9r8Ol-1dXQag1pca-JdNvGizHFp8a1qGY9b7ywgku5MdyL6THA4KCRvgM7Mj1gQ8sQBF9iMtNSn1BVqdjDxaYITe5J77DBqO4piGzTqonhHhxtRFDYmkk9hSe_GkF_ZMqrBtpcQkI1Xl0vHIJc6i8y2rOe_XCYO_cAQYhgBOz6sF6OCSkYkz82ygaM6HEGE16tTT7HPD47uy1iO3kR3yIq43yeqUByfAp8Y2V4t2rKGG7Lfpne1tnXpO53wEeLWiwlQ6--J-DCfoZLTT9PrKhGbh_z7329QRQWfKXTtWD39qA5IjK458oUBSqVpEN5kXXFA08dEIqEAn3YQW3mDTzRliA_3SOycOV0I', + priv: 'wS0n9kCle6HZA6M7BXlaAyCvUUqeMiEqaiiN4Y0KPM0', + }, + }, + 'ML-DSA-87': { + jwk: { + kty: 'AKP', + alg: 'ML-DSA-87', + // eslint-disable-next-line @stylistic/js/max-len + pub: 'R21Wwut7jawo2a2nJrqIk5cm9ay478z7u01197SY4kVDB7FBYqb3XNFrLIlV8N7Adx8MVKu99A0QdmnaoCN133FDlIU8ys9Q92Px9AaUjVWaRoKFLBSwaP-WF3jGckZwBIm5kScwbkQlK60CKlrOpdwobXYKR-2zXH0bDKSTl2kfx6N9uRPm1tYv0ELyRhVGWFG1-OFcN2ir9Tt8Nd4zfPPdpkj938K4Wytoc9V13iqwsYTKMOFBE4RE28Mq_Wx1XYUu1UOCKjYznHUwUQsqTeA62o_IPQapxfcz0wsIqU1ZSdqWvkxFmRXw7kKspvVjgpZEsY6Qj9105-dup8jeOVpWhdwqQ-2tyJtwCorgWlY9UycHXdX0XZlyGYFsI9HZO6oA0LsI8aaZeFEuGwZKoXmkABkyQt6db8kLgua5nowy3Jm5FfZ3aqGukZiS35E98oVSodRywnzDdugQ9KZ_Nt0UTUVzfm-qJFBuw24Ahsf4dRqappZPgA6szFZLa3u7sdFah9HbsYT_PoLLwJ9RzBtS0HdhZr5UHKpcGP96iIxl88tz56NEWNrzMB0XpHDx4wrS8Qdn9Sm3oWnfQFkC61u4vuLSPNDpqQlJfCKac75_-bxxDoB7Pv7KbAJisyBgRgaiilCOWHNF28K_x8dIT_meop4TTbBZzh1tuy_uV0z2sIu-YV9f6ZntDLcLgYCILEqFAWTuXhouFwRes1DwAluIgI9TCfGGPHJ6IFIaenY7-igcJqy2nGulL8gJqiVg8zh1HSOx9puqvYn_GJj7i_kgu7uSDsrpsGSxoKsnVRQ7SGpWmTOvT1lE0-5N6ETAop1jGFzKvW570_NlAcWXoLPnm4qYBKyEs2pAUe8V1bS5xxTL6dkkz1G2w3rAr4iEzpCj_iFHmeapcP4bdsPcHJjSikKZN5kLsgqXWCq1ofTRk2JbkMKgsjiVrU2Yno_2lWm7qYWlsUf5MG4P6fLkuYE8nIb1HGSa52zWa_s0s537IWaGoJloaH2I1rT8KXNx8-JEtyobkqLvwlgkcVHAtIiMD3owGc_3QD9kvVVFV2d104OU_dVPsP4GRvAyCGbyjdB4XheciXgTRKDJunM7_-sKr-8Bn_5vU0QRlzeJsb4bxczppYYz5zcWodcYtfbIcXbwkjLMMO0LkCL7ri59kGbwYxnmDhVfhUxqLmlTbB9_mFm7JlJ9G6ZTQB_zGxmYYQpm8iSPP4fQO-KsqSky-wqfC75K7Xyghu1jAZp5M2cpxQsclUG64MCUOEYpSODSlrkdnkkjc8O-ll8tgj9JuknAl5rqVvyRHLmIPLaKVXMrMxDdyiJWc2niIWSqrg-XPiFda9S92HNV82qyj_QBrMxAuIGNFI3Nrup7MYlVAVFGQkkAAvfbuxbOZB0SBr9a8k3q_Urb65aXnJxo-CGddeyDKe4jUXAp6CB8X9KIFyzIYBTxf7rp-4WcsnLi7_mpNK0BiTKfgeHMKD7c8cygyFft2AMnHDfVH30R9LbTTf_4HGT_UQhlhw3dUaECGrrqJ87dbNBxDK0_5DNa3NUxNIB4uo3VYRaJToV3yCmIZSPy3_REUXLNqMzodMDWsy6FUIV_mi8D2sh6C8iy0tXuAcIjDJTsiHV5w85_yQpZ-KnYJz5Bxn1CtakKf8szA1WQ0ZSxvd0KN6hr-NG-9YKtaqUddWWor9uyNA26x6oKl12GWTCjNI369OJ5s8x0at4Sf6ie8jXUljDmn4JCzwMhAg5T1cu18OIxiiqhGj7lMVHL0CH9uUVy7a6pAzvcSrx5frclHa49Ztj8Op7DOpjBQrkEh_u6Qx4b8Y1PzcjoeZHUrCBnBqKKxD17lchEntc6Mioc1WRrff-pQ1wuHd9vZMKYqMe9_REHeaEq7spmYT8_n1SK9H2zcraeKOflmlNKkDqRtj_uKJvuYIKvIx-9ZgHuXxpoQLlQpSmQvpfZsX2CN8t87TFwPaMIu3OQsA73awnFIDCfESG7RmmC93Nh0R0OlSOU5AgVwoQWzYnOJS4ondQ9DE5dF49XEG49L6zrl2Ka5BXWHwjN357Jx_X-9wyfwwOpUqzJuYrG_CayF_uKqtF-BibLMV0eYdsOdk5icTFJPbBMY_Hkjsg0YtOgchMx3CuGEgU0fK06iGm-lSzvtAlx4OP14PXxcQJDcvRfvumH-AD8NKjHYMFH8LiUPz8FVj-onWx8ikTuSPpjjAEshDp7C3KP0Ck6lloPe6cPIcgs4RB_YNfLPMErIDmxHfiIDjCDKZR5X-NBw-D1P3DIOqi9Ma1AeaMWK-AErHgwz6tz5OLySoC7DbWevXF2bUZhvQuts851HXXDtQ1Is2hiKTqSNFz1s0PRnQRuw-L_CxuNn7R16Q9fakfNCzeE8ucgtYKe8eiLPMNWTLaOr6bgrooTLxFBXWJ8Cy0HC4GQPXSi7p1IJLjk6EST1eBUYym_fq01K8-mD0R25gOVoHgKD5KQs6by0DYGRqZiyd5OwuwF97T3C-oDOoUtHsKvWzsD0b0kyMAwXfUs_kXhcA33y2hV9Hw0CX7aaNkny_9inkgze81BpwO0e_wn3iIqGvJXhRKJ8hSMWzfg7pQnZH6FmgNUXMlon3OWwOXx4UurJBdm7FfsMW49ScV3K050a-DCficbTScYho7cWTUFbAGrytJSwYSSbCdInimycblYmtDKQigwP35HQFOBvbarD6MBP9CHcbvdRaorffBEICEiMry63wxKjkfBv3HYnyGvPp0sAEFujf3kr9pRez8hGNicbw-AC_CusUX-1hZcWfNZiaI-GICBsI894oMVSAs0WHbrQWdqZq8S0SnM1USTFuatTSyrg0NjnPYmPl__-xvYsidSW3xk5jF9Rb4UG9et8wSjqv6ltd192ibcjlYRoUoaMt1T49A83fuHLCZuUYhgV9iB-H5Ysak8b7xJWJpK-ZxVqL8VdEIDLdG0a9Wjedu5NtZ_EWpYj_b2PsJ8lxhGdb58kkunYU7vXShuqfyXhHMRmBuxlHoN3_6tjqmzPtJ91oWU6zi-Ep_n3gohl4SzJbLMl00rJEadlxL7TNiLpAZXAF2ybPC1WrLx-4dRXIQeWnsoZvgrzKvmZWKoVFpWbTWFwUOgcE93z6UhPezw-S2h5fmWsHfGFqMt_Z45DR-NTQqGQAWuO3XaHCaWqpxneIqo8Z5rTrc1w4KH8dsva8AJWrrw7QHmfU6foLllGv04lR73Kf4fVIvUmDhu0FJ1uEulu24cA0AH7-uZcWxIDDQSwDeU79GlxbIYAW8hUOA7Z1iQ6xF4akOIKMqOxRuVRHi8GvmF2r2avSPaltYoHQR5G-EYNIR5bRcE1BNaxN0hhVMyX5UbKCXOkiT4HDU4ckJ_pWjZDT7HhlckpmwPrJXnf-0NT1DqzR-bMeZQQhWljfSsQ3cwThRtiBSqVYOPBIAeYVfpBBLQFvAMgANE', + priv: 'i4_fRfJesALZ5T9fxzvpXvFtAbSsyAGdaMU-DzQsr5g', + } + }, +}; + +const testVectors = [ + { + name: 'ML-DSA-44', + privateUsages: ['sign'], + publicUsages: ['verify'] + }, + { + name: 'ML-DSA-65', + privateUsages: ['sign'], + publicUsages: ['verify'] + }, + { + name: 'ML-DSA-87', + privateUsages: ['sign'], + publicUsages: ['verify'] + }, +]; + +async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { + + const jwk = keyData[name].jwk; + + const tests = [ + subtle.importKey( + 'jwk', + { + kty: jwk.kty, + alg: jwk.alg, + pub: jwk.pub, + }, + { name }, + extractable, publicUsages), + subtle.importKey( + 'jwk', + jwk, + { name }, + extractable, + privateUsages), + ]; + + const [ + publicKey, + privateKey, + ] = await Promise.all(tests); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.extractable, extractable); + assert.strictEqual(privateKey.extractable, extractable); + assert.deepStrictEqual(publicKey.usages, publicUsages); + assert.deepStrictEqual(privateKey.usages, privateUsages); + assert.strictEqual(publicKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm, privateKey.algorithm); + assert.strictEqual(privateKey.usages, privateKey.usages); + assert.strictEqual(publicKey.algorithm, publicKey.algorithm); + assert.strictEqual(publicKey.usages, publicKey.usages); + + if (extractable) { + // Test the round trip + const [ + pubJwk, + pvtJwk, + ] = await Promise.all([ + subtle.exportKey('jwk', publicKey), + subtle.exportKey('jwk', privateKey), + ]); + + assert.deepStrictEqual(pubJwk.key_ops, publicUsages); + assert.strictEqual(pubJwk.ext, true); + assert.strictEqual(pubJwk.kty, 'AKP'); + assert.strictEqual(pubJwk.pub, jwk.pub); + + assert.deepStrictEqual(pvtJwk.key_ops, privateUsages); + assert.strictEqual(pvtJwk.ext, true); + assert.strictEqual(pvtJwk.kty, 'AKP'); + assert.strictEqual(pvtJwk.pub, jwk.pub); + assert.strictEqual(pvtJwk.priv, jwk.priv); + + assert.strictEqual(pubJwk.alg, jwk.alg); + assert.strictEqual(pvtJwk.alg, jwk.alg); + } else { + await assert.rejects( + subtle.exportKey('jwk', publicKey), { + message: /key is not extractable/ + }); + await assert.rejects( + subtle.exportKey('jwk', privateKey), { + message: /key is not extractable/ + }); + } + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, use: 'enc' }, + { name }, + extractable, + privateUsages), + { message: 'Invalid JWK "use" Parameter' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, pub: undefined }, + { name }, + extractable, + privateUsages), + { message: 'Invalid JWK' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, priv: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }, // Public vs private mismatch + { name }, + extractable, + privateUsages), + { message: 'Invalid keyData' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, kty: 'OKP' }, + { name }, + extractable, + privateUsages), + { message: 'Invalid JWK "kty" Parameter' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk }, + { name }, + extractable, + publicUsages), // Invalid for a private key + { message: /Unsupported key usage/ }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, ext: false }, + { name }, + true, + privateUsages), + { message: 'JWK "ext" Parameter and extractable mismatch' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, priv: undefined }, + { name }, + extractable, + privateUsages), // Invalid for a public key + { message: /Unsupported key usage/ }); + + for (const alg of [undefined, name === 'ML-DSA-44' ? 'ML-DSA-87' : 'ML-DSA-44']) { + await assert.rejects( + subtle.importKey( + 'jwk', + { kty: jwk.kty, pub: jwk.pub, alg }, + { name }, + extractable, + publicUsages), + { message: 'JWK "alg" Parameter and algorithm name mismatch' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, alg }, + { name }, + extractable, + privateUsages), + { message: 'JWK "alg" Parameter and algorithm name mismatch' }); + } + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk }, + { name }, + extractable, + [/* empty usages */]), + { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { kty: jwk.kty, /* missing pub */ alg: jwk.alg }, + { name }, + extractable, + publicUsages), + { name: 'DataError', message: 'Invalid keyData' }); +} + +async function testImportRawPublic({ name, publicUsages }, extractable) { + const jwk = keyData[name].jwk; + const pub = Buffer.from(jwk.pub, 'base64url'); + + const publicKey = await subtle.importKey( + 'raw-public', + pub, + { name }, + extractable, publicUsages); + + assert.strictEqual(publicKey.type, 'public'); + assert.deepStrictEqual(publicKey.usages, publicUsages); + assert.strictEqual(publicKey.algorithm.name, name); + assert.strictEqual(publicKey.algorithm, publicKey.algorithm); + assert.strictEqual(publicKey.usages, publicKey.usages); + assert.strictEqual(publicKey.extractable, extractable); + + if (extractable) { + const value = await subtle.exportKey('raw-public', publicKey); + assert.deepStrictEqual(Buffer.from(value), pub); + + await assert.rejects(subtle.exportKey('raw', publicKey), { + name: 'NotSupportedError', + message: `Unable to export ${publicKey.algorithm.name} public key using raw format`, + }); + } + + await assert.rejects( + subtle.importKey( + 'raw-public', + pub.subarray(0, pub.byteLength - 1), + { name }, + extractable, publicUsages), + { message: 'Invalid keyData' }); + + await assert.rejects( + subtle.importKey( + 'raw-public', + pub, + { name: name === 'ML-DSA-44' ? 'ML-DSA-65' : 'ML-DSA-44' }, + extractable, publicUsages), + { message: 'Invalid keyData' }); +} + +async function testImportRawSeed({ name, privateUsages }, extractable) { + const jwk = keyData[name].jwk; + const seed = Buffer.from(jwk.priv, 'base64url'); + + const privateKey = await subtle.importKey( + 'raw-seed', + seed, + { name }, + extractable, privateUsages); + + assert.strictEqual(privateKey.type, 'private'); + assert.deepStrictEqual(privateKey.usages, privateUsages); + assert.strictEqual(privateKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm, privateKey.algorithm); + assert.strictEqual(privateKey.usages, privateKey.usages); + assert.strictEqual(privateKey.extractable, extractable); + + if (extractable) { + const value = await subtle.exportKey('raw-seed', privateKey); + assert.deepStrictEqual(Buffer.from(value), seed); + } + + await assert.rejects( + subtle.importKey( + 'raw-seed', + seed.subarray(0, 30), + { name }, + extractable, + privateUsages), + { message: 'Invalid keyData' }); +} + +(async function() { + const tests = []; + for (const vector of testVectors) { + for (const extractable of [true, false]) { + tests.push(testImportJwk(vector, extractable)); + tests.push(testImportRawSeed(vector, extractable)); + tests.push(testImportRawPublic(vector, extractable)); + } + } + await Promise.all(tests); +})().then(common.mustCall()); + +(async function() { + const alg = 'ML-DSA-44'; + const pub = Buffer.from(keyData[alg].jwk.pub, 'base64url'); + await assert.rejects(subtle.importKey('raw', pub, alg, false, []), { + name: 'NotSupportedError', + message: 'Unable to import ML-DSA-44 using raw format', + }); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index a60463bdb5f139..ad8960887b9a22 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -6,6 +6,8 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasOpenSSL } = require('../common/crypto'); + const assert = require('assert'); const { types: { isCryptoKey } } = require('util'); const { @@ -157,6 +159,18 @@ const vectors = { }, }; +if (hasOpenSSL(3, 5)) { + for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { + vectors[name] = { + result: 'CryptoKeyPair', + usages: [ + 'sign', + 'verify', + ], + }; + } +} + // Test invalid algorithms { async function test(algorithm) { @@ -683,3 +697,46 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); Promise.all(tests).then(common.mustCall()); } + +// Test ML-DSA Key Generation +if (hasOpenSSL(3, 5)) { + async function test( + name, + privateUsages, + publicUsages = privateUsages) { + + let usages = privateUsages; + if (publicUsages !== privateUsages) + usages = usages.concat(publicUsages); + + const { publicKey, privateKey } = await subtle.generateKey({ + name, + }, true, usages); + + assert(publicKey); + assert(privateKey); + assert(isCryptoKey(publicKey)); + assert(isCryptoKey(privateKey)); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.toString(), '[object CryptoKey]'); + assert.strictEqual(privateKey.toString(), '[object CryptoKey]'); + assert.strictEqual(publicKey.extractable, true); + assert.strictEqual(privateKey.extractable, true); + assert.deepStrictEqual(publicKey.usages, publicUsages); + assert.deepStrictEqual(privateKey.usages, privateUsages); + assert.strictEqual(publicKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm, privateKey.algorithm); + assert.strictEqual(privateKey.usages, privateKey.usages); + assert.strictEqual(publicKey.algorithm, publicKey.algorithm); + assert.strictEqual(publicKey.usages, publicKey.usages); + } + + const kTests = ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']; + + const tests = kTests.map((name) => test(name, ['sign'], ['verify'])); + + Promise.all(tests).then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-sign-verify-eddsa.js b/test/parallel/test-webcrypto-sign-verify-eddsa.js index 75d73a49ddea36..6ff432ac23063b 100644 --- a/test/parallel/test-webcrypto-sign-verify-eddsa.js +++ b/test/parallel/test-webcrypto-sign-verify-eddsa.js @@ -241,10 +241,10 @@ async function testSign({ name, await subtle.verify({ name: 'Ed448', context: Buffer.alloc(0) }, publicKey, sig, vector.data), true); await assert.rejects(subtle.sign({ name: 'Ed448', context: Buffer.alloc(1) }, privateKey, vector.data), { - message: /Non zero-length context is not supported/ + message: /Non zero-length Ed448Params\.context is not supported/ }); await assert.rejects(subtle.verify({ name: 'Ed448', context: Buffer.alloc(1) }, publicKey, sig, vector.data), { - message: /Non zero-length context is not supported/ + message: /Non zero-length Ed448Params\.context is not supported/ }); }).then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js new file mode 100644 index 00000000000000..5bd21f9c2735b4 --- /dev/null +++ b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js @@ -0,0 +1,228 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasOpenSSL } = require('../common/crypto'); + +if (!hasOpenSSL(3, 5)) + common.skip('requires OpenSSL >= 3.5'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { subtle } = globalThis.crypto; + +const vectors = require('../fixtures/crypto/ml-dsa')(); + +async function testVerify({ name, + publicKeyPem, + privateKeyPem, + signature, + data }) { + const [ + publicKey, + noVerifyPublicKey, + privateKey, + hmacKey, + rsaKeys, + ecKeys, + ] = await Promise.all([ + crypto.createPublicKey(publicKeyPem) + .toCryptoKey(name, false, ['verify']), + crypto.createPublicKey(publicKeyPem) + .toCryptoKey(name, false, [ /* No usages */ ]), + crypto.createPrivateKey(privateKeyPem) + .toCryptoKey(name, false, ['sign']), + subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign']), + subtle.generateKey( + { + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + false, + ['sign']), + subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-256' + }, + false, + ['sign']), + ]); + + assert(await subtle.verify({ name }, publicKey, signature, data)); + + // Test verification with altered buffers + const copy = Buffer.from(data); + const sigcopy = Buffer.from(signature); + const p = subtle.verify({ name }, publicKey, sigcopy, copy); + copy[0] = 255 - copy[0]; + sigcopy[0] = 255 - sigcopy[0]; + assert(await p); + + // Test failure when using wrong key + await assert.rejects( + subtle.verify({ name }, privateKey, signature, data), { + message: /Unable to use this key to verify/ + }); + + await assert.rejects( + subtle.verify({ name }, noVerifyPublicKey, signature, data), { + message: /Unable to use this key to verify/ + }); + + // Test failure when using the wrong algorithms + await assert.rejects( + subtle.verify({ name }, hmacKey, signature, data), { + message: /Unable to use this key to verify/ + }); + + await assert.rejects( + subtle.verify({ name }, rsaKeys.publicKey, signature, data), { + message: /Unable to use this key to verify/ + }); + + await assert.rejects( + subtle.verify({ name }, ecKeys.publicKey, signature, data), { + message: /Unable to use this key to verify/ + }); + + // Test failure when signature is altered + { + const copy = Buffer.from(signature); + copy[0] = 255 - copy[0]; + assert(!(await subtle.verify( + { name }, + publicKey, + copy, + data))); + assert(!(await subtle.verify( + { name }, + publicKey, + copy.slice(1), + data))); + } + + // Test failure when data is altered + { + const copy = Buffer.from(data); + copy[0] = 255 - copy[0]; + assert(!(await subtle.verify({ name }, publicKey, signature, copy))); + } +} + +async function testSign({ name, + publicKeyPem, + privateKeyPem, + signature, + data }) { + const [ + publicKey, + privateKey, + hmacKey, + rsaKeys, + ecKeys, + ] = await Promise.all([ + crypto.createPublicKey(publicKeyPem) + .toCryptoKey(name, false, ['verify']), + crypto.createPrivateKey(privateKeyPem) + .toCryptoKey(name, false, ['sign']), + subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign']), + subtle.generateKey( + { + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + false, + ['sign']), + subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-256' + }, + false, + ['sign']), + ]); + + { + const sig = await subtle.sign({ name }, privateKey, data); + assert.strictEqual(sig.byteLength, signature.byteLength); + assert(await subtle.verify({ name }, publicKey, sig, data)); + } + + { + const copy = Buffer.from(data); + const p = subtle.sign({ name }, privateKey, copy); + copy[0] = 255 - copy[0]; + const sig = await p; + assert(await subtle.verify({ name }, publicKey, sig, data)); + } + + // Test failure when using wrong key + await assert.rejects( + subtle.sign({ name }, publicKey, data), { + message: /Unable to use this key to sign/ + }); + + // Test failure when using the wrong algorithms + await assert.rejects( + subtle.sign({ name }, hmacKey, data), { + message: /Unable to use this key to sign/ + }); + + await assert.rejects( + subtle.sign({ name }, rsaKeys.privateKey, data), { + message: /Unable to use this key to sign/ + }); + + await assert.rejects( + subtle.sign({ name }, ecKeys.privateKey, data), { + message: /Unable to use this key to sign/ + }); +} + +(async function() { + const variations = []; + + vectors.forEach((vector) => { + variations.push(testVerify(vector)); + variations.push(testSign(vector)); + }); + + await Promise.all(variations); +})().then(common.mustCall()); + +// ContextParams context not supported +{ + const vector = vectors[0]; + const name = vector.name; + const publicKey = crypto.createPublicKey(vector.publicKeyPem) + .toCryptoKey(vector.name, false, ['verify']); + const privateKey = crypto.createPrivateKey(vector.privateKeyPem) + .toCryptoKey(vector.name, false, ['sign']); + + (async () => { + const sig = await subtle.sign({ name, context: Buffer.alloc(0) }, privateKey, vector.data); + assert.strictEqual( + await subtle.verify({ name, context: Buffer.alloc(0) }, publicKey, sig, vector.data), true); + + await assert.rejects(subtle.sign({ name, context: Buffer.alloc(1) }, privateKey, vector.data), { + message: /Non zero-length ContextParams\.context is not supported/ + }); + await assert.rejects(subtle.verify({ name, context: Buffer.alloc(1) }, publicKey, sig, vector.data), { + message: /Non zero-length ContextParams\.context is not supported/ + }); + })().then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-sign-verify.js b/test/parallel/test-webcrypto-sign-verify.js index 1b2b40152f88d2..3b56e3ad041b89 100644 --- a/test/parallel/test-webcrypto-sign-verify.js +++ b/test/parallel/test-webcrypto-sign-verify.js @@ -5,6 +5,8 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasOpenSSL } = require('../common/crypto'); + const assert = require('assert'); const { subtle } = globalThis.crypto; @@ -147,3 +149,25 @@ const { subtle } = globalThis.crypto; test('hello world').then(common.mustCall()); } } + +// Test Sign/Verify ML-DSA +if (hasOpenSSL(3, 5)) { + async function test(name, data) { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name, + }, true, ['sign', 'verify']); + + const signature = await subtle.sign({ + name, + }, privateKey, ec.encode(data)); + + assert(await subtle.verify({ + name, + }, publicKey, signature, ec.encode(data))); + } + + test('ML-DSA-44', 'hello world').then(common.mustCall()); + test('ML-DSA-65', 'hello world').then(common.mustCall()); + test('ML-DSA-87', 'hello world').then(common.mustCall()); +} diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index b6d0a69d46743b..11f0d13e6b03ba 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -120,6 +120,7 @@ const customTypesMap = { 'EcdsaParams': 'webcrypto.html#class-ecdsaparams', 'RsaPssParams': 'webcrypto.html#class-rsapssparams', 'Ed448Params': 'webcrypto.html#class-ed448params', + 'ContextParams': 'webcrypto.html#class-contextparams', 'dgram.Socket': 'dgram.html#class-dgramsocket', From f4fbcca5ce77813a04cda44e6b8b1bfd15005e5a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 2 Mar 2025 09:45:13 +0100 Subject: [PATCH 049/111] crypto: add SubtleCrypto.supports feature detection in Web Cryptography PR-URL: https://github.com/nodejs/node/pull/59365 Reviewed-By: James M Snell Reviewed-By: Ethan Arrowood Reviewed-By: Yagiz Nizipli Reviewed-By: Joyee Cheung --- doc/api/webcrypto.md | 96 +++++++ lib/internal/crypto/hkdf.js | 1 + lib/internal/crypto/pbkdf2.js | 1 + lib/internal/crypto/webcrypto.js | 141 +++++++++++ test/fixtures/webcrypto/supports-level-2.mjs | 234 ++++++++++++++++++ .../webcrypto/supports-modern-algorithms.mjs | 26 ++ .../webcrypto/supports-secure-curves.mjs | 38 +++ test/parallel/test-webcrypto-constructors.js | 7 + test/parallel/test-webcrypto-supports.mjs | 54 ++++ 9 files changed, 598 insertions(+) create mode 100644 test/fixtures/webcrypto/supports-level-2.mjs create mode 100644 test/fixtures/webcrypto/supports-modern-algorithms.mjs create mode 100644 test/fixtures/webcrypto/supports-secure-curves.mjs create mode 100644 test/parallel/test-webcrypto-supports.mjs diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index ad489cf77c74ec..c16140b17f6861 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -101,6 +101,10 @@ Key Formats: * `'raw-secret'` * `'raw-seed'` +Methods: + +* [`SubtleCrypto.supports()`][] + ## Secure Curves in the Web Cryptography API > Stability: 1.1 - Active development @@ -387,6 +391,76 @@ async function digest(data, algorithm = 'SHA-512') { } ``` +### Checking for runtime algorithm support + +[`SubtleCrypto.supports()`][] allows feature detection in Web Crypto API, +which can be used to detect whether a given algorithm identifier +(including its parameters) is supported for the given operation. + +This example derives a key from a password using Argon2, if available, +or PBKDF2, otherwise; and then encrypts and decrypts some text with it +using AES-OCB, if available, and AES-GCM, otherwise. + +```mjs +const { SubtleCrypto, crypto } = globalThis; + +const password = 'correct horse battery staple'; +const derivationAlg = + SubtleCrypto.supports?.('importKey', 'Argon2id') ? + 'Argon2id' : + 'PBKDF2'; +const encryptionAlg = + SubtleCrypto.supports?.('importKey', 'AES-OCB') ? + 'AES-OCB' : + 'AES-GCM'; +const passwordKey = await crypto.subtle.importKey( + derivationAlg === 'Argon2id' ? 'raw-secret' : 'raw', + new TextEncoder().encode(password), + derivationAlg, + false, + ['deriveKey'], +); +const nonce = crypto.getRandomValues(new Uint8Array(16)); +const derivationParams = + derivationAlg === 'Argon2id' ? + { + nonce, + parallelism: 4, + memory: 2 ** 21, + passes: 1, + } : + { + salt: nonce, + iterations: 100_000, + hash: 'SHA-256', + }; +const key = await crypto.subtle.deriveKey( + { + name: derivationAlg, + ...derivationParams, + }, + passwordKey, + { + name: encryptionAlg, + length: 256, + }, + false, + ['encrypt', 'decrypt'], +); +const plaintext = 'Hello, world!'; +const iv = crypto.getRandomValues(new Uint8Array(16)); +const encrypted = await crypto.subtle.encrypt( + { name: encryptionAlg, iv }, + key, + new TextEncoder().encode(plaintext), +); +const decrypted = new TextDecoder().decode(await crypto.subtle.decrypt( + { name: encryptionAlg, iv }, + key, + encrypted, +)); +``` + ## Algorithm matrix The table details the algorithms supported by the Node.js Web Crypto API @@ -591,6 +665,27 @@ added: v15.0.0 added: v15.0.0 --> +### Static method: `SubtleCrypto.supports(operation, algorithm[, lengthOrAdditionalAlgorithm])` + + + +> Stability: 1.1 - Active development + + + +* `operation` {string} "encrypt", "decrypt", "sign", "verify", "digest", "generateKey", "deriveKey", "deriveBits", "importKey", "exportKey", "wrapKey", or "unwrapKey" +* `algorithm` {string|Algorithm} +* `lengthOrAdditionalAlgorithm` {null|number|string|Algorithm|undefined} Depending on the operation this is either ignored, the value of the length argument when operation is "deriveBits", the algorithm of key to be derived when operation is "deriveKey", the algorithm of key to be exported before wrapping when operation is "wrapKey", or the algorithm of key to be imported after unwrapping when operation is "unwrapKey". **Default:** `null` when operation is "deriveBits", `undefined` otherwise. +* Returns: {boolean} Indicating whether the implementation supports the given operation + + + +Allows feature detection in Web Crypto API, +which can be used to detect whether a given algorithm identifier +(including its parameters) is supported for the given operation. + ### `subtle.decrypt(algorithm, key, data)` -* `algorithm` {string|Algorithm} +* `algorithm` {string|Algorithm|CShakeParams} * `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. @@ -812,6 +823,8 @@ with an {ArrayBuffer} containing the computed digest. If `algorithm` is provided as a {string}, it must be one of: +* `'cSHAKE128'`[^modern-algos] +* `'cSHAKE256'`[^modern-algos] * `'SHA-1'` * `'SHA-256'` * `'SHA-384'` @@ -1423,6 +1436,53 @@ the message. The Node.js Web Crypto API implementation only supports zero-length context which is equivalent to not providing context at all. +### Class: `CShakeParams` + + + +#### `cShakeParams.customization` + + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} + +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.functionName` + + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} + +The `functionName` member represents represents the function name, used by NIST to define +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` + + + +* Type: {number} represents the requested output length in bits. + +#### `cShakeParams.name` + + + +* Type: {string} Must be `'cSHAKE128'`[^modern-algos] or `'cSHAKE256'`[^modern-algos] + ### Class: `EcdhKeyDeriveParams` * Type: {string|Algorithm} @@ -1530,6 +1549,9 @@ If represented as a {string}, the value must be one of: * `'SHA-256'` * `'SHA-384'` * `'SHA-512'` +* `'SHA3-256'`[^modern-algos] +* `'SHA3-384'`[^modern-algos] +* `'SHA3-512'`[^modern-algos] If represented as an {Algorithm}, the object's `name` property must be one of the above listed values. @@ -1649,6 +1671,10 @@ added: v15.0.0 * Type: {string|Algorithm} @@ -1659,6 +1685,9 @@ If represented as a {string}, the value must be one of: * `'SHA-256'` * `'SHA-384'` * `'SHA-512'` +* `'SHA3-256'`[^modern-algos] +* `'SHA3-384'`[^modern-algos] +* `'SHA3-512'`[^modern-algos] If represented as an {Algorithm}, the object's `name` property must be one of the above listed values. @@ -1705,6 +1734,10 @@ added: v15.0.0 * Type: {string|Algorithm} @@ -1715,6 +1748,9 @@ If represented as a {string}, the value must be one of: * `'SHA-256'` * `'SHA-384'` * `'SHA-512'` +* `'SHA3-256'`[^modern-algos] +* `'SHA3-384'`[^modern-algos] +* `'SHA3-512'`[^modern-algos] If represented as an {Algorithm}, the object's `name` property must be one of the above listed values. @@ -1780,6 +1816,10 @@ added: v15.0.0 * Type: {string|Algorithm} @@ -1790,6 +1830,9 @@ If represented as a {string}, the value must be one of: * `'SHA-256'` * `'SHA-384'` * `'SHA-512'` +* `'SHA3-256'`[^modern-algos] +* `'SHA3-384'`[^modern-algos] +* `'SHA3-512'`[^modern-algos] If represented as an {Algorithm}, the object's `name` property must be one of the above listed values. @@ -1838,6 +1881,10 @@ added: v15.0.0 * Type: {string|Algorithm} @@ -1848,6 +1895,9 @@ If represented as a {string}, the value must be one of: * `'SHA-256'` * `'SHA-384'` * `'SHA-512'` +* `'SHA3-256'`[^modern-algos] +* `'SHA3-384'`[^modern-algos] +* `'SHA3-512'`[^modern-algos] If represented as an {Algorithm}, the object's `name` property must be one of the above listed values. @@ -1890,6 +1940,10 @@ added: v15.0.0 * Type: {string|Algorithm} @@ -1900,6 +1954,9 @@ If represented as a {string}, the value must be one of: * `'SHA-256'` * `'SHA-384'` * `'SHA-512'` +* `'SHA3-256'`[^modern-algos] +* `'SHA3-384'`[^modern-algos] +* `'SHA3-512'`[^modern-algos] If represented as an {Algorithm}, the object's `name` property must be one of the above listed values. @@ -1965,6 +2022,10 @@ added: v15.0.0 * Type: {string|Algorithm} @@ -1975,6 +2036,9 @@ If represented as a {string}, the value must be one of: * `'SHA-256'` * `'SHA-384'` * `'SHA-512'` +* `'SHA3-256'`[^modern-algos] +* `'SHA3-384'`[^modern-algos] +* `'SHA3-512'`[^modern-algos] If represented as an {Algorithm}, the object's `name` property must be one of the above listed values. diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index bb032a133c28a1..e4d94da1c5ee96 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -212,6 +212,12 @@ async function asyncDigest(algorithm, data) { // Fall through case 'SHA-512': // Fall through + case 'SHA3-256': + // Fall through + case 'SHA3-384': + // Fall through + case 'SHA3-512': + // Fall through case 'cSHAKE128': // Fall through case 'cSHAKE256': diff --git a/lib/internal/crypto/hashnames.js b/lib/internal/crypto/hashnames.js index 70fddd6aaf7306..7a625c47e2f4b2 100644 --- a/lib/internal/crypto/hashnames.js +++ b/lib/internal/crypto/hashnames.js @@ -17,7 +17,7 @@ const kHashContextJwkHmac = 6; // make it easier in the code. const kHashNames = { - sha1: { + 'sha1': { [kHashContextNode]: 'sha1', [kHashContextWebCrypto]: 'SHA-1', [kHashContextJwkRsa]: 'RS1', @@ -25,7 +25,7 @@ const kHashNames = { [kHashContextJwkRsaOaep]: 'RSA-OAEP', [kHashContextJwkHmac]: 'HS1', }, - sha256: { + 'sha256': { [kHashContextNode]: 'sha256', [kHashContextWebCrypto]: 'SHA-256', [kHashContextJwkRsa]: 'RS256', @@ -33,7 +33,7 @@ const kHashNames = { [kHashContextJwkRsaOaep]: 'RSA-OAEP-256', [kHashContextJwkHmac]: 'HS256', }, - sha384: { + 'sha384': { [kHashContextNode]: 'sha384', [kHashContextWebCrypto]: 'SHA-384', [kHashContextJwkRsa]: 'RS384', @@ -41,7 +41,7 @@ const kHashNames = { [kHashContextJwkRsaOaep]: 'RSA-OAEP-384', [kHashContextJwkHmac]: 'HS384', }, - sha512: { + 'sha512': { [kHashContextNode]: 'sha512', [kHashContextWebCrypto]: 'SHA-512', [kHashContextJwkRsa]: 'RS512', @@ -49,14 +49,26 @@ const kHashNames = { [kHashContextJwkRsaOaep]: 'RSA-OAEP-512', [kHashContextJwkHmac]: 'HS512', }, - shake128: { + 'shake128': { [kHashContextNode]: 'shake128', [kHashContextWebCrypto]: 'cSHAKE128', }, - shake256: { + 'shake256': { [kHashContextNode]: 'shake256', [kHashContextWebCrypto]: 'cSHAKE256', }, + 'sha3-256': { + [kHashContextNode]: 'sha3-256', + [kHashContextWebCrypto]: 'SHA3-256', + }, + 'sha3-384': { + [kHashContextNode]: 'sha3-384', + [kHashContextWebCrypto]: 'SHA3-384', + }, + 'sha3-512': { + [kHashContextNode]: 'sha3-512', + [kHashContextWebCrypto]: 'SHA3-512', + }, }; { diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 170292f0bbae65..4a78c45c70d84d 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -67,18 +67,6 @@ async function hmacGenerateKey(algorithm, extractable, keyUsages) { extractable); } -function getAlgorithmName(hash) { - switch (hash) { - case 'SHA-1': // Fall through - case 'SHA-256': // Fall through - case 'SHA-384': // Fall through - case 'SHA-512': // Fall through - return `HS${hash.slice(4)}`; - default: - throw lazyDOMException('Unsupported digest algorithm', 'DataError'); - } -} - function hmacImportKey( format, keyData, @@ -126,7 +114,9 @@ function hmacImportKey( } if (keyData.alg !== undefined) { - if (keyData.alg !== getAlgorithmName(algorithm.hash.name)) + const expected = + normalizeHashName(algorithm.hash.name, normalizeHashName.kContextJwkHmac); + if (expected && keyData.alg !== expected) throw lazyDOMException( 'JWK "alg" does not match the requested algorithm', 'DataError'); diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 4ecbaa89c65d93..dd0669df4ed032 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -281,7 +281,7 @@ function rsaImportKey( algorithm.name === 'RSA-PSS' ? normalizeHashName.kContextJwkRsaPss : normalizeHashName.kContextJwkRsaOaep); - if (keyData.alg !== expected) + if (expected && keyData.alg !== expected) throw lazyDOMException( 'JWK "alg" does not match the requested algorithm', 'DataError'); diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index adbafed0233a11..ce89fc7a6fc41a 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -287,6 +287,9 @@ const experimentalAlgorithms = ObjectEntries({ }, 'cSHAKE128': { digest: 'CShakeParams' }, 'cSHAKE256': { digest: 'CShakeParams' }, + 'SHA3-256': { digest: null }, + 'SHA3-384': { digest: null }, + 'SHA3-512': { digest: null }, }); for (const { 0: algorithm, 1: nid } of [ @@ -552,15 +555,28 @@ function getBlockSize(name) { // Fall through case 'SHA-512': return 1024; + case 'SHA3-256': + return 1088; + case 'SHA3-384': + return 832; + case 'SHA3-512': + return 576; } } function getDigestSizeInBytes(name) { switch (name) { - case 'SHA-1': return 20; - case 'SHA-256': return 32; - case 'SHA-384': return 48; - case 'SHA-512': return 64; + case 'SHA-1': + return 20; + case 'SHA-256': // Fall through + case 'SHA3-256': + return 32; + case 'SHA-384': // Fall through + case 'SHA3-384': + return 48; + case 'SHA-512': // Fall through + case 'SHA3-512': + return 64; } } diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index db05e7747e343d..742204b16089c1 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -452,21 +452,27 @@ async function exportKeyJWK(key) { ext: key.extractable, }; switch (key.algorithm.name) { - case 'RSASSA-PKCS1-v1_5': - parameters.alg = normalizeHashName( + case 'RSASSA-PKCS1-v1_5': { + const alg = normalizeHashName( key.algorithm.hash.name, normalizeHashName.kContextJwkRsa); + if (alg) parameters.alg = alg; break; - case 'RSA-PSS': - parameters.alg = normalizeHashName( + } + case 'RSA-PSS': { + const alg = normalizeHashName( key.algorithm.hash.name, normalizeHashName.kContextJwkRsaPss); + if (alg) parameters.alg = alg; break; - case 'RSA-OAEP': - parameters.alg = normalizeHashName( + } + case 'RSA-OAEP': { + const alg = normalizeHashName( key.algorithm.hash.name, normalizeHashName.kContextJwkRsaOaep); + if (alg) parameters.alg = alg; break; + } case 'ECDSA': // Fall through case 'ECDH': @@ -496,11 +502,13 @@ async function exportKeyJWK(key) { parameters.alg = require('internal/crypto/aes') .getAlgorithmName(key.algorithm.name, key.algorithm.length); break; - case 'HMAC': - parameters.alg = normalizeHashName( + case 'HMAC': { + const alg = normalizeHashName( key.algorithm.hash.name, normalizeHashName.kContextJwkHmac); + if (alg) parameters.alg = alg; break; + } default: return undefined; } diff --git a/test/fixtures/crypto/ecdsa.js b/test/fixtures/crypto/ecdsa.js index 4b3539edb1fc89..b8827b24d41965 100644 --- a/test/fixtures/crypto/ecdsa.js +++ b/test/fixtures/crypto/ecdsa.js @@ -2,6 +2,12 @@ module.exports = function() { const pkcs8 = { + 'P-256': Buffer.from( + '308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b020101' + + '04205119f596e17f3c55a170b674c96e6bea7269dc9240047c76c841ff49106c4989' + + 'a14403420004b1d1e7da708dfadf90bc013a4009184bdb3f9065078f5598f6ad2638' + + '65a387e249ebf1a514ad8c943635a66d8acd64ebf2c876e55448813f10026a5e1f0a' + + '9817', 'hex'), 'P-384': Buffer.from( '3081b6020100301006072a8648ce3d020106052b8104002204819e30819b02010104' + '3002a9a0d899efa87e7564110907e9d82c21bd6265a37abd9a6fdb0f80ec844dd3a1' + @@ -21,6 +27,10 @@ module.exports = function() { } const spki = { + 'P-256': Buffer.from( + '3059301306072a8648ce3d020106082a8648ce3d03010703420004b1d1e7da708dfa' + + 'df90bc013a4009184bdb3f9065078f5598f6ad263865a387e249ebf1a514ad8c9436' + + '35a66d8acd64ebf2c876e55448813f10026a5e1f0a9817', 'hex'), 'P-384': Buffer.from( '3076301006072a8648ce3d020106052b81040022036200041d319d692dca5f5754ba' + '7b32c11642c6d8d2b4fb8249c3f214d71e90b5252966d97f7beb1faab1e4f3e26055' + @@ -45,6 +55,36 @@ module.exports = function() { // For verification tests. const signatures = { + 'P-256': { + 'SHA-1': Buffer.from( + 'a6f9548fa945bca4ce6e41d4099623b409e21070c03179161867d3ca411ee4f39e' + + '51ba999723e609d0abb2cc48450e886544ca400bae09841651211b43907672', + 'hex'), + 'SHA-256': Buffer.from( + '15f4adec59122298cd1642ee9104748c705dc6a3f70ed8222a52ee0420a35ce4c8' + + '293db29689acf24f6009b98df0cb8ec1aab17f8ad448a8c0e86843dfa824a3', + 'hex'), + 'SHA-384': Buffer.from( + 'b860be71578d07ad137c2a75ac29528114b23f58021b2c2875ac1374ed3143a928' + + '4efd9402b950cfc738fc9df4e33d917594f2078b96a02d5fbf17efe94e72a6', + 'hex'), + 'SHA-512': Buffer.from( + 'b6a0a14d7e4bc6dd2eda82c9234f174b670b60c8f7d101f68fdf5889e02373b025' + + 'dcbc4c82f2929b8e06c68535da98e38fe399c53a814b097935581ef21535eb', + 'hex'), + 'SHA3-256': Buffer.from( + 'f6a48eb5557f484ed0c3e4b5c78a3cf497cbd346db06a4165d429248aa2cc51a69' + + '747d09f57af145469a8b607a9b8b9709629d74e8f5ca337c6ddc581b6f6103', + 'hex'), + 'SHA3-384': Buffer.from( + '777785978eb59da32888554dc7fd62d1ba1a3033cddaa8c36b8f3dcea8f85e1c8e' + + '6db26f509747bd144dfa9436784bf4abbcaa6abcf1ecc09cea3b921d46738c', + 'hex'), + 'SHA3-512': Buffer.from( + '0f01c2083b5dd7fccb2784563f88cd9a815d570a1690695e426643ab725780760d' + + 'e972e26e18d67f5557be89f17b4cd0065ce2937de299bdb2e972ebf7635084', + 'hex') + }, 'P-384': { 'SHA-1': Buffer.from( '65fe070ec3eac35250d00b9ee6db4d2dadd5f3bbb9c495c8671d2a0d2b99149fb2' + @@ -61,7 +101,19 @@ module.exports = function() { 'SHA-512': Buffer.from( '72fbdb369fd34c1c54264d07f4facd69b02e4206f8a8bb259b882a305c56fde2d3' + '5107e493c53cd6b4af0b31306f4d03fd43cfc762a1030e17a3d775453a1212b142' + - '9f7b3d93066a5f42a10b138cd177dc09616e827d598822d78d4627b754e6', 'hex') + '9f7b3d93066a5f42a10b138cd177dc09616e827d598822d78d4627b754e6', 'hex'), + 'SHA3-256': Buffer.from( + '0b07c078be30fa5925a307d6fc559c5f398e63fb5d007d6b24a834847f2d3d18d5' + + 'b5e840711c52a7bc6626c3ced93301e873c013a706f6b297c12cc6d47a71e0529e' + + '719f43957de9995621d3cb0217469adaa6fd3135470771d0aa9d05d7a9c6', 'hex'), + 'SHA3-384': Buffer.from( + '2f36e8b04af46f68ef900c2720e3518b06f5440865d44072bbad5d62288c575042' + + 'b183a372acd70328c738668dcecb9866801462d62df3c35450fdc6c95433103fcd' + + 'c77999b640e3f92bd4e9be6e27ab129d1bc4f0b2a4c829388666920892d3', 'hex'), + 'SHA3-512': Buffer.from( + '32a951e886c33ac57a008efe9643bc92aa3ece9521d115e0c7240caecf124d1f7c' + + 'dcba7fabb9ad5202e04f7aa591ab01ed3f060f04f493e4f24430fe8159200612f0' + + '2849108b8be6edc8494c328097ad9265928efe5cb9d91be2f013ee17ee4e', 'hex') }, 'P-521': { 'SHA-1': Buffer.from( @@ -87,12 +139,30 @@ module.exports = function() { '5ae23cfcca0aad78f6b6dee6b4718b95d0d1a715aa3378470e50b516c18e0f3305' + '01f0071e6a32867fa70f695cd39c4e87e142b9e4134d38740bd6fee354a575167e' + '13524e94832637910fe11e53a85fb21b91adb81bb1779c4e2b8bc87c717dc35084', + 'hex'), + 'SHA3-256': Buffer.from( + '00463679f47a4c705e03447360dcf34d1743e0d4b2591cc66832a6bc80d92e538c' + + '169a1fd330f98e7235ca7fec7e16ac44fb13095b8edf2c76b75c4845177d59e425' + + '0127c4359f6a4c9ccb63e7a9ff8122c0b4a8b7408e28c96817ecc3baf8c559c413' + + 'c3bb580447dec9f52139b2afde369cd51730f050bc94137556ae137f0509464219', + 'hex'), + 'SHA3-384': Buffer.from( + '01969a4db0888bc067a68a31fe5d0fc97e0b701f570565f7b25cb27707c6f020ff' + + '680f8553ec5c2d6885e9e91b39262ed1bde375525eb13fdf12089b7939c7689735' + + '0101c8b8d1129a217e8e956bef78cf7b9a0458523b04ac8e0b84ce73d54326f7a8' + + '704ee42fe183f3ef79d83e676f34dc5476e2342641a5b973d3d94e8503676fbbc5', + 'hex'), + 'SHA3-512': Buffer.from( + '000f362e914ee0136663cf57bf4085c25604af6dc198b4818751e1195ee7e41a16' + + '91be909dcbc2bae00b8917f6bb918eae3740ac1b76e0913137c2da1171d6400b55' + + '01ec6e1dc5987a27fe16fc2ce5c8e954088f898a9bbefb176eaa8bbd9ccc264c4c' + + 'cc38c83ac8b5a168f90228daf8405a2b9bf7829c263a646b4e1098e2ace38deec7', 'hex') } } - const curves = ['P-384', 'P-521']; - const hashes = ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']; + const curves = ['P-256', 'P-384', 'P-521']; + const hashes = ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512', 'SHA3-256', 'SHA3-384', 'SHA3-512']; const vectors = []; curves.forEach((namedCurve) => { diff --git a/test/fixtures/crypto/hmac.js b/test/fixtures/crypto/hmac.js index c4942976adf49b..6505c6e2ae55b5 100644 --- a/test/fixtures/crypto/hmac.js +++ b/test/fixtures/crypto/hmac.js @@ -19,6 +19,16 @@ module.exports = function () { '6b1da28eab1f582ad9718effe05e23d5fd2c9877a2d9443f90bec093bece2ea7' + 'd2354cd0bdc5e147d2e9009373494488', 'hex'), 'SHA-512': Buffer.from( + '5dcc359443aaf652fa1375d6b3e61fdcf29bb4a28bd5d3dcfa40f82f906bb280' + + '0455db03b5d31fb972a15a6d0103a24e56d156a119c0e5a1e92a44c3c5657cf9', + 'hex'), + 'SHA3-256': Buffer.from( + 'e588ec0811463d767241df1074b47ae4071b51f2ce36537ba69ccdc3fdc2b7a8', + 'hex'), + 'SHA3-384': Buffer.from( + '6b1da28eab1f582ad9718effe05e23d5fd2c9877a2d9443f90bec093bece2ea7' + + 'd2354cd0bdc5e147d2e9009373494488', 'hex'), + 'SHA3-512': Buffer.from( '5dcc359443aaf652fa1375d6b3e61fdcf29bb4a28bd5d3dcfa40f82f906bb280' + '0455db03b5d31fb972a15a6d0103a24e56d156a119c0e5a1e92a44c3c5657cf9', 'hex') @@ -35,6 +45,16 @@ module.exports = function () { 'SHA-512': Buffer.from( '61fb278c3ffb0cce2bf1cf723ddfd8ef1f931c0c618c25907324605939e3f9a2' + 'c6f4af690bda3407dc2f5770f6a0a44b954d64a332e3ee0821abf82b7f3e99c1', + 'hex'), + 'SHA3-256': Buffer.from( + 'c1ac5e11fcd50c48bf567f6e296632f5801c4eb07a8a47579b41dee971a3099b', + 'hex'), + 'SHA3-384': Buffer.from( + 'ac8c97f6dd8d9e16101063077c16b23fe291a5e6d149653e9ac7002365159317' + + 'adcfad511996578b0053a5c14b75f16c', 'hex'), + 'SHA3-512': Buffer.from( + '2162c2a8907e6b2f68599a69e81a464d8f076b5eeb555d98b4d20330034df3c7' + + 'cf35b1fa958a074ca12f0d242df39f0da3d4f1dbfb3629057798fe1f883974ee', 'hex') } diff --git a/test/fixtures/crypto/rsa_pkcs.js b/test/fixtures/crypto/rsa_pkcs.js index 49e202c512d9e2..4630e4af913580 100644 --- a/test/fixtures/crypto/rsa_pkcs.js +++ b/test/fixtures/crypto/rsa_pkcs.js @@ -96,7 +96,34 @@ module.exports = function () { '688c993b58a0ed35e8f0a106d4e8b1b360e334415c742e94675823db0fd25e22cff' + '7a6335c70e193235dcda48add6858626bd96311e60f7e5ea4491b6c1e6248afe12b' + 'bbd54f8869b043a5b0444562813f0a98b300356f306e6b783a29f3bec97ca40ea20' + - '062cab8926ec5d96aa387cc84821a6d72b8ea126e7d', 'hex') + '062cab8926ec5d96aa387cc84821a6d72b8ea126e7d', 'hex'), + 'sha3-256': Buffer.from( + 'be1b476c1911a01d71710fd8a2f3158d6f7839e91443b01bed30dfdd04336d80c6b' + + 'f692c06fad254877901c10a73853e8fb202a29cddefdf16c3adcda1fc123625897d' + + '1b81b32a9dec38957e023be221d8f31e7470ad32e761edce9170eefa37ec19bd0c3' + + 'e0b0ad2a244e98f54a08f873efb63c6fad14d7322b50eb05b6bae767305da92a90a' + + '53cdae52b0d81e158a00003ec626e50423b7377a34a7b28cc7483b55bfde05bd431' + + 'cfa436c38c285531e0d476ee13f151c8ae832ffd51ba00f2ab06f1844e73c0fe0f6' + + 'ce17d966b1e07727af4161368aa0a74a594a6fdb782b46a9ae6098799c366fc0d71' + + '1b2d965cf5eeeed9175b39b1d0bcefdd7df376e8ac9', 'hex'), + 'sha3-384': Buffer.from( + '002eaf5837443f1a33dc03729a308c503888d7a8cc013be424a91bce18105f7334a' + + '499a5eddc5f4fab2fdf80f52988d53bf8bd5e78c3ce1a43abaf3b8146c260b6ce8b' + + 'ffc9857f4b35c190cea85921c46d3ab573113744472d1afb637a0e9ab5021bcb355' + + '7f5b52faf89fa864a7d3bf5799096c54ee53fa139e1bc13842a2a5bf0f1d85f041d' + + 'a4e0e87425b421f22f0240ad62ef77ba6f090e0d48e17c07fd1e477c7e16a3196f5' + + '0142d0f0c5e525a10325569e5a1f50cb4577e782a643972857cc918ae5409587d9e' + + '44e1c1e89540e87deed7dda5005ac63ba609f522fdd92c81d95c1ffa383558a10f3' + + '064f59ca0534bfad31acbf3e2807cb7d3147c59ee4d', 'hex'), + 'sha3-512': Buffer.from( + '561585b621c916453762285c8bb6ede3f303074ad6f2826ca15b3900e49c4d94c07' + + 'aab0b875eaa79049ba2ed97e9a87c44fff9bffe638a1bf8c4db69c627b6adbe8fca' + + '2b38cb8b4c2810a16286bef498327b9db4b53043ed5012c7c58f037edf669baf772' + + '9b58e413e133ebb90a5fcb6dc3936f4f87971c0e85f362189b4279bbb2d9293a427' + + '5653068c1bc8772cebc4733a5d1df0b454d4f628c645c22bb1c8cc601fbc92dc091' + + 'db38fad4a36289ae9ed424c46643a8161a102ae511877d25f2eab7342dff6b92bf3' + + '65951e76ee84c2bd84a595f63d7cc04d00e1589870956491e518b3ba245efc37a28' + + 'ec018d8788a92ab93a90bb314f9ab0788a0b5b50489', 'hex') } const vectors = [ @@ -131,7 +158,31 @@ module.exports = function () { hash: 'SHA-512', plaintext, signature: signatures['sha-512'] - } + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSASSA-PKCS1-v1_5' }, + hash: 'SHA3-256', + plaintext, + signature: signatures['sha3-256'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSASSA-PKCS1-v1_5' }, + hash: 'SHA3-384', + plaintext, + signature: signatures['sha3-384'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSASSA-PKCS1-v1_5' }, + hash: 'SHA3-512', + plaintext, + signature: signatures['sha3-512'] + }, ]; return vectors; diff --git a/test/fixtures/crypto/rsa_pss.js b/test/fixtures/crypto/rsa_pss.js index effb3605a73f1a..101122b2ffe31c 100644 --- a/test/fixtures/crypto/rsa_pss.js +++ b/test/fixtures/crypto/rsa_pss.js @@ -61,21 +61,131 @@ module.exports = function() { const signatures = { 'sha-1, no salt': Buffer.from( - '1f1cd81ecb3bb31df2e5f0f64c5c0a310c7cf88d19eb512a5078e156d823727af88' + '68a12e5dfbfa976386784ba982c39928789b134952a4a28c6241177bcf2248f2adb' + '60077f545dd17e4f809b3b859fd430d1681e8047126d77369519eed5b618f3297a5' + '75085f0c931ed248cf60bbd7efffa0a8c2b874ba7f81ecd6bf391d01f1e881d827a' + '7b95df874d9adabb7b07f131ab33142a8b0b6d5ca9685671d49b982b67651909eaa' + '17b96b393e04fb36d972f9b258f1b79123df212d39924a4deaec506cf640f1dedd0' + '2d28845f3548d8488652788e2e2146f3ce8a86a556d84b4578f10da29abdb176a68' + '718cc1b2270b0735c2e5ca6c6bb0afac23a5bfa817a', 'hex'), + '1f1cd81ecb3bb31df2e5f0f64c5c0a310c7cf88d19eb512a5078e156d823727af88' + + '68a12e5dfbfa976386784ba982c39928789b134952a4a28c6241177bcf2248f2adb' + + '60077f545dd17e4f809b3b859fd430d1681e8047126d77369519eed5b618f3297a5' + + '75085f0c931ed248cf60bbd7efffa0a8c2b874ba7f81ecd6bf391d01f1e881d827a' + + '7b95df874d9adabb7b07f131ab33142a8b0b6d5ca9685671d49b982b67651909eaa' + + '17b96b393e04fb36d972f9b258f1b79123df212d39924a4deaec506cf640f1dedd0' + + '2d28845f3548d8488652788e2e2146f3ce8a86a556d84b4578f10da29abdb176a68' + + '718cc1b2270b0735c2e5ca6c6bb0afac23a5bfa817a', 'hex'), 'sha-256, no salt': Buffer.from( - '6157d668ed655d978b4c158c8419eb80718dfdfc7d4b34357f9917e9e116b6f3b65' + '040c9d16155c081d6887abcb3ba4ffa0191e4807ee206681aa1d4809ea20de5186b' + '77e3caced07fc9b3d71b9df0ac81b5c3273ff3f74f32a7ad34c65062a31540ced30' + '527efa4b7aa2d27ff7f80535f3e65ce352eb9e18b5054416de959354a4dcccb2542' + 'e33a8358eda620a8653dd6458f56ab94fee1dc01ef42fb8958aa134810e4d8fe1dd' + '4feee6af04742f80da5793875a78a2a4cc08d4e0a68ab03f1c022a0e8a7d3096089' + '92d24ecdd7e8f1895e3e5cd36e49906b531932d9ff958618b1a50f98455f515e0c6' + '3103d2e4e1651afc566eb9cad1e7efae1a9750c3880', 'hex'), + '6157d668ed655d978b4c158c8419eb80718dfdfc7d4b34357f9917e9e116b6f3b65' + + '040c9d16155c081d6887abcb3ba4ffa0191e4807ee206681aa1d4809ea20de5186b' + + '77e3caced07fc9b3d71b9df0ac81b5c3273ff3f74f32a7ad34c65062a31540ced30' + + '527efa4b7aa2d27ff7f80535f3e65ce352eb9e18b5054416de959354a4dcccb2542' + + 'e33a8358eda620a8653dd6458f56ab94fee1dc01ef42fb8958aa134810e4d8fe1dd' + + '4feee6af04742f80da5793875a78a2a4cc08d4e0a68ab03f1c022a0e8a7d3096089' + + '92d24ecdd7e8f1895e3e5cd36e49906b531932d9ff958618b1a50f98455f515e0c6' + + '3103d2e4e1651afc566eb9cad1e7efae1a9750c3880', 'hex'), 'sha-384, no salt': Buffer.from( - '7b95aab6b34c0962d228409e30df9b043c1b0baada08e73d887422552b8f1522e2e' + '42bf2b9ff2c6c9aa3eb0cd2370618e8f1a36873595e00bde75a9ce062ec32b5f639' + '4f2267a3f5c11840ff92e6e15bf31cc53e917ca8efc0895fb112c2ef8f681cbb6a4' + '10152f6e930caff1f260e31f983542e68cd15dea17ed3139cac735106fb05fc163b' + '2ed05a0ded939059a10c5cd7619e21b2d206907994274b34a4daefa1ce59b6b319f' + '73955a0918a5e237e1bbfdadb45c907a50083577e7192818845995b4a6d3ff1978e' + '0f9a42695853282e35c3b78133b3e0c624125aff14a1873d198f6304ffec7fc1cf2' + 'adecc6cd14b1f89b1a637f72ed1ff5de7c6b4d96599', 'hex'), + '7b95aab6b34c0962d228409e30df9b043c1b0baada08e73d887422552b8f1522e2e' + + '42bf2b9ff2c6c9aa3eb0cd2370618e8f1a36873595e00bde75a9ce062ec32b5f639' + + '4f2267a3f5c11840ff92e6e15bf31cc53e917ca8efc0895fb112c2ef8f681cbb6a4' + + '10152f6e930caff1f260e31f983542e68cd15dea17ed3139cac735106fb05fc163b' + + '2ed05a0ded939059a10c5cd7619e21b2d206907994274b34a4daefa1ce59b6b319f' + + '73955a0918a5e237e1bbfdadb45c907a50083577e7192818845995b4a6d3ff1978e' + + '0f9a42695853282e35c3b78133b3e0c624125aff14a1873d198f6304ffec7fc1cf2' + + 'adecc6cd14b1f89b1a637f72ed1ff5de7c6b4d96599', 'hex'), 'sha-512, no salt': Buffer.from( - 'af1bc07fa70add19f3ce1f1bef8dfc6e24af43671cfb97e6b869e86b7ef03550a65' + '81318fff6449afa8b67e73e2a6a14e20677d8b067145a84422574ae0cfd2a5dff70' + 'c6d7e97f6a0e166505079eb4264a43c493f2eb3fb06facc01be60774c277646a280' + '81247679622b220227e9249754867aa8fe1804015c4f98700982eda40e84d0ba033' + '6cf44f582fb8781374804e8fb43eb9d577acf4723587a39a2b4a9e168b767632b7a' + '554f77bc5272821c938c0994b162f7482636f7ffac564a19bd733f4877801dc324d' + 'c47196ef12ca9a8f4921a5496cd6737935ca555b73466ddd817eaff03feda0eb2d6' + '12e3cdb59b1989eeffdc18101d46e56b9ff5c91f95d', 'hex'), + 'af1bc07fa70add19f3ce1f1bef8dfc6e24af43671cfb97e6b869e86b7ef03550a65' + + '81318fff6449afa8b67e73e2a6a14e20677d8b067145a84422574ae0cfd2a5dff70' + + 'c6d7e97f6a0e166505079eb4264a43c493f2eb3fb06facc01be60774c277646a280' + + '81247679622b220227e9249754867aa8fe1804015c4f98700982eda40e84d0ba033' + + '6cf44f582fb8781374804e8fb43eb9d577acf4723587a39a2b4a9e168b767632b7a' + + '554f77bc5272821c938c0994b162f7482636f7ffac564a19bd733f4877801dc324d' + + 'c47196ef12ca9a8f4921a5496cd6737935ca555b73466ddd817eaff03feda0eb2d6' + + '12e3cdb59b1989eeffdc18101d46e56b9ff5c91f95d', 'hex'), 'sha-1, salted': Buffer.from( - '1f608a71d1884cfe2183b49037aa8555b0139a8a1267a5c5b9cce20701f2ad4bbd5' + 'b329740bff31accc34bf9afd1439a0536bb32b6d427d26968dbc9e9c80d2111d948' + 'c481cb1731778acd3110463241c4f23b3e13b855d162cb153851290fd95f781519e' + '2cef93745a413cfeec8e94fba7822b725d4744318458cf6b4a917b65b15ee6f54b9' + 'c391f6064a9e031f7009f592449c0b46d5457a2799cb0ebd78a102a055ee0470b26' + '0c2b3d8ffbdee0fd47644822090ec55ae6233be1062f441c432ed3c275e74d62013' + '2681ec2e801e9b5b6acc1ad71f8935388f7e2c03370d12e944e3418c2ab63bb42ab' + 'e1bb9e69530f02458ba28400b36806ff78da5791ace', 'hex'), + '1f608a71d1884cfe2183b49037aa8555b0139a8a1267a5c5b9cce20701f2ad4bbd5' + + 'b329740bff31accc34bf9afd1439a0536bb32b6d427d26968dbc9e9c80d2111d948' + + 'c481cb1731778acd3110463241c4f23b3e13b855d162cb153851290fd95f781519e' + + '2cef93745a413cfeec8e94fba7822b725d4744318458cf6b4a917b65b15ee6f54b9' + + 'c391f6064a9e031f7009f592449c0b46d5457a2799cb0ebd78a102a055ee0470b26' + + '0c2b3d8ffbdee0fd47644822090ec55ae6233be1062f441c432ed3c275e74d62013' + + '2681ec2e801e9b5b6acc1ad71f8935388f7e2c03370d12e944e3418c2ab63bb42ab' + + 'e1bb9e69530f02458ba28400b36806ff78da5791ace', 'hex'), 'sha-256, salted': Buffer.from( - '8c3d03bde8c42d9453631b0baac89e6296da20543713c004df35bc1a6fae205ab2b' + 'f585369689073cdee345ad6e2783b2dda187b4979ea0457463758156e103eedd0ef' + '1834d35bd6ad540d9b8b225fd1770e514ea0af35f707f2e7a0382be6f5ed9d6b591' + 'd536ce1215b17ef3eeb450bb48a0017497c67be0240470addd2891a81a8f1cf6e80' + 'e3f837fe42376292df555b8b05931b69530597fae36dcd01b1c81767d4ecd4caf06' + 'befc035224bdd2a5e6b89d51539235ac95570e757dbd70fdc15040001b07b937bf0' + '148ccc005f4c272acf5f8fc096a37d26208e96ac341c2d1d212c44d6d5156c934f6' + '6ef42fdbac77a208681550b048b466e32c76c7a7b07', 'hex'), + '8c3d03bde8c42d9453631b0baac89e6296da20543713c004df35bc1a6fae205ab2b' + + 'f585369689073cdee345ad6e2783b2dda187b4979ea0457463758156e103eedd0ef' + + '1834d35bd6ad540d9b8b225fd1770e514ea0af35f707f2e7a0382be6f5ed9d6b591' + + 'd536ce1215b17ef3eeb450bb48a0017497c67be0240470addd2891a81a8f1cf6e80' + + 'e3f837fe42376292df555b8b05931b69530597fae36dcd01b1c81767d4ecd4caf06' + + 'befc035224bdd2a5e6b89d51539235ac95570e757dbd70fdc15040001b07b937bf0' + + '148ccc005f4c272acf5f8fc096a37d26208e96ac341c2d1d212c44d6d5156c934f6' + + '6ef42fdbac77a208681550b048b466e32c76c7a7b07', 'hex'), 'sha-384, salted': Buffer.from( - '79f7284bb4216de68429854edb4218ef78ad1740848567377315db8867a15733c70' + '42e8bf90762e673c90c0e2c58c6c5cef497568bd92a6d219612c4756c55fac45507' + 'f81608bc2720da4eedd5b23e1f3c8740c6b4cd7e4cf0e04342b184c1110199e6508' + '0d73b985e611d66f8e97990816e4917badbb0425dd94383892e2aa96de4db0de093' + '6aee84d5482a3da31b27319f43830fc48703cc7d4eaedb20fd30323dbf3f22608db' + '51637d3b305b3197962658d80935c266d33ccfb297590621f4a967c7245e92b0158' + 'c0dcea943e2ace719ebdb196a9bae7df3ed9cc62765e27b63571743e28a0538db08' + '25cad2539eb5de5e6a320a88b573ec1972c26401530', 'hex'), + '79f7284bb4216de68429854edb4218ef78ad1740848567377315db8867a15733c70' + + '42e8bf90762e673c90c0e2c58c6c5cef497568bd92a6d219612c4756c55fac45507' + + 'f81608bc2720da4eedd5b23e1f3c8740c6b4cd7e4cf0e04342b184c1110199e6508' + + '0d73b985e611d66f8e97990816e4917badbb0425dd94383892e2aa96de4db0de093' + + '6aee84d5482a3da31b27319f43830fc48703cc7d4eaedb20fd30323dbf3f22608db' + + '51637d3b305b3197962658d80935c266d33ccfb297590621f4a967c7245e92b0158' + + 'c0dcea943e2ace719ebdb196a9bae7df3ed9cc62765e27b63571743e28a0538db08' + + '25cad2539eb5de5e6a320a88b573ec1972c26401530', 'hex'), 'sha-512, salted': Buffer.from( - 'b74f3099d80787118b1f9de79fc207893e0d2d75c4110f4b159b85ba07d63a0256f' + 'c3cd0f66ce8d9a2e3cf7a3d5a7b9c0befac6638894a3e36ce75e649ee069dd8dd98' + 'aa8b602474c98b14bb03492de551a9e8e77934ef9b684583934f218d9576be240b5' + 'c4f362eaf5e0140c8ea92639085a6269653505dcfa004226db9f63277653a64a182' + '6e4babb17ab54dd8543dcf1ce809706d6816e6a75ff846a3d4c18d11bdeb1f31b10' + 'd55a3795b6496319e6e751504d86a4e7bb6535b9f0415e815d8c789c5b1e387f2a8' + 'c00fef6e327462cb7e525b8f945be5b17248e0e0a4d855d397e22d067ce4539373d' + 'fba46d1799250afc70f535006cacd2766f5ddcf8f91', 'hex') + 'b74f3099d80787118b1f9de79fc207893e0d2d75c4110f4b159b85ba07d63a0256f' + + 'c3cd0f66ce8d9a2e3cf7a3d5a7b9c0befac6638894a3e36ce75e649ee069dd8dd98' + + 'aa8b602474c98b14bb03492de551a9e8e77934ef9b684583934f218d9576be240b5' + + 'c4f362eaf5e0140c8ea92639085a6269653505dcfa004226db9f63277653a64a182' + + '6e4babb17ab54dd8543dcf1ce809706d6816e6a75ff846a3d4c18d11bdeb1f31b10' + + 'd55a3795b6496319e6e751504d86a4e7bb6535b9f0415e815d8c789c5b1e387f2a8' + + 'c00fef6e327462cb7e525b8f945be5b17248e0e0a4d855d397e22d067ce4539373d' + + 'fba46d1799250afc70f535006cacd2766f5ddcf8f91', 'hex'), + 'sha3-256, no salt': Buffer.from( + '98787732f107a5390abc9ba3c93c2a0e30f6f31c3c76d73afee951a04525897df94' + + '67c7532ff1b5c12601369339edcac4654a173e61780a12a21b5f0500bf16e2445f9' + + 'f7e9adab1ea2bb7e901f615b514965047d53b12ff2ff19f94f320179946bbf1b19d' + + '88248e4fba7f49dc3c5af14de7a892a7718bd5962db33aa2b529c49e75d8fe936de' + + '45e1db225ed875486516cd7398b5ec19043cf6005e06ba2d60f807d34d4ced378ae' + + 'cef2b1f75f6ad52cdd674d944e48d484be2e5f799510f244d089eb3570a674b2585' + + '0616f12641e7e3e38e36fba1eefbed32d7a4809a4b5b1e557582303ab419bc128a1' + + '813857157985f075d5d89e6867b7864dac0369b2513', 'hex'), + 'sha3-384, no salt': Buffer.from( + '1d1399da211efc709b2cea90ff65e7c49162d943cadb59c78186889f7645e5d08f7' + + '490de3f7b65ee5664c140dd61334182ddf45bcdc844845e4d60c917bf00eb1321e7' + + '46cd7fce971af5ceea60b272219ccda2328b89a11b228cd42bdcc4c7eb40f0b6333' + + '5f7496931baf36c0d2497045687ad27bb156c20f7fdae3baa38d57e35918f328bdd' + + '2de7e6b23d6c676a18342a082f7ea019021903f103ccb4a6fddb2d88c30a1284764' + + 'b68c04bfe452c3adc6c10066a915231b7b404727eb6201b4921eb96d9407de2b963' + + '3879ceb71d759d9828d7b4d062f6ef100757d8328187caf57dfb859d1555345207c' + + '1cce7905c3564c08fec78867a53d5a2cf84810e1ffa', 'hex'), + 'sha3-512, no salt': Buffer.from( + 'd2430dc87abeaa7d13f7cec8510f1a296e1c608f44b1696829c59a99e8eefe9b2ee' + + '6ee8ad6fdc93c24fcba2f04d1da195924b6209717e1992c10ed9f4783478765fe34' + + '3e761203bff9d326bb6dc2061b0a7554c8ce0814b29249136c20c8e30054df0c6bc' + + '656509a82845149368896690e32ff5dd32ef01543686f01d6a69bb438b049e66a8b' + + 'df485a13edcd7dc482da4cc57d0b740aca3e56f0da247794e600afd27b22b6da13b' + + 'cc15dd2059b525f8cb6bcd07540aa843f0ae51d4b0eea27045485914b908bdd01d0' + + 'a9d42379f9f7180f4ad162ff73df5fed0200eb02ad01473975d54a77c15a9c61a3c' + + 'b5e27de5d1eecc363d45506f7123a5ddd115c5e4c9e', 'hex'), + 'sha3-256, salted': Buffer.from( + '59cb9cce6ae838eb20d38d6af4acb9b866b0753bb7df9e441037d788512c03279e8' + + '3d9a9cf5c0921fe1c0b6e8e895a8c0ad24a18b123f809b34ef2a3a1f05974030320' + + '435692ef5d378cef4368c3658c098a25371dfaf1c0b6910f653a4ec15f2c08956c1' + + '405136c2aba7f25a808fa7dbf57a4cb2978bd91af710b27ee239d955c8cac7a76ae' + + '9085cefeda2a585a99cc948f064b5da66a9c4aa4f3f767ac905a9f314b47038e05c' + + '3608fbb7e67a278e4f009a62c3cd3fdf43692e759d9361be1217999a76a69d4d119' + + 'f8791a90e207e46b3f6125721f56fd819292d06a3cdae2c62c9a1dc0d964a06036c' + + '8c18661cc6c873532a3536ab51e1ce210926db299e2', 'hex'), + 'sha3-384, salted': Buffer.from( + '8d1f9297c8169f27f0c58827dba991d862de58c1155f612ad2995d2bf862d051c4a' + + '91b48571849b0412384382e5b77990de6a3c84010046b35c4a504f175a3479483d9' + + '5c58f86bb96d53a27e59d6f67fddaae295ce90610f5086acc711557c2c85aac32d3' + + '24199cff2367ae44e1d91307a98c8cbfb085a8bce6b1c20714711bc15b0eddb7881' + + '823227d4be477ffdad8093663a6a1fc62eb39c49c2c3a821c2b202cf7904b49ca92' + + '3c83819602bb13931577354a80f99309030044935b1cd41f0513160e661db1959fb' + + '1ec15f087f3d288e875d54cbf070ec860b0aeecc951ea65e97cd5460750d4b7de52' + + '22cb9e7466b1f506ecf6a81fc399dfd8334160f9084', 'hex'), + 'sha3-512, salted': Buffer.from( + '7b6d7be418c5d37cc8070698b8b03d818ecd8b673d047d34921913f6d59c69cb496' + + '172d6118207d9ff92b8e1246acf0e03a845d935a70f8a82c3d5d6db6a1a0e337269' + + '4b904372413dcbaa7ac5486bc8ccaf70d7e9470be82b928a90017e272cf9761ed26' + + 'c160fe874a2675a4fb2acad72c50fbfffdd70b5a6f2919553d7ea1829934670f8de' + + 'f2a5c2816404b1aa153323c92c58400622f184b9b0463fa48d6b27091f68c287e3f' + + '6d9ab9eb451711a5d984c547f3d56f14a686a89ddf36c47ce25092b8c6530904de9' + + '5df7fc602fe9394315f1b1847aae304cb5ad71e2cb78acfbc997a87a9d62a6898bb' + + '6d84a81bb89b50186265f4be171a93d837a4bf777c8', 'hex') } const vectors = [ @@ -142,6 +252,54 @@ module.exports = function() { hash: 'SHA-512', plaintext, signature: signatures['sha-512, salted'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 0 }, + hash: 'SHA3-256', + plaintext, + signature: signatures['sha3-256, no salt'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 0 }, + hash: 'SHA3-384', + plaintext, + signature: signatures['sha3-384, no salt'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 0 }, + hash: 'SHA3-512', + plaintext, + signature: signatures['sha3-512, no salt'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 32 }, + hash: 'SHA3-256', + plaintext, + signature: signatures['sha3-256, salted'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 48 }, + hash: 'SHA3-384', + plaintext, + signature: signatures['sha3-384, salted'] + }, + { + publicKeyBuffer: spki, + privateKeyBuffer: pkcs8, + algorithm: { name: 'RSA-PSS', saltLength: 64 }, + hash: 'SHA3-512', + plaintext, + signature: signatures['sha3-512, salted'] } ]; diff --git a/test/fixtures/webcrypto/supports-sha3.mjs b/test/fixtures/webcrypto/supports-sha3.mjs new file mode 100644 index 00000000000000..077c6a6c40fd5c --- /dev/null +++ b/test/fixtures/webcrypto/supports-sha3.mjs @@ -0,0 +1,88 @@ +const { subtle } = globalThis.crypto; + +const RSA_KEY_GEN = { + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]) +}; + +const [ECDH, X448, X25519] = await Promise.all([ + subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits', 'deriveKey']), + subtle.generateKey('X448', false, ['deriveBits', 'deriveKey']), + subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']), +]); + +export const vectors = { + 'digest': [ + [true, 'SHA3-256'], + [true, 'SHA3-384'], + [true, 'SHA3-512'], + ], + 'generateKey': [ + [true, { name: 'HMAC', hash: 'SHA3-256' }], + [true, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [true, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [true, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [true, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [true, { name: 'HMAC', hash: 'SHA3-256' }], + [true, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], + ], + 'deriveKey': [ + [true, + { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, + { name: 'AES-CBC', length: 128 }], + [true, + { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, + { name: 'HMAC', hash: 'SHA3-256' }], + [false, + { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, + 'HKDF'], + [true, + { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, + { name: 'AES-CBC', length: 128 }], + [true, + { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, + { name: 'HMAC', hash: 'SHA3-256' }], + [false, + { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, + 'HKDF'], + [true, + { name: 'X25519', public: X25519.publicKey }, + { name: 'HMAC', hash: 'SHA3-256' }], + [true, + { name: 'X448', public: X448.publicKey }, + { name: 'HMAC', hash: 'SHA3-256' }], + [true, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA3-256' }], + ], + 'deriveBits': [ + [true, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 8], + [true, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 0], + [false, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, null], + [false, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 7], + + [true, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 8], + [true, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 0], + [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 0 }, 8], + [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, null], + [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 7], + ], + 'importKey': [ + [true, { name: 'HMAC', hash: 'SHA3-256' }], + [true, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [true, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [true, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [true, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [true, { name: 'HMAC', hash: 'SHA3-256' }], + [true, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], + ], + 'get key length': [ + [false, { name: 'HMAC', hash: 'SHA3-256' }], + ], +}; diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index f687e81a571d17..e5804a44e623f8 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -25,6 +25,9 @@ const kDerivedKeyTypes = [ ['HMAC', 256, 'SHA-256', 'sign', 'verify'], ['HMAC', 256, 'SHA-384', 'sign', 'verify'], ['HMAC', 256, 'SHA-512', 'sign', 'verify'], + ['HMAC', 256, 'SHA3-256', 'sign', 'verify'], + ['HMAC', 256, 'SHA3-384', 'sign', 'verify'], + ['HMAC', 256, 'SHA3-512', 'sign', 'verify'], ]; const kDerivedKeys = { @@ -70,7 +73,19 @@ const kDerivations = { 'b127e92631c1c051482d6690941772b4', empty: '9e4b719033742101e90f1ad61e2ff3b4' + '256863667296d74389f1f02af2c4e6a6' - } + }, + 'SHA3-256': { + normal: '386b0693d7a58c4ddf01b49bfbbd2fa87c6f911991543995170ba20ed28df599', + empty: 'd029bc828b6c6c8bb16ce3d25f5058f19c7d2517745e11c5d65c6d242e82e47f', + }, + 'SHA3-384': { + normal: '8c3b72e659bad40bcd14bdc1f7c3836059d24253795ab046a272973fd0456508', + empty: '3211ff4c676f761494c1ca2683d2d4662fe1d770ae5c58ebf6af6acb181c7d71', + }, + 'SHA3-512': { + normal: '5588c5c70cb3dd2f95323da2e9d5f299ca99c301d920a499330c449d21c645cd', + empty: '2c944b916c2751a71a1b5e57fcb487939c624335683995770b9f7cc7cbbb21f0', + }, }, empty: { 'SHA-384': { @@ -96,7 +111,19 @@ const kDerivations = { '0acea6f165476eb83460b9353ed41dfe', empty: 'c8e12774135305c9147f2cc4766e5ead' + '25d8f457b9a1953d52677361ced558fb' - } + }, + 'SHA3-256': { + normal: '9befc557f5baf4075b5fb38c014b41b92ab7534150baf64201069e8807d0e83d', + empty: '54d1fa1aa7cad99dab0622b772170e775c103756183bac36a228fd817a98a3f6', + }, + 'SHA3-384': { + normal: '46b54c015e368677edf7ac16963bccd9d2ba8246eef0e8beb04d8d188774b91b', + empty: '46eb0b2649bb0f605d70e4818ffc8176ee1be9782396e69fb4d0fd7cfe902b55', + }, + 'SHA3-512': { + normal: 'aa4375c82b5d7a3cac88a0423250b3882f140c253e98e8e7a0f6055b0908e4c2', + empty: '6613003f98602ddb53ac35f5aa256c9f5279d50ee65bb08fdf2ecf65cc5df27f', + }, } }, long: { @@ -124,7 +151,19 @@ const kDerivations = { '50b3dd9a29f30606e2cad199bec14d13', empty: 'e579d1f9e7f08e6f990ffcfcce1ed201' + 'c5e37e62cdf606f0ba4aca80427fbc44' - } + }, + 'SHA3-256': { + normal: '24f38fd1905554b7cbf8395cc3976292d11ce24a0b3131da0fd4b109832d27e3', + empty: '33d0a5151c0f52e4bb7fb67cf7a17063127624dc3e685903f49ebb07872084d1', + }, + 'SHA3-384': { + normal: '15777581a1ea81ad0baac8a97d954df4142f7260d9e8351aa7f6ef6de2d04632', + empty: 'ada4da4e28dc971633a8760b265b3019db57baf17e7bf7e13cf78b1a676f6d44', + }, + 'SHA3-512': { + normal: '621e4602b07fcba55ed6b976a8bef513b0f7c4ad0c546e0f852993051d887408', + empty: 'f1292af65b05c86cf7146b739bc65785c707450316f3207ee54a3f596a7d0f7b', + }, }, empty: { 'SHA-384': { @@ -150,7 +189,19 @@ const kDerivations = { '851cc5baadb42cad024b6290fe213436', empty: 'b4f7e7557674d501cbfbc0148ad800c0' + '750189fe295a2aca5e1bf4122c85edf9' - } + }, + 'SHA3-256': { + normal: 'fe32459f7339dd2e8df6c6fc874ed9e81e3b7aad669edad9b71196f53ed95b12', + empty: '04519be1eb94079c91306cc5b21946b3de6a78ad35ec83d4f4a37bafbda678d7', + }, + 'SHA3-384': { + normal: 'a474e8289cb4a0511e90b87eaf9ec29cadd74d4c1f2ee1fb8cb5f7d08f91a379', + empty: '726c8c4b39083a7d5755604d3a67e9aa6139db00c08028ac9e69f7fb1525bf1d', + }, + 'SHA3-512': { + normal: 'c7a7f5004d1d595c6896498c169642ac24b946e13296ff53e12b534962a88675', + empty: '7b543480b5696932551abb3100d72e05c18f57fbb63aa44fe020bef1eec3555c', + }, } }, }; @@ -294,7 +345,7 @@ async function testDeriveBitsBadHash( subtle.deriveBits( { ...algorithm, - hash: hash.substring(0, 3) + hash.substring(4) + hash: hash.replace('-', '') }, baseKeys[size], 256), { message: /Unrecognized algorithm name/, name: 'NotSupportedError', @@ -430,7 +481,7 @@ async function testDeriveKeyBadHash( subtle.deriveKey( { ...algorithm, - hash: hash.substring(0, 3) + hash.substring(4) + hash: hash.replace('-', '') }, baseKeys[size], keyType, diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index 90f76839474e16..3ee1dcf52f02a0 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -73,10 +73,20 @@ const { KeyObject } = require('crypto'); } const kTests = [ + ['hello', 'there', 'my friend', 'SHA-1', + '365ca5d3f42d050c74302e420c83975327950f1913a151eecd00526bf52614a0'], ['hello', 'there', 'my friend', 'SHA-256', '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e32941ef21bdeb0fa87b6b6'], ['hello', 'there', 'my friend', 'SHA-384', 'e36cf2cf943d8f3a88adb80f478745c336ac811b1a86d03a7d10eb0b6b52295c'], + ['hello', 'there', 'my friend', 'SHA-512', + '1e42d43fcacba361716f65853bd5f3c479f679612f0180eab3c51ed6c9d2b47d'], + ['hello', 'there', 'my friend', 'SHA3-256', + '2a49a3b6fb219117af9e251c6c65f16600cbca13bd0be6e70d96b0b9fa4cf3fd'], + ['hello', 'there', 'my friend', 'SHA3-384', + '0437bb59b95f2db2c7684c0b439028cb0fdd6f0f5d03b9f489066a87ae147221'], + ['hello', 'there', 'my friend', 'SHA3-512', + '3bbc469d38214371921e52c6f147e96cb7eb370421a81f53dea8b4851dfb8bce'], ]; const tests = Promise.all(kTests.map((args) => test(...args))); @@ -109,10 +119,20 @@ const { KeyObject } = require('crypto'); } const kTests = [ - ['hello', 'there', 10, 'SHA-256', - 'f72d1cf4853fffbd16a42751765d11f8dc7939498ee7b7ce7678b4cb16fad880'], + ['hello', 'there', 5, 'SHA-1', + 'f8f65a5fd92c9b74916083a7e9b0001c46bc89e2a14c48014cf1e0e1dbabf635'], + ['hello', 'there', 5, 'SHA-256', + '2e575eae24267db32106c7dba01615e5417557e8c5cf33ba15a311cb0c2907ee'], ['hello', 'there', 5, 'SHA-384', '201509b012c9cd2fbe7ea938f0c509b36ecb140f38bf9130e96923f55f46756d'], + ['hello', 'there', 5, 'SHA-512', + '2e8d981741f98193e0af9c79870af0e985089341221edad9a130d297eae1984b'], + ['hello', 'there', 5, 'SHA3-256', + '0aed29b61b3ca3978aea34a9793276574ea997b69e8d03727438199f90571649'], + ['hello', 'there', 5, 'SHA3-384', + '7aa4a274aa19b4623c5d3091c4b06355de85ff6f25e53a83e3126cbb86ae68df'], + ['hello', 'there', 5, 'SHA3-512', + '4d909c47a81c625f866d1f9406248e6bc3c7ea89225fbccf1f08820254c9ef56'], ]; const tests = Promise.all(kTests.map((args) => test(...args))); @@ -128,26 +148,36 @@ const { KeyObject } = require('crypto'); [{ name: 'HMAC', hash: 'SHA-1' }, 'sign', 512], [{ name: 'HMAC', hash: 'SHA-256' }, 'sign', 512], // Not long enough secret generated by ECDH - // [{ name: 'HMAC', hash: 'SHA-384' }, 'sign', 1024], - // [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], + [{ name: 'HMAC', hash: 'SHA-384' }, 'sign', 1024], + [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], + [{ name: 'HMAC', hash: 'SHA3-256' }, 'sign', 1088], + [{ name: 'HMAC', hash: 'SHA3-384' }, 'sign', 832], + [{ name: 'HMAC', hash: 'SHA3-512' }, 'sign', 576], ]; (async () => { const keyPair = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-521' }, false, ['deriveKey']); for (const [derivedKeyAlgorithm, usage, expected] of vectors) { - const derived = await subtle.deriveKey( + const [result] = await Promise.allSettled([subtle.deriveKey( { name: 'ECDH', public: keyPair.publicKey }, keyPair.privateKey, derivedKeyAlgorithm, false, - [usage]); + [usage])]); - if (derived.algorithm.name === 'HMAC') { - assert.strictEqual(derived.algorithm.length, expected); + if (expected > 528) { + assert.strictEqual(result.status, 'rejected'); + assert.match(result.reason.message, /derived bit length is too small/); } else { - // KDFs cannot be exportable and do not indicate their length - const secretKey = KeyObject.from(derived); - assert.strictEqual(secretKey.symmetricKeySize, expected / 8); + assert.strictEqual(result.status, 'fulfilled'); + const derived = result.value; + if (derived.algorithm.name === 'HMAC') { + assert.strictEqual(derived.algorithm.length, expected); + } else { + // KDFs cannot be exportable and do not indicate their length + const secretKey = KeyObject.from(derived); + assert.strictEqual(secretKey.symmetricKeySize, expected / 8); + } } } })().then(common.mustCall()); @@ -159,6 +189,9 @@ const { KeyObject } = require('crypto'); [{ name: 'HMAC', hash: 'SHA-256' }, 'sign', 512], [{ name: 'HMAC', hash: 'SHA-384' }, 'sign', 1024], [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], + [{ name: 'HMAC', hash: 'SHA3-256' }, 'sign', 1088], + [{ name: 'HMAC', hash: 'SHA3-384' }, 'sign', 832], + [{ name: 'HMAC', hash: 'SHA3-512' }, 'sign', 576], ]; (async () => { diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 87902842deaa55..debfca16ccde5c 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -15,6 +15,9 @@ const kTests = [ ['SHA-256', ['sha256'], 256], ['SHA-384', ['sha384'], 384], ['SHA-512', ['sha512'], 512], + ['SHA3-256', ['sha3-256'], 256], + ['SHA3-384', ['sha3-384'], 384], + ['SHA3-512', ['sha3-512'], 512], [{ name: 'cSHAKE128', length: 256 }, ['shake128', { outputLength: 256 >> 3 }], 256], [{ name: 'cSHAKE256', length: 512 }, ['shake256', { outputLength: 512 >> 3 }], 512], ]; @@ -163,6 +166,40 @@ const kDigestedData = { 'd5290933492f9d17411926a613dd0611668c2ac999e8' + 'c011aabaa9004323425fbad75b0f58ee6e777a94' }, + 'sha3-256': { + empty: 'a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a', + short: '3059af7aa33b517084e8ad7bbc4fb208a44c28ef32b4698d103dd540e4f91aa1', + medium: '1fa7cd1da74cd8046417508c8314e74a9a4a9d38f9f18e6cb215b8c891a0a80e', + long: 'b2cfc61e0386cdaef5e10a2be189891f5ef52a7624bfcd8edc893acc64fec600' + }, + 'sha3-384': { + empty: '0c63a75b845e4f7d01107d852e4c2485c51a50aaaa9' + + '4fc61995e71bbee983a2ac3713831264adb47fb6bd1' + + 'e058d5f004', + short: '54b8f0e4cf4974de740098f66b3024479b01631315a' + + '6773606c33eadc32556a6e778e08f0225ae79265aec' + + '666cb2390b', + medium: '437b7d8b68b250b5c1739ea4cc86db2033879dfb18' + + 'de292c9c50d9c193a4c79a08a6cae3f4e483c2795e' + + 'a5d1ef7e69d2', + long: '3b39c4c97ad87613305d0ccc987181713e2d5e84b1f9' + + '760011bcce0c297499005bdce8a3d2409b5ad0164f32' + + 'bb8778d0' + }, + 'sha3-512': { + empty: 'a69f73cca23a9ac5c8b567dc185a756e97c982164fe' + + '25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9' + + '402c3ac558f500199d95b6d3e301758586281dcd26', + short: '2dd2e07a62e6ad0498ba84f313c4d4024cb46001f78' + + 'f75db336b0d4d8bd2a9ec152c4ad20878735d82ba08' + + '72ecf59608ef3ced2b2a8669427e7da31e362333d8', + medium: 'e640a21909536640369e9b0a48931c5cb2efcbc91f' + + 'ecf247306bc96a0e4ca33307cb8e1b9af367946dd01' + + 'c243f3907508d04f1692a3161df1f898de8ee25febe', + long: 'bd262cecf565c338032de5ba0138f0aacfe7dde83d27' + + '2d0d37d952829ed25de1a1342d98659ef7d2fa4aca7c' + + 'e2b1aa0784d8fc1dcbf81bcec7a7431a3da36bf7' + } }; async function testDigest(size, alg) { diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js index 5d4ecc02c74a31..ab46de7a166e5b 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -11,7 +11,7 @@ const { subtle } = globalThis.crypto; // This is only a partial test. The WebCrypto Web Platform Tests // will provide much greater coverage. -// Test Encrypt/Decrypt RSA-OAEP +// Test Encrypt/Decrypt RSA-OAEP w/ SHA-2 { const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); @@ -56,6 +56,51 @@ const { subtle } = globalThis.crypto; test().then(common.mustCall()); } +// Test Encrypt/Decrypt RSA-OAEP w/ SHA-3 +{ + const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); + + async function test() { + const ec = new TextEncoder(); + const { publicKey, privateKey } = await subtle.generateKey({ + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA3-384', + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt({ + name: 'RSA-OAEP', + label: ec.encode('a label') + }, publicKey, buf); + + const plaintext = await subtle.decrypt({ + name: 'RSA-OAEP', + label: ec.encode('a label') + }, privateKey, ciphertext); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + + await assert.rejects(() => subtle.encrypt({ + name: 'RSA-OAEP', + }, privateKey, buf), { + name: 'InvalidAccessError', + message: 'The requested operation is not valid for the provided key' + }); + + await assert.rejects(() => subtle.decrypt({ + name: 'RSA-OAEP', + }, publicKey, ciphertext), { + name: 'InvalidAccessError', + message: 'The requested operation is not valid for the provided key' + }); + } + + test().then(common.mustCall()); +} + // Test Encrypt/Decrypt AES-CTR { const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); diff --git a/test/parallel/test-webcrypto-export-import-ec.js b/test/parallel/test-webcrypto-export-import-ec.js index 57f1b2831e33bc..46a7e9153f2668 100644 --- a/test/parallel/test-webcrypto-export-import-ec.js +++ b/test/parallel/test-webcrypto-export-import-ec.js @@ -423,14 +423,14 @@ async function testImportRaw({ name, publicUsages }, namedCurve) { subtle.importKey( 'spki', rsaPublic.export({ format: 'der', type: 'spki' }), - { name, hash: 'SHA-256', namedCurve: 'P-256' }, + { name, namedCurve: 'P-256' }, true, publicUsages), { message: /Invalid key type/ }, ).then(common.mustCall()); assert.rejects( subtle.importKey( 'pkcs8', rsaPrivate.export({ format: 'der', type: 'pkcs8' }), - { name, hash: 'SHA-256', namedCurve: 'P-256' }, + { name, namedCurve: 'P-256' }, true, privateUsages), { message: /Invalid key type/ }, ).then(common.mustCall()); } @@ -491,7 +491,7 @@ async function testImportRaw({ name, publicUsages }, namedCurve) { subtle.importKey( 'pkcs8', pkcs8, - { name, hash: 'SHA-256', namedCurve }, + { name, namedCurve }, true, privateUsages), { name: 'DataError', message: /Invalid keyData/ }, ).then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-export-import-rsa.js b/test/parallel/test-webcrypto-export-import-rsa.js index f1bdaeed4dee8e..f726579d495d7e 100644 --- a/test/parallel/test-webcrypto-export-import-rsa.js +++ b/test/parallel/test-webcrypto-export-import-rsa.js @@ -17,6 +17,9 @@ const hashes = [ 'SHA-256', 'SHA-384', 'SHA-512', + 'SHA3-256', + 'SHA3-384', + 'SHA3-512', ]; const keyData = { @@ -387,13 +390,13 @@ async function testImportJwk( let alg; switch (name) { case 'RSA-PSS': - alg = `PS${hash === 'SHA-1' ? 1 : hash.substring(4)}`; + alg = hash.startsWith('SHA-') ? `PS${hash === 'SHA-1' ? 1 : hash.substring(4)}` : undefined; break; case 'RSA-OAEP': - alg = `RSA-OAEP${hash === 'SHA-1' ? '' : hash.substring(3)}`; + alg = hash.startsWith('SHA-') ? `RSA-OAEP${hash === 'SHA-1' ? '' : hash.substring(3)}` : undefined; break; case 'RSASSA-PKCS1-v1_5': - alg = `RS${hash === 'SHA-1' ? 1 : hash.substring(4)}`; + alg = hash.startsWith('SHA-') ? `RS${hash === 'SHA-1' ? 1 : hash.substring(4)}` : undefined; break; } @@ -497,7 +500,7 @@ async function testImportJwk( { message: 'Invalid JWK "use" Parameter' }); } - { + if (alg) { await assert.rejects( subtle.importKey( 'jwk', @@ -516,7 +519,7 @@ async function testImportJwk( { message: 'JWK "alg" does not match the requested algorithm' }); } - { + if (!hash.startsWith('SHA3-')) { let invalidAlgHash = name === 'RSA-OAEP' ? name : name === 'RSA-PSS' ? 'PS' : 'RS'; switch (name) { case 'RSA-OAEP': @@ -547,7 +550,7 @@ async function testImportJwk( { message: 'JWK "alg" does not match the requested algorithm' }); } - { + if (!hash.startsWith('SHA3-')) { const invalidAlgType = name === 'RSA-PSS' ? `RS${hash.substring(4)}` : `PS${hash.substring(4)}`; await assert.rejects( subtle.importKey( diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index bd0cd056d46741..2108972186b367 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -1,12 +1,14 @@ 'use strict'; const common = require('../common'); +const fixtures = require('../common/fixtures'); if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const { subtle } = globalThis.crypto; +const { createPrivateKey, createPublicKey, createSecretKey } = require('crypto'); { async function test() { @@ -291,3 +293,41 @@ const { subtle } = globalThis.crypto; test().then(common.mustCall()); } + +// SHA-3 hashes and JWK "alg" +{ + const rsa = fixtures.readKey('rsa_private_2048.pem'); + const privateKey = createPrivateKey(rsa); + const publicKey = createPublicKey(privateKey); + + async function test(keyObject, algorithm, usages) { + const key = keyObject.toCryptoKey(algorithm, true, usages); + const jwk = await subtle.exportKey('jwk', key); + assert.strictEqual(jwk.alg, undefined); + } + + for (const hash of ['SHA3-256', 'SHA3-384', 'SHA3-512']) { + for (const name of ['RSA-OAEP', 'RSA-PSS', 'RSASSA-PKCS1-v1_5']) { + test(publicKey, { name, hash }, []).then(common.mustCall()); + test(privateKey, { name, hash }, [name === 'RSA-OAEP' ? 'unwrapKey' : 'sign']).then(common.mustCall()); + } + + test(createSecretKey(Buffer.alloc(32)), { name: 'HMAC', hash }, ['sign']); + } + + { + const jwk = createSecretKey(Buffer.alloc(16)).export({ format: 'jwk' }); + // This is rejected for SHA-2 but ignored for SHA-3 + // Otherwise, if the name attribute of hash is defined in another applicable specification: + // Perform any key import steps defined by other applicable specifications, passing format, + // jwk and hash and obtaining hash. + jwk.alg = 'HS3-256'; + + assert.rejects(subtle.importKey('jwk', jwk, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']), { + name: 'DataError', + message: 'JWK "alg" does not match the requested algorithm', + }).then(common.mustCall()); + + subtle.importKey('jwk', jwk, { name: 'HMAC', hash: 'SHA3-256' }, false, ['sign', 'verify']).then(common.mustCall()); + } +} diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index b9802d90d7eca0..08882ea976d471 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -401,15 +401,15 @@ if (hasOpenSSL(3, 5)) { 'RSASSA-PKCS1-v1_5', 1024, Buffer.from([1, 0, 1]), - 'SHA-256', + 'SHA-1', ['sign'], ['verify'], ], [ 'RSA-PSS', - 2048, + 1024, Buffer.from([1, 0, 1]), - 'SHA-512', + 'SHA-256', ['sign'], ['verify'], ], @@ -417,7 +417,7 @@ if (hasOpenSSL(3, 5)) { 'RSA-OAEP', 1024, Buffer.from([3]), - 'SHA-384', + 'SHA3-256', ['decrypt', 'unwrapKey'], ['encrypt', 'wrapKey'], ], @@ -579,6 +579,9 @@ if (hasOpenSSL(3, 5)) { case 'SHA-256': length = 512; break; case 'SHA-384': length = 1024; break; case 'SHA-512': length = 1024; break; + case 'SHA3-256': length = 1088; break; + case 'SHA3-384': length = 832; break; + case 'SHA3-512': length = 576; break; } } @@ -608,6 +611,9 @@ if (hasOpenSSL(3, 5)) { [ undefined, 'SHA-256', ['sign', 'verify']], [ undefined, 'SHA-384', ['sign', 'verify']], [ undefined, 'SHA-512', ['sign', 'verify']], + [ undefined, 'SHA3-256', ['sign', 'verify']], + [ undefined, 'SHA3-384', ['sign', 'verify']], + [ undefined, 'SHA3-512', ['sign', 'verify']], [ 128, 'SHA-256', ['sign', 'verify']], [ 1024, 'SHA-512', ['sign', 'verify']], ]; diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js index ef9f6e8bd45d72..d3e3126ea0f00a 100644 --- a/test/parallel/test-webcrypto-sign-verify-rsa.js +++ b/test/parallel/test-webcrypto-sign-verify-rsa.js @@ -236,7 +236,15 @@ async function testSaltLength(keyLength, hash, hLen) { }); for (const keyLength of [1024, 2048]) { - for (const [hash, hLen] of [['SHA-1', 20], ['SHA-256', 32], ['SHA-384', 48], ['SHA-512', 64]]) { + for (const [hash, hLen] of [ + ['SHA-1', 20], + ['SHA-256', 32], + ['SHA-384', 48], + ['SHA-512', 64], + ['SHA3-256', 32], + ['SHA3-384', 48], + ['SHA3-512', 64], + ]) { variations.push(testSaltLength(keyLength, hash, hLen)); } } diff --git a/test/parallel/test-webcrypto-supports.mjs b/test/parallel/test-webcrypto-supports.mjs index f21a239dc04a27..918fe6a38d148c 100644 --- a/test/parallel/test-webcrypto-supports.mjs +++ b/test/parallel/test-webcrypto-supports.mjs @@ -10,6 +10,7 @@ const sources = [ import('../fixtures/webcrypto/supports-level-2.mjs'), import('../fixtures/webcrypto/supports-secure-curves.mjs'), import('../fixtures/webcrypto/supports-modern-algorithms.mjs'), + import('../fixtures/webcrypto/supports-sha3.mjs'), ]; const vectors = {}; From 6fcce9058a1a1e8afd1fb75c154be045a74bc5c5 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 1 Aug 2025 22:09:12 +0200 Subject: [PATCH 052/111] crypto: add subtle.getPublicKey() utility function in Web Cryptography PR-URL: https://github.com/nodejs/node/pull/59365 Reviewed-By: James M Snell Reviewed-By: Ethan Arrowood Reviewed-By: Yagiz Nizipli Reviewed-By: Joyee Cheung --- doc/api/webcrypto.md | 78 +++++++++++-------- lib/internal/crypto/webcrypto.js | 55 +++++++++++++ .../webcrypto/supports-modern-algorithms.mjs | 16 ++++ test/parallel/test-webcrypto-constructors.js | 7 ++ .../test-webcrypto-get-public-key.mjs | 51 ++++++++++++ 5 files changed, 176 insertions(+), 31 deletions(-) create mode 100644 test/parallel/test-webcrypto-get-public-key.mjs diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index fc9533543f0750..5bf1131b6304f0 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -114,6 +114,7 @@ Key Formats: Methods: +* [`subtle.getPublicKey()`][] * [`SubtleCrypto.supports()`][] ## Secure Curves in the Web Cryptography API @@ -477,36 +478,36 @@ const decrypted = new TextDecoder().decode(await crypto.subtle.decrypt( The table details the algorithms supported by the Node.js Web Crypto API implementation and the APIs supported for each: -| Algorithm | `generateKey` | `exportKey` | `importKey` | `encrypt` | `decrypt` | `wrapKey` | `unwrapKey` | `deriveBits` | `deriveKey` | `sign` | `verify` | `digest` | -| ---------------------------- | ------------- | ----------- | ----------- | --------- | --------- | --------- | ----------- | ------------ | ----------- | ------ | -------- | -------- | -| `'AES-CBC'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | -| `'AES-CTR'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | -| `'AES-GCM'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | -| `'AES-KW'` | ✔ | ✔ | ✔ | | | ✔ | ✔ | | | | | | -| `'cSHAKE128'`[^modern-algos] | | | | | | | | | | | | ✔ | -| `'cSHAKE256'`[^modern-algos] | | | | | | | | | | | | ✔ | -| `'ECDH'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | -| `'ECDSA'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | -| `'Ed25519'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | -| `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | -| `'HKDF'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | | -| `'HMAC'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | -| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | -| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | -| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | -| `'PBKDF2'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | | -| `'RSA-OAEP'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | -| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | -| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | -| `'SHA-1'` | | | | | | | | | | | | ✔ | -| `'SHA-256'` | | | | | | | | | | | | ✔ | -| `'SHA-384'` | | | | | | | | | | | | ✔ | -| `'SHA-512'` | | | | | | | | | | | | ✔ | -| `'SHA3-256'`[^modern-algos] | | | | | | | | | | | | ✔ | -| `'SHA3-384'`[^modern-algos] | | | | | | | | | | | | ✔ | -| `'SHA3-512'`[^modern-algos] | | | | | | | | | | | | ✔ | -| `'X25519'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | -| `'X448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | +| Algorithm | `generateKey` | `exportKey` | `importKey` | `encrypt` | `decrypt` | `wrapKey` | `unwrapKey` | `deriveBits` | `deriveKey` | `sign` | `verify` | `digest` | `getPublicKey` | +| ---------------------------- | ------------- | ----------- | ----------- | --------- | --------- | --------- | ----------- | ------------ | ----------- | ------ | -------- | -------- | -------------- | +| `'AES-CBC'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | | +| `'AES-CTR'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | | +| `'AES-GCM'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | | +| `'AES-KW'` | ✔ | ✔ | ✔ | | | ✔ | ✔ | | | | | | | +| `'cSHAKE128'`[^modern-algos] | | | | | | | | | | | | ✔ | | +| `'cSHAKE256'`[^modern-algos] | | | | | | | | | | | | ✔ | | +| `'ECDH'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | ✔ | +| `'ECDSA'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | +| `'Ed25519'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | +| `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | +| `'HKDF'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | | | +| `'HMAC'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | +| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | +| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | +| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | +| `'PBKDF2'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | | | +| `'RSA-OAEP'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | ✔ | +| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | +| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | +| `'SHA-1'` | | | | | | | | | | | | ✔ | | +| `'SHA-256'` | | | | | | | | | | | | ✔ | | +| `'SHA-384'` | | | | | | | | | | | | ✔ | | +| `'SHA-512'` | | | | | | | | | | | | ✔ | | +| `'SHA3-256'`[^modern-algos] | | | | | | | | | | | | ✔ | | +| `'SHA3-384'`[^modern-algos] | | | | | | | | | | | | ✔ | | +| `'SHA3-512'`[^modern-algos] | | | | | | | | | | | | ✔ | | +| `'X25519'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | ✔ | +| `'X448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | ✔ | ## Class: `Crypto` @@ -691,7 +692,7 @@ added: REPLACEME -* `operation` {string} "encrypt", "decrypt", "sign", "verify", "digest", "generateKey", "deriveKey", "deriveBits", "importKey", "exportKey", "wrapKey", or "unwrapKey" +* `operation` {string} "encrypt", "decrypt", "sign", "verify", "digest", "generateKey", "deriveKey", "deriveBits", "importKey", "exportKey", "getPublicKey", "wrapKey", or "unwrapKey" * `algorithm` {string|Algorithm} * `lengthOrAdditionalAlgorithm` {null|number|string|Algorithm|undefined} Depending on the operation this is either ignored, the value of the length argument when operation is "deriveBits", the algorithm of key to be derived when operation is "deriveKey", the algorithm of key to be exported before wrapping when operation is "wrapKey", or the algorithm of key to be imported after unwrapping when operation is "unwrapKey". **Default:** `null` when operation is "deriveBits", `undefined` otherwise. * Returns: {boolean} Indicating whether the implementation supports the given operation @@ -925,6 +926,20 @@ specification. | `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | | `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | +### `subtle.getPublicKey(key, keyUsages)` + + + +> Stability: 1.1 - Active development + +* `key` {CryptoKey} A private key from which to derive the corresponding public key. +* `keyUsages` {string\[]} See [Key usages][]. +* Returns: {Promise} Fulfills with a {CryptoKey} upon success. + +Derives the public key from a given private key. + ### `subtle.generateKey(algorithm, extractable, keyUsages)` -* `algorithm` {RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} +* `algorithm` {RsaOaepParams|AesCtrParams|AesCbcParams|AeadParams} * `key` {CryptoKey} * `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. @@ -724,6 +734,7 @@ The algorithms currently supported include: * `'AES-CBC'` * `'AES-CTR'` * `'AES-GCM'` +* `'ChaCha20-Poly1305'`[^modern-algos] * `'RSA-OAEP'` ### `subtle.deriveBits(algorithm, baseKey[, length])` @@ -853,9 +864,13 @@ whose value is one of the above. -* `algorithm` {RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} +* `algorithm` {RsaOaepParams|AesCtrParams|AesCbcParams|AeadParams} * `key` {CryptoKey} * `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. @@ -870,6 +885,7 @@ The algorithms currently supported include: * `'AES-CBC'` * `'AES-CTR'` * `'AES-GCM'` +* `'ChaCha20-Poly1305'`[^modern-algos] * `'RSA-OAEP'` ### `subtle.exportKey(format, key)` @@ -877,6 +893,9 @@ The algorithms currently supported include: * `format` {string} Must be one of `'raw'`, `'pkcs8'`, `'spki'`, `'jwk'`, `'raw-secret'`[^modern-algos], @@ -1106,7 +1138,7 @@ added: v15.0.0 -* `unwrapAlgo` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} +* `unwrapAlgo` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AeadParams} * `unwrappedKeyAlgo` {string|Algorithm|RsaHashedImportParams|EcKeyImportParams|HmacImportParams} @@ -1130,6 +1162,7 @@ The wrapping algorithms currently supported include: * `'AES-CTR'` * `'AES-GCM'` * `'AES-KW'` +* `'ChaCha20-Poly1305'`[^modern-algos] * `'RSA-OAEP'` The unwrapped key algorithms supported include: @@ -1138,6 +1171,7 @@ The unwrapped key algorithms supported include: * `'AES-CTR'` * `'AES-GCM'` * `'AES-KW'` +* `'ChaCha20-Poly1305'`[^modern-algos] * `'ECDH'` * `'ECDSA'` * `'Ed25519'` @@ -1198,6 +1232,10 @@ The algorithms currently supported include: @@ -1206,7 +1244,7 @@ added: v15.0.0 `'raw-public'`[^modern-algos], or `'raw-seed'`[^modern-algos]. * `key` {CryptoKey} * `wrappingKey` {CryptoKey} -* `wrapAlgo` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AesGcmParams} +* `wrapAlgo` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AeadParams} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. @@ -1227,6 +1265,7 @@ The wrapping algorithms currently supported include: * `'AES-CTR'` * `'AES-GCM'` * `'AES-KW'` +* `'ChaCha20-Poly1305'`[^modern-algos] * `'RSA-OAEP'` ## Algorithm parameters @@ -1249,112 +1288,108 @@ added: v15.0.0 * Type: {string} -### Class: `AesDerivedKeyParams` +### Class: `AeadParams` -#### `aesDerivedKeyParams.name` +#### `aeadParams.additionalData` -* Type: {string} Must be one of `'AES-CBC'`, `'AES-CTR'`, `'AES-GCM'`, or - `'AES-KW'` +* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} -#### `aesDerivedKeyParams.length` +Extra input that is not encrypted but is included in the authentication +of the data. The use of `additionalData` is optional. + +#### `aeadParams.iv` -* Type: {number} +* Type: {ArrayBuffer|TypedArray|DataView|Buffer} -The length of the AES key to be derived. This must be either `128`, `192`, -or `256`. +The initialization vector must be unique for every encryption operation using a +given key. -### Class: `AesCbcParams` +#### `aeadParams.name` -#### `aesCbcParams.iv` +* Type: {string} Must be `'AES-GCM'` or `'ChaCha20-Poly1305'`. + +#### `aeadParams.tagLength` -* Type: {ArrayBuffer|TypedArray|DataView|Buffer} - -Provides the initialization vector. It must be exactly 16-bytes in length -and should be unpredictable and cryptographically random. +* Type: {number} The size in bits of the generated authentication tag. -#### `aesCbcParams.name` +### Class: `AesDerivedKeyParams` -* Type: {string} Must be `'AES-CBC'`. - -### Class: `AesCtrParams` +#### `aesDerivedKeyParams.name` -#### `aesCtrParams.counter` +* Type: {string} Must be one of `'AES-CBC'`, `'AES-CTR'`, `'AES-GCM'`, or + `'AES-KW'` + +#### `aesDerivedKeyParams.length` -* Type: {ArrayBuffer|TypedArray|DataView|Buffer} - -The initial value of the counter block. This must be exactly 16 bytes long. +* Type: {number} -The `AES-CTR` method uses the rightmost `length` bits of the block as the -counter and the remaining bits as the nonce. +The length of the AES key to be derived. This must be either `128`, `192`, +or `256`. -#### `aesCtrParams.length` +### Class: `AesCbcParams` -* Type: {number} The number of bits in the `aesCtrParams.counter` that are - to be used as the counter. - -#### `aesCtrParams.name` +#### `aesCbcParams.iv` -* Type: {string} Must be `'AES-CTR'`. +* Type: {ArrayBuffer|TypedArray|DataView|Buffer} -### Class: `AesGcmParams` +Provides the initialization vector. It must be exactly 16-bytes in length +and should be unpredictable and cryptographically random. + +#### `aesCbcParams.name` -#### `aesGcmParams.additionalData` +* Type: {string} Must be `'AES-CBC'`. + +### Class: `AesCtrParams` -* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} - -With the AES-GCM method, the `additionalData` is extra input that is not -encrypted but is included in the authentication of the data. The use of -`additionalData` is optional. - -#### `aesGcmParams.iv` +#### `aesCtrParams.counter` -* Type: {string} Must be `'AES-GCM'`. +* Type: {number} The number of bits in the `aesCtrParams.counter` that are + to be used as the counter. -#### `aesGcmParams.tagLength` +#### `aesCtrParams.name` -* Type: {number} The size in bits of the generated authentication tag. - This values must be one of `32`, `64`, `96`, `104`, `112`, `120`, or - `128`. **Default:** `128`. +* Type: {string} Must be `'AES-CTR'`. ### Class: `AesKeyAlgorithm` @@ -2152,7 +2182,6 @@ The length (in bytes) of the random salt to use. [JSON Web Key]: https://tools.ietf.org/html/rfc7517 [Key usages]: #cryptokeyusages [Modern Algorithms in the Web Cryptography API]: #modern-algorithms-in-the-web-cryptography-api -[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf [RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt [Secure Curves in the Web Cryptography API]: #secure-curves-in-the-web-cryptography-api [Web Crypto API]: https://www.w3.org/TR/WebCryptoAPI/ diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js new file mode 100644 index 00000000000000..bcc778b24d7738 --- /dev/null +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -0,0 +1,191 @@ +'use strict'; + +const { + ArrayBufferIsView, + ArrayBufferPrototypeSlice, + ArrayFrom, + PromiseReject, + SafeSet, + TypedArrayPrototypeSlice, +} = primordials; + +const { + ChaCha20Poly1305CipherJob, + KeyObjectHandle, + kCryptoJobAsync, + kWebCryptoCipherDecrypt, + kWebCryptoCipherEncrypt, +} = internalBinding('crypto'); + +const { + hasAnyNotIn, + jobPromise, + validateKeyOps, + kHandle, + kKeyObject, +} = require('internal/crypto/util'); + +const { + lazyDOMException, + promisify, +} = require('internal/util'); + +const { + InternalCryptoKey, + SecretKeyObject, + createSecretKey, +} = require('internal/crypto/keys'); + +const { + randomBytes: _randomBytes, +} = require('internal/crypto/random'); + +const randomBytes = promisify(_randomBytes); + +function validateKeyLength(length) { + if (length !== 256) + throw lazyDOMException('Invalid key length', 'DataError'); +} + +function c20pCipher(mode, key, data, algorithm) { + let tag; + switch (mode) { + case kWebCryptoCipherDecrypt: { + const slice = ArrayBufferIsView(data) ? + TypedArrayPrototypeSlice : ArrayBufferPrototypeSlice; + + if (data.byteLength < 16) { + return PromiseReject(lazyDOMException( + 'The provided data is too small.', + 'OperationError')); + } + + tag = slice(data, -16); + data = slice(data, 0, -16); + break; + } + case kWebCryptoCipherEncrypt: + tag = 16; + break; + } + + return jobPromise(() => new ChaCha20Poly1305CipherJob( + kCryptoJobAsync, + mode, + key[kKeyObject][kHandle], + data, + algorithm.iv, + tag, + algorithm.additionalData)); +} + +async function c20pGenerateKey(algorithm, extractable, keyUsages) { + const { name } = algorithm; + + const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; + + const usagesSet = new SafeSet(keyUsages); + if (hasAnyNotIn(usagesSet, checkUsages)) { + throw lazyDOMException( + `Unsupported key usage for a ${algorithm.name} key`, + 'SyntaxError'); + } + + const keyData = await randomBytes(32).catch((err) => { + throw lazyDOMException( + 'The operation failed for an operation-specific reason' + + `[${err.message}]`, + { name: 'OperationError', cause: err }); + }); + + return new InternalCryptoKey( + createSecretKey(keyData), + { name }, + ArrayFrom(usagesSet), + extractable); +} + +function c20pImportKey( + algorithm, + format, + keyData, + extractable, + keyUsages) { + const { name } = algorithm; + const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; + + const usagesSet = new SafeSet(keyUsages); + if (hasAnyNotIn(usagesSet, checkUsages)) { + throw lazyDOMException( + `Unsupported key usage for a ${algorithm.name} key`, + 'SyntaxError'); + } + + let keyObject; + switch (format) { + case 'KeyObject': { + keyObject = keyData; + break; + } + case 'raw-secret': { + keyObject = createSecretKey(keyData); + break; + } + case 'jwk': { + if (!keyData.kty) + throw lazyDOMException('Invalid keyData', 'DataError'); + + if (keyData.kty !== 'oct') + throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); + + if (usagesSet.size > 0 && + keyData.use !== undefined && + keyData.use !== 'enc') { + throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); + } + + validateKeyOps(keyData.key_ops, usagesSet); + + if (keyData.ext !== undefined && + keyData.ext === false && + extractable === true) { + throw lazyDOMException( + 'JWK "ext" Parameter and extractable mismatch', + 'DataError'); + } + + const handle = new KeyObjectHandle(); + try { + handle.initJwk(keyData); + } catch (err) { + throw lazyDOMException( + 'Invalid keyData', { name: 'DataError', cause: err }); + } + + if (keyData.alg !== undefined && keyData.alg !== 'C20P') { + throw lazyDOMException( + 'JWK "alg" does not match the requested algorithm', + 'DataError'); + } + + keyObject = new SecretKeyObject(handle); + break; + } + default: + return undefined; + } + + validateKeyLength(keyObject.symmetricKeySize * 8); + + return new InternalCryptoKey( + keyObject, + { name }, + keyUsages, + extractable); +} + +module.exports = { + c20pCipher, + c20pGenerateKey, + c20pImportKey, +}; diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 33c84c86a24581..c34f36a277ea37 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -199,6 +199,10 @@ const { result = require('internal/crypto/aes') .aesImportKey(algorithm, 'KeyObject', this, extractable, keyUsages); break; + case 'ChaCha20-Poly1305': + result = require('internal/crypto/chacha20_poly1305') + .c20pImportKey(algorithm, 'KeyObject', this, extractable, keyUsages); + break; case 'HKDF': // Fall through case 'PBKDF2': diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index ce89fc7a6fc41a..6a8c00fa3e18f7 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -245,13 +245,13 @@ const kSupportedAlgorithms = { 'encrypt': { 'RSA-OAEP': 'RsaOaepParams', 'AES-CBC': 'AesCbcParams', - 'AES-GCM': 'AesGcmParams', + 'AES-GCM': 'AeadParams', 'AES-CTR': 'AesCtrParams', }, 'decrypt': { 'RSA-OAEP': 'RsaOaepParams', 'AES-CBC': 'AesCbcParams', - 'AES-GCM': 'AesGcmParams', + 'AES-GCM': 'AeadParams', 'AES-CTR': 'AesCtrParams', }, 'get key length': { @@ -290,6 +290,14 @@ const experimentalAlgorithms = ObjectEntries({ 'SHA3-256': { digest: null }, 'SHA3-384': { digest: null }, 'SHA3-512': { digest: null }, + 'ChaCha20-Poly1305': { + 'encrypt': 'AeadParams', + 'decrypt': 'AeadParams', + 'generateKey': null, + 'importKey': null, + 'exportKey': null, + 'get key length': null, + }, }); for (const { 0: algorithm, 1: nid } of [ @@ -325,7 +333,7 @@ for (let i = 0; i < experimentalAlgorithms.length; i++) { } const simpleAlgorithmDictionaries = { - AesGcmParams: { iv: 'BufferSource', additionalData: 'BufferSource' }, + AeadParams: { iv: 'BufferSource', additionalData: 'BufferSource' }, RsaHashedKeyGenParams: { hash: 'HashAlgorithmIdentifier' }, EcKeyGenParams: {}, HmacKeyGenParams: { hash: 'HashAlgorithmIdentifier' }, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 48d138df63a9a7..2eabd11578af45 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -155,6 +155,11 @@ async function generateKey( result = await require('internal/crypto/aes') .aesGenerateKey(algorithm, extractable, keyUsages); break; + case 'ChaCha20-Poly1305': + resultType = 'CryptoKey'; + result = await require('internal/crypto/chacha20_poly1305') + .c20pGenerateKey(algorithm, extractable, keyUsages); + break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': @@ -252,6 +257,8 @@ function getKeyLength({ name, length, hash }) { case 'HKDF': case 'PBKDF2': return null; + case 'ChaCha20-Poly1305': + return 256; } } @@ -431,7 +438,7 @@ async function exportKeyRawSeed(key) { } } -async function exportKeyRawSecret(key) { +async function exportKeyRawSecret(key, format) { switch (key.algorithm.name) { case 'AES-CTR': // Fall through @@ -443,6 +450,11 @@ async function exportKeyRawSecret(key) { // Fall through case 'HMAC': return key[kKeyObject][kHandle].export().buffer; + case 'ChaCha20-Poly1305': + if (format === 'raw-secret') { + return key[kKeyObject][kHandle].export().buffer; + } + return undefined; default: return undefined; } @@ -504,6 +516,9 @@ async function exportKeyJWK(key) { parameters.alg = require('internal/crypto/aes') .getAlgorithmName(key.algorithm.name, key.algorithm.length); break; + case 'ChaCha20-Poly1305': + parameters.alg = 'C20P'; + break; case 'HMAC': { const alg = normalizeHashName( key.algorithm.hash.name, @@ -563,7 +578,7 @@ async function exportKey(format, key) { } case 'raw-secret': { if (key.type === 'secret') { - result = await exportKeyRawSecret(key); + result = await exportKeyRawSecret(key, format); } break; } @@ -581,7 +596,7 @@ async function exportKey(format, key) { } case 'raw': { if (key.type === 'secret') { - result = await exportKeyRawSecret(key); + result = await exportKeyRawSecret(key, format); } else if (key.type === 'public') { result = await exportKeyRawPublic(key, format); } @@ -687,6 +702,10 @@ async function importKey( result = require('internal/crypto/aes') .aesImportKey(algorithm, format, keyData, extractable, keyUsages); break; + case 'ChaCha20-Poly1305': + result = require('internal/crypto/chacha20_poly1305') + .c20pImportKey(algorithm, format, keyData, extractable, keyUsages); + break; case 'HKDF': // Fall through case 'PBKDF2': @@ -975,6 +994,9 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { case 'AES-GCM': return require('internal/crypto/aes') .aesCipher(mode, key, data, algorithm); + case 'ChaCha20-Poly1305': + return require('internal/crypto/chacha20_poly1305') + .c20pCipher(mode, key, data, algorithm); case 'AES-KW': if (op === 'wrapKey' || op === 'unwrapKey') { return require('internal/crypto/aes') diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 224287af043a04..cf930086e169ea 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -23,6 +23,8 @@ const { ObjectPrototypeIsPrototypeOf, SafeArrayIterator, String, + StringPrototypeStartsWith, + StringPrototypeToLowerCase, TypedArrayPrototypeGetBuffer, TypedArrayPrototypeGetSymbolToStringTag, } = primordials; @@ -195,7 +197,7 @@ const isNonSharedArrayBuffer = isArrayBuffer; function ensureSHA(V, label) { if ( typeof V === 'string' ? - !V.toLowerCase().startsWith('sha') : + !StringPrototypeStartsWith(StringPrototypeToLowerCase(V), 'sha') : V.name?.toLowerCase?.().startsWith('sha') === false ) throw lazyDOMException( @@ -679,13 +681,22 @@ converters.AesCbcParams = createDictionaryConverter( }, ]); -converters.AesGcmParams = createDictionaryConverter( - 'AesGcmParams', [ +converters.AeadParams = createDictionaryConverter( + 'AeadParams', [ ...new SafeArrayIterator(dictAlgorithm), { key: 'iv', converter: converters.BufferSource, - validator: (V, dict) => validateMaxBufferLength(V, 'algorithm.iv'), + validator: (V, dict) => { + switch (StringPrototypeToLowerCase(dict.name)) { + case 'chacha20-poly1305': + validateByteLength(V, 'algorithm.iv', 12); + break; + case 'aes-gcm': + validateMaxBufferLength(V, 'algorithm.iv'); + break; + } + }, required: true, }, { @@ -693,10 +704,21 @@ converters.AesGcmParams = createDictionaryConverter( converter: (V, opts) => converters.octet(V, { ...opts, enforceRange: true }), validator: (V, dict) => { - if (!ArrayPrototypeIncludes([32, 64, 96, 104, 112, 120, 128], V)) { - throw lazyDOMException( - `${V} is not a valid AES-GCM tag length`, - 'OperationError'); + switch (StringPrototypeToLowerCase(dict.name)) { + case 'chacha20-poly1305': + if (V !== 128) { + throw lazyDOMException( + `${V} is not a valid ChaCha20-Poly1305 tag length`, + 'OperationError'); + } + break; + case 'aes-gcm': + if (!ArrayPrototypeIncludes([32, 64, 96, 104, 112, 120, 128], V)) { + throw lazyDOMException( + `${V} is not a valid AES-GCM tag length`, + 'OperationError'); + } + break; } }, }, @@ -744,9 +766,9 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter( throw lazyDOMException( 'algorithm.public must be a public key', 'InvalidAccessError'); - if (V.algorithm.name.toUpperCase() !== dict.name.toUpperCase()) + if (StringPrototypeToLowerCase(V.algorithm.name) !== StringPrototypeToLowerCase(dict.name)) throw lazyDOMException( - `algorithm.public must be an ${dict.name.toUpperCase()} key`, + 'key algorithm mismatch', 'InvalidAccessError'); }, required: true, diff --git a/node.gyp b/node.gyp index bcf01a6b1b5634..970569125fb8ef 100644 --- a/node.gyp +++ b/node.gyp @@ -325,6 +325,7 @@ 'node_crypto_sources': [ 'src/crypto/crypto_aes.cc', 'src/crypto/crypto_bio.cc', + 'src/crypto/crypto_chacha20_poly1305.cc', 'src/crypto/crypto_common.cc', 'src/crypto/crypto_dsa.cc', 'src/crypto/crypto_hkdf.cc', diff --git a/src/crypto/crypto_chacha20_poly1305.cc b/src/crypto/crypto_chacha20_poly1305.cc new file mode 100644 index 00000000000000..bfe904c49ad771 --- /dev/null +++ b/src/crypto/crypto_chacha20_poly1305.cc @@ -0,0 +1,322 @@ +#include "crypto/crypto_chacha20_poly1305.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "crypto/crypto_cipher.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +#include + +namespace node { + +using ncrypto::Cipher; +using ncrypto::CipherCtxPointer; +using ncrypto::DataPointer; +using v8::FunctionCallbackInfo; +using v8::JustVoid; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Object; +using v8::Value; + +namespace crypto { +namespace { +constexpr size_t kChaCha20Poly1305KeySize = 32; +constexpr size_t kChaCha20Poly1305IvSize = 12; +constexpr size_t kChaCha20Poly1305TagSize = 16; + +bool ValidateIV(Environment* env, + CryptoJobMode mode, + Local value, + ChaCha20Poly1305CipherConfig* params) { + ArrayBufferOrViewContents iv(value); + if (!iv.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "iv is too large"); + return false; + } + + if (iv.size() != kChaCha20Poly1305IvSize) { + THROW_ERR_CRYPTO_INVALID_IV(env); + return false; + } + + if (mode == kCryptoJobAsync) { + params->iv = iv.ToCopy(); + } else { + params->iv = iv.ToByteSource(); + } + + return true; +} + +bool ValidateAuthTag(Environment* env, + CryptoJobMode mode, + WebCryptoCipherMode cipher_mode, + Local value, + ChaCha20Poly1305CipherConfig* params) { + switch (cipher_mode) { + case kWebCryptoCipherDecrypt: { + if (!IsAnyBufferSource(value)) { + THROW_ERR_CRYPTO_INVALID_TAG_LENGTH( + env, "Authentication tag must be a buffer"); + return false; + } + + ArrayBufferOrViewContents tag(value); + if (!tag.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "tag is too large"); + return false; + } + + if (tag.size() != kChaCha20Poly1305TagSize) { + THROW_ERR_CRYPTO_INVALID_TAG_LENGTH( + env, "Invalid authentication tag length"); + return false; + } + + if (mode == kCryptoJobAsync) { + params->tag = tag.ToCopy(); + } else { + params->tag = tag.ToByteSource(); + } + break; + } + case kWebCryptoCipherEncrypt: { + // For encryption, the value should be the tag length (passed from + // JavaScript) We expect it to be the tag size constant for + // ChaCha20-Poly1305 + if (!value->IsUint32()) { + THROW_ERR_CRYPTO_INVALID_TAG_LENGTH(env, "Tag length must be a number"); + return false; + } + + uint32_t tag_length = value.As()->Value(); + if (tag_length != kChaCha20Poly1305TagSize) { + THROW_ERR_CRYPTO_INVALID_TAG_LENGTH( + env, "Invalid tag length for ChaCha20-Poly1305"); + return false; + } + // Tag is generated during encryption, not provided + break; + } + default: + UNREACHABLE(); + } + + return true; +} + +bool ValidateAdditionalData(Environment* env, + CryptoJobMode mode, + Local value, + ChaCha20Poly1305CipherConfig* params) { + if (IsAnyBufferSource(value)) { + ArrayBufferOrViewContents additional_data(value); + if (!additional_data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "additional data is too large"); + return false; + } + + if (mode == kCryptoJobAsync) { + params->additional_data = additional_data.ToCopy(); + } else { + params->additional_data = additional_data.ToByteSource(); + } + } + + return true; +} +} // namespace + +ChaCha20Poly1305CipherConfig::ChaCha20Poly1305CipherConfig( + ChaCha20Poly1305CipherConfig&& other) noexcept + : mode(other.mode), + cipher(other.cipher), + iv(std::move(other.iv)), + additional_data(std::move(other.additional_data)), + tag(std::move(other.tag)) {} + +ChaCha20Poly1305CipherConfig& ChaCha20Poly1305CipherConfig::operator=( + ChaCha20Poly1305CipherConfig&& other) noexcept { + if (&other == this) return *this; + this->~ChaCha20Poly1305CipherConfig(); + return *new (this) ChaCha20Poly1305CipherConfig(std::move(other)); +} + +void ChaCha20Poly1305CipherConfig::MemoryInfo(MemoryTracker* tracker) const { + // If mode is sync, then the data in each of these properties + // is not owned by the ChaCha20Poly1305CipherConfig, so we ignore it. + if (mode == kCryptoJobAsync) { + tracker->TrackFieldWithSize("iv", iv.size()); + tracker->TrackFieldWithSize("additional_data", additional_data.size()); + tracker->TrackFieldWithSize("tag", tag.size()); + } +} + +Maybe ChaCha20Poly1305CipherTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + WebCryptoCipherMode cipher_mode, + ChaCha20Poly1305CipherConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->mode = mode; + params->cipher = ncrypto::Cipher::CHACHA20_POLY1305; + + if (!params->cipher) { + THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env); + return Nothing(); + } + + // IV parameter (required) + if (!ValidateIV(env, mode, args[offset], params)) { + return Nothing(); + } + + // Authentication tag parameter (only for decryption) or tag length (for + // encryption) + if (static_cast(args.Length()) > offset + 1) { + if (!ValidateAuthTag(env, mode, cipher_mode, args[offset + 1], params)) { + return Nothing(); + } + } + + // Additional authenticated data parameter (optional) + if (static_cast(args.Length()) > offset + 2) { + if (!ValidateAdditionalData(env, mode, args[offset + 2], params)) { + return Nothing(); + } + } + + return JustVoid(); +} + +WebCryptoCipherStatus ChaCha20Poly1305CipherTraits::DoCipher( + Environment* env, + const KeyObjectData& key_data, + WebCryptoCipherMode cipher_mode, + const ChaCha20Poly1305CipherConfig& params, + const ByteSource& in, + ByteSource* out) { + CHECK_EQ(key_data.GetKeyType(), kKeyTypeSecret); + + // Validate key size + if (key_data.GetSymmetricKeySize() != kChaCha20Poly1305KeySize) { + return WebCryptoCipherStatus::INVALID_KEY_TYPE; + } + + auto ctx = CipherCtxPointer::New(); + CHECK(ctx); + + const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt; + + if (!ctx.init(params.cipher, encrypt)) { + return WebCryptoCipherStatus::FAILED; + } + + if (!ctx.setKeyLength(key_data.GetSymmetricKeySize()) || + !ctx.init( + Cipher(), + encrypt, + reinterpret_cast(key_data.GetSymmetricKey()), + params.iv.data())) { + return WebCryptoCipherStatus::FAILED; + } + + size_t tag_len = 0; + + switch (cipher_mode) { + case kWebCryptoCipherDecrypt: { + if (params.tag.size() != kChaCha20Poly1305TagSize) { + return WebCryptoCipherStatus::FAILED; + } + if (!ctx.setAeadTag(ncrypto::Buffer{ + .data = params.tag.data(), + .len = params.tag.size(), + })) { + return WebCryptoCipherStatus::FAILED; + } + break; + } + case kWebCryptoCipherEncrypt: { + tag_len = kChaCha20Poly1305TagSize; + break; + } + default: + UNREACHABLE(); + } + + size_t total = 0; + int buf_len = in.size() + ctx.getBlockSize() + tag_len; + int out_len; + + // Process additional authenticated data if present + ncrypto::Buffer buffer = { + .data = params.additional_data.data(), + .len = params.additional_data.size(), + }; + if (params.additional_data.size() && !ctx.update(buffer, nullptr, &out_len)) { + return WebCryptoCipherStatus::FAILED; + } + + auto buf = DataPointer::Alloc(buf_len); + auto ptr = static_cast(buf.get()); + + // Process the input data + buffer = { + .data = in.data(), + .len = in.size(), + }; + if (in.empty()) { + if (!ctx.update({}, ptr, &out_len)) { + return WebCryptoCipherStatus::FAILED; + } + } else if (!ctx.update(buffer, ptr, &out_len)) { + return WebCryptoCipherStatus::FAILED; + } + + total += out_len; + CHECK_LE(out_len, buf_len); + out_len = ctx.getBlockSize(); + if (!ctx.update({}, ptr + total, &out_len, true)) { + return WebCryptoCipherStatus::FAILED; + } + total += out_len; + + // If encrypting, grab the generated auth tag and append it to the ciphertext + if (encrypt) { + if (!ctx.getAeadTag(kChaCha20Poly1305TagSize, ptr + total)) { + return WebCryptoCipherStatus::FAILED; + } + total += kChaCha20Poly1305TagSize; + } + + if (total == 0) { + *out = ByteSource(); + return WebCryptoCipherStatus::OK; + } + + // Size down to the actual used space + buf = buf.resize(total); + *out = ByteSource::Allocated(buf.release()); + + return WebCryptoCipherStatus::OK; +} + +void ChaCha20Poly1305::Initialize(Environment* env, Local target) { + ChaCha20Poly1305CryptoJob::Initialize(env, target); +} + +void ChaCha20Poly1305::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + ChaCha20Poly1305CryptoJob::RegisterExternalReferences(registry); +} + +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_chacha20_poly1305.h b/src/crypto/crypto_chacha20_poly1305.h new file mode 100644 index 00000000000000..5b4d5cde2c3929 --- /dev/null +++ b/src/crypto/crypto_chacha20_poly1305.h @@ -0,0 +1,64 @@ +#ifndef SRC_CRYPTO_CRYPTO_CHACHA20_POLY1305_H_ +#define SRC_CRYPTO_CRYPTO_CHACHA20_POLY1305_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_cipher.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "env.h" +#include "v8.h" + +namespace node::crypto { +constexpr unsigned kChaCha20Poly1305AuthTagLength = 16; + +struct ChaCha20Poly1305CipherConfig final : public MemoryRetainer { + CryptoJobMode mode; + ncrypto::Cipher cipher; + ByteSource iv; + ByteSource additional_data; + ByteSource tag; + + ChaCha20Poly1305CipherConfig() = default; + + ChaCha20Poly1305CipherConfig(ChaCha20Poly1305CipherConfig&& other) noexcept; + + ChaCha20Poly1305CipherConfig& operator=( + ChaCha20Poly1305CipherConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(ChaCha20Poly1305CipherConfig) + SET_SELF_SIZE(ChaCha20Poly1305CipherConfig) +}; + +struct ChaCha20Poly1305CipherTraits final { + static constexpr const char* JobName = "ChaCha20Poly1305CipherJob"; + + using AdditionalParameters = ChaCha20Poly1305CipherConfig; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + WebCryptoCipherMode cipher_mode, + ChaCha20Poly1305CipherConfig* config); + + static WebCryptoCipherStatus DoCipher( + Environment* env, + const KeyObjectData& key_data, + WebCryptoCipherMode cipher_mode, + const ChaCha20Poly1305CipherConfig& params, + const ByteSource& in, + ByteSource* out); +}; + +using ChaCha20Poly1305CryptoJob = CipherJob; + +namespace ChaCha20Poly1305 { +void Initialize(Environment* env, v8::Local target); +void RegisterExternalReferences(ExternalReferenceRegistry* registry); +} // namespace ChaCha20Poly1305 +} // namespace node::crypto + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_CHACHA20_POLY1305_H_ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index a94ef62d2c6d78..168491796bd075 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -38,6 +38,7 @@ namespace crypto { #define CRYPTO_NAMESPACE_LIST_BASE(V) \ V(AES) \ + V(ChaCha20Poly1305) \ V(CipherBase) \ V(DiffieHellman) \ V(DSAAlg) \ diff --git a/src/node_crypto.h b/src/node_crypto.h index d34ff52daff8bc..78597168e6fc22 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -30,6 +30,7 @@ // code should include the relevant src/crypto headers directly. #include "crypto/crypto_aes.h" #include "crypto/crypto_bio.h" +#include "crypto/crypto_chacha20_poly1305.h" #include "crypto/crypto_cipher.h" #include "crypto/crypto_context.h" #include "crypto/crypto_dh.h" diff --git a/test/fixtures/crypto/chacha20_poly1305.js b/test/fixtures/crypto/chacha20_poly1305.js new file mode 100644 index 00000000000000..a43edbf6f96d99 --- /dev/null +++ b/test/fixtures/crypto/chacha20_poly1305.js @@ -0,0 +1,102 @@ +'use strict'; + +module.exports = function() { + const kPlaintext = + Buffer.from('546869732073706563696669636174696f6e206465736372696265' + + '732061204a6176615363726970742041504920666f722070657266' + + '6f726d696e672062617369632063727970746f6772617068696320' + + '6f7065726174696f6e7320696e20776562206170706c6963617469' + + '6f6e732c20737563682061732068617368696e672c207369676e61' + + '747572652067656e65726174696f6e20616e642076657269666963' + + '6174696f6e2c20616e6420656e6372797074696f6e20616e642064' + + '656372797074696f6e2e204164646974696f6e616c6c792c206974' + + '2064657363726962657320616e2041504920666f72206170706c69' + + '636174696f6e7320746f2067656e657261746520616e642f6f7220' + + '6d616e61676520746865206b6579696e67206d6174657269616c20' + + '6e656365737361727920746f20706572666f726d20746865736520' + + '6f7065726174696f6e732e205573657320666f7220746869732041' + + '50492072616e67652066726f6d2075736572206f72207365727669' + + '63652061757468656e7469636174696f6e2c20646f63756d656e74' + + '206f7220636f6465207369676e696e672c20616e64207468652063' + + '6f6e666964656e7469616c69747920616e6420696e746567726974' + + '79206f6620636f6d6d756e69636174696f6e732e', 'hex'); + + const kKeyBytes = Buffer.from('67693823fb1d58073f91ece9cc3af910e5532616a4d27b1' + + '3eb7b74d8000bbf30', 'hex') + + const iv = Buffer.from('3a92732aa6ea39bf3986e0c73', 'hex'); + + const additionalData = Buffer.from( + '5468657265206172652037206675727468657220656469746f72696' + + '16c206e6f74657320696e2074686520646f63756d656e742e', 'hex'); + + const tag = Buffer.from('87d611a2b8012f4eb792ccdee7998d22', 'hex') + + const tag_with_empty_ad = Buffer.from('2ba3e8380c1f49f10665fd15a4ac599e', 'hex') + + const kCiphertext = Buffer.from( + '01e15951ce23d7672df9d13f19c54ff5b3fe17114eb637ec25c1a8ac2' + + '24eebe154b3a1206187e18abd31d022b1a66551fbbf0ae2d9fa4e9ab4' + + 'a680b185528000a7654731f05f405ce164cfc904d1759afa758ac459f' + + 'e26420fccac9692af9259243f53e7e0f42d56c6a4b4827056ca76bc9d' + + 'e92577a9f405810fd1e4cb7289f7d528772bde654fef456f031b87802' + + '6616df7349bef4fdff4a52953afadddbd61a3bd1a43815daf1b1ab962' + + '8aaeaee52866466dcb45650b489b2226a01da24d85c20af24e2beb790' + + '233081c5651258cf77e5c47e87ac070aeaa470d13b28b4df82729c350' + + '3cd80a65ac50a8d7a10dabe29ac696410b70209064c3b698343f97f5a' + + '38d63265504ee0922cf5a7c03fe0f3ac1fce28f8eed0153d2f6c500ef' + + '68c71e56e3f1abbfc194be4dd75b73983c3e7c0c68555b71eb4695110' + + 'bb8cd8f495ce7c1e4512c72fca23a095897a9a0dfd584abc3e949cf3e' + + '0fa1d855284d74a915b6e7455e0307985a356c01878700b21c6e0afac' + + 'ee72021a81c3164193e0126d5b841018da2c7c9aa0afb8cd746b378e3' + + '04590eb8b0428b4409b7bcb0cb4a5e9072bb693f011edbe9ab6c5e0c5' + + 'ca51f344fb29034cdbe78b3b66d23467a75e5d28f7e7c92e4e7246ba0' + + 'db7aa408efa3b33e57a4d67fda86d346fc690f07981631', 'hex'); + + const kTagLengths = [128]; + + const passing = []; + kTagLengths.forEach((tagLength) => { + const byteCount = tagLength / 8; + const result = + new Uint8Array(kCiphertext.byteLength + byteCount); + result.set(kCiphertext, 0); + result.set(tag.slice(0, byteCount), + kCiphertext.byteLength); + passing.push({ + keyBuffer: kKeyBytes, + algorithm: { name: 'ChaCha20-Poly1305', iv, additionalData, tagLength }, + plaintext: kPlaintext, + result + }); + + const noadresult = + new Uint8Array(kCiphertext.byteLength + byteCount); + noadresult.set(kCiphertext, 0); + noadresult.set(tag_with_empty_ad.slice(0, byteCount), + kCiphertext.byteLength); + passing.push({ + keyBuffer: kKeyBytes, + algorithm: { name: 'ChaCha20-Poly1305', iv, tagLength }, + plaintext: kPlaintext, + result: noadresult + }); + }); + + const failing = []; + [24, 48, 72, 95, 129].forEach((badTagLength) => { + failing.push({ + keyBuffer: kKeyBytes, + algorithm: { + name: 'ChaCha20-Poly1305', + iv, + additionalData, + tagLength: badTagLength + }, + plaintext: kPlaintext, + result: kCiphertext + }); + }); + + return { passing, failing, decryptionFailing: [] }; +}; diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index bcfe42ce23c2ce..4d972e3956f773 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -12,16 +12,19 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], + [true, 'ChaCha20-Poly1305'], ], 'importKey': [ [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], + [true, 'ChaCha20-Poly1305'], ], 'exportKey': [ [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], + [true, 'ChaCha20-Poly1305'], ], 'getPublicKey': [ [true, 'RSA-OAEP'], @@ -38,5 +41,13 @@ export const vectors = { [false, 'AES-CBC'], [false, 'AES-GCM'], [false, 'AES-KW'], + [false, 'ChaCha20-Poly1305'], ], + 'encrypt': [ + [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], + [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(16) }], + [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], + [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 64 }], + [false, 'ChaCha20-Poly1305'], + ] }; diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index 8488647b3ed204..0fe1b06996c19d 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -25,13 +25,16 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { { for (const length of [128, 192, 256]) { - const aes = createSecretKey(randomBytes(length >> 3)); - for (const algorithm of ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']) { + const key = createSecretKey(randomBytes(length >> 3)); + const algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']; + if (length === 256) + algorithms.push('ChaCha20-Poly1305'); + for (const algorithm of algorithms) { const usages = algorithm === 'AES-KW' ? ['wrapKey', 'unwrapKey'] : ['encrypt', 'decrypt']; for (const extractable of [true, false]) { - const cryptoKey = aes.toCryptoKey(algorithm, extractable, usages); - assertCryptoKey(cryptoKey, aes, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.length, length); + const cryptoKey = key.toCryptoKey(algorithm, extractable, usages); + assertCryptoKey(cryptoKey, key, algorithm, extractable, usages); + assert.strictEqual(cryptoKey.algorithm.length, algorithm !== 'ChaCha20-Poly1305' ? length : undefined); } } } diff --git a/test/parallel/test-webcrypto-derivebits-cfrg.js b/test/parallel/test-webcrypto-derivebits-cfrg.js index d45e59fd5087b9..f73bcab37d8196 100644 --- a/test/parallel/test-webcrypto-derivebits-cfrg.js +++ b/test/parallel/test-webcrypto-derivebits-cfrg.js @@ -182,7 +182,7 @@ async function prepareKeys() { }, keys.X448.privateKey, 8 * keys.X448.size), - { message: 'algorithm.public must be an X448 key' }); + { message: 'key algorithm mismatch' }); } { diff --git a/test/parallel/test-webcrypto-derivebits-ecdh.js b/test/parallel/test-webcrypto-derivebits-ecdh.js index 7a7fa09d2b3754..6bc7a9ab846941 100644 --- a/test/parallel/test-webcrypto-derivebits-ecdh.js +++ b/test/parallel/test-webcrypto-derivebits-ecdh.js @@ -218,7 +218,7 @@ async function prepareKeys() { name: 'ECDH', public: publicKey }, keys['P-521'].privateKey, null), { - message: 'algorithm.public must be an ECDH key' + message: 'key algorithm mismatch' }); } diff --git a/test/parallel/test-webcrypto-derivekey-cfrg.js b/test/parallel/test-webcrypto-derivekey-cfrg.js index 7d8467b4a37cde..f0e0cb82430872 100644 --- a/test/parallel/test-webcrypto-derivekey-cfrg.js +++ b/test/parallel/test-webcrypto-derivekey-cfrg.js @@ -136,7 +136,7 @@ async function prepareKeys() { }, keys.X448.privateKey, ...otherArgs), - { message: 'algorithm.public must be an X448 key' }); + { message: 'key algorithm mismatch' }); } { diff --git a/test/parallel/test-webcrypto-derivekey-ecdh.js b/test/parallel/test-webcrypto-derivekey-ecdh.js index 3b4f86cc02d5ba..61a284cb8f8dea 100644 --- a/test/parallel/test-webcrypto-derivekey-ecdh.js +++ b/test/parallel/test-webcrypto-derivekey-ecdh.js @@ -174,7 +174,7 @@ async function prepareKeys() { }, keys['P-521'].privateKey, ...otherArgs), - { message: 'algorithm.public must be an ECDH key' }); + { message: 'key algorithm mismatch' }); } { diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js new file mode 100644 index 00000000000000..aea9528f2463db --- /dev/null +++ b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js @@ -0,0 +1,255 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (process.features.openssl_is_boringssl) + common.skip('Skipping unsupported ChaCha20-Poly1305 test case'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { + // Using a copy of plaintext to prevent tampering of the original + plaintext = Buffer.from(plaintext); + + const key = await subtle.importKey( + 'raw-secret', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt', 'decrypt']); + + const output = await subtle.encrypt(algorithm, key, plaintext); + plaintext[0] = 255 - plaintext[0]; + + assert.strictEqual( + Buffer.from(output).toString('hex'), + Buffer.from(result).toString('hex')); + + // Converting the returned ArrayBuffer into a Buffer right away, + // so that the next line works + const check = Buffer.from(await subtle.decrypt(algorithm, key, output)); + check[0] = 255 - check[0]; + + assert.strictEqual( + Buffer.from(check).toString('hex'), + Buffer.from(plaintext).toString('hex')); +} + +async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) { + const key = await subtle.importKey( + 'raw-secret', + keyBuffer, + { name: algorithm.name }, + false, + ['decrypt']); + + return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { + message: /The requested operation is not valid for the provided key/ + }); +} + +async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) { + const key = await subtle.importKey( + 'raw-secret', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt']); + + const output = await subtle.encrypt(algorithm, key, plaintext); + + return assert.rejects(subtle.decrypt(algorithm, key, output), { + message: /The requested operation is not valid for the provided key/ + }); +} + +async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) { + assert.notStrictEqual(algorithm.name, alg); + const key = await subtle.importKey( + 'raw-secret', + keyBuffer, + { name: alg }, + false, + ['encrypt']); + + return assert.rejects(subtle.encrypt(algorithm, key, plaintext), { + message: /The requested operation is not valid for the provided key/ + }); +} + +async function testDecrypt({ keyBuffer, algorithm, result }) { + const key = await subtle.importKey( + 'raw-secret', + keyBuffer, + { name: algorithm.name }, + false, + ['encrypt', 'decrypt']); + + await subtle.decrypt(algorithm, key, result); +} + +{ + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/chacha20_poly1305')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-GCM')); + }); + + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /is not a valid ChaCha20-Poly1305 tag length/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /is not a valid ChaCha20-Poly1305 tag length/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + name: 'OperationError' + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + +{ + (async function() { + const secretKey = await subtle.generateKey( + { + name: 'ChaCha20-Poly1305', + }, + false, + ['encrypt', 'decrypt'], + ); + + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); + const aad = globalThis.crypto.getRandomValues(new Uint8Array(32)); + + const encrypted = await subtle.encrypt( + { + name: 'ChaCha20-Poly1305', + iv, + additionalData: aad, + }, + secretKey, + globalThis.crypto.getRandomValues(new Uint8Array(32)) + ); + + await subtle.decrypt( + { + name: 'ChaCha20-Poly1305', + iv, + additionalData: aad, + }, + secretKey, + new Uint8Array(encrypted), + ); + })().then(common.mustCall()); +} + +{ + + async function testRejectsImportKey(format, keyData, algorithm, extractable, usages, expectedError) { + await assert.rejects( + subtle.importKey(format, keyData, algorithm, extractable, usages), + expectedError + ); + } + + async function testRejectsGenerateKey(algorithm, extractable, usages, expectedError) { + await assert.rejects( + subtle.generateKey(algorithm, extractable, usages), + expectedError + ); + } + + (async function() { + const baseJwk = { kty: 'oct', k: 'AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA' }; + const alg = { name: 'ChaCha20-Poly1305' }; + const keyData32 = globalThis.crypto.getRandomValues(new Uint8Array(32)); + + // Test decrypt with data too small + const secretKey = await subtle.generateKey(alg, false, ['encrypt', 'decrypt']); + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); + await assert.rejects( + subtle.decrypt({ name: 'ChaCha20-Poly1305', iv }, secretKey, new Uint8Array(8)), + { name: 'OperationError', message: /The provided data is too small/ } + ); + + // Test invalid tagLength values + await assert.rejects( + subtle.encrypt({ name: 'ChaCha20-Poly1305', iv, tagLength: 64 }, secretKey, keyData32), + { name: 'OperationError', message: /is not a valid ChaCha20-Poly1305 tag length/ } + ); + await assert.rejects( + subtle.encrypt({ name: 'ChaCha20-Poly1305', iv, tagLength: 96 }, secretKey, keyData32), + { name: 'OperationError', message: /is not a valid ChaCha20-Poly1305 tag length/ } + ); + + // JWK error conditions + const jwkTests = [ + [{ k: baseJwk.k }, /Invalid keyData/], + [{ ...baseJwk, kty: 'RSA' }, /Invalid JWK "kty" Parameter/], + [{ ...baseJwk, use: 'sig' }, /Invalid JWK "use" Parameter/], + [{ ...baseJwk, ext: false }, /JWK "ext" Parameter and extractable mismatch/, true], + [{ ...baseJwk, alg: 'A256GCM' }, /JWK "alg" does not match the requested algorithm/], + [{ ...baseJwk, key_ops: ['sign'] }, /Key operations and usage mismatch|Unsupported key usage/], + [{ ...baseJwk, key_ops: ['encrypt'] }, /Key operations and usage mismatch/, false, ['decrypt']], + ]; + + for (const [jwk, errorPattern, extractable = false, usages = ['encrypt']] of jwkTests) { + await testRejectsImportKey('jwk', jwk, alg, extractable, usages, + { name: 'DataError', message: errorPattern }); + } + + // Valid JWK imports + const validKeys = await Promise.all([ + subtle.importKey('jwk', { ...baseJwk, alg: 'C20P' }, alg, false, ['encrypt']), + subtle.importKey('jwk', { ...baseJwk, use: 'enc' }, alg, false, ['encrypt']), + ]); + validKeys.forEach((key) => assert.strictEqual(key.algorithm.name, 'ChaCha20-Poly1305')); + + // Invalid key usages + const usageTests = [ + [['sign'], 'generateKey'], + [['verify'], 'importKey'], + ]; + + for (const [usages, method] of usageTests) { + const fn = method === 'generateKey' ? + () => testRejectsGenerateKey(alg, false, usages, { name: 'SyntaxError', message: /Unsupported key usage/ }) : + () => testRejectsImportKey('raw-secret', keyData32, alg, false, usages, { name: 'SyntaxError', message: /Unsupported key usage/ }); + await fn(); + } + + // Valid wrapKey/unwrapKey usage + const wrapKey = await subtle.importKey('raw-secret', keyData32, alg, false, ['wrapKey', 'unwrapKey']); + assert.strictEqual(wrapKey.algorithm.name, 'ChaCha20-Poly1305'); + + // Invalid key lengths + for (const size of [16, 64]) { + await testRejectsImportKey('raw-secret', new Uint8Array(size), alg, false, ['encrypt'], + { name: 'DataError', message: /Invalid key length/ }); + } + + // Invalid JWK keyData + await testRejectsImportKey('jwk', { ...baseJwk, k: 'invalid-base64-!@#$%^&*()' }, alg, false, ['encrypt'], + { name: 'DataError' }); + })().then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 08882ea976d471..8f3e4ac8cc547c 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -59,6 +59,15 @@ const vectors = { 'unwrapKey', ], }, + 'ChaCha20-Poly1305': { + result: 'CryptoKey', + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey', + ], + }, 'AES-KW': { algorithm: { length: 256 }, result: 'CryptoKey', diff --git a/test/parallel/test-webcrypto-webidl.js b/test/parallel/test-webcrypto-webidl.js index e1675fe5c4e558..107a28adb766be 100644 --- a/test/parallel/test-webcrypto-webidl.js +++ b/test/parallel/test-webcrypto-webidl.js @@ -462,19 +462,19 @@ const opts = { prefix, context }; }); } -// AesGcmParams +// AeadParams { for (const good of [ { name: 'AES-GCM', iv: Buffer.alloc(0) }, { name: 'AES-GCM', iv: Buffer.alloc(0), tagLength: 64 }, { name: 'AES-GCM', iv: Buffer.alloc(0), tagLength: 64, additionalData: Buffer.alloc(0) }, ]) { - assert.deepStrictEqual(converters.AesGcmParams({ ...good, filtered: 'out' }, opts), good); + assert.deepStrictEqual(converters.AeadParams({ ...good, filtered: 'out' }, opts), good); - assert.throws(() => converters.AesGcmParams({ ...good, iv: undefined }, opts), { + assert.throws(() => converters.AeadParams({ ...good, iv: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'AesGcmParams' because 'iv' is required in 'AesGcmParams'.`, + message: `${prefix}: ${context} can not be converted to 'AeadParams' because 'iv' is required in 'AeadParams'.`, }); } } diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 327303cfe33213..188ceb40a80d55 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -39,6 +39,14 @@ const kWrappingData = { }, pair: false }, + 'ChaCha20-Poly1305': { + wrap: { + iv: new Uint8Array(12), + additionalData: new Uint8Array(16), + tagLength: 128 + }, + pair: false + }, 'AES-KW': { generate: { length: 128 }, wrap: { }, @@ -170,6 +178,13 @@ async function generateKeysToWrap() { usages: ['encrypt', 'decrypt'], pair: false, }, + { + algorithm: { + name: 'ChaCha20-Poly1305' + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, { algorithm: { name: 'AES-KW', @@ -235,6 +250,7 @@ async function generateKeysToWrap() { function getFormats(key) { switch (key.type) { case 'secret': { + if (key.algorithm.name === 'ChaCha20-Poly1305') return ['raw-secret', 'jwk']; return ['raw-secret', 'raw', 'jwk']; }; case 'public': { diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 8f2692f74e2f75..55d30bafb0479d 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -98,7 +98,7 @@ const customTypesMap = { 'AesCtrParams': 'webcrypto.html#class-aesctrparams', 'AesCbcParams': 'webcrypto.html#class-aescbcparams', 'AesDerivedKeyParams': 'webcrypto.html#class-aesderivedkeyparams', - 'AesGcmParams': 'webcrypto.html#class-aesgcmparams', + 'AeadParams': 'webcrypto.html#class-aeadparams', 'EcdhKeyDeriveParams': 'webcrypto.html#class-ecdhkeyderiveparams', 'HkdfParams': 'webcrypto.html#class-hkdfparams', 'KeyAlgorithm': 'webcrypto.html#class-keyalgorithm', From a95386fbf91a3a157b3b09bd2f24673576d18b21 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 9 Aug 2025 09:21:18 +0200 Subject: [PATCH 057/111] crypto: subject some algorithms in Web Cryptography on BoringSSL absence PR-URL: https://github.com/nodejs/node/pull/59365 Reviewed-By: James M Snell Reviewed-By: Ethan Arrowood Reviewed-By: Yagiz Nizipli Reviewed-By: Joyee Cheung --- lib/internal/crypto/util.js | 90 +++++++----- test/fixtures/crypto/eddsa.js | 10 +- test/fixtures/webcrypto/supports-level-2.mjs | 14 +- .../webcrypto/supports-modern-algorithms.mjs | 29 +++- .../webcrypto/supports-secure-curves.mjs | 39 ++--- test/fixtures/webcrypto/supports-sha3.mjs | 62 ++++---- .../test-webcrypto-derivebits-cfrg.js | 53 ++++--- .../test-webcrypto-derivebits-hkdf.js | 17 ++- test/parallel/test-webcrypto-derivebits.js | 4 +- .../parallel/test-webcrypto-derivekey-cfrg.js | 49 ++++--- test/parallel/test-webcrypto-derivekey.js | 23 ++- test/parallel/test-webcrypto-digest.js | 41 +++--- .../test-webcrypto-encrypt-decrypt.js | 2 +- .../test-webcrypto-export-import-cfrg.js | 27 ++-- .../test-webcrypto-export-import-rsa.js | 13 +- test/parallel/test-webcrypto-export-import.js | 2 +- test/parallel/test-webcrypto-keygen.js | 135 +++++++++++------- .../test-webcrypto-sign-verify-eddsa.js | 4 +- .../test-webcrypto-sign-verify-rsa.js | 4 +- test/parallel/test-webcrypto-sign-verify.js | 12 +- test/parallel/test-webcrypto-wrap-unwrap.js | 81 ++++++----- 21 files changed, 434 insertions(+), 277 deletions(-) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 6a8c00fa3e18f7..3020682b86bdb2 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -187,7 +187,6 @@ const kSupportedAlgorithms = { 'AES-CTR': 'AesKeyGenParams', 'AES-CBC': 'AesKeyGenParams', 'AES-GCM': 'AesKeyGenParams', - 'AES-KW': 'AesKeyGenParams', 'HMAC': 'HmacKeyGenParams', 'Ed25519': null, 'X25519': null, @@ -202,7 +201,6 @@ const kSupportedAlgorithms = { 'AES-CTR': null, 'AES-CBC': null, 'AES-GCM': null, - 'AES-KW': null, 'Ed25519': null, 'X25519': null, }, @@ -232,7 +230,6 @@ const kSupportedAlgorithms = { 'AES-CTR': null, 'AES-CBC': null, 'AES-GCM': null, - 'AES-KW': null, 'Ed25519': null, 'X25519': null, }, @@ -258,48 +255,69 @@ const kSupportedAlgorithms = { 'AES-CBC': 'AesDerivedKeyParams', 'AES-CTR': 'AesDerivedKeyParams', 'AES-GCM': 'AesDerivedKeyParams', - 'AES-KW': 'AesDerivedKeyParams', 'HMAC': 'HmacImportParams', 'HKDF': null, 'PBKDF2': null, }, - 'wrapKey': { - 'AES-KW': null, - }, - 'unwrapKey': { - 'AES-KW': null, - }, + 'wrapKey': {}, + 'unwrapKey': {}, }; -const experimentalAlgorithms = ObjectEntries({ - 'X448': { - generateKey: null, - importKey: null, - deriveBits: 'EcdhKeyDeriveParams', - exportKey: null, - }, - 'Ed448': { - generateKey: null, - sign: 'Ed448Params', - verify: 'Ed448Params', - importKey: null, - exportKey: null, - }, - 'cSHAKE128': { digest: 'CShakeParams' }, - 'cSHAKE256': { digest: 'CShakeParams' }, - 'SHA3-256': { digest: null }, - 'SHA3-384': { digest: null }, - 'SHA3-512': { digest: null }, - 'ChaCha20-Poly1305': { - 'encrypt': 'AeadParams', - 'decrypt': 'AeadParams', - 'generateKey': null, - 'importKey': null, +const conditionalAlgorithms = ObjectEntries({ + 'AES-KW': [{ + 'generateKey': 'AesKeyGenParams', 'exportKey': null, - 'get key length': null, - }, + 'importKey': null, + 'get key length': 'AesDerivedKeyParams', + 'wrapKey': null, + 'unwrapKey': null, + }, !process.features.openssl_is_boringssl], }); +for (let i = 0; i < conditionalAlgorithms.length; i++) { + if (conditionalAlgorithms[i][1][1]) { + const name = conditionalAlgorithms[i][0]; + const ops = ObjectEntries(conditionalAlgorithms[i][1][0]); + for (let j = 0; j < ops.length; j++) { + const { 0: op, 1: dict } = ops[j]; + kSupportedAlgorithms[op][name] = dict; + } + } +} + +const experimentalAlgorithms = ObjectEntries({}); + +if (!process.features.openssl_is_boringssl) { + ArrayPrototypePush(experimentalAlgorithms, + ['Ed448', { + generateKey: null, + sign: 'Ed448Params', + verify: 'Ed448Params', + importKey: null, + exportKey: null, + }], + ['X448', { + generateKey: null, + importKey: null, + deriveBits: 'EcdhKeyDeriveParams', + exportKey: null, + }], + ['cSHAKE128', { digest: 'CShakeParams' }], + ['cSHAKE256', { digest: 'CShakeParams' }], + ['ChaCha20-Poly1305', { + 'encrypt': 'AeadParams', + 'decrypt': 'AeadParams', + 'generateKey': null, + 'importKey': null, + 'exportKey': null, + 'get key length': null, + }], + ['SHA3-256', { digest: null }], + ['SHA3-384', { digest: null }], + ['SHA3-512', { digest: null }], + ); +} + for (const { 0: algorithm, 1: nid } of [ ['ML-DSA-44', EVP_PKEY_ML_DSA_44], ['ML-DSA-65', EVP_PKEY_ML_DSA_65], diff --git a/test/fixtures/crypto/eddsa.js b/test/fixtures/crypto/eddsa.js index 8b1a5ce1c5a45d..f752d0d4427732 100644 --- a/test/fixtures/crypto/eddsa.js +++ b/test/fixtures/crypto/eddsa.js @@ -1,5 +1,7 @@ 'use strict'; +const common = require('../../common'); + module.exports = function() { const pkcs8 = { 'Ed25519': Buffer.from( @@ -37,7 +39,13 @@ module.exports = function() { '025a2a5a572b9d23b0642f00', 'hex') } - const algorithms = ['Ed25519', 'Ed448']; + const algorithms = ['Ed25519']; + + if (!process.features.openssl_is_boringssl) { + algorithms.push('Ed448') + } else { + common.printSkipMessage(`Skipping unsupported Ed448 test cases`); + } const vectors = algorithms.map((algorithm) => ({ publicKeyBuffer: spki[algorithm], diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index b36564efc5c116..7a8f8cd9d5dd9e 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -11,6 +11,8 @@ const [ECDH, X448, X25519] = await Promise.all([ subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']), ]); +const boringSSL = process.features.openssl_is_boringssl; + export const vectors = { 'encrypt': [ [false, 'Invalid'], @@ -79,7 +81,7 @@ export const vectors = { [false, { name: 'AES-CBC', length: 25 }], [true, { name: 'AES-GCM', length: 128 }], [false, { name: 'AES-GCM', length: 25 }], - [true, { name: 'AES-KW', length: 128 }], + [!boringSSL, { name: 'AES-KW', length: 128 }], [false, { name: 'AES-KW', length: 25 }], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], @@ -189,7 +191,7 @@ export const vectors = { [true, 'AES-CTR'], [true, 'AES-CBC'], [true, 'AES-GCM'], - [true, 'AES-KW'], + [!boringSSL, 'AES-KW'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], @@ -211,18 +213,18 @@ export const vectors = { [true, 'AES-CTR'], [true, 'AES-CBC'], [true, 'AES-GCM'], - [true, 'AES-KW'], + [!boringSSL, 'AES-KW'], [true, 'Ed25519'], [true, 'X25519'], ], 'wrapKey': [ [false, 'AES-KW'], - [true, 'AES-KW', 'AES-CTR'], - [true, 'AES-KW', 'HMAC'], + [!boringSSL, 'AES-KW', 'AES-CTR'], + [!boringSSL, 'AES-KW', 'HMAC'], ], 'unwrapKey': [ [false, 'AES-KW'], - [true, 'AES-KW', 'AES-CTR'], + [!boringSSL, 'AES-KW', 'AES-CTR'], ], 'unsupported operation': [ [false, ''], diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 4d972e3956f773..cb63856c715dff 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -1,8 +1,27 @@ +import * as crypto from 'node:crypto' + import { hasOpenSSL } from '../../common/crypto.js' const pqc = hasOpenSSL(3, 5); +const shake128 = crypto.getHashes().includes('shake128'); +const shake256 = crypto.getHashes().includes('shake256'); +const chacha = crypto.getCiphers().includes('chacha20-poly1305'); 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 }], + [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 }], + ], 'sign': [ [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], @@ -12,19 +31,19 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [true, 'ChaCha20-Poly1305'], + [chacha, 'ChaCha20-Poly1305'], ], 'importKey': [ [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [true, 'ChaCha20-Poly1305'], + [chacha, 'ChaCha20-Poly1305'], ], 'exportKey': [ [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [true, 'ChaCha20-Poly1305'], + [chacha, 'ChaCha20-Poly1305'], ], 'getPublicKey': [ [true, 'RSA-OAEP'], @@ -44,9 +63,9 @@ export const vectors = { [false, 'ChaCha20-Poly1305'], ], 'encrypt': [ - [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], + [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(16) }], - [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], + [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 64 }], [false, 'ChaCha20-Poly1305'], ] diff --git a/test/fixtures/webcrypto/supports-secure-curves.mjs b/test/fixtures/webcrypto/supports-secure-curves.mjs index fb85a74755f4de..eed95aa1f98b0d 100644 --- a/test/fixtures/webcrypto/supports-secure-curves.mjs +++ b/test/fixtures/webcrypto/supports-secure-curves.mjs @@ -1,38 +1,41 @@ const { subtle } = globalThis.crypto; -const [X448, X25519] = await Promise.all([ - subtle.generateKey('X448', false, ['deriveBits', 'deriveKey']), - subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']), -]); +const boringSSL = process.features.openssl_is_boringssl; + +const X25519 = await subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']); +let X448; +if (!boringSSL) { + X448 = await subtle.generateKey('X448', false, ['deriveBits', 'deriveKey']) +} export const vectors = { 'generateKey': [ - [true, 'X448'], - [true, 'Ed448'], + [!boringSSL, 'X448'], + [!boringSSL, 'Ed448'], ], 'deriveKey': [ - [true, - { name: 'X448', public: X448.publicKey }, + [!boringSSL, + { name: 'X448', public: X448?.publicKey }, { name: 'AES-CBC', length: 128 }], - [true, - { name: 'X448', public: X448.publicKey }, + [!boringSSL, + { name: 'X448', public: X448?.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], - [true, - { name: 'X448', public: X448.publicKey }, + [!boringSSL, + { name: 'X448', public: X448?.publicKey }, 'HKDF'], ], 'deriveBits': [ - [true, { name: 'X448', public: X448.publicKey }], + [!boringSSL, { name: 'X448', public: X448?.publicKey }], [false, { name: 'X448', public: X25519.publicKey }], - [false, { name: 'X448', public: X448.privateKey }], + [false, { name: 'X448', public: X448?.privateKey }], [false, 'X448'], ], 'importKey': [ - [true, 'X448'], - [true, 'Ed448'], + [!boringSSL, 'X448'], + [!boringSSL, 'Ed448'], ], 'exportKey': [ - [true, 'Ed448'], - [true, 'X448'], + [!boringSSL, 'Ed448'], + [!boringSSL, 'X448'], ], }; diff --git a/test/fixtures/webcrypto/supports-sha3.mjs b/test/fixtures/webcrypto/supports-sha3.mjs index 077c6a6c40fd5c..0b7be66746ce64 100644 --- a/test/fixtures/webcrypto/supports-sha3.mjs +++ b/test/fixtures/webcrypto/supports-sha3.mjs @@ -1,84 +1,82 @@ const { subtle } = globalThis.crypto; +const boringSSL = process.features.openssl_is_boringssl; + const RSA_KEY_GEN = { modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]) }; -const [ECDH, X448, X25519] = await Promise.all([ +const [ECDH, X25519] = await Promise.all([ subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits', 'deriveKey']), - subtle.generateKey('X448', false, ['deriveBits', 'deriveKey']), subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']), ]); export const vectors = { 'digest': [ - [true, 'SHA3-256'], - [true, 'SHA3-384'], - [true, 'SHA3-512'], + [!boringSSL, 'SHA3-256'], + [!boringSSL, 'SHA3-384'], + [!boringSSL, 'SHA3-512'], ], 'generateKey': [ - [true, { name: 'HMAC', hash: 'SHA3-256' }], - [true, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], - [true, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [true, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [true, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [true, { name: 'HMAC', hash: 'SHA3-256' }], - [true, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [!boringSSL, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [!boringSSL, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [!boringSSL, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], ], 'deriveKey': [ - [true, + [!boringSSL, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, { name: 'AES-CBC', length: 128 }], - [true, + [!boringSSL, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, { name: 'HMAC', hash: 'SHA3-256' }], [false, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 'HKDF'], - [true, + [!boringSSL, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, { name: 'AES-CBC', length: 128 }], - [true, + [!boringSSL, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, { name: 'HMAC', hash: 'SHA3-256' }], [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 'HKDF'], - [true, + [!boringSSL, { name: 'X25519', public: X25519.publicKey }, { name: 'HMAC', hash: 'SHA3-256' }], - [true, - { name: 'X448', public: X448.publicKey }, - { name: 'HMAC', hash: 'SHA3-256' }], - [true, + [!boringSSL, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA3-256' }], ], 'deriveBits': [ - [true, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 8], - [true, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 0], + [!boringSSL, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 8], + [!boringSSL, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 0], [false, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, null], [false, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 7], - [true, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 8], - [true, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 0], + [!boringSSL, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 8], + [!boringSSL, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 0], [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 0 }, 8], [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, null], [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 7], ], 'importKey': [ - [true, { name: 'HMAC', hash: 'SHA3-256' }], - [true, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], - [true, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [true, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [true, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [true, { name: 'HMAC', hash: 'SHA3-256' }], - [true, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + [!boringSSL, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [!boringSSL, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [!boringSSL, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], ], diff --git a/test/parallel/test-webcrypto-derivebits-cfrg.js b/test/parallel/test-webcrypto-derivebits-cfrg.js index f73bcab37d8196..757c8127253662 100644 --- a/test/parallel/test-webcrypto-derivebits-cfrg.js +++ b/test/parallel/test-webcrypto-derivebits-cfrg.js @@ -18,19 +18,26 @@ const kTests = [ '64ea51fae5b3307cfe9706', result: '2768409dfab99ec23b8c89b93ff5880295f76176088f89e43dfebe7ea1950008' }, - { - name: 'X448', - size: 56, - pkcs8: '3046020100300506032b656f043a043858c7d29a3eb519b29d00cfb191bb64fc6' + +]; + +if (!process.features.openssl_is_boringssl) { + kTests.push( + { + name: 'X448', + size: 56, + pkcs8: '3046020100300506032b656f043a043858c7d29a3eb519b29d00cfb191bb64fc6' + 'd8a42d8f17176272b89f2272d1819295c6525c0829671b052ef0727530f188e31' + 'd0cc53bf26929e', - spki: '3042300506032b656f033900b604a1d1a5cd1d9426d561ef630a9eb16cbe69d5b9' + + spki: '3042300506032b656f033900b604a1d1a5cd1d9426d561ef630a9eb16cbe69d5b9' + 'ca615edc53633efb52ea31e6e6a0a1dbacc6e76cbce6482d7e4ba3d55d9e802765' + 'ce6f', - result: 'f0f6c5f17f94f4291eab7178866d37ec8906dd6c514143dc85be7cf28deff39b' + + result: 'f0f6c5f17f94f4291eab7178866d37ec8906dd6c514143dc85be7cf28deff39b' + '726e0f6dcf810eb594dca97b4882bd44c43ea7dc67f49a4e', - }, -]; + } + ); +} else { + common.printSkipMessage('Skipping unsupported X448 test case'); +} async function prepareKeys() { const keys = {}; @@ -153,9 +160,9 @@ async function prepareKeys() { // Missing public property await assert.rejects( subtle.deriveBits( - { name: 'X448' }, - keys.X448.privateKey, - 8 * keys.X448.size), + { name: 'X25519' }, + keys.X25519.privateKey, + 8 * keys.X25519.size), { code: 'ERR_MISSING_OPTION' }); } @@ -164,15 +171,15 @@ async function prepareKeys() { await assert.rejects( subtle.deriveBits( { - name: 'X448', + name: 'X25519', public: { message: 'Not a CryptoKey' } }, - keys.X448.privateKey, - 8 * keys.X448.size), + keys.X25519.privateKey, + 8 * keys.X25519.size), { code: 'ERR_INVALID_ARG_TYPE' }); } - { + if (keys.X25519 && keys.X448) { // Mismatched types await assert.rejects( subtle.deriveBits( @@ -188,9 +195,9 @@ async function prepareKeys() { { // Base key is not a private key await assert.rejects(subtle.deriveBits({ - name: 'X448', - public: keys.X448.publicKey - }, keys.X448.publicKey, null), { + name: 'X25519', + public: keys.X25519.publicKey + }, keys.X25519.publicKey, null), { name: 'InvalidAccessError' }); } @@ -198,9 +205,9 @@ async function prepareKeys() { { // Base key is not a private key await assert.rejects(subtle.deriveBits({ - name: 'X448', - public: keys.X448.privateKey - }, keys.X448.publicKey, null), { + name: 'X25519', + public: keys.X25519.privateKey + }, keys.X25519.publicKey, null), { name: 'InvalidAccessError' }); } @@ -215,9 +222,9 @@ async function prepareKeys() { false, ['encrypt']); await assert.rejects(subtle.deriveBits({ - name: 'X448', + name: 'X25519', public: key - }, keys.X448.publicKey, null), { + }, keys.X25519.publicKey, null), { name: 'InvalidAccessError' }); } diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index e5804a44e623f8..590fef60dc9efe 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -19,17 +19,24 @@ const kDerivedKeyTypes = [ ['AES-CTR', 256, undefined, 'encrypt', 'decrypt'], ['AES-GCM', 128, undefined, 'encrypt', 'decrypt'], ['AES-GCM', 256, undefined, 'encrypt', 'decrypt'], - ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], - ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ['HMAC', 256, 'SHA-1', 'sign', 'verify'], ['HMAC', 256, 'SHA-256', 'sign', 'verify'], ['HMAC', 256, 'SHA-384', 'sign', 'verify'], ['HMAC', 256, 'SHA-512', 'sign', 'verify'], - ['HMAC', 256, 'SHA3-256', 'sign', 'verify'], - ['HMAC', 256, 'SHA3-384', 'sign', 'verify'], - ['HMAC', 256, 'SHA3-512', 'sign', 'verify'], ]; +if (!process.features.openssl_is_boringssl) { + kDerivedKeyTypes.push( + ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], + ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], + ['HMAC', 256, 'SHA3-256', 'sign', 'verify'], + ['HMAC', 256, 'SHA3-384', 'sign', 'verify'], + ['HMAC', 256, 'SHA3-512', 'sign', 'verify'], + ); +} else { + common.printSkipMessage('Skipping unsupported AES-KW test cases'); +} + const kDerivedKeys = { short: '5040737377307264', long: '55736572732073686f756c64207069636b206c6f6e6720706173737068726' + diff --git a/test/parallel/test-webcrypto-derivebits.js b/test/parallel/test-webcrypto-derivebits.js index 0db467b852283a..50892be7400e4a 100644 --- a/test/parallel/test-webcrypto-derivebits.js +++ b/test/parallel/test-webcrypto-derivebits.js @@ -123,8 +123,10 @@ const { subtle } = globalThis.crypto; assert.deepStrictEqual(secret1, secret2); } + test('X25519').then(common.mustCall()); if (!process.features.openssl_is_boringssl) { - test('X25519').then(common.mustCall()); test('X448').then(common.mustCall()); + } else { + common.printSkipMessage('Skipping unsupported X448 test case'); } } diff --git a/test/parallel/test-webcrypto-derivekey-cfrg.js b/test/parallel/test-webcrypto-derivekey-cfrg.js index f0e0cb82430872..c5a5b1f3518fef 100644 --- a/test/parallel/test-webcrypto-derivekey-cfrg.js +++ b/test/parallel/test-webcrypto-derivekey-cfrg.js @@ -18,18 +18,25 @@ const kTests = [ '64ea51fae5b3307cfe9706', result: '2768409dfab99ec23b8c89b93ff5880295f76176088f89e43dfebe7ea1950008' }, - { - name: 'X448', - size: 56, - pkcs8: '3046020100300506032b656f043a043858c7d29a3eb519b29d00cfb191bb64fc6' + +]; + +if (!process.features.openssl_is_boringssl) { + kTests.push( + { + name: 'X448', + size: 56, + pkcs8: '3046020100300506032b656f043a043858c7d29a3eb519b29d00cfb191bb64fc6' + 'd8a42d8f17176272b89f2272d1819295c6525c0829671b052ef0727530f188e31' + 'd0cc53bf26929e', - spki: '3042300506032b656f033900b604a1d1a5cd1d9426d561ef630a9eb16cbe69d5b9' + + spki: '3042300506032b656f033900b604a1d1a5cd1d9426d561ef630a9eb16cbe69d5b9' + 'ca615edc53633efb52ea31e6e6a0a1dbacc6e76cbce6482d7e4ba3d55d9e802765' + 'ce6f', - result: 'f0f6c5f17f94f4291eab7178866d37ec8906dd6c514143dc85be7cf28deff39b' - }, -]; + result: 'f0f6c5f17f94f4291eab7178866d37ec8906dd6c514143dc85be7cf28deff39b' + }, + ); +} else { + common.printSkipMessage('Skipping unsupported X448 test case'); +} async function prepareKeys() { const keys = {}; @@ -107,8 +114,8 @@ async function prepareKeys() { // Missing public property await assert.rejects( subtle.deriveKey( - { name: 'X448' }, - keys.X448.privateKey, + { name: 'X25519' }, + keys.X25519.privateKey, ...otherArgs), { code: 'ERR_MISSING_OPTION' }); } @@ -118,15 +125,15 @@ async function prepareKeys() { await assert.rejects( subtle.deriveKey( { - name: 'X448', + name: 'X25519', public: { message: 'Not a CryptoKey' } }, - keys.X448.privateKey, + keys.X25519.privateKey, ...otherArgs), { code: 'ERR_INVALID_ARG_TYPE' }); } - { + if (keys.X25519 && keys.X448) { // Mismatched named curves await assert.rejects( subtle.deriveKey( @@ -144,10 +151,10 @@ async function prepareKeys() { await assert.rejects( subtle.deriveKey( { - name: 'X448', - public: keys.X448.publicKey + name: 'X25519', + public: keys.X25519.publicKey }, - keys.X448.publicKey, + keys.X25519.publicKey, ...otherArgs), { name: 'InvalidAccessError' }); } @@ -157,10 +164,10 @@ async function prepareKeys() { await assert.rejects( subtle.deriveKey( { - name: 'X448', - public: keys.X448.privateKey + name: 'X25519', + public: keys.X25519.privateKey }, - keys.X448.privateKey, + keys.X25519.privateKey, ...otherArgs), { name: 'InvalidAccessError' }); } @@ -177,10 +184,10 @@ async function prepareKeys() { await assert.rejects( subtle.deriveKey( { - name: 'X448', + name: 'X25519', public: key }, - keys.X448.publicKey, + keys.X25519.publicKey, ...otherArgs), { name: 'InvalidAccessError' }); } diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index 3ee1dcf52f02a0..7a319643fa5f7b 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -81,14 +81,21 @@ const { KeyObject } = require('crypto'); 'e36cf2cf943d8f3a88adb80f478745c336ac811b1a86d03a7d10eb0b6b52295c'], ['hello', 'there', 'my friend', 'SHA-512', '1e42d43fcacba361716f65853bd5f3c479f679612f0180eab3c51ed6c9d2b47d'], - ['hello', 'there', 'my friend', 'SHA3-256', - '2a49a3b6fb219117af9e251c6c65f16600cbca13bd0be6e70d96b0b9fa4cf3fd'], - ['hello', 'there', 'my friend', 'SHA3-384', - '0437bb59b95f2db2c7684c0b439028cb0fdd6f0f5d03b9f489066a87ae147221'], - ['hello', 'there', 'my friend', 'SHA3-512', - '3bbc469d38214371921e52c6f147e96cb7eb370421a81f53dea8b4851dfb8bce'], ]; + if (!process.features.openssl_is_boringssl) { + kTests.push( + ['hello', 'there', 'my friend', 'SHA3-256', + '2a49a3b6fb219117af9e251c6c65f16600cbca13bd0be6e70d96b0b9fa4cf3fd'], + ['hello', 'there', 'my friend', 'SHA3-384', + '0437bb59b95f2db2c7684c0b439028cb0fdd6f0f5d03b9f489066a87ae147221'], + ['hello', 'there', 'my friend', 'SHA3-512', + '3bbc469d38214371921e52c6f147e96cb7eb370421a81f53dea8b4851dfb8bce'], + ); + } else { + common.printSkipMessage('Skipping unsupported SHA-3 test cases'); + } + const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); @@ -239,8 +246,10 @@ const { KeyObject } = require('crypto'); assert.deepStrictEqual(raw1, raw2); } + test('X25519').then(common.mustCall()); if (!process.features.openssl_is_boringssl) { - test('X25519').then(common.mustCall()); test('X448').then(common.mustCall()); + } else { + common.printSkipMessage('Skipping unsupported X448 test case'); } } diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index debfca16ccde5c..04507d77b59142 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -8,20 +8,27 @@ if (!common.hasCrypto) const assert = require('assert'); const { Buffer } = require('buffer'); const { subtle } = globalThis.crypto; -const { createHash } = require('crypto'); +const { createHash, getHashes } = require('crypto'); const kTests = [ ['SHA-1', ['sha1'], 160], ['SHA-256', ['sha256'], 256], ['SHA-384', ['sha384'], 384], ['SHA-512', ['sha512'], 512], - ['SHA3-256', ['sha3-256'], 256], - ['SHA3-384', ['sha3-384'], 384], - ['SHA3-512', ['sha3-512'], 512], - [{ name: 'cSHAKE128', length: 256 }, ['shake128', { outputLength: 256 >> 3 }], 256], - [{ name: 'cSHAKE256', length: 512 }, ['shake256', { outputLength: 512 >> 3 }], 512], ]; +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], + ['SHA3-256', ['sha3-256'], 256], + ['SHA3-384', ['sha3-384'], 384], + ['SHA3-512', ['sha3-512'], 512], + ); +} else { + common.printSkipMessage('Skipping unsupported test cases'); +} + // Empty hash just works, not checking result subtle.digest('SHA-512', Buffer.alloc(0)) .then(common.mustCall()); @@ -247,14 +254,16 @@ function applyXOF(name) { })().then(common.mustCall()); // CShake edge cases -(async () => { - assert.deepStrictEqual( - new Uint8Array(await subtle.digest({ name: 'cSHAKE128', length: 0 }, Buffer.alloc(1))), - new Uint8Array(0), - ); +if (getHashes().includes('shake128')) { + (async () => { + assert.deepStrictEqual( + new Uint8Array(await subtle.digest({ name: 'cSHAKE128', length: 0 }, Buffer.alloc(1))), + new Uint8Array(0), + ); - await assert.rejects(subtle.digest({ name: 'cSHAKE128', length: 7 }, Buffer.alloc(1)), { - name: 'NotSupportedError', - message: 'Unsupported CShakeParams length', - }); -})().then(common.mustCall()); + await assert.rejects(subtle.digest({ name: 'cSHAKE128', length: 7 }, Buffer.alloc(1)), { + name: 'NotSupportedError', + message: 'Unsupported CShakeParams length', + }); + })().then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js index ab46de7a166e5b..c21aa3c29d0afe 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -57,7 +57,7 @@ const { subtle } = globalThis.crypto; } // Test Encrypt/Decrypt RSA-OAEP w/ SHA-3 -{ +if (!process.features.openssl_is_boringssl) { const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); async function test() { diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js index ea1dd8176ae413..ae203e1005de0a 100644 --- a/test/parallel/test-webcrypto-export-import-cfrg.js +++ b/test/parallel/test-webcrypto-export-import-cfrg.js @@ -87,23 +87,30 @@ const testVectors = [ privateUsages: ['sign'], publicUsages: ['verify'] }, - { - name: 'Ed448', - privateUsages: ['sign'], - publicUsages: ['verify'] - }, { name: 'X25519', privateUsages: ['deriveKey', 'deriveBits'], publicUsages: [] }, - { - name: 'X448', - privateUsages: ['deriveKey', 'deriveBits'], - publicUsages: [] - }, ]; +if (!process.features.openssl_is_boringssl) { + testVectors.push( + { + name: 'Ed448', + privateUsages: ['sign'], + publicUsages: ['verify'] + }, + { + name: 'X448', + privateUsages: ['deriveKey', 'deriveBits'], + publicUsages: [] + }, + ); +} else { + common.printSkipMessage('Skipping unsupported Curve448 test cases'); +} + async function testImportSpki({ name, publicUsages }, extractable) { const key = await subtle.importKey( 'spki', diff --git a/test/parallel/test-webcrypto-export-import-rsa.js b/test/parallel/test-webcrypto-export-import-rsa.js index f726579d495d7e..d3af8ec6c3adb9 100644 --- a/test/parallel/test-webcrypto-export-import-rsa.js +++ b/test/parallel/test-webcrypto-export-import-rsa.js @@ -17,11 +17,18 @@ const hashes = [ 'SHA-256', 'SHA-384', 'SHA-512', - 'SHA3-256', - 'SHA3-384', - 'SHA3-512', ]; +if (!process.features.openssl_is_boringssl) { + hashes.push( + 'SHA3-256', + 'SHA3-384', + 'SHA3-512', + ); +} else { + common.printSkipMessage('Skipping unsupported SHA-3 test cases'); +} + const keyData = { 1024: { spki: Buffer.from( diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index 2108972186b367..a93afadcf914cc 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -295,7 +295,7 @@ const { createPrivateKey, createPublicKey, createSecretKey } = require('crypto') } // SHA-3 hashes and JWK "alg" -{ +if (!process.features.openssl_is_boringssl) { const rsa = fixtures.readKey('rsa_private_2048.pem'); const privateKey = createPrivateKey(rsa); const publicKey = createPublicKey(privateKey); diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 8f3e4ac8cc547c..95be384c16b046 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -59,23 +59,6 @@ const vectors = { 'unwrapKey', ], }, - 'ChaCha20-Poly1305': { - result: 'CryptoKey', - usages: [ - 'encrypt', - 'decrypt', - 'wrapKey', - 'unwrapKey', - ], - }, - 'AES-KW': { - algorithm: { length: 256 }, - result: 'CryptoKey', - usages: [ - 'wrapKey', - 'unwrapKey', - ], - }, 'HMAC': { algorithm: { length: 256, hash: 'SHA-256' }, result: 'CryptoKey', @@ -145,13 +128,6 @@ const vectors = { 'verify', ], }, - 'Ed448': { - result: 'CryptoKeyPair', - usages: [ - 'sign', - 'verify', - ], - }, 'X25519': { result: 'CryptoKeyPair', usages: [ @@ -159,14 +135,43 @@ const vectors = { 'deriveBits', ], }, - 'X448': { +}; + +if (!process.features.openssl_is_boringssl) { + vectors.Ed448 = { + result: 'CryptoKeyPair', + usages: [ + 'sign', + 'verify', + ], + }; + vectors.X448 = { result: 'CryptoKeyPair', usages: [ 'deriveKey', 'deriveBits', ], - }, -}; + }; + vectors['AES-KW'] = { + algorithm: { length: 256 }, + result: 'CryptoKey', + usages: [ + 'wrapKey', + 'unwrapKey', + ], + }; + vectors['ChaCha20-Poly1305'] = { + result: 'CryptoKey', + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey', + ], + }; +} else { + common.printSkipMessage('Skipping unsupported test cases'); +} if (hasOpenSSL(3, 5)) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { @@ -422,16 +427,24 @@ if (hasOpenSSL(3, 5)) { ['sign'], ['verify'], ], - [ - 'RSA-OAEP', - 1024, - Buffer.from([3]), - 'SHA3-256', - ['decrypt', 'unwrapKey'], - ['encrypt', 'wrapKey'], - ], ]; + + if (!process.features.openssl_is_boringssl) { + kTests.push( + [ + 'RSA-OAEP', + 1024, + Buffer.from([3]), + 'SHA3-256', + ['decrypt', 'unwrapKey'], + ['encrypt', 'wrapKey'], + ], + ); + } else { + common.printSkipMessage('Skipping unsupported SHA-3 test case'); + } + const tests = kTests.map((args) => test(...args)); Promise.all(tests).then(common.mustCall()); @@ -564,10 +577,17 @@ if (hasOpenSSL(3, 5)) { [ 'AES-CBC', 256, ['encrypt', 'decrypt']], [ 'AES-GCM', 128, ['encrypt', 'decrypt']], [ 'AES-GCM', 256, ['encrypt', 'decrypt']], - [ 'AES-KW', 128, ['wrapKey', 'unwrapKey']], - [ 'AES-KW', 256, ['wrapKey', 'unwrapKey']], ]; + if (!process.features.openssl_is_boringssl) { + kTests.push( + [ 'AES-KW', 128, ['wrapKey', 'unwrapKey']], + [ 'AES-KW', 256, ['wrapKey', 'unwrapKey']], + ); + } else { + common.printSkipMessage('Skipping unsupported AES-KW test cases'); + } + const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); @@ -620,13 +640,21 @@ if (hasOpenSSL(3, 5)) { [ undefined, 'SHA-256', ['sign', 'verify']], [ undefined, 'SHA-384', ['sign', 'verify']], [ undefined, 'SHA-512', ['sign', 'verify']], - [ undefined, 'SHA3-256', ['sign', 'verify']], - [ undefined, 'SHA3-384', ['sign', 'verify']], - [ undefined, 'SHA3-512', ['sign', 'verify']], [ 128, 'SHA-256', ['sign', 'verify']], [ 1024, 'SHA-512', ['sign', 'verify']], ]; + if (!process.features.openssl_is_boringssl) { + kTests.push( + + [ undefined, 'SHA3-256', ['sign', 'verify']], + [ undefined, 'SHA3-384', ['sign', 'verify']], + [ undefined, 'SHA3-512', ['sign', 'verify']], + ); + } else { + common.printSkipMessage('Skipping unsupported SHA-3 test cases'); + } + const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); @@ -684,23 +712,30 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); ['sign'], ['verify'], ], - [ - 'Ed448', - ['sign'], - ['verify'], - ], [ 'X25519', ['deriveKey', 'deriveBits'], [], ], - [ - 'X448', - ['deriveKey', 'deriveBits'], - [], - ], ]; + if (!process.features.openssl_is_boringssl) { + kTests.push( + [ + 'Ed448', + ['sign'], + ['verify'], + ], + [ + 'X448', + ['deriveKey', 'deriveBits'], + [], + ], + ); + } else { + common.printSkipMessage('Skipping unsupported Curve448 test cases'); + } + const tests = kTests.map((args) => test(...args)); Promise.all(tests).then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-sign-verify-eddsa.js b/test/parallel/test-webcrypto-sign-verify-eddsa.js index 6ff432ac23063b..87f562043ad2ca 100644 --- a/test/parallel/test-webcrypto-sign-verify-eddsa.js +++ b/test/parallel/test-webcrypto-sign-verify-eddsa.js @@ -219,7 +219,7 @@ async function testSign({ name, })().then(common.mustCall()); // Ed448 context -{ +if (!process.features.openssl_is_boringssl) { const vector = vectors.find(({ name }) => name === 'Ed448'); Promise.all([ subtle.importKey( @@ -247,4 +247,6 @@ async function testSign({ name, message: /Non zero-length Ed448Params\.context is not supported/ }); }).then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported Ed448 test case'); } diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js index d3e3126ea0f00a..7e90388cc4c270 100644 --- a/test/parallel/test-webcrypto-sign-verify-rsa.js +++ b/test/parallel/test-webcrypto-sign-verify-rsa.js @@ -245,7 +245,9 @@ async function testSaltLength(keyLength, hash, hLen) { ['SHA3-384', 48], ['SHA3-512', 64], ]) { - variations.push(testSaltLength(keyLength, hash, hLen)); + if (hash.startsWith('SHA-3') && !process.features.openssl_is_boringssl) { + variations.push(testSaltLength(keyLength, hash, hLen)); + } } } diff --git a/test/parallel/test-webcrypto-sign-verify.js b/test/parallel/test-webcrypto-sign-verify.js index 3b56e3ad041b89..a43123ce294d27 100644 --- a/test/parallel/test-webcrypto-sign-verify.js +++ b/test/parallel/test-webcrypto-sign-verify.js @@ -123,13 +123,11 @@ const { subtle } = globalThis.crypto; name: 'Ed25519', }, publicKey, signature, ec.encode(data))); } - if (!process.features.openssl_is_boringssl) { - test('hello world').then(common.mustCall()); - } + test('hello world').then(common.mustCall()); } // Test Sign/Verify Ed448 -{ +if (!process.features.openssl_is_boringssl) { async function test(data) { const ec = new TextEncoder(); const { publicKey, privateKey } = await subtle.generateKey({ @@ -145,9 +143,9 @@ const { subtle } = globalThis.crypto; }, publicKey, signature, ec.encode(data))); } - if (!process.features.openssl_is_boringssl) { - test('hello world').then(common.mustCall()); - } + test('hello world').then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported Ed448 test case'); } // Test Sign/Verify ML-DSA diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 188ceb40a80d55..6a504692ec53ed 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -39,20 +39,25 @@ const kWrappingData = { }, pair: false }, - 'ChaCha20-Poly1305': { +}; + +if (!process.features.openssl_is_boringssl) { + kWrappingData['AES-KW'] = { + generate: { length: 128 }, + wrap: { }, + pair: false + }; + kWrappingData['ChaCha20-Poly1305'] = { wrap: { iv: new Uint8Array(12), additionalData: new Uint8Array(16), tagLength: 128 }, pair: false - }, - 'AES-KW': { - generate: { length: 128 }, - wrap: { }, - pair: false - } -}; + }; +} else { + common.printSkipMessage('Skipping unsupported AES-KW test case'); +} function generateWrappingKeys() { return Promise.all(Object.keys(kWrappingData).map(async (name) => { @@ -131,14 +136,6 @@ async function generateKeysToWrap() { publicUsages: ['verify'], pair: true, }, - { - algorithm: { - name: 'Ed448', - }, - privateUsages: ['sign'], - publicUsages: ['verify'], - pair: true, - }, { algorithm: { name: 'X25519', @@ -147,14 +144,6 @@ async function generateKeysToWrap() { publicUsages: [], pair: true, }, - { - algorithm: { - name: 'X448', - }, - privateUsages: ['deriveBits'], - publicUsages: [], - pair: true, - }, { algorithm: { name: 'AES-CTR', @@ -185,14 +174,6 @@ async function generateKeysToWrap() { usages: ['encrypt', 'decrypt'], pair: false, }, - { - algorithm: { - name: 'AES-KW', - length: 128 - }, - usages: ['wrapKey', 'unwrapKey'], - pair: false, - }, { algorithm: { name: 'HMAC', @@ -204,6 +185,19 @@ async function generateKeysToWrap() { }, ]; + if (!process.features.openssl_is_boringssl) { + parameters.push({ + algorithm: { + name: 'AES-KW', + length: 128 + }, + usages: ['wrapKey', 'unwrapKey'], + pair: false, + }); + } else { + common.printSkipMessage('Skipping unsupported AES-KW test case'); + } + if (hasOpenSSL(3, 5)) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { parameters.push({ @@ -215,6 +209,29 @@ async function generateKeysToWrap() { } } + if (!process.features.openssl_is_boringssl) { + parameters.push( + { + algorithm: { + name: 'Ed448', + }, + privateUsages: ['sign'], + publicUsages: ['verify'], + pair: true, + }, + { + algorithm: { + name: 'X448', + }, + privateUsages: ['deriveBits'], + publicUsages: [], + pair: true, + }, + ); + } else { + common.printSkipMessage('Skipping unsupported Curve test cases'); + } + const allkeys = await Promise.all(parameters.map(async (params) => { const usages = 'usages' in params ? params.usages : From 93fc80a1e2d15f68c568357df96262a253e9baa2 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 9 Aug 2025 13:41:45 +0200 Subject: [PATCH 058/111] lib: refactor kSupportedAlgorithms PR-URL: https://github.com/nodejs/node/pull/59365 Reviewed-By: James M Snell Reviewed-By: Ethan Arrowood Reviewed-By: Yagiz Nizipli Reviewed-By: Joyee Cheung --- lib/internal/crypto/util.js | 365 ++++++++++++++++++++---------------- 1 file changed, 205 insertions(+), 160 deletions(-) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 3020682b86bdb2..685637ab7435df 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -171,185 +171,230 @@ const kNamedCurveAliases = { 'P-521': 'secp521r1', }; -const kSupportedAlgorithms = { - 'digest': { - 'SHA-1': null, - 'SHA-256': null, - 'SHA-384': null, - 'SHA-512': null, +// Algorithm definitions organized by algorithm name +const kAlgorithmDefinitions = { + 'AES-CBC': { + 'generateKey': 'AesKeyGenParams', + 'exportKey': null, + 'importKey': null, + 'encrypt': 'AesCbcParams', + 'decrypt': 'AesCbcParams', + 'get key length': 'AesDerivedKeyParams', + }, + 'AES-CTR': { + 'generateKey': 'AesKeyGenParams', + 'exportKey': null, + 'importKey': null, + 'encrypt': 'AesCtrParams', + 'decrypt': 'AesCtrParams', + 'get key length': 'AesDerivedKeyParams', }, - 'generateKey': { - 'RSASSA-PKCS1-v1_5': 'RsaHashedKeyGenParams', - 'RSA-PSS': 'RsaHashedKeyGenParams', - 'RSA-OAEP': 'RsaHashedKeyGenParams', - 'ECDSA': 'EcKeyGenParams', - 'ECDH': 'EcKeyGenParams', - 'AES-CTR': 'AesKeyGenParams', - 'AES-CBC': 'AesKeyGenParams', - 'AES-GCM': 'AesKeyGenParams', - 'HMAC': 'HmacKeyGenParams', - 'Ed25519': null, - 'X25519': null, + 'AES-GCM': { + 'generateKey': 'AesKeyGenParams', + 'exportKey': null, + 'importKey': null, + 'encrypt': 'AeadParams', + 'decrypt': 'AeadParams', + 'get key length': 'AesDerivedKeyParams', }, - 'exportKey': { - 'RSASSA-PKCS1-v1_5': null, - 'RSA-PSS': null, - 'RSA-OAEP': null, - 'ECDSA': null, - 'ECDH': null, - 'HMAC': null, - 'AES-CTR': null, - 'AES-CBC': null, - 'AES-GCM': null, - 'Ed25519': null, - 'X25519': null, + 'AES-KW': { + 'generateKey': 'AesKeyGenParams', + 'exportKey': null, + 'importKey': null, + 'get key length': 'AesDerivedKeyParams', + 'wrapKey': null, + 'unwrapKey': null, }, - 'sign': { - 'RSASSA-PKCS1-v1_5': null, - 'RSA-PSS': 'RsaPssParams', - 'ECDSA': 'EcdsaParams', - 'HMAC': null, - 'Ed25519': null, + 'ChaCha20-Poly1305': { + 'generateKey': null, + 'exportKey': null, + 'importKey': null, + 'encrypt': 'AeadParams', + 'decrypt': 'AeadParams', + 'get key length': null, }, - 'verify': { - 'RSASSA-PKCS1-v1_5': null, - 'RSA-PSS': 'RsaPssParams', - 'ECDSA': 'EcdsaParams', - 'HMAC': null, - 'Ed25519': null, + 'cSHAKE128': { 'digest': 'CShakeParams' }, + 'cSHAKE256': { 'digest': 'CShakeParams' }, + 'ECDH': { + 'generateKey': 'EcKeyGenParams', + 'exportKey': null, + 'importKey': 'EcKeyImportParams', + 'deriveBits': 'EcdhKeyDeriveParams', }, - 'importKey': { - 'RSASSA-PKCS1-v1_5': 'RsaHashedImportParams', - 'RSA-PSS': 'RsaHashedImportParams', - 'RSA-OAEP': 'RsaHashedImportParams', - 'ECDSA': 'EcKeyImportParams', - 'ECDH': 'EcKeyImportParams', - 'HMAC': 'HmacImportParams', - 'HKDF': null, - 'PBKDF2': null, - 'AES-CTR': null, - 'AES-CBC': null, - 'AES-GCM': null, - 'Ed25519': null, - 'X25519': null, + 'ECDSA': { + 'generateKey': 'EcKeyGenParams', + 'exportKey': null, + 'importKey': 'EcKeyImportParams', + 'sign': 'EcdsaParams', + 'verify': 'EcdsaParams', }, - 'deriveBits': { - 'HKDF': 'HkdfParams', - 'PBKDF2': 'Pbkdf2Params', - 'ECDH': 'EcdhKeyDeriveParams', - 'X25519': 'EcdhKeyDeriveParams', + 'Ed25519': { + 'generateKey': null, + 'exportKey': null, + 'importKey': null, + 'sign': null, + 'verify': null, }, - 'encrypt': { - 'RSA-OAEP': 'RsaOaepParams', - 'AES-CBC': 'AesCbcParams', - 'AES-GCM': 'AeadParams', - 'AES-CTR': 'AesCtrParams', + 'Ed448': { + 'generateKey': null, + 'exportKey': null, + 'importKey': null, + 'sign': 'Ed448Params', + 'verify': 'Ed448Params', }, - 'decrypt': { - 'RSA-OAEP': 'RsaOaepParams', - 'AES-CBC': 'AesCbcParams', - 'AES-GCM': 'AeadParams', - 'AES-CTR': 'AesCtrParams', + 'HKDF': { + 'importKey': null, + 'deriveBits': 'HkdfParams', + 'get key length': null, }, - 'get key length': { - 'AES-CBC': 'AesDerivedKeyParams', - 'AES-CTR': 'AesDerivedKeyParams', - 'AES-GCM': 'AesDerivedKeyParams', - 'HMAC': 'HmacImportParams', - 'HKDF': null, - 'PBKDF2': null, + 'HMAC': { + 'generateKey': 'HmacKeyGenParams', + 'exportKey': null, + 'importKey': 'HmacImportParams', + 'sign': null, + 'verify': null, + 'get key length': 'HmacImportParams', }, - 'wrapKey': {}, - 'unwrapKey': {}, -}; - -const conditionalAlgorithms = ObjectEntries({ - 'AES-KW': [{ - 'generateKey': 'AesKeyGenParams', + 'ML-DSA-44': { + 'generateKey': null, 'exportKey': null, 'importKey': null, - 'get key length': 'AesDerivedKeyParams', - 'wrapKey': null, - 'unwrapKey': null, - }, !process.features.openssl_is_boringssl], -}); + 'sign': 'ContextParams', + 'verify': 'ContextParams', + }, + 'ML-DSA-65': { + 'generateKey': null, + 'exportKey': null, + 'importKey': null, + 'sign': 'ContextParams', + 'verify': 'ContextParams', + }, + 'ML-DSA-87': { + 'generateKey': null, + 'exportKey': null, + 'importKey': null, + 'sign': 'ContextParams', + 'verify': 'ContextParams', + }, + 'PBKDF2': { + 'importKey': null, + 'deriveBits': 'Pbkdf2Params', + 'get key length': null, + }, + 'RSA-OAEP': { + 'generateKey': 'RsaHashedKeyGenParams', + 'exportKey': null, + 'importKey': 'RsaHashedImportParams', + 'encrypt': 'RsaOaepParams', + 'decrypt': 'RsaOaepParams', + }, + 'RSA-PSS': { + 'generateKey': 'RsaHashedKeyGenParams', + 'exportKey': null, + 'importKey': 'RsaHashedImportParams', + 'sign': 'RsaPssParams', + 'verify': 'RsaPssParams', + }, + 'RSASSA-PKCS1-v1_5': { + 'generateKey': 'RsaHashedKeyGenParams', + 'exportKey': null, + 'importKey': 'RsaHashedImportParams', + 'sign': null, + 'verify': null, + }, + 'SHA-1': { 'digest': null }, + 'SHA-256': { 'digest': null }, + 'SHA-384': { 'digest': null }, + 'SHA-512': { 'digest': null }, + 'SHA3-256': { 'digest': null }, + 'SHA3-384': { 'digest': null }, + 'SHA3-512': { 'digest': null }, + 'X25519': { + 'generateKey': null, + 'exportKey': null, + 'importKey': null, + 'deriveBits': 'EcdhKeyDeriveParams', + }, + 'X448': { + 'generateKey': null, + 'exportKey': null, + 'importKey': null, + 'deriveBits': 'EcdhKeyDeriveParams', + }, +}; -for (let i = 0; i < conditionalAlgorithms.length; i++) { - if (conditionalAlgorithms[i][1][1]) { - const name = conditionalAlgorithms[i][0]; - const ops = ObjectEntries(conditionalAlgorithms[i][1][0]); - for (let j = 0; j < ops.length; j++) { - const { 0: op, 1: dict } = ops[j]; - kSupportedAlgorithms[op][name] = dict; - } - } -} +// Conditionally supported algorithms +const conditionalAlgorithms = { + 'AES-KW': !process.features.openssl_is_boringssl, + 'ChaCha20-Poly1305': !process.features.openssl_is_boringssl || + ArrayPrototypeIncludes(getCiphers(), 'chacha20-poly1305'), + 'cSHAKE128': !process.features.openssl_is_boringssl || + ArrayPrototypeIncludes(getHashes(), 'shake128'), + 'cSHAKE256': !process.features.openssl_is_boringssl || + ArrayPrototypeIncludes(getHashes(), 'shake256'), + 'Ed448': !process.features.openssl_is_boringssl, + 'ML-DSA-44': !!EVP_PKEY_ML_DSA_44, + 'ML-DSA-65': !!EVP_PKEY_ML_DSA_65, + 'ML-DSA-87': !!EVP_PKEY_ML_DSA_87, + 'SHA3-256': !process.features.openssl_is_boringssl || + ArrayPrototypeIncludes(getHashes(), 'sha3-256'), + 'SHA3-384': !process.features.openssl_is_boringssl || + ArrayPrototypeIncludes(getHashes(), 'sha3-384'), + 'SHA3-512': !process.features.openssl_is_boringssl || + ArrayPrototypeIncludes(getHashes(), 'sha3-512'), + 'X448': !process.features.openssl_is_boringssl, +}; -const experimentalAlgorithms = ObjectEntries({}); - -if (!process.features.openssl_is_boringssl) { - ArrayPrototypePush(experimentalAlgorithms, - ['Ed448', { - generateKey: null, - sign: 'Ed448Params', - verify: 'Ed448Params', - importKey: null, - exportKey: null, - }], - ['X448', { - generateKey: null, - importKey: null, - deriveBits: 'EcdhKeyDeriveParams', - exportKey: null, - }], - ['cSHAKE128', { digest: 'CShakeParams' }], - ['cSHAKE256', { digest: 'CShakeParams' }], - ['ChaCha20-Poly1305', { - 'encrypt': 'AeadParams', - 'decrypt': 'AeadParams', - 'generateKey': null, - 'importKey': null, - 'exportKey': null, - 'get key length': null, - }], - ['SHA3-256', { digest: null }], - ['SHA3-384', { digest: null }], - ['SHA3-512', { digest: null }], - ); -} +// Experimental algorithms +const experimentalAlgorithms = [ + 'ChaCha20-Poly1305', + 'cSHAKE128', + 'cSHAKE256', + 'Ed448', + 'ML-DSA-44', + 'ML-DSA-65', + 'ML-DSA-87', + 'SHA3-256', + 'SHA3-384', + 'SHA3-512', + 'X448', +]; + +// Transform the algorithm definitions into the operation-keyed structure +function createSupportedAlgorithms(algorithmDefs) { + const result = {}; + + for (const { 0: algorithmName, 1: operations } of ObjectEntries(algorithmDefs)) { + // Skip algorithms that are conditionally not supported + if (ObjectPrototypeHasOwnProperty(conditionalAlgorithms, algorithmName) && + !conditionalAlgorithms[algorithmName]) { + continue; + } -for (const { 0: algorithm, 1: nid } of [ - ['ML-DSA-44', EVP_PKEY_ML_DSA_44], - ['ML-DSA-65', EVP_PKEY_ML_DSA_65], - ['ML-DSA-87', EVP_PKEY_ML_DSA_87], -]) { - if (nid) { - ArrayPrototypePush(experimentalAlgorithms, [algorithm, { - generateKey: null, - sign: 'ContextParams', - verify: 'ContextParams', - importKey: null, - exportKey: null, - }]); + for (const { 0: operation, 1: dict } of ObjectEntries(operations)) { + result[operation] ||= {}; + + // Add experimental warnings for experimental algorithms + if (ArrayPrototypeIncludes(experimentalAlgorithms, algorithmName)) { + ObjectDefineProperty(result[operation], algorithmName, { + get() { + emitExperimentalWarning(`The ${algorithmName} Web Crypto API algorithm`); + return dict; + }, + __proto__: null, + enumerable: true, + }); + } else { + result[operation][algorithmName] = dict; + } + } } -} -for (let i = 0; i < experimentalAlgorithms.length; i++) { - const name = experimentalAlgorithms[i][0]; - const ops = ObjectEntries(experimentalAlgorithms[i][1]); - for (let j = 0; j < ops.length; j++) { - const { 0: op, 1: dict } = ops[j]; - ObjectDefineProperty(kSupportedAlgorithms[op], name, { - get() { - emitExperimentalWarning(`The ${name} Web Crypto API algorithm`); - return dict; - }, - __proto__: null, - enumerable: true, - }); - } + return result; } +const kSupportedAlgorithms = createSupportedAlgorithms(kAlgorithmDefinitions); + const simpleAlgorithmDictionaries = { AeadParams: { iv: 'BufferSource', additionalData: 'BufferSource' }, RsaHashedKeyGenParams: { hash: 'HashAlgorithmIdentifier' }, From 4fe383e45ab6010dc9a4894a748ae75414cae696 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 11 Aug 2025 14:19:05 +0200 Subject: [PATCH 059/111] crypto: support ML-DSA spki/pkcs8 key formats in Web Cryptography PR-URL: https://github.com/nodejs/node/pull/59365 Reviewed-By: James M Snell Reviewed-By: Ethan Arrowood Reviewed-By: Yagiz Nizipli Reviewed-By: Joyee Cheung --- doc/api/webcrypto.md | 12 +- lib/internal/crypto/ml_dsa.js | 80 +++++++- lib/internal/crypto/webcrypto.js | 16 ++ .../test-webcrypto-export-import-ml-dsa.js | 194 +++++++++++++++++- 4 files changed, 280 insertions(+), 22 deletions(-) diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index 570ff14bd13cd3..2ec2ba3598ef59 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -939,9 +939,9 @@ specification. | `'Ed25519'` | ✔ | ✔ | ✔ | ✔ | | ✔ | | | `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | ✔ | | ✔ | | | `'HMAC'` | | | ✔ | ✔ | ✔ | | | -| `'ML-DSA-44'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | -| `'ML-DSA-65'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | -| `'ML-DSA-87'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | +| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ | +| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ | +| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ | | `'RSA-OAEP'` | ✔ | ✔ | ✔ | | | | | | `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | | `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | @@ -1070,9 +1070,9 @@ The algorithms currently supported include: | `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | ✔ | | ✔ | | | `'HDKF'` | | | | ✔ | ✔ | | | | `'HMAC'` | | | ✔ | ✔ | ✔ | | | -| `'ML-DSA-44'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | -| `'ML-DSA-65'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | -| `'ML-DSA-87'`[^modern-algos] | | | ✔ | | | ✔ | ✔ | +| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ | +| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ | +| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ | | `'PBKDF2'` | | | | ✔ | ✔ | | | | `'RSA-OAEP'` | ✔ | ✔ | ✔ | | | | | | `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index a8f3e7d5a1d3e9..21dc95786b10e1 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -2,6 +2,7 @@ const { SafeSet, + Uint8Array, } = primordials; const { Buffer } = require('buffer'); @@ -14,6 +15,10 @@ const { kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, + kKeyFormatDER, + kWebCryptoKeyFormatRaw, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatSPKI, } = internalBinding('crypto'); const { @@ -44,6 +49,7 @@ const { InternalCryptoKey, PrivateKeyObject, PublicKeyObject, + createPrivateKey, createPublicKey, } = require('internal/crypto/keys'); @@ -106,15 +112,47 @@ async function mlDsaGenerateKey(algorithm, extractable, keyUsages) { return { __proto__: null, privateKey, publicKey }; } -function mlDsaExportKey(key) { +function mlDsaExportKey(key, format) { try { - if (key.type === 'private') { - const { priv } = key[kKeyObject][kHandle].exportJwk({}, false); - return Buffer.alloc(32, priv, 'base64url').buffer; - } + switch (format) { + case kWebCryptoKeyFormatRaw: { + if (key.type === 'private') { + const { priv } = key[kKeyObject][kHandle].exportJwk({}, false); + return Buffer.alloc(32, priv, 'base64url').buffer; + } - const { pub } = key[kKeyObject][kHandle].exportJwk({}, false); - return Buffer.alloc(Buffer.byteLength(pub, 'base64url'), pub, 'base64url').buffer; + const { pub } = key[kKeyObject][kHandle].exportJwk({}, false); + return Buffer.alloc(Buffer.byteLength(pub, 'base64url'), pub, 'base64url').buffer; + } + case kWebCryptoKeyFormatSPKI: { + return key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI).buffer; + } + case kWebCryptoKeyFormatPKCS8: { + const { priv } = key[kKeyObject][kHandle].exportJwk({}, false); + const seed = Buffer.alloc(32, priv, 'base64url'); + const buffer = new Uint8Array(54); + buffer.set([ + 0x30, 0x34, 0x02, 0x01, 0x00, 0x30, 0x0B, 0x06, + 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, + 0x03, 0x00, 0x04, 0x22, 0x80, 0x20, + ], 0); + switch (key.algorithm.name) { + case 'ML-DSA-44': + buffer.set([0x11], 17); + break; + case 'ML-DSA-65': + buffer.set([0x12], 17); + break; + case 'ML-DSA-87': + buffer.set([0x13], 17); + break; + } + buffer.set(seed, 22); + return buffer.buffer; + } + default: + return undefined; + } } catch (err) { throw lazyDOMException( 'The operation failed for an operation-specific reason', @@ -138,6 +176,34 @@ function mlDsaImportKey( keyObject = keyData; break; } + case 'spki': { + verifyAcceptableMlDsaKeyUse(name, true, usagesSet); + try { + keyObject = createPublicKey({ + key: keyData, + format: 'der', + type: 'spki', + }); + } catch (err) { + throw lazyDOMException( + 'Invalid keyData', { name: 'DataError', cause: err }); + } + break; + } + case 'pkcs8': { + verifyAcceptableMlDsaKeyUse(name, false, usagesSet); + try { + keyObject = createPrivateKey({ + key: keyData, + format: 'der', + type: 'pkcs8', + }); + } catch (err) { + throw lazyDOMException( + 'Invalid keyData', { name: 'DataError', cause: err }); + } + break; + } case 'jwk': { if (!keyData.kty) throw lazyDOMException('Invalid keyData', 'DataError'); diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 2eabd11578af45..5961ee3e67dbf7 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -357,6 +357,14 @@ async function exportKeySpki(key) { case 'X448': return require('internal/crypto/cfrg') .cfrgExportKey(key, kWebCryptoKeyFormatSPKI); + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': { + return require('internal/crypto/ml_dsa') + .mlDsaExportKey(key, kWebCryptoKeyFormatSPKI); + } default: return undefined; } @@ -385,6 +393,14 @@ async function exportKeyPkcs8(key) { case 'X448': return require('internal/crypto/cfrg') .cfrgExportKey(key, kWebCryptoKeyFormatPKCS8); + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': { + return require('internal/crypto/ml_dsa') + .mlDsaExportKey(key, kWebCryptoKeyFormatPKCS8); + } default: return undefined; } diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js index 942be715f89534..390b6b07c30e61 100644 --- a/test/parallel/test-webcrypto-export-import-ml-dsa.js +++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js @@ -13,35 +13,57 @@ if (!hasOpenSSL(3, 5)) const assert = require('assert'); const { subtle } = globalThis.crypto; +const fixtures = require('../common/fixtures'); + +function getKeyFileName(type, suffix) { + return `${type.replaceAll('-', '_')}_${suffix}.pem`; +} + +function toDer(pem) { + const der = pem.replace(/(?:-----(?:BEGIN|END) (?:PRIVATE|PUBLIC) KEY-----|\s)/g, ''); + return Buffer.alloc(Buffer.byteLength(der, 'base64'), der, 'base64'); +} + +/* eslint-disable @stylistic/js/max-len */ const keyData = { 'ML-DSA-44': { + pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-44', 'private_seed_only'), 'ascii')), + pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-dsa-44', 'private'), 'ascii')), + pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-44', 'private_priv_only'), 'ascii')), + spki: toDer(fixtures.readKey(getKeyFileName('ml-dsa-44', 'public'), 'ascii')), jwk: { kty: 'AKP', alg: 'ML-DSA-44', - // eslint-disable-next-line @stylistic/js/max-len - pub: 'gtP6UVGz71mY6VgeI-VIcelf_Dr6FrqOpyU_uU3Vm6NsWHuLd5RhxYdMdi-yZLV2auTSvjL-vaHIdnAatD1bw_ggrvPSIGFdEL4MCM6XTvEovOP_vk9KUBf9w44YZhV5VYezSpJxlT7eYPmCp_yHu2Se6bIqmCBgibi5-ZBhwBM6W4Aojq2a6G-a4M5aH4tBNT7nemoOEiqgteqDWx-xaduFjRShqTzn5OT6TXn1hNIeavnUuTDgxnveA5z_SrIQs_IVMlzgN2a1nnPr0rjO4WLhpNk3vMZ4zqeqQ2iIXMauh0yAM9zI9kR3AIshIg6oWXF7_bq7RXdPobaIWzAhVdhqllyhKnwKhY-mPbYvHz7h1G7lhye1QOwC63gtBxlZ64KAi73nTnxKlaWRFm96gSKBwNZuZVxNLUkpNDmGWZVBjXlnPCCaZ8CYSM2kiuRUJcZaQnS9JJmvf-ZYsZdQvG-crEVGq56MwjUSJtuSPFYdwD5k7Pz1BVJADmI9ushzFVFo8pgfGGkxR9fStg_gVpD4rwnB3-DmiXeRtIcsxkFGzhjUorLauAP1ZQSYqCHZc3OCtLV77bBaGc0I8Xbu-QbF82Cm1xepulXF1MqbroGUqH-sWZn6_7dy8pg1gAXvED-rTZVACr90HnxIcfzRMWpUpbbUk7H1ywA6qHMIXJXKsNjo2gqkae5PVRCskf1NLZ4z8xyDiCC3vkw9_Hd4dUGUhhLwifFOoHuffZg3a5Sxinc4DHW2qyClZ7iN-QfJ00Uz0wZPts1Acwl1dfo2B7UAFpYeL9YlVehQgJvAcfsrffwvSVIWt9XHBmydABVJctrwz-p4s4EJNSK-tXrpgGS6w4uo7_6gDoZyukLYLJOy-YBhMSUrdJg4RhnjezTekV8huCk6qq4oj1JNAzOjKjc-X-CHnhIsyNjxfjcrO2SVntJLMDJWr52eyxcyhKmrmcjJaL39dE21WxGIcAdFAvD9IoAL6fuaSpOwxi2XTDBmXBJwWmdHY2HXGUma2RlfMNe0el-XcPxgAyv1nbypBW1pcK3k9e82mrE0ldNYwPifaZZp9ebQpYJ5Snj6m59twNCGFmiKUn3N67seCEkxzUeLnxcHTGmpQxIt5y53VHY535npd-H0dxRMnISTSrQ2tVtf-vQPU5QW_ldcqiYHRBSNv2Art8NDjYJZQU4qvvRyox5mhH94g3Xoiq5N3lFBtUtBIrMCNFxGHye8sNaOjJcimWBqZdoM1bScDflqnR1W_tOnIlLHJzrNtKHDObdtHbWf0K30Y93g239CZ_pSz7JsKmu2h5CnXRYMKxYY8tzwje222rlvqh8XcDHFBOHGDFT3etQvD7hD1_XaglYwNm_UIl1XRjQuYTNIvekUHFoA7GDOBfE17Txdbir1Ne_QUt_tufBuU5p8CuWbJMWKq50Dt0Oj6GT6eK7rfELsJWrqezcKzXQzm4xTXLKdfx1Lz_INsekVZUu4963VPubqNir0zBGFScznG_zUeGlMIYyLUGxqk7BPXLql_xaaeLblx8HfeMSRi6k4Cw0OFHX6Vy6sPIk5r00D6_ROD6DN8bcExC_3bF8K2qCrg914EY4pZ9BjgVqKVGvp2HWPpgQqvQCq6M5C_FKVhtfsd3f1flO3_xu08R2edvfL-1bSqCy9zEUpEc5vGBfKLY3DnTU5dG5V-xZNwqjWrZKDNQVf5wOyl-PZlWlp3DI36PH5NNlktUy0rMXkT-X-Odjy5xGbFQ', - priv: '1Z0s934w8QPUceWNuRN5dfrhQb5_4GxQYICWnBFxnOE', + pub: 'fYmD1Rx_jkoW9KG7Bs_5zyYEiWEZs15tYBxNdKq9NircZnvZBwwwaGbj0UsxJNc4Dyfp2IFAZZPO3rFCSUdpXHPrGRHwIVMzwiwfu2V7V02xoheW4mrkPThA3JRJSmNdsx6YGu37MaeJkIk6AlUexo46JfGrkRXZp_IyZxiL_L2dPrfwx-32j7WFI5sBadp7cDWfNkJjdQwW4puTe5Rw7h16GHb-DMOAKpfeMHujh7IYHuLCU6lVi90j1m8Ru0dxdmeQ1eY1vDnO7fNQKfzOLhpUNnj7BBZ24GTqFc-SN5HDCSCsSGKScTYYBwiSVTdSGG1GNqIiN2FgE4z1Jj6JFVB_OIUnl4sKbb3m8kB0BwtUPbkC0FVokGRUEGt6ba1Pc_IMpB5Gs3g9PFREI_C9o1yVW3NS2PzH_Vk4Tpf0N1K1kzIK_3IqekLfyqXmVDNsOovsS7Sw9TdmdWUNGRmhXFKRkex5VjpMIx7OwBGsYJCc4FhauWdrVtbkvHGggSpsla73ZcA4Vzh7aq47LMv0KS2YLp-DMn7SEohPHGg74118eLLn88yptxwtwt1dBFj8BKUfPrytuN1EIRQy34hwbkBLN9wDqhgn3Z3fvksRvmgN_4ZQ8YjeD-H3OFh5WJ_Rd66wHSl-YFat-_JF4UPcdlkNUbxPvDi5VL909Pe3VlwEZhT5otdtXQX4U3dUfqWKEh2kN0Q2lo8wbf3OMmBOFTfyX0eYa_5088ZnJvvliefn-TCDyc6WlcZrNqwBOF8N8-IN3b_8RPq-RuV8-mK-M83Hi4ElQB7Z44eZMmfUwFrozEG4Wq2K6MwQ_edG4dWeUVMCloTpGDFOtlLQlDoAN4m_sS2Lbwm_3ra29noUcK8_j10yy-hENE2Yluh1pIL-GoWZj3uYO-rEKVbszaagdE0DJ_uQcHUdNnBHKn64-cQ6xihXzxaeHx9OxkWWMKbzLtKpuYDK_X7EVvm8YTjl_oTsr2SWT2usjNJko32DhRV-OXLKKHo5FJpCy2bGFLXGG26CglUvgZQ2dyXiWeGVNKffOv1cQ5R_RlU2MpLiZ1bigy9hh4lu_XAHLfjQfhf71jeMuF4nEBWV-YOAjDTaDB2hcGqv_XcGXcmLWHqOWgc5Mb6lkb2zYs_oyOskmyFx6C0P7UrV8kCiN4zbuTqZNdNjlWL_QJUmU3vk6CpNa0XN1M3sLjZpOEsaqgRVPLcIDH-juVhyWiymuxe-8yNCOFSKxhscew08EQ9DEckP_iIA8qU2gcreHtvAS5VA5Emz1K2ypYe6oS3ogP-CX4nOAEfvjsb1HHJoclgiwjL1BtCLFgOE-0vn1M-nVOE6WbHGHoNKMJMHP2a3HQC7DmDfSOw5P6Cj5X7QVqhCY6tAGZWEPu3hUssp7K5UJePEdBn_LrErt4ucyXW6y1PAA2Fn8EuHaRyf2ggibDGnzq8E15m_R4LMvZAuGR0bN9jBTlm_x4ZQMqFwKkIdllkN1QTErazOyNsgU6fhA_20h5EIYT6-LqXr_Otj3Kp8MkJB9c3XNGoo5sbHTQCt0VNOHoxCFP_swiAJLtm743eOsI1M6naWLIqPagSCioosAvJYowypJQGvM-N3hBu8KUr0f911KRN7WqTAXTOHZ_vvTqcWKet0dFdh1EHuP3TrU8hSMciaphGvuK93T3gaWuJ6lcCkQndWvEo9S6FQB7eLU_ALKOQ3ROybUUkXgfyTkWDPxbHdeJCgMRv6Ig1PShPyxYb4ig', + priv: '273AhMPiZWLlSQCY41yi1fMj6xavGH0btB23zMhI1uY', }, }, 'ML-DSA-65': { + pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-65', 'private_seed_only'), 'ascii')), + pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-dsa-65', 'private'), 'ascii')), + pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-65', 'private_priv_only'), 'ascii')), + spki: toDer(fixtures.readKey(getKeyFileName('ml-dsa-65', 'public'), 'ascii')), jwk: { kty: 'AKP', alg: 'ML-DSA-65', - // eslint-disable-next-line @stylistic/js/max-len - pub: 'gMe-7J_WYur_NsxzG0iYWqh0LHtK8rbArEnvuNwNHYVmB1gLqnMjYMeK7_ScIGigoMddrW0Rjy7iv1srvbIpsx75K0ifzIqtf8z-gjguZCKWLihbt58KuhPmwPmTUmYtW-oztmVGpAMk5oH6dhxAyPJQO8hP5ZIh78e4bcRjYeyA3yCvQtT4tpDRtCDBnXA7mvdDLmitQI0-rvHtA3638EHZmbho1FxAf_BrmdLdUZR3d9-KlEN-7NWRkn52VYGitaG14SXcCorKvaxfo01v0v-BqAeLxBFzwa93NZKVQKrjHx0m8wXKJGkZ7qQPC1HSm40HGps7kxRNjSDL0T7pOwEZorCqL5n1W6GWzMY0NCm5B_t7hQdZenBkSarWJjHKDiRTWpKygDOQLwqKXUjX42Y5kKh4N2H2iXqsCyb5b2p3D8CzXn5OMkTFHARpP4lGr88m22zq_r6f_L-Q_cW0gWhFILxyUQdzwQYtDfVIJVvdrkCEwCyN66nPKQR9b393j1uunJh4iSfV9SVgL3Ok1P5UT7h65cufsaikMNEnsN4XAtUXtFivUkxLFJwSJZMnZpZnVpzgIJWYnmqrHoRhPiHK1JtH9o1XxQcnGpYWDiW6_-WLycpfSbo4pRNkMjr-fGv7xmdXejQELByZMDxcM-QY3B_BRK7gTgmhr-_hjwg6B4x5MUQXKFeON2qmqYqcelkLWi3q-8UuGm5WIRF3u0ODHxqo896wVnq6nTbgycrLfvLUT_AM2qAAxR-w4SfQ6KZxGeGidNe6dFN4TacW8-NbsMzGTuyhzWsZxLIG8kTyveyNMFwqOYMj2mpPTcg52tRu2izczeFQqXiFhRVGATt-KXGMjmfaKavGkf2SUMcTSJqnTHJaZv7QmMNosgXYinkQAz4XyOfwsi8fgXVyfbh_05o2roM6AiEDff3aqD-m0Vp1d-AFHLxYDkRFvuW9BxbQe24sTopODF6hn42Hh_ZQF2EU1cIqFGzecPV1XGeGfP6Df0rO78701LOw1PemGXCsU30Mn1sIK1fmbqK8gHyZwP9mwS6WjxX5ByIMS_iyqQwWvzcFI3d4h5vYCKC9o0uetorwpvcv8SRTR7959qvPF6eYFO8OgvckB1Ydq6KREMpphL7-EXb1Ot-wxm8v9X2ZhUvTtV-LPxx-pq17qmcAM_ifwinktBrbvGifxGJ7Ku4ZFO7RtuCCB7FOWucGXGsHfs7LMwHdF8XRmTeci-XsG983zAKGYDxyulLD81TGrO2o9ongsG4XIAH9y85ebnfCG9QKfOSdPBxtk8nSJXVb4gSOpioeyt0WSnNBxiUyqOUKTLsrzmK4FZPvEQgZhJSbamoE6_MNkuJ40FOmH28Xb56WETJNJjibd9Mj0d6_ozq0ykpAdHLTDO1EHGIVImEqDQlCyp2zQRun4SVH7MDP8zZpSI2g6GlofDT2A5Yft4OPX6VTX3uaePxIOPmZr1MrBiRNrP9NboJQ_uw6X9pWFCu93jts7XQWnVubfKPcDDHFzZhUMy1y7GaZxqxuW0AlI60XrVElqR_RPa6o2LVd1_CAMWPPj3UAIOoZ4tfkDUo_Ss9HStkDV6gswh9whvTCOW-Nt7C4z3dunvtiT4chxq_0d3_xHzqY7xAnbENIdIIVDWhRUQI53_ThwBVGGpKzijNj_IZxFfz0R4pDyp4msYII8jIe4K87bZ0SuKf2MTLL_sI7E0Rqv-kGasONULQGkVoEMqprOS7BY4P7VrADU_riVLBeasJ3gZ0rh9YaN7GkUDsYa0Lp2Ts_NKIOjH3pZOY8MYyuv2bIfgWE4zYVlmmxyACC2K8EwQQe0QArx2DZAKQ6k-MwrwidGRQfe4dJ0-XCYD5947Zx44sTLmYri00awyuCk_o9NG_KLOZU8mpLUmFFQazh2wEsyY0fYgtK95hZfGLsVcci5LPQPl_B8WhgFoKNXxqGAqht7-8YXwGEdUxT68EvAPFXFuQKyOZu4_q64ApPxe1YWTDntePR1_bhOLlK8as0zZB8iJEeZDTVRmTA5CknGDfIt5rSA1_jcSfteXecNk4xTH_iU4CPSpePUES79kQGVG82cD6sQ03N8Y_buP0GDpViZJOB187hh3NhosghozMoqFnFUgcQaYpMxqMYyVEZczaJdQbWnEWdb1ygfmosfRxceHFF9Sb7-SwE8jRXa_5QMTOhu1nohV27Zownoq0Bd9bqBLMNYsuUOa-s6frJkOZVVvr97v6qJMaq9r8Ol-1dXQag1pca-JdNvGizHFp8a1qGY9b7ywgku5MdyL6THA4KCRvgM7Mj1gQ8sQBF9iMtNSn1BVqdjDxaYITe5J77DBqO4piGzTqonhHhxtRFDYmkk9hSe_GkF_ZMqrBtpcQkI1Xl0vHIJc6i8y2rOe_XCYO_cAQYhgBOz6sF6OCSkYkz82ygaM6HEGE16tTT7HPD47uy1iO3kR3yIq43yeqUByfAp8Y2V4t2rKGG7Lfpne1tnXpO53wEeLWiwlQ6--J-DCfoZLTT9PrKhGbh_z7329QRQWfKXTtWD39qA5IjK458oUBSqVpEN5kXXFA08dEIqEAn3YQW3mDTzRliA_3SOycOV0I', - priv: 'wS0n9kCle6HZA6M7BXlaAyCvUUqeMiEqaiiN4Y0KPM0', + pub: 'hxPP5LvG83t2fJyfA1TUssJK_ydrzryrCHGZuKFxmnl5Y3sxHRCPW_JpHEoiIgR6kgELnwibZnueax1zFerTOTA7o0NwXHFiaEB-8AmqJI93DkvtbUOSTCixa3admQBKW_PtgMCVtaEVuuvCuOEFhOyuZkyfvnpBwUKOkz3t-O1wpgrSmf-rdPXOEv8YcsSn-xfLYPSLzPCnt7gnIX_fwtkgnXjref-QqjFKlKZE2e7MkmHeViJ4iGy78r3UzVhBHsmFGC0ZNc8-iT3muH5Sn0SXmNq-F2EoerWLIAsPxL2KE6UrqPAwTbHn1B5sAGWvhsVVLlFPI1s1JLVLBNRJ5vhif525xNIpMAMuAZrteD827pve3zQo9_GHjWgykj9VzM9PEcVmVqxZ5u41kUXsM4PWZF29Oh2sYsmJ2LdiJ9RcA91vRLG2DqEYm-V5JwIz8uxL17DUsEC7zYthvtqGASq05CbfPTBev33rQUv4H0Etz99U89WooTk0FisHDz1uEUilU_VY1tN5byIDitXNf0jnz3SIHDUUZARn7ll0YwO0jtksT68sQW3Liy6Exhlp1td0so2qZUrbVZasjyCOVuibwbwvrdpP3QRsoG5UqkAqk8Rm2iCpdQSg87pswOscgA8AC8TczGHNfXc9PqzAmbsEPKvmZuE60HLGzqpRqFULf3nyYUQUqbdmKJsKQ29LXeDVbyy3-fkTUDuYqNC2tBY7PkzHJSA9Z4hDC_BHEFxcelibScSNyf7y4lDVWnuJMXpQ0WRh3UkUPa007IerhixwxvBvFXQR-ytYinixvjirlcEF1wQI1DzE8KjOXYYuFPS4Yl8HeZHQ-64Q0RuxlKIRP3YvjZWh4IDVvEVs5ZLzZPbE3Twe0N5a7iCu0BzZWTeHNbcMoViFyJTpec2w3vVHeI4PJB-5HeI8xuh-9y8ytTau8QtMe4thoROoajizDQLrkw3e6ryJJ3R84i0oni4vmZWyLDilwcLqPOkQJCIDMjq7exdmVX5t3DtAW4F6Coz0z3sf7tGlSMVxA7izCoVbG0y2_l1P2h7fWBuEPT7PWlMdPqu9Pj5jqXY6jJ0nkaR_pp7dDhO1HKae5edcBYunHZqVQQjRZ_DvKzbPrDk5t6Xq9fdSkiAeP3B4qn5uU-nx7OaX7DRoVEnbbiEDynIRPSEY-Ts3alPJtBv8zuzaGNyX05Z9MyZ0w-VlC-WxOBdVEsIAp_4uJ3kQ3UsfE9DLJH8WPuDI4t4i2VnNNyFlI0XSUocc_0rWgqp2I1UzSzkVbklwkuFywPI645u4G2XAlfdd_wpjFGC-IUPXgpeSfspPwW15sBP-ITS-gwtvfzQVLpRS0euzN97xo_GMhNPZ4bW-YyZt8z_R8bsQ8ktfoP-5RUV-yzYDt0tA01QJsZdBLf5J_H7qP8l4c8V4hPe_CFL032obbxmAnVPAP69u2SaMBlL8azjk4wGVFQpQp1JqMJCao2W8ZImCVegkPZxhGbx0nkgVfyFx4ihMeDNM288JbGC4CGON8C02Q84rQzhwzZE83Y9rSe1Bb0fUMHMu6ihD5jLdeltuBL4ZdJlKgL24KZK5o5pq4_l9SyzGAjB1KAQnClNOB1SxV89CtILu-65wb17s0z3qw2-NF0B6UVlGQFebjbSyLQv2ARaETh_8cBiPugVMgBIV3K1KBwNyWejyI1ZDCssvIZHJCF2SRW3HmJerTiB23eGHFYKSLdxW7LEzoHIc2xZEc3pwR43gavjeoL0pNc-HNFV_c19wiH7Tnw3IHld_FfTqAIPnqKMNIY7D_D0DmFNTOdnzcipqKxUB0Avc-wr8Fz0gjeRpLH2iDSCJWtvWjoeYvHTktGsblDAM5j9xznwEvZfQvj8fTUnFxl3clkD6e9V1jrDQDkXfOtl-bDIv9PtMwamfJFu-z2ubF-gKytUewPNo10uhwr2TDNdUayCZDR2T3HoRLN8goIw2bFoPJ98LoPcSukEvKABjH0DiHNeqFELNZPx_uCx5N-YFkUZxHWA1QUoGhqQ3REtcT3c-SZf_TDFOPvws6bmwt4lcWpLmubOAJLFt6J8m8HCkVUshRdFzvHQm_0JEvA3JtyXZzvsPUv5njdk0nxTZktvsnqX054RQk5x8U-lBY-bK3uMOoFnHju45LMoHCUgGJi22eUm7nLGZEh84ZAbNlPLXpfavXvPJh21OW5EOAeuQ-yWNHY2xbmAiHNnb-J2VpZc1Vy82sxn8umFtKduuQuIQMOsf4qHqj5MzDY_1NjrM4Wm7XAiLC4MpQ22w9PWNQXSZWvo2fj8WUnfEibpgyRkoD2P25GRQqsRJ3-Ykl5bm_2Vfe6i3oXHOwQwZwKGXfAqXyo4iU1UI7e-qC4sj5U64oB_A_NSBaJJrZoQ2fVeGTnFxA4QMMoWCT0VlwBXK0B3jht8Xal3WcI-i9ctQB1-GrmwwgG2ttePHt1IKy69bSZE3FLkFicaHg6VxypG6ef8rVsmMrfpTATOnF5_iEaLNY9428HHGW0iz4vXwaE-MkYy7NK2KMPFiCB0ec9OjIROwayK4LREv4qknWHnVQRSm25Rr9DcVFXKj16Au7X1hv7TuVH7h25U', + priv: '1X9VEr_iXMRwBvnSytEmHrtA-DpD6FWAUqMrDNlJVBg', }, }, 'ML-DSA-87': { + pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-87', 'private_seed_only'), 'ascii')), + pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-dsa-87', 'private'), 'ascii')), + pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-87', 'private_priv_only'), 'ascii')), + spki: toDer(fixtures.readKey(getKeyFileName('ml-dsa-87', 'public'), 'ascii')), jwk: { kty: 'AKP', alg: 'ML-DSA-87', - // eslint-disable-next-line @stylistic/js/max-len - pub: 'R21Wwut7jawo2a2nJrqIk5cm9ay478z7u01197SY4kVDB7FBYqb3XNFrLIlV8N7Adx8MVKu99A0QdmnaoCN133FDlIU8ys9Q92Px9AaUjVWaRoKFLBSwaP-WF3jGckZwBIm5kScwbkQlK60CKlrOpdwobXYKR-2zXH0bDKSTl2kfx6N9uRPm1tYv0ELyRhVGWFG1-OFcN2ir9Tt8Nd4zfPPdpkj938K4Wytoc9V13iqwsYTKMOFBE4RE28Mq_Wx1XYUu1UOCKjYznHUwUQsqTeA62o_IPQapxfcz0wsIqU1ZSdqWvkxFmRXw7kKspvVjgpZEsY6Qj9105-dup8jeOVpWhdwqQ-2tyJtwCorgWlY9UycHXdX0XZlyGYFsI9HZO6oA0LsI8aaZeFEuGwZKoXmkABkyQt6db8kLgua5nowy3Jm5FfZ3aqGukZiS35E98oVSodRywnzDdugQ9KZ_Nt0UTUVzfm-qJFBuw24Ahsf4dRqappZPgA6szFZLa3u7sdFah9HbsYT_PoLLwJ9RzBtS0HdhZr5UHKpcGP96iIxl88tz56NEWNrzMB0XpHDx4wrS8Qdn9Sm3oWnfQFkC61u4vuLSPNDpqQlJfCKac75_-bxxDoB7Pv7KbAJisyBgRgaiilCOWHNF28K_x8dIT_meop4TTbBZzh1tuy_uV0z2sIu-YV9f6ZntDLcLgYCILEqFAWTuXhouFwRes1DwAluIgI9TCfGGPHJ6IFIaenY7-igcJqy2nGulL8gJqiVg8zh1HSOx9puqvYn_GJj7i_kgu7uSDsrpsGSxoKsnVRQ7SGpWmTOvT1lE0-5N6ETAop1jGFzKvW570_NlAcWXoLPnm4qYBKyEs2pAUe8V1bS5xxTL6dkkz1G2w3rAr4iEzpCj_iFHmeapcP4bdsPcHJjSikKZN5kLsgqXWCq1ofTRk2JbkMKgsjiVrU2Yno_2lWm7qYWlsUf5MG4P6fLkuYE8nIb1HGSa52zWa_s0s537IWaGoJloaH2I1rT8KXNx8-JEtyobkqLvwlgkcVHAtIiMD3owGc_3QD9kvVVFV2d104OU_dVPsP4GRvAyCGbyjdB4XheciXgTRKDJunM7_-sKr-8Bn_5vU0QRlzeJsb4bxczppYYz5zcWodcYtfbIcXbwkjLMMO0LkCL7ri59kGbwYxnmDhVfhUxqLmlTbB9_mFm7JlJ9G6ZTQB_zGxmYYQpm8iSPP4fQO-KsqSky-wqfC75K7Xyghu1jAZp5M2cpxQsclUG64MCUOEYpSODSlrkdnkkjc8O-ll8tgj9JuknAl5rqVvyRHLmIPLaKVXMrMxDdyiJWc2niIWSqrg-XPiFda9S92HNV82qyj_QBrMxAuIGNFI3Nrup7MYlVAVFGQkkAAvfbuxbOZB0SBr9a8k3q_Urb65aXnJxo-CGddeyDKe4jUXAp6CB8X9KIFyzIYBTxf7rp-4WcsnLi7_mpNK0BiTKfgeHMKD7c8cygyFft2AMnHDfVH30R9LbTTf_4HGT_UQhlhw3dUaECGrrqJ87dbNBxDK0_5DNa3NUxNIB4uo3VYRaJToV3yCmIZSPy3_REUXLNqMzodMDWsy6FUIV_mi8D2sh6C8iy0tXuAcIjDJTsiHV5w85_yQpZ-KnYJz5Bxn1CtakKf8szA1WQ0ZSxvd0KN6hr-NG-9YKtaqUddWWor9uyNA26x6oKl12GWTCjNI369OJ5s8x0at4Sf6ie8jXUljDmn4JCzwMhAg5T1cu18OIxiiqhGj7lMVHL0CH9uUVy7a6pAzvcSrx5frclHa49Ztj8Op7DOpjBQrkEh_u6Qx4b8Y1PzcjoeZHUrCBnBqKKxD17lchEntc6Mioc1WRrff-pQ1wuHd9vZMKYqMe9_REHeaEq7spmYT8_n1SK9H2zcraeKOflmlNKkDqRtj_uKJvuYIKvIx-9ZgHuXxpoQLlQpSmQvpfZsX2CN8t87TFwPaMIu3OQsA73awnFIDCfESG7RmmC93Nh0R0OlSOU5AgVwoQWzYnOJS4ondQ9DE5dF49XEG49L6zrl2Ka5BXWHwjN357Jx_X-9wyfwwOpUqzJuYrG_CayF_uKqtF-BibLMV0eYdsOdk5icTFJPbBMY_Hkjsg0YtOgchMx3CuGEgU0fK06iGm-lSzvtAlx4OP14PXxcQJDcvRfvumH-AD8NKjHYMFH8LiUPz8FVj-onWx8ikTuSPpjjAEshDp7C3KP0Ck6lloPe6cPIcgs4RB_YNfLPMErIDmxHfiIDjCDKZR5X-NBw-D1P3DIOqi9Ma1AeaMWK-AErHgwz6tz5OLySoC7DbWevXF2bUZhvQuts851HXXDtQ1Is2hiKTqSNFz1s0PRnQRuw-L_CxuNn7R16Q9fakfNCzeE8ucgtYKe8eiLPMNWTLaOr6bgrooTLxFBXWJ8Cy0HC4GQPXSi7p1IJLjk6EST1eBUYym_fq01K8-mD0R25gOVoHgKD5KQs6by0DYGRqZiyd5OwuwF97T3C-oDOoUtHsKvWzsD0b0kyMAwXfUs_kXhcA33y2hV9Hw0CX7aaNkny_9inkgze81BpwO0e_wn3iIqGvJXhRKJ8hSMWzfg7pQnZH6FmgNUXMlon3OWwOXx4UurJBdm7FfsMW49ScV3K050a-DCficbTScYho7cWTUFbAGrytJSwYSSbCdInimycblYmtDKQigwP35HQFOBvbarD6MBP9CHcbvdRaorffBEICEiMry63wxKjkfBv3HYnyGvPp0sAEFujf3kr9pRez8hGNicbw-AC_CusUX-1hZcWfNZiaI-GICBsI894oMVSAs0WHbrQWdqZq8S0SnM1USTFuatTSyrg0NjnPYmPl__-xvYsidSW3xk5jF9Rb4UG9et8wSjqv6ltd192ibcjlYRoUoaMt1T49A83fuHLCZuUYhgV9iB-H5Ysak8b7xJWJpK-ZxVqL8VdEIDLdG0a9Wjedu5NtZ_EWpYj_b2PsJ8lxhGdb58kkunYU7vXShuqfyXhHMRmBuxlHoN3_6tjqmzPtJ91oWU6zi-Ep_n3gohl4SzJbLMl00rJEadlxL7TNiLpAZXAF2ybPC1WrLx-4dRXIQeWnsoZvgrzKvmZWKoVFpWbTWFwUOgcE93z6UhPezw-S2h5fmWsHfGFqMt_Z45DR-NTQqGQAWuO3XaHCaWqpxneIqo8Z5rTrc1w4KH8dsva8AJWrrw7QHmfU6foLllGv04lR73Kf4fVIvUmDhu0FJ1uEulu24cA0AH7-uZcWxIDDQSwDeU79GlxbIYAW8hUOA7Z1iQ6xF4akOIKMqOxRuVRHi8GvmF2r2avSPaltYoHQR5G-EYNIR5bRcE1BNaxN0hhVMyX5UbKCXOkiT4HDU4ckJ_pWjZDT7HhlckpmwPrJXnf-0NT1DqzR-bMeZQQhWljfSsQ3cwThRtiBSqVYOPBIAeYVfpBBLQFvAMgANE', - priv: 'i4_fRfJesALZ5T9fxzvpXvFtAbSsyAGdaMU-DzQsr5g', + pub: 'DZXqaBATRN0GRtigxzkLxp7C9fFYxI7Gl-tdfXqJHbzVCTTvRfRwZcu3YmpsUYXBdX2pVsQ51QlqxMslKBRmfNCanBLcfd57qoEIb0K6GIKZGHxlsr9aXNjEGcKMo0ICon0LYTvTWrl72Oz-2yEA_abPK3_dBUFGAYQ6kOQhAHcT1CMmTTck23PnEd5WUpYfZOA9giFX9dNVrrdFWczj_vDOty81ObNKsxfVWT1nG7c60UCJxb2c2tMrBx3rp7Hfc_aOg54W5KHocJi0Eai0ok4buySTe0UCSCUTkeoCcdiABgOFBiXRYzpm3Lz4uot6hSgFpuh67fE9Zpgtn64vfI-O1mgcPrPPpd3yA92Jrq-dvXuM55w1RmA_hha3U5Sh2vm0tD1U57q945UppFReIv_8NAKBkxQ_vHil7ySm-m7IAM-sTUY86_IqMZqisxoz7Ff7ZR2vIiUm3-L0ow4B8uPsCv2ZlUoVXvMF6XQiOHsgqgP1rfH8DmfmPFudwiXrAW6wEmi10skPmkN92aC3TPG6nmaNryQ7f8J82yVmGxW9U7zMbg21qNkRGBi_1YwEt6D8V2pUWv5U1a4p4-Ma0f4uQG4g0odM-WGomlh7pZWZf3sffiPXk9wBrGisxtCJuaB5vtkheWxpEfWqnhc3QdOWfrsRg6P1h7M95SNVW0U8A38wwrqPOpzEnckCVdCrZz2b2KVln6a4twfINg1-3lEZR4rkEmTaTYlLFlXzbRFWBBPGATxeRxhQ_9N5VhHi7STWPFD5HIJyVqz436bbVvM6Py_oldT_xt_tWlPc0w4Pesy2CgaCPlJCnx6cjEg_sRBUcRkBoHqa7aZj4JFFm9bzEaiJ2MKfkHVT4xdbEimMHsD0HkIQpg5-zoB2Jsqgc6Qi3L57hZi-Q1V0G2lmdZ5WZkQ2m5hxle4hHtAmghgynK2p0qzDWHScxHcdd2sInYqQgMYbnvs04YYKWpIfndCUBs9q_EONN5tn8gfSwHEKlQ-KpplEL5kc-99h2uaZsxRlJOF6_z8EZ-aKaKY8jvoAV0g4kZJH5UKy_MkBiva6r-zXUmo88qJjXQatOOSdfvJUTiZiSfcpBQqF9SSDD9WWsgInaOCKO_fAFf9fuacXDMEj0esUx1YrVEe_77S5uObg-UrK405U1JhKJJvd7o8xQKxenv5BJdsbbyYQDbSSe9BrCqeHEgmfRHTXdSvl_3QOP0Ej-dT8YJJHZ1lrujU7Zg5f5Kg99tU5GdLMbHX2kt4F2a0NX09HikEemvUg4NLPhjOihfVkChr-zdF69nfsnTiaQrMgpIcl9jttN99_8Gju-LU8OWbb92m9RLxAUFP115v22f77YPoILm92IjMZMkxEhGneoclWhnudkyR7YoTBjCnT5b7AC9_05uls637FmVf7Ck8-MF4gLil3dstXi4g24bitYhxxqWwiqF4vsDouSGUnuKCMwx3TLsII_xk77TjpQP4vpLdYM3tn94AVlTMMhnI-OZkVJk-_mIbywCwRHlQb5nzVCc0BWlM1kb9PJys2IfciS8LWEoxeq9moDX5w72yJKoLN3CWpD3VdJAiW79zUaySw-IeW0XaHnlze5fYnOozG8lIeyQ9sMZasMiFovGnR3b7jyMtA38U33v16fouWuBILOu0m_QOpRDI9i3rjRM6hdC48zCtNSzc1_1VPYkWDSFK1oVAjdd8-2rjyqdPeUwnqD26VA9_d3R7x8ThrazdbRC8U1hr9jpqNHuZ4LGYu3Ui8wB-lSt9QMaHz517MY_zBEoNGyvbQWtlM7mvLu12KoMM7nvGrPJnvD-HmxTqsVQolD8_lIV5ao72yiKDpArVr6RuV4PpI0j_Wy4-yDCuwBW0gjnB9GvCwOTeByYXJT6Ul7dgHck4BbF3IyFgvmY--ceWr5mBrbAC9LJP_4Wf5O6ul3hFrhiG6zSV4zzBYLnEwfW6LNLEZjZKgmBYiC5s1xlxYWDdcQ5FGmLQ9uEDkr4VItXQWvIIdeBQPyujxmd965Mig9-Sa7SCyV_3wH8fQnGlvU-jJMGL0zvzB2gcu7hMLMagUBj1AKXj-UxpbX1i95f2TOiZDwMeCCszgvCjQ21XKg07TBXrrOiFcgcADgdo-HJr7O1T3ozOIulq1PDM7QZH6i3wDD0j3b0NCdqKCWqhfLXz2-FszyUHmA_GCzOLVzrLT2DcGWIcQbkvF0yZPgTyqKArKa8qytOerdH6oCJ0bRl96855sMVjuUdyVLX7XW_rVTwsOwV0gVAx8SrzovtDFeHRNl7BQKMsyQ1BjWu25jqKJ598vAi3LCZv0kMdiC24qPdgZU4e2aUkco11EnD6nJgdqsVFxufCl4BD_D9g5Wy42fJt4ZgNPAcbUf341KERyReeBEQj-qlPB3IUTIXcJw68GScebhxb0W_tGKMBC6-ip4QfNW7UTxUxVxmCV7h0yRnBBlkuUR1eYQwWRmEPjKd3dLHvgHtr266NQmE1tnKtJlsdPKb0ztrI9vogsENsgGNFQ2tHoeX8vqxcagGznlPVPfc3DlqjBSeTFQaPWvmCQHVKgxkbvffKzFQyFvXEqt6bGGtkwBoRJ_IIwtSeWQ2nFPBe3rlyKrtSnQFIMibJbbYvPVE03Cld9R61r-GGDSQz4aXekLzePEVwnxpe4mWJGco7ctQyE73PekL1uo2g0bRK-KgaE878OiLRBo5T7c633xEf2hMy9532M2GVdTZuoE0LL-wpAh9GmNdvJZc7g2sINvwZi778v2WHcYEKqXvdmrX-Shyh3QkzgIGZrDzM4UlxxUWaXfZ0Z6PNguk7Jafqf3xuUe9Z8zfAJl5c_VA3k8dn7IRg99hRsh-TGBCzqzgjJq4p4XMWP2QuxFTGSgHRe2GSCFzd5-lPjrj76ZyT3MPUxQe_bV2VE-Oys3MT-VkCCM8jFCANXdrfltG6jSiUZ2uJUWNqdNnxPglmmyrgff_m-5CyIRWYXQsIdZGspqdjzb4F6RbBKfL2PQlM6zUfo9JNmE8YQq815Nxkex8vDOrImnew312fZA6rRjr9_uE4lEbw3U7PFlCKBUPvPnsdgedjKYhiS0xU6iS1NDKOvYhcrkCkiU67EmFD4U0-OCv9Kpbb5bIxTJuv405NxJBElAMVI-ya0ns7D4-xUPn05E7PhtGZT0eHwItjT6omThTsTHwB_bQqYfNrjrObO1l1go2hQ-cUadZYsG5l47CB5RlFhANtaC8tiq4KJi48TmEEApB_0VwOI4EmI7SR0oaqx3HRXZfeGevCx2yC9aCYM4HqcyqP2g_1HwsOYzwq4XDEbK5Yl1dtYABxPoo7t8FBq2sSmfrBJWFv_nvreb_DPwbfoSeCy9knqvOktSQRrPMmo-nNGpandBvjmrjSk3EdeziAP7XNre5I-bn_2voDxkzGFtUM-wzlL379ASRGej8FkNWaOyqGP6Anq5PSJ', + priv: 'LZSOlEPbU9S5_mSsMULffTyxZu6qKEOQ1nfEi2NCscg', } }, }; +/* eslint-enable @stylistic/js/max-len */ const testVectors = [ { @@ -61,6 +83,156 @@ const testVectors = [ }, ]; +async function testImportSpki({ name, publicUsages }, extractable) { + const key = await subtle.importKey( + 'spki', + keyData[name].spki, + { name }, + extractable, + publicUsages); + assert.strictEqual(key.type, 'public'); + assert.strictEqual(key.extractable, extractable); + assert.deepStrictEqual(key.usages, publicUsages); + assert.deepStrictEqual(key.algorithm.name, name); + assert.strictEqual(key.algorithm, key.algorithm); + assert.strictEqual(key.usages, key.usages); + + if (extractable) { + // Test the roundtrip + const spki = await subtle.exportKey('spki', key); + assert.strictEqual( + Buffer.from(spki).toString('hex'), + keyData[name].spki.toString('hex')); + } else { + await assert.rejects( + subtle.exportKey('spki', key), { + message: /key is not extractable/ + }); + } + + // Bad usage + await assert.rejects( + subtle.importKey( + 'spki', + keyData[name].spki, + { name }, + extractable, + ['wrapKey']), + { message: /Unsupported key usage/ }); +} + +async function testImportPkcs8({ name, privateUsages }, extractable) { + const key = await subtle.importKey( + 'pkcs8', + keyData[name].pkcs8, + { name }, + extractable, + privateUsages); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.extractable, extractable); + assert.deepStrictEqual(key.usages, privateUsages); + assert.deepStrictEqual(key.algorithm.name, name); + assert.strictEqual(key.algorithm, key.algorithm); + assert.strictEqual(key.usages, key.usages); + + if (extractable) { + // Test the roundtrip + const pkcs8 = await subtle.exportKey('pkcs8', key); + assert.strictEqual( + Buffer.from(pkcs8).toString('hex'), + keyData[name].pkcs8_seed_only.toString('hex')); + } else { + await assert.rejects( + subtle.exportKey('pkcs8', key), { + message: /key is not extractable/ + }); + } + + await assert.rejects( + subtle.importKey( + 'pkcs8', + keyData[name].pkcs8, + { name }, + extractable, + [/* empty usages */]), + { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' }); +} + +async function testImportPkcs8SeedOnly({ name, privateUsages }, extractable) { + const key = await subtle.importKey( + 'pkcs8', + keyData[name].pkcs8_seed_only, + { name }, + extractable, + privateUsages); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.extractable, extractable); + assert.deepStrictEqual(key.usages, privateUsages); + assert.deepStrictEqual(key.algorithm.name, name); + assert.strictEqual(key.algorithm, key.algorithm); + assert.strictEqual(key.usages, key.usages); + + if (extractable) { + // Test the roundtrip + const pkcs8 = await subtle.exportKey('pkcs8', key); + assert.strictEqual( + Buffer.from(pkcs8).toString('hex'), + keyData[name].pkcs8_seed_only.toString('hex')); + } else { + await assert.rejects( + subtle.exportKey('pkcs8', key), { + message: /key is not extractable/ + }); + } + + await assert.rejects( + subtle.importKey( + 'pkcs8', + keyData[name].pkcs8_seed_only, + { name }, + extractable, + [/* empty usages */]), + { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' }); +} + +async function testImportPkcs8PrivOnly({ name, privateUsages }, extractable) { + const key = await subtle.importKey( + 'pkcs8', + keyData[name].pkcs8_priv_only, + { name }, + extractable, + privateUsages); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.extractable, extractable); + assert.deepStrictEqual(key.usages, privateUsages); + assert.deepStrictEqual(key.algorithm.name, name); + assert.strictEqual(key.algorithm, key.algorithm); + assert.strictEqual(key.usages, key.usages); + + if (extractable) { + await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED'); + assert.strictEqual(err.cause.message, 'key does not have an available seed'); + return true; + }); + } else { + await assert.rejects( + subtle.exportKey('pkcs8', key), { + message: /key is not extractable/ + }); + } + + await assert.rejects( + subtle.importKey( + 'pkcs8', + keyData[name].pkcs8_seed_only, + { name }, + extractable, + [/* empty usages */]), + { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' }); +} + async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { const jwk = keyData[name].jwk; @@ -317,6 +489,10 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { const tests = []; for (const vector of testVectors) { for (const extractable of [true, false]) { + tests.push(testImportSpki(vector, extractable)); + tests.push(testImportPkcs8(vector, extractable)); + tests.push(testImportPkcs8SeedOnly(vector, extractable)); + tests.push(testImportPkcs8PrivOnly(vector, extractable)); tests.push(testImportJwk(vector, extractable)); tests.push(testImportRawSeed(vector, extractable)); tests.push(testImportRawPublic(vector, extractable)); From e13de4542f011aeb3e9d1f35d76ff0cd20bb4aa0 Mon Sep 17 00:00:00 2001 From: hotpineapple <77835382+hotpineapple@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:14:48 +0900 Subject: [PATCH 060/111] child_process: remove unsafe array iteration PR-URL: https://github.com/nodejs/node/pull/59347 Reviewed-By: Antoine du Hamel --- lib/child_process.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/child_process.js b/lib/child_process.js index baa0a56d1ecdc7..17c6b69c118a75 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -648,7 +648,8 @@ function normalizeSpawnArguments(file, args, options) { 'DEP0190'); emittedDEP0190Already = true; } - const command = ArrayPrototypeJoin([file, ...args], ' '); + + const command = args.length > 0 ? `${file} ${ArrayPrototypeJoin(args, ' ')}` : file; // Set the shell, switches, and commands. if (process.platform === 'win32') { if (typeof options.shell === 'string') From 31092972d6aca1881f767b59c5b49a6e0715d68d Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Tue, 19 Aug 2025 17:55:44 +0100 Subject: [PATCH 061/111] test: update WPT for WebCryptoAPI to ff26d9b307 PR-URL: https://github.com/nodejs/node/pull/59497 Reviewed-By: Luigi Pinca Reviewed-By: Filip Skokan Reviewed-By: Jason Zhang --- test/fixtures/wpt/README.md | 2 +- .../import_export/okp_importKey_failures_fixtures.js | 6 +++--- test/fixtures/wpt/versions.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 06872e85e556f2..4044b555c7a0d5 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -34,7 +34,7 @@ Last update: - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi - wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi - web-locks: https://github.com/web-platform-tests/wpt/tree/10a122a6bc/web-locks -- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/1d2c5fb36a/WebCryptoAPI +- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/ff26d9b307/WebCryptoAPI - webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/2f96fa1996/webidl/ecmascript-binding/es-exceptions - webmessaging/broadcastchannel: https://github.com/web-platform-tests/wpt/tree/6495c91853/webmessaging/broadcastchannel - webstorage: https://github.com/web-platform-tests/wpt/tree/1d2c5fb36a/webstorage diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_fixtures.js b/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_fixtures.js index cac6db1dcac758..6a7f583e0165c5 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_fixtures.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_fixtures.js @@ -432,7 +432,7 @@ var mismatchedKtyField = { // The 'kty' field doesn't match the key algorithm. var mismatchedCrvField = { "Ed25519": "X25519", - "X25519": "Ed448", - "Ed448": "X25519", - "X448": "Ed25519", + "X25519": "Ed25519", + "Ed448": "X448", + "X448": "Ed448", } diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 5c4c0305254155..521ec897f9a7be 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -96,7 +96,7 @@ "path": "web-locks" }, "WebCryptoAPI": { - "commit": "1d2c5fb36a6e477c8f915bde7eca027be6abe792", + "commit": "ff26d9b307b981b3f1b88f454268e6bb8933e6c2", "path": "WebCryptoAPI" }, "webidl/ecmascript-binding/es-exceptions": { From e6a6cdb9df24de5e2f7eb853a7b5e4a1c5d44699 Mon Sep 17 00:00:00 2001 From: RANDRIAMANANTENA Narindra Tiana Annaick Date: Tue, 19 Aug 2025 21:38:44 +0300 Subject: [PATCH 062/111] doc: add missing Zstd strategy constants PR-URL: https://github.com/nodejs/node/pull/59312 Fixes: https://github.com/nodejs/node/issues/59290 Reviewed-By: James M Snell Reviewed-By: Luigi Pinca Reviewed-By: Antoine du Hamel --- doc/api/zlib.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/doc/api/zlib.md b/doc/api/zlib.md index c3f827ee0c23d0..e07b0eb4ccd297 100644 --- a/doc/api/zlib.md +++ b/doc/api/zlib.md @@ -750,6 +750,34 @@ The most important options are: * `ZSTD_c_compressionLevel` * Set compression parameters according to pre-defined cLevel table. Default level is ZSTD\_CLEVEL\_DEFAULT==3. +* `ZSTD_c_strategy` + * Select the compression strategy. + * Possible values are listed in the strategy options section below. + +#### Strategy options + +The following constants can be used as values for the `ZSTD_c_strategy` +parameter: + +* `zlib.constants.ZSTD_fast` +* `zlib.constants.ZSTD_dfast` +* `zlib.constants.ZSTD_greedy` +* `zlib.constants.ZSTD_lazy` +* `zlib.constants.ZSTD_lazy2` +* `zlib.constants.ZSTD_btlazy2` +* `zlib.constants.ZSTD_btopt` +* `zlib.constants.ZSTD_btultra` +* `zlib.constants.ZSTD_btultra2` + +Example: + +```js +const stream = zlib.createZstdCompress({ + params: { + [zlib.constants.ZSTD_c_strategy]: zlib.constants.ZSTD_btultra, + }, +}); +``` #### Pledged Source Size From d3afc63c44a8f7e8f862a53c36c529a49231abf9 Mon Sep 17 00:00:00 2001 From: Ranieri Althoff <1993083+ranisalt@users.noreply.github.com> Date: Tue, 19 Aug 2025 21:30:38 +0200 Subject: [PATCH 063/111] crypto: add argon2() and argon2Sync() methods Co-authored-by: Filip Skokan Co-authored-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/50353 Reviewed-By: Ethan Arrowood Reviewed-By: Filip Skokan Reviewed-By: Yagiz Nizipli --- benchmark/common.js | 3 + benchmark/crypto/argon2.js | 53 +++++ deps/ncrypto/ncrypto.cc | 101 ++++++++++ deps/ncrypto/ncrypto.h | 17 ++ doc/api/crypto.md | 166 ++++++++++++++++ doc/api/errors.md | 6 + lib/crypto.js | 6 + lib/internal/crypto/argon2.js | 185 ++++++++++++++++++ lib/internal/errors.js | 1 + node.gyp | 12 +- src/async_wrap.h | 29 +-- src/crypto/README.md | 1 + src/crypto/crypto_argon2.cc | 172 ++++++++++++++++ src/crypto/crypto_argon2.h | 86 ++++++++ src/node_crypto.cc | 7 + src/node_crypto.h | 1 + src/node_errors.h | 2 + .../test-crypto-argon2-unsupported.js | 14 ++ test/parallel/test-crypto-argon2.js | 139 +++++++++++++ test/sequential/test-async-wrap-getasyncid.js | 1 + 20 files changed, 983 insertions(+), 19 deletions(-) create mode 100644 benchmark/crypto/argon2.js create mode 100644 lib/internal/crypto/argon2.js create mode 100644 src/crypto/crypto_argon2.cc create mode 100644 src/crypto/crypto_argon2.h create mode 100644 test/parallel/test-crypto-argon2-unsupported.js create mode 100644 test/parallel/test-crypto-argon2.js diff --git a/benchmark/common.js b/benchmark/common.js index 0d3dbef24d07ad..0bb3fa3dfae32f 100644 --- a/benchmark/common.js +++ b/benchmark/common.js @@ -81,6 +81,9 @@ class Benchmark { if (typeof value === 'number') { if (key === 'dur' || key === 'duration') { value = 0.05; + } else if (key === 'memory') { + // minimum Argon2 memcost with 1 lane is 8 + value = 8; } else if (value > 1) { value = 1; } diff --git a/benchmark/crypto/argon2.js b/benchmark/crypto/argon2.js new file mode 100644 index 00000000000000..ce6a824233e636 --- /dev/null +++ b/benchmark/crypto/argon2.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common.js'); +const { hasOpenSSL } = require('../../test/common/crypto.js'); +const assert = require('node:assert'); +const { + argon2, + argon2Sync, + randomBytes, +} = require('node:crypto'); + +if (!hasOpenSSL(3, 2)) { + console.log('Skipping: Argon2 requires OpenSSL >= 3.2'); + process.exit(0); +} + +const bench = common.createBenchmark(main, { + mode: ['sync', 'async'], + algorithm: ['argon2d', 'argon2i', 'argon2id'], + passes: [1, 3], + parallelism: [2, 4, 8], + memory: [2 ** 11, 2 ** 16, 2 ** 21], + n: [50], +}); + +function measureSync(n, algorithm, message, nonce, options) { + bench.start(); + for (let i = 0; i < n; ++i) + argon2Sync(algorithm, { ...options, message, nonce, tagLength: 64 }); + bench.end(n); +} + +function measureAsync(n, algorithm, message, nonce, options) { + let remaining = n; + function done(err) { + assert.ifError(err); + if (--remaining === 0) + bench.end(n); + } + bench.start(); + for (let i = 0; i < n; ++i) + argon2(algorithm, { ...options, message, nonce, tagLength: 64 }, done); +} + +function main({ n, mode, algorithm, ...options }) { + // Message, nonce, secret, associated data & tag length do not affect performance + const message = randomBytes(32); + const nonce = randomBytes(16); + if (mode === 'sync') + measureSync(n, algorithm, message, nonce, options); + else + measureAsync(n, algorithm, message, nonce, options); +} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index c6ef072e051a9a..d451a20c28af67 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -10,7 +10,12 @@ #include #include #if OPENSSL_VERSION_MAJOR >= 3 +#include +#include #include +#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#include +#endif #endif #if OPENSSL_WITH_PQC struct PQCMapping { @@ -1868,6 +1873,102 @@ DataPointer pbkdf2(const Digest& md, return {}; } +#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#ifndef OPENSSL_NO_ARGON2 +DataPointer argon2(const Buffer& pass, + const Buffer& salt, + uint32_t lanes, + size_t length, + uint32_t memcost, + uint32_t iter, + uint32_t version, + const Buffer& secret, + const Buffer& ad, + Argon2Type type) { + ClearErrorOnReturn clearErrorOnReturn; + + std::string_view algorithm; + switch (type) { + case Argon2Type::ARGON2I: + algorithm = "ARGON2I"; + break; + case Argon2Type::ARGON2D: + algorithm = "ARGON2D"; + break; + case Argon2Type::ARGON2ID: + algorithm = "ARGON2ID"; + break; + default: + // Invalid Argon2 type + return {}; + } + + // creates a new library context to avoid locking when running concurrently + auto ctx = DeleteFnPtr{OSSL_LIB_CTX_new()}; + if (!ctx) { + return {}; + } + + // required if threads > 1 + if (lanes > 1 && OSSL_set_max_threads(ctx.get(), lanes) != 1) { + return {}; + } + + auto kdf = DeleteFnPtr{ + EVP_KDF_fetch(ctx.get(), algorithm.data(), nullptr)}; + if (!kdf) { + return {}; + } + + auto kctx = + DeleteFnPtr{EVP_KDF_CTX_new(kdf.get())}; + if (!kctx) { + return {}; + } + + std::vector params; + params.reserve(9); + + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_PASSWORD, + const_cast(pass.len > 0 ? pass.data : ""), + pass.len)); + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SALT, const_cast(salt.data), salt.len)); + params.push_back(OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &lanes)); + params.push_back( + OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &lanes)); + params.push_back( + OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &memcost)); + params.push_back(OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, &iter)); + + if (ad.len != 0) { + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_ARGON2_AD, const_cast(ad.data), ad.len)); + } + + if (secret.len != 0) { + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SECRET, + const_cast(secret.data), + secret.len)); + } + + params.push_back(OSSL_PARAM_construct_end()); + + auto dp = DataPointer::Alloc(length); + if (dp && EVP_KDF_derive(kctx.get(), + reinterpret_cast(dp.get()), + length, + params.data()) == 1) { + return dp; + } + + return {}; +} +#endif +#endif + // ============================================================================ EVPKeyPointer::PrivateKeyEncodingConfig::PrivateKeyEncodingConfig( diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 42f9cf6aa2a246..b2af688de33827 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -1557,6 +1557,23 @@ DataPointer pbkdf2(const Digest& md, uint32_t iterations, size_t length); +#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#ifndef OPENSSL_NO_ARGON2 +enum class Argon2Type { ARGON2D, ARGON2I, ARGON2ID }; + +DataPointer argon2(const Buffer& pass, + const Buffer& salt, + uint32_t lanes, + size_t length, + uint32_t memcost, + uint32_t iter, + uint32_t version, + const Buffer& secret, + const Buffer& ad, + Argon2Type type); +#endif +#endif + // ============================================================================ // Version metadata #define NCRYPTO_VERSION "0.0.1" diff --git a/doc/api/crypto.md b/doc/api/crypto.md index a95a2a4173f32f..85c8af86e112fa 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2970,6 +2970,171 @@ Does not perform any other validation checks on the certificate. ## `node:crypto` module methods and properties +### `crypto.argon2(algorithm, parameters, callback)` + + + +> Stability: 1.2 - Release candidate + +* `algorithm` {string} Variant of Argon2, one of `"argon2d"`, `"argon2i"` or `"argon2id"`. +* `parameters` {Object} + * `message` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, this is the password for password + hashing applications of Argon2. + * `nonce` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, must be at + least 8 bytes long. This is the salt for password hashing applications of Argon2. + * `parallelism` {number} REQUIRED, degree of parallelism determines how many computational chains (lanes) + can be run. Must be greater than 1 and less than `2**24-1`. + * `tagLength` {number} REQUIRED, the length of the key to generate. Must be greater than 4 and + less than `2**32-1`. + * `memory` {number} REQUIRED, memory cost in 1KiB blocks. Must be greater than + `8 * parallelism` and less than `2**32-1`. The actual number of blocks is rounded + down to the nearest multiple of `4 * parallelism`. + * `passes` {number} REQUIRED, number of passes (iterations). Must be greater than 1 and less + than `2**32-1`. + * `secret` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Random additional input, + similar to the salt, that should **NOT** be stored with the derived key. This is known as pepper in + password hashing applications. If used, must have a length not greater than `2**32-1` bytes. + * `associatedData` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Additional data to + be added to the hash, functionally equivalent to salt or secret, but meant for + non-random data. If used, must have a length not greater than `2**32-1` bytes. +* `callback` {Function} + * `err` {Error} + * `derivedKey` {Buffer} + +Provides an asynchronous [Argon2][] implementation. Argon2 is a password-based +key derivation function that is designed to be expensive computationally and +memory-wise in order to make brute-force attacks unrewarding. + +The `nonce` should be as unique as possible. It is recommended that a nonce is +random and at least 16 bytes long. See [NIST SP 800-132][] for details. + +When passing strings for `message`, `nonce`, `secret` or `associatedData`, please +consider [caveats when using strings as inputs to cryptographic APIs][]. + +The `callback` function is called with two arguments: `err` and `derivedKey`. +`err` is an exception object when key derivation fails, otherwise `err` is +`null`. `derivedKey` is passed to the callback as a [`Buffer`][]. + +An exception is thrown when any of the input arguments specify invalid values +or types. + +```mjs +const { argon2, randomBytes } = await import('node:crypto'); + +const parameters = { + message: 'password', + nonce: randomBytes(16), + parallelism: 4, + tagLength: 64, + memory: 65536, + passes: 3, +}; + +argon2('argon2id', parameters, (err, derivedKey) => { + if (err) throw err; + console.log(derivedKey.toString('hex')); // 'af91dad...9520f15' +}); +``` + +```cjs +const { argon2, randomBytes } = require('node:crypto'); + +const parameters = { + message: 'password', + nonce: randomBytes(16), + parallelism: 4, + tagLength: 64, + memory: 65536, + passes: 3, +}; + +argon2('argon2id', parameters, (err, derivedKey) => { + if (err) throw err; + console.log(derivedKey.toString('hex')); // 'af91dad...9520f15' +}); +``` + +### `crypto.argon2Sync(algorithm, parameters)` + + + +> Stability: 1.2 - Release candidate + +* `algorithm` {string} Variant of Argon2, one of `"argon2d"`, `"argon2i"` or `"argon2id"`. +* `parameters` {Object} + * `message` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, this is the password for password + hashing applications of Argon2. + * `nonce` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, must be at + least 8 bytes long. This is the salt for password hashing applications of Argon2. + * `parallelism` {number} REQUIRED, degree of parallelism determines how many computational chains (lanes) + can be run. Must be greater than 1 and less than `2**24-1`. + * `tagLength` {number} REQUIRED, the length of the key to generate. Must be greater than 4 and + less than `2**32-1`. + * `memory` {number} REQUIRED, memory cost in 1KiB blocks. Must be greater than + `8 * parallelism` and less than `2**32-1`. The actual number of blocks is rounded + down to the nearest multiple of `4 * parallelism`. + * `passes` {number} REQUIRED, number of passes (iterations). Must be greater than 1 and less + than `2**32-1`. + * `secret` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Random additional input, + similar to the salt, that should **NOT** be stored with the derived key. This is known as pepper in + password hashing applications. If used, must have a length not greater than `2**32-1` bytes. + * `associatedData` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Additional data to + be added to the hash, functionally equivalent to salt or secret, but meant for + non-random data. If used, must have a length not greater than `2**32-1` bytes. +* Returns: {Buffer} + +Provides a synchronous [Argon2][] implementation. Argon2 is a password-based +key derivation function that is designed to be expensive computationally and +memory-wise in order to make brute-force attacks unrewarding. + +The `nonce` should be as unique as possible. It is recommended that a nonce is +random and at least 16 bytes long. See [NIST SP 800-132][] for details. + +When passing strings for `message`, `nonce`, `secret` or `associatedData`, please +consider [caveats when using strings as inputs to cryptographic APIs][]. + +An exception is thrown when key derivation fails, otherwise the derived key is +returned as a [`Buffer`][]. + +An exception is thrown when any of the input arguments specify invalid values +or types. + +```mjs +const { argon2Sync, randomBytes } = await import('node:crypto'); + +const parameters = { + message: 'password', + nonce: randomBytes(16), + parallelism: 4, + tagLength: 64, + memory: 65536, + passes: 3, +}; + +const derivedKey = argon2Sync('argon2id', parameters); +console.log(derivedKey.toString('hex')); // 'af91dad...9520f15' +``` + +```cjs +const { argon2Sync, randomBytes } = require('node:crypto'); + +const parameters = { + message: 'password', + nonce: randomBytes(16), + parallelism: 4, + tagLength: 64, + memory: 65536, + passes: 3, +}; + +const derivedKey = argon2Sync('argon2id', parameters); +console.log(derivedKey.toString('hex')); // 'af91dad...9520f15' +``` + ### `crypto.checkPrime(candidate[, options], callback)` + +> Stability: 1.2 - Release candidate + +* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} Private Key +* `ciphertext` {ArrayBuffer|Buffer|TypedArray|DataView} +* `callback` {Function} + * `err` {Error} + * `sharedKey` {Buffer} +* Returns: {Buffer} if the `callback` function is not provided. + + + +Key decapsulation using a KEM algorithm with a private key. + +Supported key types and their KEM algorithms are: + +* `'rsa'`[^openssl30] RSA Secret Value Encapsulation +* `'ec'`[^openssl32] DHKEM(P-256, HKDF-SHA256), DHKEM(P-384, HKDF-SHA256), DHKEM(P-521, HKDF-SHA256) +* `'x25519'`[^openssl32] DHKEM(X25519, HKDF-SHA256) +* `'x448'`[^openssl32] DHKEM(X448, HKDF-SHA512) +* `'ml-kem-512'`[^openssl35] ML-KEM +* `'ml-kem-768'`[^openssl35] ML-KEM +* `'ml-kem-1024'`[^openssl35] ML-KEM + +If `key` is not a [`KeyObject`][], this function behaves as if `key` had been +passed to [`crypto.createPrivateKey()`][]. + +If the `callback` function is provided this function uses libuv's threadpool. + ### `crypto.diffieHellman(options[, callback])` + +> Stability: 1.2 - Release candidate + +* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} Public Key +* `callback` {Function} + * `err` {Error} + * `result` {Object} + * `sharedKey` {Buffer} + * `ciphertext` {Buffer} +* Returns: {Object} if the `callback` function is not provided. + * `sharedKey` {Buffer} + * `ciphertext` {Buffer} + + + +Key encapsulation using a KEM algorithm with a public key. + +Supported key types and their KEM algorithms are: + +* `'rsa'`[^openssl30] RSA Secret Value Encapsulation +* `'ec'`[^openssl32] DHKEM(P-256, HKDF-SHA256), DHKEM(P-384, HKDF-SHA256), DHKEM(P-521, HKDF-SHA256) +* `'x25519'`[^openssl32] DHKEM(X25519, HKDF-SHA256) +* `'x448'`[^openssl32] DHKEM(X448, HKDF-SHA512) +* `'ml-kem-512'`[^openssl35] ML-KEM +* `'ml-kem-768'`[^openssl35] ML-KEM +* `'ml-kem-1024'`[^openssl35] ML-KEM + +If `key` is not a [`KeyObject`][], this function behaves as if `key` had been +passed to [`crypto.createPublicKey()`][]. + +If the `callback` function is provided this function uses libuv's threadpool. + ### `crypto.fips` + +Attempted to use KEM operations while Node.js was not compiled with +OpenSSL with KEM support. + ### `ERR_CRYPTO_OPERATION_FAILED` diff --git a/lib/crypto.js b/lib/crypto.js index cfd41832cb065f..4290989f3cd6b1 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -122,6 +122,10 @@ const { secureHeapUsed, } = require('internal/crypto/util'); const Certificate = require('internal/crypto/certificate'); +const { + encapsulate, + decapsulate, +} = require('internal/crypto/kem'); let webcrypto; function lazyWebCrypto() { @@ -225,6 +229,8 @@ module.exports = { setFips, verify: verifyOneShot, hash, + encapsulate, + decapsulate, // Classes Certificate, diff --git a/lib/internal/crypto/kem.js b/lib/internal/crypto/kem.js new file mode 100644 index 00000000000000..43c7bde52ea99f --- /dev/null +++ b/lib/internal/crypto/kem.js @@ -0,0 +1,112 @@ +'use strict'; + +const { + FunctionPrototypeCall, +} = primordials; + +const { + codes: { + ERR_CRYPTO_KEM_NOT_SUPPORTED, + }, +} = require('internal/errors'); + +const { + validateFunction, +} = require('internal/validators'); + +const { + kCryptoJobAsync, + kCryptoJobSync, + KEMDecapsulateJob, + KEMEncapsulateJob, +} = internalBinding('crypto'); + +const { + preparePrivateKey, + preparePublicOrPrivateKey, +} = require('internal/crypto/keys'); + +const { + getArrayBufferOrView, +} = require('internal/crypto/util'); + +function encapsulate(key, callback) { + if (!KEMEncapsulateJob) + throw new ERR_CRYPTO_KEM_NOT_SUPPORTED(); + + if (callback !== undefined) + validateFunction(callback, 'callback'); + + const { + data: keyData, + format: keyFormat, + type: keyType, + passphrase: keyPassphrase, + } = preparePublicOrPrivateKey(key); + + const job = new KEMEncapsulateJob( + callback ? kCryptoJobAsync : kCryptoJobSync, + keyData, + keyFormat, + keyType, + keyPassphrase); + + if (!callback) { + const { 0: err, 1: result } = job.run(); + if (err !== undefined) + throw err; + const { 0: sharedKey, 1: ciphertext } = result; + return { sharedKey, ciphertext }; + } + + job.ondone = (error, result) => { + if (error) return FunctionPrototypeCall(callback, job, error); + const { 0: sharedKey, 1: ciphertext } = result; + FunctionPrototypeCall(callback, job, null, { sharedKey, ciphertext }); + }; + job.run(); +} + +function decapsulate(key, ciphertext, callback) { + if (!KEMDecapsulateJob) + throw new ERR_CRYPTO_KEM_NOT_SUPPORTED(); + + if (callback !== undefined) + validateFunction(callback, 'callback'); + + const { + data: keyData, + format: keyFormat, + type: keyType, + passphrase: keyPassphrase, + } = preparePrivateKey(key); + + ciphertext = getArrayBufferOrView(ciphertext, 'ciphertext'); + + const job = new KEMDecapsulateJob( + callback ? kCryptoJobAsync : kCryptoJobSync, + keyData, + keyFormat, + keyType, + keyPassphrase, + ciphertext); + + if (!callback) { + const { 0: err, 1: result } = job.run(); + if (err !== undefined) + throw err; + + return result; + } + + job.ondone = (error, result) => { + if (error) return FunctionPrototypeCall(callback, job, error); + FunctionPrototypeCall(callback, job, null, result); + }; + job.run(); +} + +module.exports = { + encapsulate, + decapsulate, +}; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index ca0fae01cfae8f..8a6f5b26f5391c 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1171,6 +1171,7 @@ E('ERR_CRYPTO_INVALID_JWK', 'Invalid JWK data', TypeError); E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', 'Invalid key object type %s, expected %s.', TypeError); E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error); +E('ERR_CRYPTO_KEM_NOT_SUPPORTED', 'KEM is not supported', Error); E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error); E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error); // Switch to TypeError. The current implementation does not seem right. diff --git a/node.gyp b/node.gyp index a0fdb1b85e9d55..51863d37ab7e47 100644 --- a/node.gyp +++ b/node.gyp @@ -337,6 +337,7 @@ 'src/crypto/crypto_context.cc', 'src/crypto/crypto_ec.cc', 'src/crypto/crypto_ml_dsa.cc', + 'src/crypto/crypto_kem.cc', 'src/crypto/crypto_hmac.cc', 'src/crypto/crypto_random.cc', 'src/crypto/crypto_rsa.cc', diff --git a/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc new file mode 100644 index 00000000000000..d6227bb66c6dc1 --- /dev/null +++ b/src/crypto/crypto_kem.cc @@ -0,0 +1,262 @@ +#include "crypto/crypto_kem.h" + +#if OPENSSL_VERSION_MAJOR >= 3 + +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_buffer.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +namespace node { + +using ncrypto::EVPKeyPointer; +using v8::Array; +using v8::FunctionCallbackInfo; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::Value; + +namespace crypto { + +KEMConfiguration::KEMConfiguration(KEMConfiguration&& other) noexcept + : job_mode(other.job_mode), + mode(other.mode), + key(std::move(other.key)), + ciphertext(std::move(other.ciphertext)) {} + +KEMConfiguration& KEMConfiguration::operator=( + KEMConfiguration&& other) noexcept { + if (&other == this) return *this; + this->~KEMConfiguration(); + return *new (this) KEMConfiguration(std::move(other)); +} + +void KEMConfiguration::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("key", key); + if (job_mode == kCryptoJobAsync) { + tracker->TrackFieldWithSize("ciphertext", ciphertext.size()); + } +} + +namespace { + +bool DoKEMEncapsulate(Environment* env, + const EVPKeyPointer& public_key, + ByteSource* out, + CryptoJobMode mode) { + auto result = ncrypto::KEM::Encapsulate(public_key); + if (!result) { + if (mode == kCryptoJobSync) { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to perform encapsulation"); + } + return false; + } + + // Pack the result: [ciphertext_len][shared_key_len][ciphertext][shared_key] + size_t ciphertext_len = result->ciphertext.size(); + size_t shared_key_len = result->shared_key.size(); + size_t total_len = + sizeof(uint32_t) + sizeof(uint32_t) + ciphertext_len + shared_key_len; + + auto data = ncrypto::DataPointer::Alloc(total_len); + if (!data) { + if (mode == kCryptoJobSync) { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to allocate output buffer"); + } + return false; + } + + unsigned char* ptr = static_cast(data.get()); + + // Write size headers + *reinterpret_cast(ptr) = static_cast(ciphertext_len); + *reinterpret_cast(ptr + sizeof(uint32_t)) = + static_cast(shared_key_len); + + // Write ciphertext and shared key data + unsigned char* ciphertext_ptr = ptr + 2 * sizeof(uint32_t); + unsigned char* shared_key_ptr = ciphertext_ptr + ciphertext_len; + + std::memcpy(ciphertext_ptr, result->ciphertext.get(), ciphertext_len); + std::memcpy(shared_key_ptr, result->shared_key.get(), shared_key_len); + + *out = ByteSource::Allocated(data.release()); + return true; +} + +bool DoKEMDecapsulate(Environment* env, + const EVPKeyPointer& private_key, + const ByteSource& ciphertext, + ByteSource* out, + CryptoJobMode mode) { + ncrypto::Buffer ciphertext_buf{ciphertext.data(), + ciphertext.size()}; + auto shared_key = ncrypto::KEM::Decapsulate(private_key, ciphertext_buf); + if (!shared_key) { + if (mode == kCryptoJobSync) { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to perform decapsulation"); + } + return false; + } + + *out = ByteSource::Allocated(shared_key.release()); + return true; +} + +} // anonymous namespace + +// KEMEncapsulateTraits implementation +Maybe KEMEncapsulateTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + KEMConfiguration* params) { + params->job_mode = mode; + params->mode = KEMMode::Encapsulate; + + unsigned int key_offset = offset; + auto public_key_data = + KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &key_offset); + if (!public_key_data) { + return Nothing(); + } + params->key = std::move(public_key_data); + + return v8::JustVoid(); +} + +bool KEMEncapsulateTraits::DeriveBits(Environment* env, + const KEMConfiguration& params, + ByteSource* out, + CryptoJobMode mode) { + Mutex::ScopedLock lock(params.key.mutex()); + const auto& public_key = params.key.GetAsymmetricKey(); + + return DoKEMEncapsulate(env, public_key, out, mode); +} + +MaybeLocal KEMEncapsulateTraits::EncodeOutput( + Environment* env, const KEMConfiguration& params, ByteSource* out) { + // The output contains: + // [ciphertext_len][shared_key_len][ciphertext][shared_key] + const unsigned char* data = out->data(); + + uint32_t ciphertext_len = *reinterpret_cast(data); + uint32_t shared_key_len = + *reinterpret_cast(data + sizeof(uint32_t)); + + const unsigned char* ciphertext_ptr = data + 2 * sizeof(uint32_t); + const unsigned char* shared_key_ptr = ciphertext_ptr + ciphertext_len; + + MaybeLocal ciphertext_buf = + node::Buffer::Copy(env->isolate(), + reinterpret_cast(ciphertext_ptr), + ciphertext_len); + + MaybeLocal shared_key_buf = + node::Buffer::Copy(env->isolate(), + reinterpret_cast(shared_key_ptr), + shared_key_len); + + Local ciphertext_obj; + Local shared_key_obj; + if (!ciphertext_buf.ToLocal(&ciphertext_obj) || + !shared_key_buf.ToLocal(&shared_key_obj)) { + return MaybeLocal(); + } + + // Return an array [sharedKey, ciphertext]. + Local result = Array::New(env->isolate(), 2); + if (result->Set(env->context(), 0, shared_key_obj).IsNothing() || + result->Set(env->context(), 1, ciphertext_obj).IsNothing()) { + return MaybeLocal(); + } + + return result; +} + +// KEMDecapsulateTraits implementation +Maybe KEMDecapsulateTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + KEMConfiguration* params) { + Environment* env = Environment::GetCurrent(args); + + params->job_mode = mode; + params->mode = KEMMode::Decapsulate; + + unsigned int key_offset = offset; + auto private_key_data = + KeyObjectData::GetPrivateKeyFromJs(args, &key_offset, true); + if (!private_key_data) { + return Nothing(); + } + params->key = std::move(private_key_data); + + ArrayBufferOrViewContents ciphertext(args[key_offset]); + if (!ciphertext.CheckSizeInt32()) { + THROW_ERR_OUT_OF_RANGE(env, "ciphertext is too big"); + return Nothing(); + } + + params->ciphertext = + mode == kCryptoJobAsync ? ciphertext.ToCopy() : ciphertext.ToByteSource(); + + return v8::JustVoid(); +} + +bool KEMDecapsulateTraits::DeriveBits(Environment* env, + const KEMConfiguration& params, + ByteSource* out, + CryptoJobMode mode) { + Mutex::ScopedLock lock(params.key.mutex()); + const auto& private_key = params.key.GetAsymmetricKey(); + + return DoKEMDecapsulate(env, private_key, params.ciphertext, out, mode); +} + +MaybeLocal KEMDecapsulateTraits::EncodeOutput( + Environment* env, const KEMConfiguration& params, ByteSource* out) { + return out->ToBuffer(env); +} + +void InitializeKEM(Environment* env, Local target) { + KEMEncapsulateJob::Initialize(env, target); + KEMDecapsulateJob::Initialize(env, target); + + constexpr int kKEMEncapsulate = static_cast(KEMMode::Encapsulate); + constexpr int kKEMDecapsulate = static_cast(KEMMode::Decapsulate); + + NODE_DEFINE_CONSTANT(target, kKEMEncapsulate); + NODE_DEFINE_CONSTANT(target, kKEMDecapsulate); +} + +void RegisterKEMExternalReferences(ExternalReferenceRegistry* registry) { + KEMEncapsulateJob::RegisterExternalReferences(registry); + KEMDecapsulateJob::RegisterExternalReferences(registry); +} + +namespace KEM { +void Initialize(Environment* env, Local target) { + InitializeKEM(env, target); +} + +void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + RegisterKEMExternalReferences(registry); +} +} // namespace KEM + +} // namespace crypto +} // namespace node + +#endif diff --git a/src/crypto/crypto_kem.h b/src/crypto/crypto_kem.h new file mode 100644 index 00000000000000..02cef53c27c8c8 --- /dev/null +++ b/src/crypto/crypto_kem.h @@ -0,0 +1,113 @@ +#ifndef SRC_CRYPTO_CRYPTO_KEM_H_ +#define SRC_CRYPTO_CRYPTO_KEM_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "base_object.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" +#include "env.h" +#include "memory_tracker.h" +#include "node_external_reference.h" + +#if OPENSSL_VERSION_MAJOR >= 3 + +namespace node { +namespace crypto { + +enum class KEMMode { Encapsulate, Decapsulate }; + +struct KEMConfiguration final : public MemoryRetainer { + CryptoJobMode job_mode; + KEMMode mode; + KeyObjectData key; + ByteSource ciphertext; + + KEMConfiguration() = default; + explicit KEMConfiguration(KEMConfiguration&& other) noexcept; + KEMConfiguration& operator=(KEMConfiguration&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(KEMConfiguration) + SET_SELF_SIZE(KEMConfiguration) +}; + +struct KEMEncapsulateTraits final { + using AdditionalParameters = KEMConfiguration; + static constexpr const char* JobName = "KEMEncapsulateJob"; + + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_DERIVEBITSREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + KEMConfiguration* params); + + static bool DeriveBits(Environment* env, + const KEMConfiguration& params, + ByteSource* out, + CryptoJobMode mode); + + static v8::MaybeLocal EncodeOutput(Environment* env, + const KEMConfiguration& params, + ByteSource* out); +}; + +struct KEMDecapsulateTraits final { + using AdditionalParameters = KEMConfiguration; + static constexpr const char* JobName = "KEMDecapsulateJob"; + + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_DERIVEBITSREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + KEMConfiguration* params); + + static bool DeriveBits(Environment* env, + const KEMConfiguration& params, + ByteSource* out, + CryptoJobMode mode); + + static v8::MaybeLocal EncodeOutput(Environment* env, + const KEMConfiguration& params, + ByteSource* out); +}; + +using KEMEncapsulateJob = DeriveBitsJob; +using KEMDecapsulateJob = DeriveBitsJob; + +void InitializeKEM(Environment* env, v8::Local target); +void RegisterKEMExternalReferences(ExternalReferenceRegistry* registry); + +namespace KEM { +void Initialize(Environment* env, v8::Local target); +void RegisterExternalReferences(ExternalReferenceRegistry* registry); +} // namespace KEM + +} // namespace crypto +} // namespace node + +#else + +// Provide stub implementations when OpenSSL < 3.0 +namespace node { +namespace crypto { +namespace KEM { +inline void Initialize(Environment* env, v8::Local target) { + // No-op when OpenSSL < 3.0 +} +inline void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + // No-op when OpenSSL < 3.0 +} +} // namespace KEM +} // namespace crypto +} // namespace node + +#endif +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_KEM_H_ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 6af94c959873d2..9bdb7201475f35 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -66,6 +66,13 @@ namespace crypto { #define ARGON2_NAMESPACE_LIST(V) #endif // !OPENSSL_NO_ARGON2 && OpenSSL >= 3.2 +// KEM functionality requires OpenSSL 3.0.0 or later +#if OPENSSL_VERSION_MAJOR >= 3 +#define KEM_NAMESPACE_LIST(V) V(KEM) +#else +#define KEM_NAMESPACE_LIST(V) +#endif + #ifdef OPENSSL_NO_SCRYPT #define SCRYPT_NAMESPACE_LIST(V) #else @@ -75,6 +82,7 @@ namespace crypto { #define CRYPTO_NAMESPACE_LIST(V) \ CRYPTO_NAMESPACE_LIST_BASE(V) \ ARGON2_NAMESPACE_LIST(V) \ + KEM_NAMESPACE_LIST(V) \ SCRYPT_NAMESPACE_LIST(V) void Initialize(Local target, diff --git a/src/node_crypto.h b/src/node_crypto.h index 1c493db8100935..21380d304fdfe0 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -40,6 +40,9 @@ #include "crypto/crypto_hash.h" #include "crypto/crypto_hkdf.h" #include "crypto/crypto_hmac.h" +#if OPENSSL_VERSION_MAJOR >= 3 +#include "crypto/crypto_kem.h" +#endif #include "crypto/crypto_keygen.h" #include "crypto/crypto_keys.h" #include "crypto/crypto_ml_dsa.h" diff --git a/test/parallel/test-crypto-encap-decap.js b/test/parallel/test-crypto-encap-decap.js new file mode 100644 index 00000000000000..2c2ccd42ca2365 --- /dev/null +++ b/test/parallel/test-crypto-encap-decap.js @@ -0,0 +1,211 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const fixtures = require('../common/fixtures'); +const { hasOpenSSL } = require('../common/crypto'); +const { promisify } = require('util'); + +if (!hasOpenSSL(3)) { + assert.throws(() => crypto.encapsulate(), { code: 'ERR_CRYPTO_KEM_NOT_SUPPORTED' }); + return; +} + +assert.throws(() => crypto.encapsulate(), { code: 'ERR_INVALID_ARG_TYPE', + message: /The "key" argument must be of type/ }); +assert.throws(() => crypto.decapsulate(), { code: 'ERR_INVALID_ARG_TYPE', + message: /The "key" argument must be of type/ }); + +const keys = { + 'rsa': { + supported: hasOpenSSL(3), // RSASVE was added in 3.0 + publicKey: fixtures.readKey('rsa_public_2048.pem', 'ascii'), + privateKey: fixtures.readKey('rsa_private_2048.pem', 'ascii'), + sharedSecretLength: 256, + ciphertextLength: 256, + }, + 'rsa-pss': { + supported: false, // Only raw RSA is supported + publicKey: fixtures.readKey('rsa_pss_public_2048.pem', 'ascii'), + privateKey: fixtures.readKey('rsa_pss_private_2048.pem', 'ascii'), + }, + 'p-256': { + supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + publicKey: fixtures.readKey('ec_p256_public.pem', 'ascii'), + privateKey: fixtures.readKey('ec_p256_private.pem', 'ascii'), + sharedSecretLength: 32, + ciphertextLength: 65, + }, + 'p-384': { + supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + publicKey: fixtures.readKey('ec_p384_public.pem', 'ascii'), + privateKey: fixtures.readKey('ec_p384_private.pem', 'ascii'), + sharedSecretLength: 48, + ciphertextLength: 97, + }, + 'p-521': { + supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + publicKey: fixtures.readKey('ec_p521_public.pem', 'ascii'), + privateKey: fixtures.readKey('ec_p521_private.pem', 'ascii'), + sharedSecretLength: 64, + ciphertextLength: 133, + }, + 'secp256k1': { + supported: false, // only P-256, P-384, and P-521 are supported + publicKey: fixtures.readKey('ec_secp256k1_public.pem', 'ascii'), + privateKey: fixtures.readKey('ec_secp256k1_private.pem', 'ascii'), + }, + 'x25519': { + supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + publicKey: fixtures.readKey('x25519_public.pem', 'ascii'), + privateKey: fixtures.readKey('x25519_private.pem', 'ascii'), + sharedSecretLength: 32, + ciphertextLength: 32, + }, + 'x448': { + supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + publicKey: fixtures.readKey('x448_public.pem', 'ascii'), + privateKey: fixtures.readKey('x448_private.pem', 'ascii'), + sharedSecretLength: 64, + ciphertextLength: 56, + }, + 'ml-kem-512': { + supported: hasOpenSSL(3, 5), + publicKey: fixtures.readKey('ml_kem_512_public.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_512_private.pem', 'ascii'), + sharedSecretLength: 32, + ciphertextLength: 768, + }, + 'ml-kem-768': { + supported: hasOpenSSL(3, 5), + publicKey: fixtures.readKey('ml_kem_768_public.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_768_private.pem', 'ascii'), + sharedSecretLength: 32, + ciphertextLength: 1088, + }, + 'ml-kem-1024': { + supported: hasOpenSSL(3, 5), + publicKey: fixtures.readKey('ml_kem_1024_public.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_1024_private.pem', 'ascii'), + sharedSecretLength: 32, + ciphertextLength: 1568, + }, +}; + +for (const [name, { supported, publicKey, privateKey, sharedSecretLength, ciphertextLength }] of Object.entries(keys)) { + if (!supported) { + assert.throws(() => crypto.encapsulate(publicKey), + { code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_CRYPTO_OPERATION_FAILED/ }); + continue; + } + + { + assert.throws(() => crypto.decapsulate(privateKey, null), + { + code: 'ERR_INVALID_ARG_TYPE', + message: /instance of ArrayBuffer, Buffer, TypedArray, or DataView\. Received null/ + }); + } + + function formatKeyAs(key, params) { + return { ...params, key: key.export(params) }; + } + + const keyObjects = { + publicKey: crypto.createPublicKey(publicKey), + privateKey: crypto.createPrivateKey(privateKey), + }; + + const keyPairs = [ + keyObjects, + { publicKey, privateKey }, + { + publicKey: formatKeyAs(keyObjects.publicKey, { format: 'der', type: 'spki' }), + privateKey: formatKeyAs(keyObjects.privateKey, { format: 'der', type: 'pkcs8' }) + }, + ]; + + // TODO(@panva): ML-KEM does not have a JWK format defined yet, add once standardized + if (!keyObjects.privateKey.asymmetricKeyType.startsWith('ml')) { + keyPairs.push({ + publicKey: formatKeyAs(keyObjects.publicKey, { format: 'jwk' }), + privateKey: formatKeyAs(keyObjects.privateKey, { format: 'jwk' }) + }); + } + + for (const kp of keyPairs) { + // sync + { + const { sharedKey, ciphertext } = crypto.encapsulate(kp.publicKey); + assert(Buffer.isBuffer(sharedKey)); + assert.strictEqual(sharedKey.byteLength, sharedSecretLength); + assert(Buffer.isBuffer(ciphertext)); + assert.strictEqual(ciphertext.byteLength, ciphertextLength); + const sharedKey2 = crypto.decapsulate(kp.privateKey, ciphertext); + assert(Buffer.isBuffer(sharedKey2)); + assert.strictEqual(sharedKey2.byteLength, sharedSecretLength); + assert(sharedKey.equals(sharedKey2)); + } + + // async + { + crypto.encapsulate(kp.publicKey, common.mustSucceed(({ sharedKey, ciphertext }) => { + assert(Buffer.isBuffer(sharedKey)); + assert.strictEqual(sharedKey.byteLength, sharedSecretLength); + assert(Buffer.isBuffer(ciphertext)); + assert.strictEqual(ciphertext.byteLength, ciphertextLength); + crypto.decapsulate(kp.privateKey, ciphertext, common.mustSucceed((sharedKey2) => { + assert(Buffer.isBuffer(sharedKey2)); + assert.strictEqual(sharedKey2.byteLength, sharedSecretLength); + assert(sharedKey.equals(sharedKey2)); + })); + })); + } + + // promisified + (async () => { + const { sharedKey, ciphertext } = await promisify(crypto.encapsulate)(kp.publicKey); + assert(Buffer.isBuffer(sharedKey)); + assert.strictEqual(sharedKey.byteLength, sharedSecretLength); + assert(Buffer.isBuffer(ciphertext)); + assert.strictEqual(ciphertext.byteLength, ciphertextLength); + const sharedKey2 = await promisify(crypto.decapsulate)(kp.privateKey, ciphertext); + assert(Buffer.isBuffer(sharedKey2)); + assert.strictEqual(sharedKey2.byteLength, sharedSecretLength); + assert(sharedKey.equals(sharedKey2)); + })().then(common.mustCall()); + } + + let wrongPrivateKey; + if (name.startsWith('x')) { + wrongPrivateKey = name === 'x448' ? keys.x25519.privateKey : keys.x448.privateKey; + } else if (name.startsWith('p-')) { + wrongPrivateKey = name === 'p-256' ? keys['p-384'].privateKey : keys['p-256'].privateKey; + } else if (name.startsWith('ml-')) { + wrongPrivateKey = name === 'ml-kem-512' ? keys['ml-kem-768'].privateKey : keys['ml-kem-512'].privateKey; + } else { + wrongPrivateKey = keys.x25519.privateKey; + } + + // sync errors + { + const { ciphertext } = crypto.encapsulate(publicKey); + assert.throws(() => crypto.decapsulate(wrongPrivateKey, ciphertext), { + message: /Failed to (initialize|perform) decapsulation/, + code: 'ERR_CRYPTO_OPERATION_FAILED', + }); + } + + // async errors + { + crypto.encapsulate(publicKey, common.mustSucceed(({ ciphertext }) => { + crypto.decapsulate(wrongPrivateKey, ciphertext, common.mustCall((err) => { + assert(err); + assert.strictEqual(err.message, 'Deriving bits failed'); + })); + })); + } +} From 5347c4997a14fca60fca24a6a6f4d35c19184a9c Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 18 Aug 2025 16:06:02 +0200 Subject: [PATCH 070/111] esm: show race error message for inner module job race The race can not only happen when the ESM is loaded by the CommonJS loader, but can also happen to inner module jobs in the dependency graph. PR-URL: https://github.com/nodejs/node/pull/59519 Fixes: https://github.com/nodejs/node/issues/59366 Refs: https://github.com/abejfehr/node-22.18-issue-repro Reviewed-By: Geoffrey Booth Reviewed-By: Filip Skokan Reviewed-By: Chengzhong Wu --- lib/internal/modules/esm/loader.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 5d90b5f4c8c8f4..03317d13916ca1 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -471,6 +471,10 @@ class ModuleLoader { const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes; let job = this.loadCache.get(url, resolvedImportAttributes.type); if (job !== undefined) { + // TODO(node:55782): this race may stop happening when the ESM resolution and loading become synchronous. + if (!job.module) { + assert.fail(getRaceMessage(url, parentURL)); + } // This module is being evaluated, which means it's imported in a previous link // in a cycle. if (job.module.getStatus() === kEvaluating) { From 2fafe4c3bb309f85587567ae66482259a02e595d Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 18 Aug 2025 16:08:59 +0200 Subject: [PATCH 071/111] esm: link modules synchronously when no async loader hooks are used When no async loader hooks are registered, perform the linking as synchronously as possible to reduce the chance of races from the the shared module loading cache. PR-URL: https://github.com/nodejs/node/pull/59519 Fixes: https://github.com/nodejs/node/issues/59366 Refs: https://github.com/abejfehr/node-22.18-issue-repro Reviewed-By: Geoffrey Booth Reviewed-By: Filip Skokan Reviewed-By: Chengzhong Wu --- lib/internal/modules/esm/loader.js | 18 +++++++++++++----- lib/internal/modules/esm/module_job.js | 10 +++++----- test/es-module/test-esm-error-cache.js | 4 +++- .../test-esm-import-attributes-errors.js | 4 ++-- .../test-esm-import-attributes-errors.mjs | 4 ++-- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 03317d13916ca1..0443838c8a4ba5 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -65,6 +65,8 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { debug = fn; }); +const { isPromise } = require('internal/util/types'); + /** * @typedef {import('./hooks.js').HooksProxy} HooksProxy * @typedef {import('./module_job.js').ModuleJobBase} ModuleJobBase @@ -592,15 +594,21 @@ class ModuleLoader { /** * Load a module and translate it into a ModuleWrap for ordinary imported ESM. - * This is run asynchronously. + * This may be run asynchronously if there are asynchronous module loader hooks registered. * @param {string} url URL of the module to be translated. * @param {object} loadContext See {@link load} * @param {boolean} isMain Whether the module to be translated is the entry point. - * @returns {Promise} + * @returns {Promise|ModuleWrap} */ - async loadAndTranslate(url, loadContext, isMain) { - const { format, source } = await this.load(url, loadContext); - return this.#translate(url, format, source, isMain); + loadAndTranslate(url, loadContext, isMain) { + const maybePromise = this.load(url, loadContext); + const afterLoad = ({ format, source }) => { + return this.#translate(url, format, source, isMain); + }; + if (isPromise(maybePromise)) { + return maybePromise.then(afterLoad); + } + return afterLoad(maybePromise); } /** diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 268d8154457295..a3b36add4fbe6a 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -37,6 +37,7 @@ const { }, } = internalBinding('util'); const { decorateErrorStack, kEmptyObject } = require('internal/util'); +const { isPromise } = require('internal/util/types'); const { getSourceMapsSupport, } = require('internal/source_map/source_map_cache'); @@ -138,12 +139,11 @@ class ModuleJob extends ModuleJobBase { this.#loader = loader; // Expose the promise to the ModuleWrap directly for linking below. - if (isForRequireInImportedCJS) { - this.module = moduleOrModulePromise; - assert(this.module instanceof ModuleWrap); - this.modulePromise = PromiseResolve(this.module); - } else { + if (isPromise(moduleOrModulePromise)) { this.modulePromise = moduleOrModulePromise; + } else { + this.module = moduleOrModulePromise; + this.modulePromise = PromiseResolve(moduleOrModulePromise); } if (this.phase === kEvaluationPhase) { diff --git a/test/es-module/test-esm-error-cache.js b/test/es-module/test-esm-error-cache.js index d780f1a22164d7..e39ddb895acc1e 100644 --- a/test/es-module/test-esm-error-cache.js +++ b/test/es-module/test-esm-error-cache.js @@ -19,7 +19,9 @@ let error; await assert.rejects( () => import(file), (e) => { - assert.strictEqual(error, e); + // The module may be compiled again and a new SyntaxError would be thrown but + // with the same content. + assert.deepStrictEqual(error, e); return true; } ); diff --git a/test/es-module/test-esm-import-attributes-errors.js b/test/es-module/test-esm-import-attributes-errors.js index 63513a51e5abb5..9789484be986b3 100644 --- a/test/es-module/test-esm-import-attributes-errors.js +++ b/test/es-module/test-esm-import-attributes-errors.js @@ -28,7 +28,7 @@ async function test() { await rejects( import(jsModuleDataUrl, { with: { type: 'json', other: 'unsupported' } }), - { code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' } + { code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' } ); await rejects( @@ -48,7 +48,7 @@ async function test() { await rejects( import(jsonModuleDataUrl, { with: { foo: 'bar' } }), - { code: 'ERR_IMPORT_ATTRIBUTE_MISSING' } + { code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' } ); await rejects( diff --git a/test/es-module/test-esm-import-attributes-errors.mjs b/test/es-module/test-esm-import-attributes-errors.mjs index d7ffc6f92c99ea..8095662b104135 100644 --- a/test/es-module/test-esm-import-attributes-errors.mjs +++ b/test/es-module/test-esm-import-attributes-errors.mjs @@ -23,7 +23,7 @@ await rejects( await rejects( import(jsModuleDataUrl, { with: { type: 'json', other: 'unsupported' } }), - { code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' } + { code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' } ); await rejects( @@ -43,7 +43,7 @@ await rejects( await rejects( import(jsonModuleDataUrl, { with: { foo: 'bar' } }), - { code: 'ERR_IMPORT_ATTRIBUTE_MISSING' } + { code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' } ); await rejects( From 1784c35a49f2bf57425b492b290d5b8541b88b8c Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Wed, 20 Aug 2025 14:57:45 -0300 Subject: [PATCH 072/111] doc: add security incident reponse plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59470 Reviewed-By: Luigi Pinca Reviewed-By: Ulises Gascón --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index d5cc79095371e3..0c7c1c3c3d313f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -284,3 +284,8 @@ Security notifications will be distributed via the following methods. If you have suggestions on how this process could be improved, please visit the [nodejs/security-wg](https://github.com/nodejs/security-wg) repository. + +## Incident Response Plan + +In the event of a security incident, please refer to the +[Security Incident Response Plan](https://github.com/nodejs/security-wg/blob/main/INCIDENT_RESPONSE_PLAN.md). From fe86bc6da8ac793e56165129346a4717e324ccb1 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 20 Aug 2025 20:24:04 +0200 Subject: [PATCH 073/111] test: fix `test-setproctitle` status when `ps` is not available PR-URL: https://github.com/nodejs/node/pull/59523 Reviewed-By: Luigi Pinca Reviewed-By: Yagiz Nizipli --- test/parallel/test-setproctitle.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/parallel/test-setproctitle.js b/test/parallel/test-setproctitle.js index b08302e0a35ac0..368bc85800a9a9 100644 --- a/test/parallel/test-setproctitle.js +++ b/test/parallel/test-setproctitle.js @@ -4,7 +4,7 @@ const common = require('../common'); const { isMainThread } = require('worker_threads'); // FIXME add sunos support -if (common.isSunOS || common.isIBMi) { +if (common.isSunOS || common.isIBMi || common.isWindows) { common.skip(`Unsupported platform [${process.platform}]`); } @@ -25,15 +25,10 @@ assert.notStrictEqual(process.title, title); process.title = title; assert.strictEqual(process.title, title); -// Test setting the title but do not try to run `ps` on Windows. -if (common.isWindows) { - common.skip('Windows does not have "ps" utility'); -} - try { execSync('command -v ps'); } catch (err) { - if (err.status === 1) { + if (err.status === 1 || err.status === 127) { common.skip('The "ps" utility is not available'); } throw err; @@ -53,5 +48,5 @@ exec(cmd, common.mustSucceed((stdout, stderr) => { title += ` (${path.basename(process.execPath)})`; // Omitting trailing whitespace and \n - assert.strictEqual(stdout.replace(/\s+$/, '').endsWith(title), true); + assert.ok(stdout.trimEnd().endsWith(title)); })); From 99da7fbe11bb2e9e7b1ac6ebde3553372af0e475 Mon Sep 17 00:00:00 2001 From: Pietro Marchini Date: Wed, 20 Aug 2025 21:33:00 +0200 Subject: [PATCH 074/111] tools: avoid parsing test files twice PR-URL: https://github.com/nodejs/node/pull/59526 Reviewed-By: Antoine du Hamel Reviewed-By: Marco Ippolito --- tools/test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/test.py b/tools/test.py index e27222af3ac519..59801e1b07498b 100755 --- a/tools/test.py +++ b/tools/test.py @@ -788,6 +788,11 @@ def Execute(args, context, timeout=None, env=None, disable_core_files=False, for key, value in env.items(): env_copy[key] = value + # We append NODE_SKIP_FLAG_CHECK (ref: test/common/index.js) + # to avoid parsing the test files twice when looking for + # flags or environment variables defined via // Flags: and // Env: + env_copy["NODE_SKIP_FLAG_CHECK"] = "true" + preexec_fn = None def disableCoreFiles(): From ed339580af788b93acd62b1b9ea97bb09d2bcfae Mon Sep 17 00:00:00 2001 From: Pietro Marchini Date: Wed, 20 Aug 2025 21:50:01 +0200 Subject: [PATCH 075/111] test: lazy-load internalTTy PR-URL: https://github.com/nodejs/node/pull/59517 Reviewed-By: Moshe Atlow Reviewed-By: Antoine du Hamel Reviewed-By: Chemi Atlow --- test/parallel/test-runner-output.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-runner-output.mjs b/test/parallel/test-runner-output.mjs index ccfca45e502fd5..7169c28e293dbe 100644 --- a/test/parallel/test-runner-output.mjs +++ b/test/parallel/test-runner-output.mjs @@ -6,13 +6,13 @@ import { describe, it } from 'node:test'; import { hostname } from 'node:os'; import { chdir, cwd } from 'node:process'; import { fileURLToPath } from 'node:url'; -import internalTTy from 'internal/tty'; const skipForceColors = process.config.variables.icu_gyp_path !== 'tools/icu/icu-generic.gyp' || process.config.variables.node_shared_openssl; -const canColorize = internalTTy.getColorDepth() > 2; +// We're using dynamic import here to not break `NODE_REGENERATE_SNAPSHOTS`. +const canColorize = (await import('internal/tty')).default.getColorDepth() > 2; const skipCoverageColors = !canColorize; function replaceTestDuration(str) { From 8dc6f5b696bebe227f67033f4163b7f559a45d2d Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 20 Aug 2025 22:22:19 -0400 Subject: [PATCH 076/111] stream: add brotli support to CompressionStream and DecompressionStream Refs: https://github.com/whatwg/compression/issues/34 PR-URL: https://github.com/nodejs/node/pull/59464 Reviewed-By: Mattias Buelens Reviewed-By: Jason Zhang Reviewed-By: Ethan Arrowood --- doc/api/globals.md | 6 ++++++ doc/api/webstreams.md | 10 ++++++++-- lib/internal/webstreams/compression.js | 11 +++++++++-- test/parallel/test-whatwg-webstreams-compression.js | 2 +- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/doc/api/globals.md b/doc/api/globals.md index 7d272e127cecfa..9d146b2e97dd2e 100644 --- a/doc/api/globals.md +++ b/doc/api/globals.md @@ -330,6 +330,9 @@ with the [`--no-experimental-websocket`][] CLI flag. -* `format` {string} One of `'deflate'`, `'deflate-raw'`, or `'gzip'`. +* `format` {string} One of `'deflate'`, `'deflate-raw'`, `'gzip'`, or `'brotli'`. #### `compressionStream.readable` @@ -1520,6 +1523,9 @@ changes: -* `format` {string} One of `'deflate'`, `'deflate-raw'`, or `'gzip'`. +* `format` {string} One of `'deflate'`, `'deflate-raw'`, `'gzip'`, or `'brotli'`. #### `decompressionStream.readable` diff --git a/lib/internal/webstreams/compression.js b/lib/internal/webstreams/compression.js index b33b7134096c8b..836c02f7341f70 100644 --- a/lib/internal/webstreams/compression.js +++ b/lib/internal/webstreams/compression.js @@ -28,6 +28,7 @@ const formatConverter = createEnumConverter('CompressionFormat', [ 'deflate', 'deflate-raw', 'gzip', + 'brotli', ]); /** @@ -40,7 +41,7 @@ class CompressionStream { #transform; /** - * @param {'deflate'|'deflate-raw'|'gzip'} format + * @param {'deflate'|'deflate-raw'|'gzip'|'brotli'} format */ constructor(format) { format = formatConverter(format, { @@ -57,6 +58,9 @@ class CompressionStream { case 'gzip': this.#handle = lazyZlib().createGzip(); break; + case 'brotli': + this.#handle = lazyZlib().createBrotliCompress(); + break; } this.#transform = newReadableWritablePairFromDuplex(this.#handle); } @@ -90,7 +94,7 @@ class DecompressionStream { #transform; /** - * @param {'deflate'|'deflate-raw'|'gzip'} format + * @param {'deflate'|'deflate-raw'|'gzip'|'brotli'} format */ constructor(format) { format = formatConverter(format, { @@ -111,6 +115,9 @@ class DecompressionStream { rejectGarbageAfterEnd: true, }); break; + case 'brotli': + this.#handle = lazyZlib().createBrotliDecompress(); + break; } this.#transform = newReadableWritablePairFromDuplex(this.#handle); diff --git a/test/parallel/test-whatwg-webstreams-compression.js b/test/parallel/test-whatwg-webstreams-compression.js index c144a0a2e3d43d..bf87696eed1b2f 100644 --- a/test/parallel/test-whatwg-webstreams-compression.js +++ b/test/parallel/test-whatwg-webstreams-compression.js @@ -41,7 +41,7 @@ async function test(format) { ]); } -Promise.all(['gzip', 'deflate', 'deflate-raw'].map((i) => test(i))).then(common.mustCall()); +Promise.all(['gzip', 'deflate', 'deflate-raw', 'brotli'].map((i) => test(i))).then(common.mustCall()); [1, 'hello', false, {}].forEach((i) => { assert.throws(() => new CompressionStream(i), { From dafee05358f3d3c4890a7cb0c670169f0e8889f8 Mon Sep 17 00:00:00 2001 From: Tim Perry <1526883+pimterry@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:18:10 +0100 Subject: [PATCH 077/111] http2: add support for raw header arrays in h2Stream.respond() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59455 Reviewed-By: Ethan Arrowood Reviewed-By: Gerhard Stöbich --- doc/api/http2.md | 11 ++- lib/internal/http2/core.js | 99 +++++++++++++++---- lib/internal/http2/util.js | 7 +- .../test-http2-raw-headers-defaults.js | 76 ++++++++++++++ test/parallel/test-http2-raw-headers.js | 46 ++++++--- 5 files changed, 204 insertions(+), 35 deletions(-) create mode 100644 test/parallel/test-http2-raw-headers-defaults.js diff --git a/doc/api/http2.md b/doc/api/http2.md index 084bf1d3a2d3eb..bacf6653d970f9 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -1080,6 +1080,11 @@ changes: pr-url: https://github.com/nodejs/node/pull/58313 description: Following the deprecation of priority signaling as of RFC 9113, `weight` option is deprecated. + - version: + - v24.0.0 + - v22.17.0 + pr-url: https://github.com/nodejs/node/pull/57917 + description: Allow passing headers in raw array format. --> * `headers` {HTTP/2 Headers Object|Array} @@ -1850,6 +1855,10 @@ and will throw an error. -* `headers` {HTTP/2 Headers Object} +* `headers` {HTTP/2 Headers Object|Array} * `options` {Object} * `endStream` {boolean} Set to `true` to indicate that the response will not include payload data. diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index b1a424279b4e2d..4462fa639317f0 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -2541,8 +2541,31 @@ function callStreamClose(stream) { stream.close(); } -function processHeaders(oldHeaders, options) { - assertIsObject(oldHeaders, 'headers'); +function prepareResponseHeaders(stream, headersParam, options) { + let headers; + let statusCode; + + if (ArrayIsArray(headersParam)) { + ({ + headers, + statusCode, + } = prepareResponseHeadersArray(headersParam, options)); + stream[kRawHeaders] = headers; + } else { + ({ + headers, + statusCode, + } = prepareResponseHeadersObject(headersParam, options)); + stream[kSentHeaders] = headers; + } + + const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse); + + return { headers, headersList, statusCode }; +} + +function prepareResponseHeadersObject(oldHeaders, options) { + assertIsObject(oldHeaders, 'headers', ['Object', 'Array']); const headers = { __proto__: null }; if (oldHeaders !== null && oldHeaders !== undefined) { @@ -2563,6 +2586,44 @@ function processHeaders(oldHeaders, options) { headers[HTTP2_HEADER_DATE] ??= utcDate(); } + validatePreparedResponseHeaders(headers, statusCode); + + return { + headers, + statusCode: headers[HTTP2_HEADER_STATUS], + }; +} + +function prepareResponseHeadersArray(headers, options) { + let statusCode; + let isDateSet = false; + + for (let i = 0; i < headers.length; i += 2) { + const header = headers[i].toLowerCase(); + const value = headers[i + 1]; + + if (header === HTTP2_HEADER_STATUS) { + statusCode = value | 0; + } else if (header === HTTP2_HEADER_DATE) { + isDateSet = true; + } + } + + if (!statusCode) { + statusCode = HTTP_STATUS_OK; + headers.unshift(HTTP2_HEADER_STATUS, statusCode); + } + + if (!isDateSet && (options.sendDate == null || options.sendDate)) { + headers.push(HTTP2_HEADER_DATE, utcDate()); + } + + validatePreparedResponseHeaders(headers, statusCode); + + return { headers, statusCode }; +} + +function validatePreparedResponseHeaders(headers, statusCode) { // This is intentionally stricter than the HTTP/1 implementation, which // allows values between 100 and 999 (inclusive) in order to allow for // backwards compatibility with non-spec compliant code. With HTTP/2, @@ -2570,16 +2631,13 @@ function processHeaders(oldHeaders, options) { // This will have an impact on the compatibility layer for anyone using // non-standard, non-compliant status codes. if (statusCode < 200 || statusCode > 599) - throw new ERR_HTTP2_STATUS_INVALID(headers[HTTP2_HEADER_STATUS]); + throw new ERR_HTTP2_STATUS_INVALID(statusCode); const neverIndex = headers[kSensitiveHeaders]; if (neverIndex !== undefined && !ArrayIsArray(neverIndex)) throw new ERR_INVALID_ARG_VALUE('headers[http2.neverIndex]', neverIndex); - - return headers; } - function onFileUnpipe() { const stream = this.sink[kOwner]; if (stream.ownsFd) @@ -2882,7 +2940,7 @@ class ServerHttp2Stream extends Http2Stream { } // Initiate a response on this Http2Stream - respond(headers, options) { + respond(headersParam, options) { if (this.destroyed || this.closed) throw new ERR_HTTP2_INVALID_STREAM(); if (this.headersSent) @@ -2907,15 +2965,16 @@ class ServerHttp2Stream extends Http2Stream { state.flags |= STREAM_FLAGS_HAS_TRAILERS; } - headers = processHeaders(headers, options); - const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse); - this[kSentHeaders] = headers; + const { + headers, + headersList, + statusCode, + } = prepareResponseHeaders(this, headersParam, options); state.flags |= STREAM_FLAGS_HEADERS_SENT; // Close the writable side if the endStream option is set or status // is one of known codes with no payload, or it's a head request - const statusCode = headers[HTTP2_HEADER_STATUS] | 0; if (!!options.endStream || statusCode === HTTP_STATUS_NO_CONTENT || statusCode === HTTP_STATUS_RESET_CONTENT || @@ -2945,7 +3004,7 @@ class ServerHttp2Stream extends Http2Stream { // regular file, here the fd is passed directly. If the underlying // mechanism is not able to read from the fd, then the stream will be // reset with an error code. - respondWithFD(fd, headers, options) { + respondWithFD(fd, headersParam, options) { if (this.destroyed || this.closed) throw new ERR_HTTP2_INVALID_STREAM(); if (this.headersSent) @@ -2982,8 +3041,11 @@ class ServerHttp2Stream extends Http2Stream { this[kUpdateTimer](); this.ownsFd = false; - headers = processHeaders(headers, options); - const statusCode = headers[HTTP2_HEADER_STATUS] |= 0; + const { + headers, + statusCode, + } = prepareResponseHeadersObject(headersParam, options); + // Payload/DATA frames are not permitted in these cases if (statusCode === HTTP_STATUS_NO_CONTENT || statusCode === HTTP_STATUS_RESET_CONTENT || @@ -3011,7 +3073,7 @@ class ServerHttp2Stream extends Http2Stream { // giving the user an opportunity to verify the details and set additional // headers. If statCheck returns false, the operation is aborted and no // file details are sent. - respondWithFile(path, headers, options) { + respondWithFile(path, headersParam, options) { if (this.destroyed || this.closed) throw new ERR_HTTP2_INVALID_STREAM(); if (this.headersSent) @@ -3042,8 +3104,11 @@ class ServerHttp2Stream extends Http2Stream { this[kUpdateTimer](); this.ownsFd = true; - headers = processHeaders(headers, options); - const statusCode = headers[HTTP2_HEADER_STATUS] |= 0; + const { + headers, + statusCode, + } = prepareResponseHeadersObject(headersParam, options); + // Payload/DATA frames are not permitted in these cases if (statusCode === HTTP_STATUS_NO_CONTENT || statusCode === HTTP_STATUS_RESET_CONTENT || diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js index 19cbc08f8a9c7d..77e2386c1bf4aa 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -690,7 +690,6 @@ function prepareRequestHeadersArray(headers, session) { const headersList = buildNgHeaderString( rawHeaders, assertValidPseudoHeader, - headers[kSensitiveHeaders], ); return { @@ -755,14 +754,14 @@ const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE); * @returns {[string, number]} */ function buildNgHeaderString(arrayOrMap, - assertValuePseudoHeader = assertValidPseudoHeader, - sensitiveHeaders = arrayOrMap[kSensitiveHeaders]) { + assertValuePseudoHeader = assertValidPseudoHeader) { let headers = ''; let pseudoHeaders = ''; let count = 0; const singles = new SafeSet(); - const neverIndex = (sensitiveHeaders || emptyArray).map((v) => v.toLowerCase()); + const sensitiveHeaders = arrayOrMap[kSensitiveHeaders] || emptyArray; + const neverIndex = sensitiveHeaders.map((v) => v.toLowerCase()); function processHeader(key, value) { key = key.toLowerCase(); diff --git a/test/parallel/test-http2-raw-headers-defaults.js b/test/parallel/test-http2-raw-headers-defaults.js new file mode 100644 index 00000000000000..fa742605a02a9a --- /dev/null +++ b/test/parallel/test-http2-raw-headers-defaults.js @@ -0,0 +1,76 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +{ + const server = http2.createServer(); + server.on('stream', common.mustCall((stream, _headers, _flags, rawHeaders) => { + assert.deepStrictEqual(rawHeaders, [ + ':method', 'GET', + ':authority', `localhost:${server.address().port}`, + ':scheme', 'http', + ':path', '/', + 'a', 'b', + 'x-foo', 'bar', // Lowercased as required for HTTP/2 + 'a', 'c', // Duplicate header order preserved + ]); + stream.respond([ + 'x', '1', + 'x-FOO', 'bar', + 'x', '2', + ]); + + assert.partialDeepStrictEqual(stream.sentHeaders, { + '__proto__': null, + ':status': 200, + 'x': [ '1', '2' ], + 'x-FOO': 'bar', + }); + + assert.strictEqual(typeof stream.sentHeaders.date, 'string'); + + stream.end(); + })); + + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const req = client.request([ + 'a', 'b', + 'x-FOO', 'bar', + 'a', 'c', + ]).end(); + + assert.deepStrictEqual(req.sentHeaders, { + '__proto__': null, + ':path': '/', + ':scheme': 'http', + ':authority': `localhost:${server.address().port}`, + ':method': 'GET', + 'a': [ 'b', 'c' ], + 'x-FOO': 'bar', + }); + + req.on('response', common.mustCall((_headers, _flags, rawHeaders) => { + assert.strictEqual(rawHeaders.length, 10); + assert.deepStrictEqual(rawHeaders.slice(0, 8), [ + ':status', '200', + 'x', '1', + 'x-foo', 'bar', // Lowercased as required for HTTP/2 + 'x', '2', // Duplicate header order preserved + ]); + + assert.strictEqual(rawHeaders[8], 'date'); + assert.strictEqual(typeof rawHeaders[9], 'string'); + + client.close(); + server.close(); + })); + })); +} diff --git a/test/parallel/test-http2-raw-headers.js b/test/parallel/test-http2-raw-headers.js index 8a84542a130fae..a77fe2db515962 100644 --- a/test/parallel/test-http2-raw-headers.js +++ b/test/parallel/test-http2-raw-headers.js @@ -8,19 +8,33 @@ const http2 = require('http2'); { const server = http2.createServer(); - server.on('stream', common.mustCall((stream, headers, flags, rawHeaders) => { + server.on('stream', common.mustCall((stream, _headers, _flags, rawHeaders) => { assert.deepStrictEqual(rawHeaders, [ ':path', '/foobar', ':scheme', 'http', - ':authority', `localhost:${server.address().port}`, - ':method', 'GET', + ':authority', `test.invalid:${server.address().port}`, + ':method', 'POST', 'a', 'b', - 'x-foo', 'bar', - 'a', 'c', + 'x-foo', 'bar', // Lowercased as required for HTTP/2 + 'a', 'c', // Duplicate header order preserved + ]); + + stream.respond([ + ':status', '404', + 'x', '1', + 'x-FOO', 'bar', + 'x', '2', + 'DATE', '0000', ]); - stream.respond({ - ':status': 200 + + assert.deepStrictEqual(stream.sentHeaders, { + '__proto__': null, + ':status': '404', + 'x': [ '1', '2' ], + 'x-FOO': 'bar', + 'DATE': '0000', }); + stream.end(); })); @@ -32,8 +46,8 @@ const http2 = require('http2'); const req = client.request([ ':path', '/foobar', ':scheme', 'http', - ':authority', `localhost:${server.address().port}`, - ':method', 'GET', + ':authority', `test.invalid:${server.address().port}`, + ':method', 'POST', 'a', 'b', 'x-FOO', 'bar', 'a', 'c', @@ -43,14 +57,20 @@ const http2 = require('http2'); '__proto__': null, ':path': '/foobar', ':scheme': 'http', - ':authority': `localhost:${server.address().port}`, - ':method': 'GET', + ':authority': `test.invalid:${server.address().port}`, + ':method': 'POST', 'a': [ 'b', 'c' ], 'x-FOO': 'bar', }); - req.on('response', common.mustCall((headers) => { - assert.strictEqual(headers[':status'], 200); + req.on('response', common.mustCall((_headers, _flags, rawHeaders) => { + assert.deepStrictEqual(rawHeaders, [ + ':status', '404', + 'x', '1', + 'x-foo', 'bar', // Lowercased as required for HTTP/2 + 'x', '2', // Duplicate header order preserved + 'date', '0000', // Server doesn't automatically set its own value + ]); client.close(); server.close(); })); From 0821b446fbe2e06820edc1cc690f03489d6ce6bd Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Thu, 21 Aug 2025 14:54:51 +0100 Subject: [PATCH 078/111] deps: update undici to 7.14.0 PR-URL: https://github.com/nodejs/node/pull/59507 Reviewed-By: Matteo Collina Reviewed-By: Matthew Aitken Reviewed-By: Trivikram Kamat Reviewed-By: Rafael Gonzaga Reviewed-By: Richard Lau --- deps/undici/src/README.md | 10 +- .../src/docs/docs/api/DiagnosticsChannel.md | 26 +- deps/undici/src/lib/dispatcher/proxy-agent.js | 3 +- deps/undici/src/lib/handler/cache-handler.js | 26 +- deps/undici/src/lib/interceptor/cache.js | 4 +- deps/undici/src/lib/llhttp/wasm_build_env.txt | 2 +- deps/undici/src/lib/mock/snapshot-agent.js | 132 +- deps/undici/src/lib/mock/snapshot-recorder.js | 445 ++-- deps/undici/src/lib/mock/snapshot-utils.js | 158 ++ deps/undici/src/lib/util/cache.js | 6 +- deps/undici/src/lib/web/cache/cache.js | 8 +- .../src/lib/web/eventsource/eventsource.js | 19 +- deps/undici/src/lib/web/fetch/formdata.js | 2 +- deps/undici/src/lib/web/fetch/response.js | 12 +- .../web/websocket/stream/websocketstream.js | 4 +- .../undici/src/lib/web/websocket/websocket.js | 15 +- deps/undici/src/package-lock.json | 2064 ++++++++--------- deps/undici/src/package.json | 10 +- deps/undici/src/scripts/generate-pem.js | 2 +- deps/undici/src/types/eventsource.d.ts | 7 +- deps/undici/src/types/index.d.ts | 5 +- deps/undici/undici.js | 65 +- src/undici_version.h | 2 +- 23 files changed, 1653 insertions(+), 1374 deletions(-) create mode 100644 deps/undici/src/lib/mock/snapshot-utils.js diff --git a/deps/undici/src/README.md b/deps/undici/src/README.md index 93008eb09918d9..eb69c0ca8f4a5a 100644 --- a/deps/undici/src/README.md +++ b/deps/undici/src/README.md @@ -622,11 +622,11 @@ and `undici.Agent`) which will enable the family autoselection algorithm when es Undici aligns with the Node.js LTS schedule. The following table shows the supported versions: -| Version | Node.js | End of Life | -|---------|-------------|-------------| -| 5.x | v18.x | 2024-04-30 | -| 6.x | v20.x v22.x | 2026-04-30 | -| 7.x | v24.x | 2027-04-30 | +| Undici Version | Bundled in Node.js | Node.js Versions Supported | End of Life | +|----------------|-------------------|----------------------------|-------------| +| 5.x | 18.x | ≥14.0 (tested: 14, 16, 18) | 2024-04-30 | +| 6.x | 20.x, 22.x | ≥18.17 (tested: 18, 20, 21, 22) | 2026-04-30 | +| 7.x | 24.x | ≥20.18.1 (tested: 20, 22, 24) | 2027-04-30 | ## License diff --git a/deps/undici/src/docs/docs/api/DiagnosticsChannel.md b/deps/undici/src/docs/docs/api/DiagnosticsChannel.md index dab13a7df6edb3..096bd58ce29882 100644 --- a/deps/undici/src/docs/docs/api/DiagnosticsChannel.md +++ b/deps/undici/src/docs/docs/api/DiagnosticsChannel.md @@ -169,14 +169,38 @@ This message is published after the client has successfully connected to a serve ```js import diagnosticsChannel from 'diagnostics_channel' -diagnosticsChannel.channel('undici:websocket:open').subscribe(({ address, protocol, extensions, websocket }) => { +diagnosticsChannel.channel('undici:websocket:open').subscribe(({ + address, // { address: string, family: string, port: number } + protocol, // string - negotiated subprotocol + extensions, // string - negotiated extensions + websocket, // WebSocket - the WebSocket instance + handshakeResponse // object - HTTP response that upgraded the connection +}) => { console.log(address) // address, family, and port console.log(protocol) // negotiated subprotocols console.log(extensions) // negotiated extensions console.log(websocket) // the WebSocket instance + + // Handshake response details + console.log(handshakeResponse.status) // 101 for successful WebSocket upgrade + console.log(handshakeResponse.statusText) // 'Switching Protocols' + console.log(handshakeResponse.headers) // Object containing response headers }) ``` +### Handshake Response Object + +The `handshakeResponse` object contains the HTTP response that upgraded the connection to WebSocket: + +- `status` (number): The HTTP status code (101 for successful WebSocket upgrade) +- `statusText` (string): The HTTP status message ('Switching Protocols' for successful upgrade) +- `headers` (object): The HTTP response headers from the server, including: + - `upgrade: 'websocket'` + - `connection: 'upgrade'` + - `sec-websocket-accept` and other WebSocket-related headers + +This information is particularly useful for debugging and monitoring WebSocket connections, as it provides access to the initial HTTP handshake response that established the WebSocket connection. + ## `undici:websocket:close` This message is published after the connection has closed. diff --git a/deps/undici/src/lib/dispatcher/proxy-agent.js b/deps/undici/src/lib/dispatcher/proxy-agent.js index 139ae6d17274d5..f0a71f7adbfb75 100644 --- a/deps/undici/src/lib/dispatcher/proxy-agent.js +++ b/deps/undici/src/lib/dispatcher/proxy-agent.js @@ -1,7 +1,6 @@ 'use strict' const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols') -const { URL } = require('node:url') const Agent = require('./agent') const Pool = require('./pool') const DispatcherBase = require('./dispatcher-base') @@ -208,7 +207,7 @@ class ProxyAgent extends DispatcherBase { } /** - * @param {import('../types/proxy-agent').ProxyAgent.Options | string | URL} opts + * @param {import('../../types/proxy-agent').ProxyAgent.Options | string | URL} opts * @returns {URL} */ #getUrl (opts) { diff --git a/deps/undici/src/lib/handler/cache-handler.js b/deps/undici/src/lib/handler/cache-handler.js index 937790aca12292..c21a7206551660 100644 --- a/deps/undici/src/lib/handler/cache-handler.js +++ b/deps/undici/src/lib/handler/cache-handler.js @@ -15,6 +15,15 @@ const HEURISTICALLY_CACHEABLE_STATUS_CODES = [ 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501 ] +// Status codes which semantic is not handled by the cache +// https://datatracker.ietf.org/doc/html/rfc9111#section-3 +// This list should not grow beyond 206 and 304 unless the RFC is updated +// by a newer one including more. Please introduce another list if +// implementing caching of responses with the 'must-understand' directive. +const NOT_UNDERSTOOD_STATUS_CODES = [ + 206, 304 +] + const MAX_RESPONSE_AGE = 2147483647000 /** @@ -241,10 +250,19 @@ class CacheHandler { * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives */ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) { - // Allow caching for status codes 200 and 307 (original behavior) - // Also allow caching for other status codes that are heuristically cacheable - // when they have explicit cache directives - if (statusCode !== 200 && statusCode !== 307 && !HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode)) { + // Status code must be final and understood. + if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) { + return false + } + // Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching + // directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3 + if (!HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) && !resHeaders['expires'] && + !cacheControlDirectives.public && + cacheControlDirectives['max-age'] === undefined && + // RFC 9111: a private response directive, if the cache is not shared + !(cacheControlDirectives.private && cacheType === 'private') && + !(cacheControlDirectives['s-maxage'] !== undefined && cacheType === 'shared') + ) { return false } diff --git a/deps/undici/src/lib/interceptor/cache.js b/deps/undici/src/lib/interceptor/cache.js index a2f235b8db34e4..6565baf0a51014 100644 --- a/deps/undici/src/lib/interceptor/cache.js +++ b/deps/undici/src/lib/interceptor/cache.js @@ -6,7 +6,7 @@ const util = require('../core/util') const CacheHandler = require('../handler/cache-handler') const MemoryCacheStore = require('../cache/memory-cache-store') const CacheRevalidationHandler = require('../handler/cache-revalidation-handler') -const { assertCacheStore, assertCacheMethods, makeCacheKey, normaliseHeaders, parseCacheControlHeader } = require('../util/cache.js') +const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = require('../util/cache.js') const { AbortError } = require('../core/errors.js') /** @@ -326,7 +326,7 @@ module.exports = (opts = {}) => { opts = { ...opts, - headers: normaliseHeaders(opts) + headers: normalizeHeaders(opts) } const reqCacheControl = opts.headers?.['cache-control'] diff --git a/deps/undici/src/lib/llhttp/wasm_build_env.txt b/deps/undici/src/lib/llhttp/wasm_build_env.txt index 77e7ad40adc06a..ecb13a031dc5c8 100644 --- a/deps/undici/src/lib/llhttp/wasm_build_env.txt +++ b/deps/undici/src/lib/llhttp/wasm_build_env.txt @@ -1,5 +1,5 @@ -> undici@7.13.0 build:wasm +> undici@7.14.0 build:wasm > node build/wasm.js --docker > docker run --rm --platform=linux/x86_64 --user 1001:118 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/build/lib/llhttp --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/build,target=/home/node/build/build --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/deps,target=/home/node/build/deps -t ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970 node build/wasm.js diff --git a/deps/undici/src/lib/mock/snapshot-agent.js b/deps/undici/src/lib/mock/snapshot-agent.js index abdd8227e1e367..dbe53575f1d4e8 100644 --- a/deps/undici/src/lib/mock/snapshot-agent.js +++ b/deps/undici/src/lib/mock/snapshot-agent.js @@ -5,6 +5,7 @@ const MockAgent = require('./mock-agent') const { SnapshotRecorder } = require('./snapshot-recorder') const WrapHandler = require('../handler/wrap-handler') const { InvalidArgumentError, UndiciError } = require('../core/errors') +const { validateSnapshotMode } = require('./snapshot-utils') const kSnapshotRecorder = Symbol('kSnapshotRecorder') const kSnapshotMode = Symbol('kSnapshotMode') @@ -12,7 +13,7 @@ const kSnapshotPath = Symbol('kSnapshotPath') const kSnapshotLoaded = Symbol('kSnapshotLoaded') const kRealAgent = Symbol('kRealAgent') -// Static flag to ensure warning is only emitted once +// Static flag to ensure warning is only emitted once per process let warningEmitted = false class SnapshotAgent extends MockAgent { @@ -26,26 +27,24 @@ class SnapshotAgent extends MockAgent { warningEmitted = true } - const mockOptions = { ...opts } - delete mockOptions.mode - delete mockOptions.snapshotPath + const { + mode = 'record', + snapshotPath = null, + ...mockAgentOpts + } = opts - super(mockOptions) + super(mockAgentOpts) - // Validate mode option - const validModes = ['record', 'playback', 'update'] - const mode = opts.mode || 'record' - if (!validModes.includes(mode)) { - throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validModes.join(', ')}`) - } + validateSnapshotMode(mode) // Validate snapshotPath is provided when required - if ((mode === 'playback' || mode === 'update') && !opts.snapshotPath) { + if ((mode === 'playback' || mode === 'update') && !snapshotPath) { throw new InvalidArgumentError(`snapshotPath is required when mode is '${mode}'`) } this[kSnapshotMode] = mode - this[kSnapshotPath] = opts.snapshotPath + this[kSnapshotPath] = snapshotPath + this[kSnapshotRecorder] = new SnapshotRecorder({ snapshotPath: this[kSnapshotPath], mode: this[kSnapshotMode], @@ -85,7 +84,7 @@ class SnapshotAgent extends MockAgent { // Ensure snapshots are loaded if (!this[kSnapshotLoaded]) { // Need to load asynchronously, delegate to async version - return this._asyncDispatch(opts, handler) + return this.#asyncDispatch(opts, handler) } // Try to find existing snapshot (synchronous) @@ -93,10 +92,10 @@ class SnapshotAgent extends MockAgent { if (snapshot) { // Use recorded response (synchronous) - return this._replaySnapshot(snapshot, handler) + return this.#replaySnapshot(snapshot, handler) } else if (mode === 'update') { // Make real request and record it (async required) - return this._recordAndReplay(opts, handler) + return this.#recordAndReplay(opts, handler) } else { // Playback mode but no snapshot found const error = new UndiciError(`No snapshot found for ${opts.method || 'GET'} ${opts.path}`) @@ -108,16 +107,14 @@ class SnapshotAgent extends MockAgent { } } else if (mode === 'record') { // Record mode - make real request and save response (async required) - return this._recordAndReplay(opts, handler) - } else { - throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be 'record', 'playback', or 'update'`) + return this.#recordAndReplay(opts, handler) } } /** * Async version of dispatch for when we need to load snapshots first */ - async _asyncDispatch (opts, handler) { + async #asyncDispatch (opts, handler) { await this.loadSnapshots() return this.dispatch(opts, handler) } @@ -125,7 +122,7 @@ class SnapshotAgent extends MockAgent { /** * Records a real request and replays the response */ - _recordAndReplay (opts, handler) { + #recordAndReplay (opts, handler) { const responseData = { statusCode: null, headers: {}, @@ -180,45 +177,46 @@ class SnapshotAgent extends MockAgent { /** * Replays a recorded response + * + * @param {Object} snapshot - The recorded snapshot to replay. + * @param {Object} handler - The handler to call with the response data. + * @returns {void} */ - _replaySnapshot (snapshot, handler) { - return new Promise((resolve) => { - // Simulate the response - setImmediate(() => { - try { - const { response } = snapshot - - const controller = { - pause () {}, - resume () {}, - abort (reason) { - this.aborted = true - this.reason = reason - }, - - aborted: false, - paused: false - } - - handler.onRequestStart(controller) - - handler.onResponseStart(controller, response.statusCode, response.headers) - - // Body is always stored as base64 string - const body = Buffer.from(response.body, 'base64') - handler.onResponseData(controller, body) - - handler.onResponseEnd(controller, response.trailers) - resolve() - } catch (error) { - handler.onError?.(error) - } - }) - }) + #replaySnapshot (snapshot, handler) { + try { + const { response } = snapshot + + const controller = { + pause () { }, + resume () { }, + abort (reason) { + this.aborted = true + this.reason = reason + }, + + aborted: false, + paused: false + } + + handler.onRequestStart(controller) + + handler.onResponseStart(controller, response.statusCode, response.headers) + + // Body is always stored as base64 string + const body = Buffer.from(response.body, 'base64') + handler.onResponseData(controller, body) + + handler.onResponseEnd(controller, response.trailers) + } catch (error) { + handler.onError?.(error) + } } /** * Loads snapshots from file + * + * @param {string} [filePath] - Optional file path to load snapshots from. + * @returns {Promise} - Resolves when snapshots are loaded. */ async loadSnapshots (filePath) { await this[kSnapshotRecorder].loadSnapshots(filePath || this[kSnapshotPath]) @@ -226,12 +224,15 @@ class SnapshotAgent extends MockAgent { // In playback mode, set up MockAgent interceptors for all snapshots if (this[kSnapshotMode] === 'playback') { - this._setupMockInterceptors() + this.#setupMockInterceptors() } } /** * Saves snapshots to file + * + * @param {string} [filePath] - Optional file path to save snapshots to. + * @returns {Promise} - Resolves when snapshots are saved. */ async saveSnapshots (filePath) { return this[kSnapshotRecorder].saveSnapshots(filePath || this[kSnapshotPath]) @@ -248,9 +249,9 @@ class SnapshotAgent extends MockAgent { * * Called automatically when loading snapshots in playback mode. * - * @private + * @returns {void} */ - _setupMockInterceptors () { + #setupMockInterceptors () { for (const snapshot of this[kSnapshotRecorder].getSnapshots()) { const { request, responses, response } = snapshot const url = new URL(request.url) @@ -275,6 +276,7 @@ class SnapshotAgent extends MockAgent { /** * Gets the snapshot recorder + * @return {SnapshotRecorder} - The snapshot recorder instance */ getRecorder () { return this[kSnapshotRecorder] @@ -282,6 +284,7 @@ class SnapshotAgent extends MockAgent { /** * Gets the current mode + * @return {import('./snapshot-utils').SnapshotMode} - The current snapshot mode */ getMode () { return this[kSnapshotMode] @@ -289,6 +292,7 @@ class SnapshotAgent extends MockAgent { /** * Clears all snapshots + * @returns {void} */ clearSnapshots () { this[kSnapshotRecorder].clear() @@ -296,6 +300,7 @@ class SnapshotAgent extends MockAgent { /** * Resets call counts for all snapshots (useful for test cleanup) + * @returns {void} */ resetCallCounts () { this[kSnapshotRecorder].resetCallCounts() @@ -303,6 +308,8 @@ class SnapshotAgent extends MockAgent { /** * Deletes a specific snapshot by request options + * @param {import('./snapshot-recorder').SnapshotRequestOptions} requestOpts - Request options to identify the snapshot + * @return {Promise} - Returns true if the snapshot was deleted, false if not found */ deleteSnapshot (requestOpts) { return this[kSnapshotRecorder].deleteSnapshot(requestOpts) @@ -310,6 +317,7 @@ class SnapshotAgent extends MockAgent { /** * Gets information about a specific snapshot + * @returns {import('./snapshot-recorder').SnapshotInfo|null} - Snapshot information or null if not found */ getSnapshotInfo (requestOpts) { return this[kSnapshotRecorder].getSnapshotInfo(requestOpts) @@ -317,13 +325,19 @@ class SnapshotAgent extends MockAgent { /** * Replaces all snapshots with new data (full replacement) + * @param {Array<{hash: string; snapshot: import('./snapshot-recorder').SnapshotEntryshotEntry}>|Record} snapshotData - New snapshot data to replace existing snapshots + * @returns {void} */ replaceSnapshots (snapshotData) { this[kSnapshotRecorder].replaceSnapshots(snapshotData) } + /** + * Closes the agent, saving snapshots and cleaning up resources. + * + * @returns {Promise} + */ async close () { - // Close recorder (saves snapshots and cleans up timers) await this[kSnapshotRecorder].close() await this[kRealAgent]?.close() await super.close() diff --git a/deps/undici/src/lib/mock/snapshot-recorder.js b/deps/undici/src/lib/mock/snapshot-recorder.js index 7482b5c1914a56..e810fe795072a7 100644 --- a/deps/undici/src/lib/mock/snapshot-recorder.js +++ b/deps/undici/src/lib/mock/snapshot-recorder.js @@ -2,13 +2,93 @@ const { writeFile, readFile, mkdir } = require('node:fs/promises') const { dirname, resolve } = require('node:path') +const { setTimeout, clearTimeout } = require('node:timers') const { InvalidArgumentError, UndiciError } = require('../core/errors') +const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require('./snapshot-utils') + +/** + * @typedef {Object} SnapshotRequestOptions + * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) + * @property {string} path - Request path + * @property {string} origin - Request origin (base URL) + * @property {import('./snapshot-utils').Headers|import('./snapshot-utils').UndiciHeaders} headers - Request headers + * @property {import('./snapshot-utils').NormalizedHeaders} _normalizedHeaders - Request headers as a lowercase object + * @property {string|Buffer} [body] - Request body (optional) + */ + +/** + * @typedef {Object} SnapshotEntryRequest + * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) + * @property {string} url - Full URL of the request + * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object + * @property {string|Buffer} [body] - Request body (optional) + */ + +/** + * @typedef {Object} SnapshotEntryResponse + * @property {number} statusCode - HTTP status code of the response + * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized response headers as a lowercase object + * @property {string} body - Response body as a base64url encoded string + * @property {Object} [trailers] - Optional response trailers + */ + +/** + * @typedef {Object} SnapshotEntry + * @property {SnapshotEntryRequest} request - The request object + * @property {Array} responses - Array of response objects + * @property {number} callCount - Number of times this snapshot has been called + * @property {string} timestamp - ISO timestamp of when the snapshot was created + */ + +/** + * @typedef {Object} SnapshotRecorderMatchOptions + * @property {Array} [matchHeaders=[]] - Headers to match (empty array means match all headers) + * @property {Array} [ignoreHeaders=[]] - Headers to ignore for matching + * @property {Array} [excludeHeaders=[]] - Headers to exclude from matching + * @property {boolean} [matchBody=true] - Whether to match request body + * @property {boolean} [matchQuery=true] - Whether to match query properties + * @property {boolean} [caseSensitive=false] - Whether header matching is case-sensitive + */ + +/** + * @typedef {Object} SnapshotRecorderOptions + * @property {string} [snapshotPath] - Path to save/load snapshots + * @property {import('./snapshot-utils').SnapshotMode} [mode='record'] - Mode: 'record' or 'playback' + * @property {number} [maxSnapshots=Infinity] - Maximum number of snapshots to keep + * @property {boolean} [autoFlush=false] - Whether to automatically flush snapshots to disk + * @property {number} [flushInterval=30000] - Auto-flush interval in milliseconds (default: 30 seconds) + * @property {Array} [excludeUrls=[]] - URLs to exclude from recording + * @property {function} [shouldRecord=null] - Function to filter requests for recording + * @property {function} [shouldPlayback=null] - Function to filter requests + */ + +/** + * @typedef {Object} SnapshotFormattedRequest + * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) + * @property {string} url - Full URL of the request (with query parameters if matchQuery is true) + * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object + * @property {string} body - Request body (optional, only if matchBody is true) + */ + +/** + * @typedef {Object} SnapshotInfo + * @property {string} hash - Hash key for the snapshot + * @property {SnapshotEntryRequest} request - The request object + * @property {number} responseCount - Number of responses recorded for this request + * @property {number} callCount - Number of times this snapshot has been called + * @property {string} timestamp - ISO timestamp of when the snapshot was created + */ /** * Formats a request for consistent snapshot storage * Caches normalized headers to avoid repeated processing + * + * @param {SnapshotRequestOptions} opts - Request options + * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached header sets for performance + * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers and body + * @returns {SnapshotFormattedRequest} - Formatted request object */ -function formatRequestKey (opts, cachedSets, matchOptions = {}) { +function formatRequestKey (opts, headerFilters, matchOptions = {}) { const url = new URL(opts.path, opts.origin) // Cache normalized headers if not already done @@ -20,37 +100,40 @@ function formatRequestKey (opts, cachedSets, matchOptions = {}) { return { method: opts.method || 'GET', url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`, - headers: filterHeadersForMatching(normalized, cachedSets, matchOptions), - body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : undefined + headers: filterHeadersForMatching(normalized, headerFilters, matchOptions), + body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : '' } } /** * Filters headers based on matching configuration + * + * @param {import('./snapshot-utils').Headers} headers - Headers to filter + * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers + * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers */ -function filterHeadersForMatching (headers, cachedSets, matchOptions = {}) { +function filterHeadersForMatching (headers, headerFilters, matchOptions = {}) { if (!headers || typeof headers !== 'object') return {} const { - matchHeaders = null, caseSensitive = false } = matchOptions const filtered = {} - const { ignoreSet, excludeSet, matchSet } = cachedSets + const { ignore, exclude, match } = headerFilters for (const [key, value] of Object.entries(headers)) { const headerKey = caseSensitive ? key : key.toLowerCase() // Skip if in exclude list (for security) - if (excludeSet.has(headerKey)) continue + if (exclude.has(headerKey)) continue // Skip if in ignore list (for matching) - if (ignoreSet.has(headerKey)) continue + if (ignore.has(headerKey)) continue // If matchHeaders is specified, only include those headers - if (matchHeaders && Array.isArray(matchHeaders)) { - if (!matchSet.has(headerKey)) continue + if (match.size !== 0) { + if (!match.has(headerKey)) continue } filtered[headerKey] = value @@ -61,17 +144,20 @@ function filterHeadersForMatching (headers, cachedSets, matchOptions = {}) { /** * Filters headers for storage (only excludes sensitive headers) + * + * @param {import('./snapshot-utils').Headers} headers - Headers to filter + * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers + * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers */ -function filterHeadersForStorage (headers, matchOptions = {}) { +function filterHeadersForStorage (headers, headerFilters, matchOptions = {}) { if (!headers || typeof headers !== 'object') return {} const { - excludeHeaders = [], caseSensitive = false } = matchOptions const filtered = {} - const excludeSet = new Set(excludeHeaders.map(h => caseSensitive ? h : h.toLowerCase())) + const { exclude: excludeSet } = headerFilters for (const [key, value] of Object.entries(headers)) { const headerKey = caseSensitive ? key : key.toLowerCase() @@ -86,106 +172,81 @@ function filterHeadersForStorage (headers, matchOptions = {}) { } /** - * Creates cached header sets for performance + * Creates a hash key for request matching + * Properly orders headers to avoid conflicts and uses crypto hashing when available + * + * @param {SnapshotFormattedRequest} formattedRequest - Request object + * @returns {string} - Base64url encoded hash of the request */ -function createHeaderSetsCache (matchOptions = {}) { - const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = null, caseSensitive = false } = matchOptions +function createRequestHash (formattedRequest) { + const parts = [ + formattedRequest.method, + formattedRequest.url + ] - return { - ignoreSet: new Set(ignoreHeaders.map(h => caseSensitive ? h : h.toLowerCase())), - excludeSet: new Set(excludeHeaders.map(h => caseSensitive ? h : h.toLowerCase())), - matchSet: matchHeaders && Array.isArray(matchHeaders) - ? new Set(matchHeaders.map(h => caseSensitive ? h : h.toLowerCase())) - : null - } -} + // Process headers in a deterministic way to avoid conflicts + if (formattedRequest.headers && typeof formattedRequest.headers === 'object') { + const headerKeys = Object.keys(formattedRequest.headers).sort() + for (const key of headerKeys) { + const values = Array.isArray(formattedRequest.headers[key]) + ? formattedRequest.headers[key] + : [formattedRequest.headers[key]] -/** - * Normalizes headers for consistent comparison - */ -function normalizeHeaders (headers) { - if (!headers) return {} - - const normalized = {} - - // Handle array format (undici internal format: [name, value, name, value, ...]) - if (Array.isArray(headers)) { - for (let i = 0; i < headers.length; i += 2) { - const key = headers[i] - const value = headers[i + 1] - if (key && value !== undefined) { - // Convert Buffers to strings if needed - const keyStr = Buffer.isBuffer(key) ? key.toString() : String(key) - const valueStr = Buffer.isBuffer(value) ? value.toString() : String(value) - normalized[keyStr.toLowerCase()] = valueStr - } - } - return normalized - } + // Add header name + parts.push(key) - // Handle object format - if (headers && typeof headers === 'object') { - for (const [key, value] of Object.entries(headers)) { - if (key && typeof key === 'string') { - normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value) + // Add all values for this header, sorted for consistency + for (const value of values.sort()) { + parts.push(String(value)) } } } - return normalized -} + // Add body + parts.push(formattedRequest.body) -/** - * Creates a hash key for request matching - */ -function createRequestHash (request) { - const parts = [ - request.method, - request.url, - JSON.stringify(request.headers, Object.keys(request.headers).sort()), - request.body || '' - ] - return Buffer.from(parts.join('|')).toString('base64url') -} + const content = parts.join('|') -/** - * Checks if a URL matches any of the exclude patterns - */ -function isUrlExcluded (url, excludePatterns = []) { - if (!excludePatterns.length) return false - - for (const pattern of excludePatterns) { - if (typeof pattern === 'string') { - // Simple string match (case-insensitive) - if (url.toLowerCase().includes(pattern.toLowerCase())) { - return true - } - } else if (pattern instanceof RegExp) { - // Regex pattern match - if (pattern.test(url)) { - return true - } - } - } - - return false + return hashId(content) } class SnapshotRecorder { + /** @type {NodeJS.Timeout | null} */ + #flushTimeout + + /** @type {import('./snapshot-utils').IsUrlExcluded} */ + #isUrlExcluded + + /** @type {Map} */ + #snapshots = new Map() + + /** @type {string|undefined} */ + #snapshotPath + + /** @type {number} */ + #maxSnapshots = Infinity + + /** @type {boolean} */ + #autoFlush = false + + /** @type {import('./snapshot-utils').HeaderFilters} */ + #headerFilters + + /** + * Creates a new SnapshotRecorder instance + * @param {SnapshotRecorderOptions&SnapshotRecorderMatchOptions} [options={}] - Configuration options for the recorder + */ constructor (options = {}) { - this.snapshots = new Map() - this.snapshotPath = options.snapshotPath - this.mode = options.mode || 'record' - this.loaded = false - this.maxSnapshots = options.maxSnapshots || Infinity - this.autoFlush = options.autoFlush || false + this.#snapshotPath = options.snapshotPath + this.#maxSnapshots = options.maxSnapshots || Infinity + this.#autoFlush = options.autoFlush || false this.flushInterval = options.flushInterval || 30000 // 30 seconds default this._flushTimer = null - this._flushTimeout = null // Matching configuration + /** @type {Required} */ this.matchOptions = { - matchHeaders: options.matchHeaders || null, // null means match all headers + matchHeaders: options.matchHeaders || [], // empty means match all headers ignoreHeaders: options.ignoreHeaders || [], excludeHeaders: options.excludeHeaders || [], matchBody: options.matchBody !== false, // default: true @@ -194,46 +255,49 @@ class SnapshotRecorder { } // Cache processed header sets to avoid recreating them on every request - this._headerSetsCache = createHeaderSetsCache(this.matchOptions) + this.#headerFilters = createHeaderFilters(this.matchOptions) // Request filtering callbacks - this.shouldRecord = options.shouldRecord || null // function(requestOpts) -> boolean - this.shouldPlayback = options.shouldPlayback || null // function(requestOpts) -> boolean + this.shouldRecord = options.shouldRecord || (() => true) // function(requestOpts) -> boolean + this.shouldPlayback = options.shouldPlayback || (() => true) // function(requestOpts) -> boolean // URL pattern filtering - this.excludeUrls = options.excludeUrls || [] // Array of regex patterns or strings + this.#isUrlExcluded = isUrlExcludedFactory(options.excludeUrls) // Array of regex patterns or strings // Start auto-flush timer if enabled - if (this.autoFlush && this.snapshotPath) { - this._startAutoFlush() + if (this.#autoFlush && this.#snapshotPath) { + this.#startAutoFlush() } } /** * Records a request-response interaction + * @param {SnapshotRequestOptions} requestOpts - Request options + * @param {SnapshotEntryResponse} response - Response data to record + * @return {Promise} - Resolves when the recording is complete */ async record (requestOpts, response) { // Check if recording should be filtered out - if (this.shouldRecord && typeof this.shouldRecord === 'function') { - if (!this.shouldRecord(requestOpts)) { - return // Skip recording - } + if (!this.shouldRecord(requestOpts)) { + return // Skip recording } // Check URL exclusion patterns const url = new URL(requestOpts.path, requestOpts.origin).toString() - if (isUrlExcluded(url, this.excludeUrls)) { + if (this.#isUrlExcluded(url)) { return // Skip recording } - const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions) + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) const hash = createRequestHash(request) // Extract response data - always store body as base64 const normalizedHeaders = normalizeHeaders(response.headers) + + /** @type {SnapshotEntryResponse} */ const responseData = { statusCode: response.statusCode, - headers: filterHeadersForStorage(normalizedHeaders, this.matchOptions), + headers: filterHeadersForStorage(normalizedHeaders, this.#headerFilters, this.matchOptions), body: Buffer.isBuffer(response.body) ? response.body.toString('base64') : Buffer.from(String(response.body || '')).toString('base64'), @@ -241,18 +305,18 @@ class SnapshotRecorder { } // Remove oldest snapshot if we exceed maxSnapshots limit - if (this.snapshots.size >= this.maxSnapshots && !this.snapshots.has(hash)) { - const oldestKey = this.snapshots.keys().next().value - this.snapshots.delete(oldestKey) + if (this.#snapshots.size >= this.#maxSnapshots && !this.#snapshots.has(hash)) { + const oldestKey = this.#snapshots.keys().next().value + this.#snapshots.delete(oldestKey) } // Support sequential responses - if snapshot exists, add to responses array - const existingSnapshot = this.snapshots.get(hash) + const existingSnapshot = this.#snapshots.get(hash) if (existingSnapshot && existingSnapshot.responses) { existingSnapshot.responses.push(responseData) existingSnapshot.timestamp = new Date().toISOString() } else { - this.snapshots.set(hash, { + this.#snapshots.set(hash, { request, responses: [responseData], // Always store as array for consistency callCount: 0, @@ -261,67 +325,54 @@ class SnapshotRecorder { } // Auto-flush if enabled - if (this.autoFlush && this.snapshotPath) { - this._scheduleFlush() + if (this.#autoFlush && this.#snapshotPath) { + this.#scheduleFlush() } } /** * Finds a matching snapshot for the given request * Returns the appropriate response based on call count for sequential responses + * + * @param {SnapshotRequestOptions} requestOpts - Request options to match + * @returns {SnapshotEntry&Record<'response', SnapshotEntryResponse>|undefined} - Matching snapshot response or undefined if not found */ findSnapshot (requestOpts) { // Check if playback should be filtered out - if (this.shouldPlayback && typeof this.shouldPlayback === 'function') { - if (!this.shouldPlayback(requestOpts)) { - return undefined // Skip playback - } + if (!this.shouldPlayback(requestOpts)) { + return undefined // Skip playback } // Check URL exclusion patterns const url = new URL(requestOpts.path, requestOpts.origin).toString() - if (isUrlExcluded(url, this.excludeUrls)) { + if (this.#isUrlExcluded(url)) { return undefined // Skip playback } - const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions) + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) const hash = createRequestHash(request) - const snapshot = this.snapshots.get(hash) + const snapshot = this.#snapshots.get(hash) if (!snapshot) return undefined // Handle sequential responses - if (snapshot.responses && Array.isArray(snapshot.responses)) { - const currentCallCount = snapshot.callCount || 0 - const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1) - snapshot.callCount = currentCallCount + 1 - - return { - ...snapshot, - response: snapshot.responses[responseIndex] - } - } + const currentCallCount = snapshot.callCount || 0 + const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1) + snapshot.callCount = currentCallCount + 1 - // Legacy format compatibility - convert single response to array format - if (snapshot.response && !snapshot.responses) { - snapshot.responses = [snapshot.response] - snapshot.callCount = 1 - delete snapshot.response - - return { - ...snapshot, - response: snapshot.responses[0] - } + return { + ...snapshot, + response: snapshot.responses[responseIndex] } - - return snapshot } /** * Loads snapshots from file + * @param {string} [filePath] - Optional file path to load snapshots from + * @return {Promise} - Resolves when snapshots are loaded */ async loadSnapshots (filePath) { - const path = filePath || this.snapshotPath + const path = filePath || this.#snapshotPath if (!path) { throw new InvalidArgumentError('Snapshot path is required') } @@ -332,21 +383,18 @@ class SnapshotRecorder { // Convert array format back to Map if (Array.isArray(parsed)) { - this.snapshots.clear() + this.#snapshots.clear() for (const { hash, snapshot } of parsed) { - this.snapshots.set(hash, snapshot) + this.#snapshots.set(hash, snapshot) } } else { // Legacy object format - this.snapshots = new Map(Object.entries(parsed)) + this.#snapshots = new Map(Object.entries(parsed)) } - - this.loaded = true } catch (error) { if (error.code === 'ENOENT') { // File doesn't exist yet - that's ok for recording mode - this.snapshots.clear() - this.loaded = true + this.#snapshots.clear() } else { throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error }) } @@ -355,9 +403,12 @@ class SnapshotRecorder { /** * Saves snapshots to file + * + * @param {string} [filePath] - Optional file path to save snapshots + * @returns {Promise} - Resolves when snapshots are saved */ async saveSnapshots (filePath) { - const path = filePath || this.snapshotPath + const path = filePath || this.#snapshotPath if (!path) { throw new InvalidArgumentError('Snapshot path is required') } @@ -368,67 +419,75 @@ class SnapshotRecorder { await mkdir(dirname(resolvedPath), { recursive: true }) // Convert Map to serializable format - const data = Array.from(this.snapshots.entries()).map(([hash, snapshot]) => ({ + const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({ hash, snapshot })) - await writeFile(resolvedPath, JSON.stringify(data, null, 2), 'utf8', { flush: true }) + await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true }) } /** * Clears all recorded snapshots + * @returns {void} */ clear () { - this.snapshots.clear() + this.#snapshots.clear() } /** * Gets all recorded snapshots + * @return {Array} - Array of all recorded snapshots */ getSnapshots () { - return Array.from(this.snapshots.values()) + return Array.from(this.#snapshots.values()) } /** * Gets snapshot count + * @return {number} - Number of recorded snapshots */ size () { - return this.snapshots.size + return this.#snapshots.size } /** * Resets call counts for all snapshots (useful for test cleanup) + * @returns {void} */ resetCallCounts () { - for (const snapshot of this.snapshots.values()) { + for (const snapshot of this.#snapshots.values()) { snapshot.callCount = 0 } } /** * Deletes a specific snapshot by request options + * @param {SnapshotRequestOptions} requestOpts - Request options to match + * @returns {boolean} - True if snapshot was deleted, false if not found */ deleteSnapshot (requestOpts) { - const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions) + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) const hash = createRequestHash(request) - return this.snapshots.delete(hash) + return this.#snapshots.delete(hash) } /** * Gets information about a specific snapshot + * @param {SnapshotRequestOptions} requestOpts - Request options to match + * @returns {SnapshotInfo|null} - Snapshot information or null if not found */ getSnapshotInfo (requestOpts) { - const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions) + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) const hash = createRequestHash(request) - const snapshot = this.snapshots.get(hash) + const snapshot = this.#snapshots.get(hash) if (!snapshot) return null return { hash, request: snapshot.request, - responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0), + responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0), // .response for legacy snapshots callCount: snapshot.callCount || 0, timestamp: snapshot.timestamp } @@ -436,76 +495,80 @@ class SnapshotRecorder { /** * Replaces all snapshots with new data (full replacement) + * @param {Array<{hash: string; snapshot: SnapshotEntry}>|Record} snapshotData - New snapshot data to replace existing ones + * @returns {void} */ replaceSnapshots (snapshotData) { - this.snapshots.clear() + this.#snapshots.clear() if (Array.isArray(snapshotData)) { for (const { hash, snapshot } of snapshotData) { - this.snapshots.set(hash, snapshot) + this.#snapshots.set(hash, snapshot) } } else if (snapshotData && typeof snapshotData === 'object') { // Legacy object format - this.snapshots = new Map(Object.entries(snapshotData)) + this.#snapshots = new Map(Object.entries(snapshotData)) } } /** * Starts the auto-flush timer + * @returns {void} */ - _startAutoFlush () { - if (!this._flushTimer) { - this._flushTimer = setInterval(() => { - this.saveSnapshots().catch(() => { - // Ignore flush errors - they shouldn't interrupt normal operation - }) - }, this.flushInterval) - } + #startAutoFlush () { + return this.#scheduleFlush() } /** * Stops the auto-flush timer + * @returns {void} */ - _stopAutoFlush () { - if (this._flushTimer) { - clearInterval(this._flushTimer) - this._flushTimer = null + #stopAutoFlush () { + if (this.#flushTimeout) { + clearTimeout(this.#flushTimeout) + // Ensure any pending flush is completed + this.saveSnapshots().catch(() => { + // Ignore flush errors + }) + this.#flushTimeout = null } } /** * Schedules a flush (debounced to avoid excessive writes) */ - _scheduleFlush () { - // Simple debouncing - clear existing timeout and set new one - if (this._flushTimeout) { - clearTimeout(this._flushTimeout) - } - this._flushTimeout = setTimeout(() => { + #scheduleFlush () { + this.#flushTimeout = setTimeout(() => { this.saveSnapshots().catch(() => { // Ignore flush errors }) - this._flushTimeout = null + if (this.#autoFlush) { + this.#flushTimeout?.refresh() + } else { + this.#flushTimeout = null + } }, 1000) // 1 second debounce } /** * Cleanup method to stop timers + * @returns {void} */ destroy () { - this._stopAutoFlush() - if (this._flushTimeout) { - clearTimeout(this._flushTimeout) - this._flushTimeout = null + this.#stopAutoFlush() + if (this.#flushTimeout) { + clearTimeout(this.#flushTimeout) + this.#flushTimeout = null } } /** * Async close method that saves all recordings and performs cleanup + * @returns {Promise} */ async close () { // Save any pending recordings if we have a snapshot path - if (this.snapshotPath && this.snapshots.size > 0) { + if (this.#snapshotPath && this.#snapshots.size !== 0) { await this.saveSnapshots() } @@ -514,4 +577,4 @@ class SnapshotRecorder { } } -module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, isUrlExcluded, createHeaderSetsCache } +module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, createHeaderFilters } diff --git a/deps/undici/src/lib/mock/snapshot-utils.js b/deps/undici/src/lib/mock/snapshot-utils.js new file mode 100644 index 00000000000000..ebad12e888ff29 --- /dev/null +++ b/deps/undici/src/lib/mock/snapshot-utils.js @@ -0,0 +1,158 @@ +'use strict' + +const { InvalidArgumentError } = require('../core/errors') + +/** + * @typedef {Object} HeaderFilters + * @property {Set} ignore - Set of headers to ignore for matching + * @property {Set} exclude - Set of headers to exclude from matching + * @property {Set} match - Set of headers to match (empty means match + */ + +/** + * Creates cached header sets for performance + * + * @param {import('./snapshot-recorder').SnapshotRecorderMatchOptions} matchOptions - Matching options for headers + * @returns {HeaderFilters} - Cached sets for ignore, exclude, and match headers + */ +function createHeaderFilters (matchOptions = {}) { + const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = [], caseSensitive = false } = matchOptions + + return { + ignore: new Set(ignoreHeaders.map(header => caseSensitive ? header : header.toLowerCase())), + exclude: new Set(excludeHeaders.map(header => caseSensitive ? header : header.toLowerCase())), + match: new Set(matchHeaders.map(header => caseSensitive ? header : header.toLowerCase())) + } +} + +let crypto +try { + crypto = require('node:crypto') +} catch { /* Fallback if crypto is not available */ } + +/** + * @callback HashIdFunction + * @param {string} value - The value to hash + * @returns {string} - The base64url encoded hash of the value + */ + +/** + * Generates a hash for a given value + * @type {HashIdFunction} + */ +const hashId = crypto?.hash + ? (value) => crypto.hash('sha256', value, 'base64url') + : (value) => Buffer.from(value).toString('base64url') + +/** + * @typedef {(url: string) => boolean} IsUrlExcluded Checks if a URL matches any of the exclude patterns + */ + +/** @typedef {{[key: Lowercase]: string}} NormalizedHeaders */ +/** @typedef {Array} UndiciHeaders */ +/** @typedef {Record} Headers */ + +/** + * @param {*} headers + * @returns {headers is UndiciHeaders} + */ +function isUndiciHeaders (headers) { + return Array.isArray(headers) && (headers.length & 1) === 0 +} + +/** + * Factory function to create a URL exclusion checker + * @param {Array} [excludePatterns=[]] - Array of patterns to exclude + * @returns {IsUrlExcluded} - A function that checks if a URL matches any of the exclude patterns + */ +function isUrlExcludedFactory (excludePatterns = []) { + if (excludePatterns.length === 0) { + return () => false + } + + return function isUrlExcluded (url) { + let urlLowerCased + + for (const pattern of excludePatterns) { + if (typeof pattern === 'string') { + if (!urlLowerCased) { + // Convert URL to lowercase only once + urlLowerCased = url.toLowerCase() + } + // Simple string match (case-insensitive) + if (urlLowerCased.includes(pattern.toLowerCase())) { + return true + } + } else if (pattern instanceof RegExp) { + // Regex pattern match + if (pattern.test(url)) { + return true + } + } + } + + return false + } +} + +/** + * Normalizes headers for consistent comparison + * + * @param {Object|UndiciHeaders} headers - Headers to normalize + * @returns {NormalizedHeaders} - Normalized headers as a lowercase object + */ +function normalizeHeaders (headers) { + /** @type {NormalizedHeaders} */ + const normalizedHeaders = {} + + if (!headers) return normalizedHeaders + + // Handle array format (undici internal format: [name, value, name, value, ...]) + if (isUndiciHeaders(headers)) { + for (let i = 0; i < headers.length; i += 2) { + const key = headers[i] + const value = headers[i + 1] + if (key && value !== undefined) { + // Convert Buffers to strings if needed + const keyStr = Buffer.isBuffer(key) ? key.toString() : key + const valueStr = Buffer.isBuffer(value) ? value.toString() : value + normalizedHeaders[keyStr.toLowerCase()] = valueStr + } + } + return normalizedHeaders + } + + // Handle object format + if (headers && typeof headers === 'object') { + for (const [key, value] of Object.entries(headers)) { + if (key && typeof key === 'string') { + normalizedHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value) + } + } + } + + return normalizedHeaders +} + +const validSnapshotModes = /** @type {const} */ (['record', 'playback', 'update']) + +/** @typedef {typeof validSnapshotModes[number]} SnapshotMode */ + +/** + * @param {*} mode - The snapshot mode to validate + * @returns {asserts mode is SnapshotMode} + */ +function validateSnapshotMode (mode) { + if (!validSnapshotModes.includes(mode)) { + throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validSnapshotModes.join(', ')}`) + } +} + +module.exports = { + createHeaderFilters, + hashId, + isUndiciHeaders, + normalizeHeaders, + isUrlExcludedFactory, + validateSnapshotMode +} diff --git a/deps/undici/src/lib/util/cache.js b/deps/undici/src/lib/util/cache.js index 53851df1d1bbd9..3c2eb0dbd29d6a 100644 --- a/deps/undici/src/lib/util/cache.js +++ b/deps/undici/src/lib/util/cache.js @@ -34,7 +34,7 @@ function makeCacheKey (opts) { * @param {Record} * @returns {Record} */ -function normaliseHeaders (opts) { +function normalizeHeaders (opts) { let headers if (opts.headers == null) { headers = {} @@ -234,7 +234,7 @@ function parseCacheControlHeader (header) { } } } else { - // Something like `no-cache=some-header` + // Something like `no-cache="some-header"` if (key in output) { output[key] = output[key].concat(value) } else { @@ -367,7 +367,7 @@ function assertCacheMethods (methods, name = 'CacheMethods') { module.exports = { makeCacheKey, - normaliseHeaders, + normalizeHeaders, assertCacheKey, assertCacheValue, parseCacheControlHeader, diff --git a/deps/undici/src/lib/web/cache/cache.js b/deps/undici/src/lib/web/cache/cache.js index dd9e2f8163ad3a..70a3787a71d415 100644 --- a/deps/undici/src/lib/web/cache/cache.js +++ b/deps/undici/src/lib/web/cache/cache.js @@ -18,7 +18,7 @@ const { createDeferredPromise } = require('../../util/promise') * @property {'delete' | 'put'} type * @property {any} request * @property {any} response - * @property {import('../../types/cache').CacheQueryOptions} options + * @property {import('../../../types/cache').CacheQueryOptions} options */ /** @@ -452,7 +452,7 @@ class Cache { /** * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys * @param {any} request - * @param {import('../../types/cache').CacheQueryOptions} options + * @param {import('../../../types/cache').CacheQueryOptions} options * @returns {Promise} */ async keys (request = undefined, options = {}) { @@ -670,7 +670,7 @@ class Cache { /** * @see https://w3c.github.io/ServiceWorker/#query-cache * @param {any} requestQuery - * @param {import('../../types/cache').CacheQueryOptions} options + * @param {import('../../../types/cache').CacheQueryOptions} options * @param {requestResponseList} targetStorage * @returns {requestResponseList} */ @@ -695,7 +695,7 @@ class Cache { * @param {any} requestQuery * @param {any} request * @param {any | null} response - * @param {import('../../types/cache').CacheQueryOptions | undefined} options + * @param {import('../../../types/cache').CacheQueryOptions | undefined} options * @returns {boolean} */ #requestMatchesCachedItem (requestQuery, request, response = null, options) { diff --git a/deps/undici/src/lib/web/eventsource/eventsource.js b/deps/undici/src/lib/web/eventsource/eventsource.js index 746400b8ea97a5..1ff4e36ca2a071 100644 --- a/deps/undici/src/lib/web/eventsource/eventsource.js +++ b/deps/undici/src/lib/web/eventsource/eventsource.js @@ -124,10 +124,10 @@ class EventSource extends EventTarget { url = webidl.converters.USVString(url) eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict, prefix, 'eventSourceInitDict') - this.#dispatcher = eventSourceInitDict.dispatcher + this.#dispatcher = eventSourceInitDict.node.dispatcher || eventSourceInitDict.dispatcher this.#state = { lastEventId: '', - reconnectionTime: defaultReconnectionTime + reconnectionTime: eventSourceInitDict.node.reconnectionTime } // 2. Let settings be ev's relevant settings object. @@ -472,6 +472,21 @@ webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([ { key: 'dispatcher', // undici only converter: webidl.converters.any + }, + { + key: 'node', // undici only + converter: webidl.dictionaryConverter([ + { + key: 'reconnectionTime', + converter: webidl.converters['unsigned long'], + defaultValue: () => defaultReconnectionTime + }, + { + key: 'dispatcher', + converter: webidl.converters.any + } + ]), + defaultValue: () => ({}) } ]) diff --git a/deps/undici/src/lib/web/fetch/formdata.js b/deps/undici/src/lib/web/fetch/formdata.js index e21ee3f553e96e..c21fb06a3eeb62 100644 --- a/deps/undici/src/lib/web/fetch/formdata.js +++ b/deps/undici/src/lib/web/fetch/formdata.js @@ -9,7 +9,7 @@ const nodeUtil = require('node:util') class FormData { #state = [] - constructor (form) { + constructor (form = undefined) { webidl.util.markAsUncloneable(this) if (form !== undefined) { diff --git a/deps/undici/src/lib/web/fetch/response.js b/deps/undici/src/lib/web/fetch/response.js index fba44ef6dcacf0..5f11f449477f8b 100644 --- a/deps/undici/src/lib/web/fetch/response.js +++ b/deps/undici/src/lib/web/fetch/response.js @@ -22,7 +22,8 @@ const { webidl } = require('../webidl') const { URLSerializer } = require('./data-url') const { kConstruct } = require('../../core/symbols') const assert = require('node:assert') -const { types } = require('node:util') + +const { isArrayBuffer } = nodeUtil.types const textEncoder = new TextEncoder('utf-8') @@ -243,6 +244,11 @@ class Response { // 2. Let clonedResponse be the result of cloning this’s response. const clonedResponse = cloneResponse(this.#state) + // Note: To re-register because of a new stream. + if (this.#state.body?.stream) { + streamRegistry.register(this, new WeakRef(this.#state.body.stream)) + } + // 3. Return the result of creating a Response object, given // clonedResponse, this’s headers’s guard, and this’s relevant Realm. return fromInnerResponse(clonedResponse, getHeadersGuard(this.#headers)) @@ -353,8 +359,6 @@ function cloneResponse (response) { // result of cloning response’s body. if (response.body != null) { newResponse.body = cloneBody(response.body) - - streamRegistry.register(newResponse, new WeakRef(response.body.stream)) } // 4. Return newResponse. @@ -576,7 +580,7 @@ webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) { return V } - if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) { + if (ArrayBuffer.isView(V) || isArrayBuffer(V)) { return V } diff --git a/deps/undici/src/lib/web/websocket/stream/websocketstream.js b/deps/undici/src/lib/web/websocket/stream/websocketstream.js index dc364a00e66160..e7a8bce614a11d 100644 --- a/deps/undici/src/lib/web/websocket/stream/websocketstream.js +++ b/deps/undici/src/lib/web/websocket/stream/websocketstream.js @@ -6,7 +6,7 @@ const { states, opcodes, sentCloseFrameState } = require('../constants') const { webidl } = require('../../webidl') const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util') const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection') -const { types } = require('node:util') +const { isArrayBuffer } = require('node:util/types') const { channels } = require('../../../core/diagnostics') const { WebsocketFrameSend } = require('../frame') const { ByteParser } = require('../receiver') @@ -210,7 +210,7 @@ class WebSocketStream { let opcode = null // 4. If chunk is a BufferSource , - if (ArrayBuffer.isView(chunk) || types.isArrayBuffer(chunk)) { + if (ArrayBuffer.isView(chunk) || isArrayBuffer(chunk)) { // 4.1. Set data to a copy of the bytes given chunk . data = new Uint8Array(ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) : chunk) diff --git a/deps/undici/src/lib/web/websocket/websocket.js b/deps/undici/src/lib/web/websocket/websocket.js index 5688c8ad8a0563..1f10cb0a73a7ed 100644 --- a/deps/undici/src/lib/web/websocket/websocket.js +++ b/deps/undici/src/lib/web/websocket/websocket.js @@ -1,5 +1,6 @@ 'use strict' +const { isArrayBuffer } = require('node:util/types') const { webidl } = require('../webidl') const { URLSerializer } = require('../fetch/data-url') const { environmentSettingsObject } = require('../fetch/util') @@ -19,7 +20,6 @@ const { establishWebSocketConnection, closeWebSocketConnection, failWebsocketCon const { ByteParser } = require('./receiver') const { kEnumerableProperty } = require('../../core/util') const { getGlobalDispatcher } = require('../../global') -const { types } = require('node:util') const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events') const { SendQueue } = require('./sender') const { WebsocketFrameSend } = require('./frame') @@ -257,7 +257,7 @@ class WebSocket extends EventTarget { this.#sendQueue.add(buffer, () => { this.#bufferedAmount -= buffer.byteLength }, sendHints.text) - } else if (types.isArrayBuffer(data)) { + } else if (isArrayBuffer(data)) { // If the WebSocket connection is established, and the WebSocket // closing handshake has not yet started, then the user agent must // send a WebSocket Message comprised of data using a binary frame @@ -482,11 +482,18 @@ class WebSocket extends EventTarget { fireEvent('open', this) if (channels.open.hasSubscribers) { + // Convert headers to a plain object for the event + const headers = response.headersList.entries channels.open.publish({ address: response.socket.address(), protocol: this.#protocol, extensions: this.#extensions, - websocket: this + websocket: this, + handshakeResponse: { + status: response.status, + statusText: response.statusText, + headers + } }) } } @@ -728,7 +735,7 @@ webidl.converters.WebSocketSendData = function (V) { return V } - if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) { + if (ArrayBuffer.isView(V) || isArrayBuffer(V)) { return V } } diff --git a/deps/undici/src/package-lock.json b/deps/undici/src/package-lock.json index 57498279f5040c..80ef7918ad3918 100644 --- a/deps/undici/src/package-lock.json +++ b/deps/undici/src/package-lock.json @@ -1,33 +1,33 @@ { "name": "undici", - "version": "7.13.0", + "version": "7.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "undici", - "version": "7.13.0", + "version": "7.14.0", "license": "MIT", "devDependencies": { "@fastify/busboy": "3.1.1", "@matteo.collina/tspl": "^0.2.0", + "@metcoder95/https-pem": "^1.0.0", "@sinonjs/fake-timers": "^12.0.0", "@types/node": "^18.19.50", "abort-controller": "^3.0.0", "borp": "^0.20.0", "c8": "^10.0.0", - "cross-env": "^7.0.3", + "cross-env": "^10.0.0", "dns-packet": "^5.4.0", "esbuild": "^0.25.2", "eslint": "^9.9.0", "fast-check": "^4.1.1", - "https-pem": "^3.0.0", "husky": "^9.0.7", - "jest": "^29.0.2", + "jest": "^30.0.5", "neostandard": "^0.12.0", "node-forge": "^1.3.1", "proxy": "^2.1.1", - "tsd": "^0.32.0", + "tsd": "^0.33.0", "typescript": "^5.6.2", "ws": "^8.11.0" }, @@ -114,22 +114,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -145,14 +145,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -203,15 +203,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -261,9 +261,9 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, "license": "MIT", "dependencies": { @@ -275,13 +275,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -545,18 +545,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.2", "debug": "^4.3.1" }, "engines": { @@ -621,10 +621,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -639,9 +646,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -656,9 +663,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -673,9 +680,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -690,9 +697,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -707,9 +714,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -724,9 +731,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -741,9 +748,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -758,9 +765,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -775,9 +782,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -792,9 +799,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -809,9 +816,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -826,9 +833,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -843,9 +850,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -860,9 +867,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -877,9 +884,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -894,9 +901,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -911,9 +918,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -928,9 +935,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -945,9 +952,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -962,9 +969,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -979,9 +986,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -996,9 +1003,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -1013,9 +1020,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -1030,9 +1037,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -1047,9 +1054,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -1121,9 +1128,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1131,9 +1138,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1168,9 +1175,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "engines": { @@ -1191,13 +1198,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -1457,61 +1464,61 @@ } }, "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz", + "integrity": "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.0.5", "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", + "chalk": "^4.1.2", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz", + "integrity": "sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/console": "30.0.5", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.5", + "jest-config": "30.0.5", + "jest-haste-map": "30.0.5", + "jest-message-util": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.5", + "jest-resolve-dependencies": "30.0.5", + "jest-runner": "30.0.5", + "jest-runtime": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "jest-watcher": "30.0.5", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -1522,150 +1529,160 @@ } } }, - "node_modules/@jest/core/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", + "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-mock": "^29.7.0" + "jest-mock": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" + "expect": "30.0.5", + "jest-snapshot": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", + "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", "dev": true, "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3" + "@jest/get-type": "30.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", + "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", + "@jest/types": "30.0.5", + "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/fake-timers/node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz", + "integrity": "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" + "@jest/environment": "30.0.5", + "@jest/expect": "30.0.5", + "@jest/types": "30.0.5", + "jest-mock": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz", + "integrity": "sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", + "@jest/console": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", + "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "jest-worker": "30.0.5", "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", + "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -1683,160 +1700,132 @@ "dev": true, "license": "MIT" }, - "node_modules/@jest/reporters/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/reporters/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/reporters/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/@jest/snapshot-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz", + "integrity": "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ==", "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz", + "integrity": "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "@jest/console": "30.0.5", + "@jest/types": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz", + "integrity": "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", + "@jest/test-result": "30.0.5", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz", + "integrity": "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", + "@babel/core": "^7.27.4", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "write-file-atomic": "^5.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -1855,16 +1844,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1886,6 +1875,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@metcoder95/https-pem": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@metcoder95/https-pem/-/https-pem-1.0.0.tgz", + "integrity": "sha512-v+nv59Y6SqdtbXBmAlVx++Vrc2DkBt4F7AVOmbNMB+pjD8QLCoReRdsTCydJC/wY4iZX8O4Cm0F1DhgDgwhIsg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "selfsigned": "^3.0.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1958,10 +1961,23 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@reporters/github": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@reporters/github/-/github-1.8.0.tgz", - "integrity": "sha512-EJNbv7qvqbICrVbyaPLKWT/mGzdkkdskKuPg1hG0tVKeAEtH6D1gCZwZ84N/26CQ8FBsyfiUyVjwtgYEByGKWQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@reporters/github/-/github-1.9.1.tgz", + "integrity": "sha512-MIuO3Ci0vCKV5CkueQOieiYKPqJvHkHJxYsk+raotCHcB4yPU4ns6yfy3tcXDsUFCkj+vbB+XXucdac139J6uA==", "dev": true, "license": "MIT", "dependencies": { @@ -1977,9 +1993,9 @@ "license": "MIT" }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "0.34.40", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz", + "integrity": "sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==", "dev": true, "license": "MIT" }, @@ -2050,9 +2066,9 @@ } }, "node_modules/@tsd/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-oKarNCN1QUhG148M88mtZdOlBZWWGcInquef+U8QL7gwJkRuNo5WS45Fjsd+3hM9cDJWGpqSZ4Oo097KDx4IWA==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-mSMM0QtEPdMd+rdMDd17yCUYD4yI3pKHap89+jEZrZ3KIO5PhDofBjER0OtgHdvOXF74KMLO3fyD6k3Hz0v03A==", "dev": true, "license": "MIT", "engines": { @@ -2133,16 +2149,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2185,25 +2191,15 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.121", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.121.tgz", - "integrity": "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ==", + "version": "18.19.123", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.123.tgz", + "integrity": "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, - "node_modules/@types/node-forge": { - "version": "1.3.13", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.13.tgz", - "integrity": "sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -2236,17 +2232,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", - "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", + "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/type-utils": "8.38.0", - "@typescript-eslint/utils": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/type-utils": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2260,9 +2256,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.38.0", + "@typescript-eslint/parser": "^8.39.1", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -2276,16 +2272,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", - "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", + "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4" }, "engines": { @@ -2297,18 +2293,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", - "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.38.0", - "@typescript-eslint/types": "^8.38.0", + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.1", "debug": "^4.3.4" }, "engines": { @@ -2319,18 +2315,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", - "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", + "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0" + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2341,9 +2337,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", - "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", + "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", "dev": true, "license": "MIT", "engines": { @@ -2354,19 +2350,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", - "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", + "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2379,13 +2375,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", - "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", "dev": true, "license": "MIT", "engines": { @@ -2397,16 +2393,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", - "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", + "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.38.0", - "@typescript-eslint/tsconfig-utils": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/project-service": "8.39.1", + "@typescript-eslint/tsconfig-utils": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2422,7 +2418,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -2465,16 +2461,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", - "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", + "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0" + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2485,17 +2481,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", - "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", + "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/types": "8.39.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2506,6 +2502,13 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", @@ -2845,9 +2848,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", "dev": true, "license": "MIT", "engines": { @@ -3193,42 +3196,42 @@ } }, "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", + "integrity": "sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", + "@jest/transform": "30.0.5", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.8.0" + "@babel/core": "^7.11.0" } }, "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", "test-exclude": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=12" } }, "node_modules/babel-plugin-istanbul/node_modules/glob": { @@ -3253,23 +3256,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/babel-plugin-istanbul/node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -3286,19 +3272,18 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/babel-preset-current-node-syntax": { @@ -3329,20 +3314,20 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.11.0" } }, "node_modules/balanced-match": { @@ -3401,9 +3386,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", "dev": true, "funding": [ { @@ -3421,8 +3406,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -3632,9 +3617,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", "dev": true, "funding": [ { @@ -3680,9 +3665,9 @@ } }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", "dev": true, "funding": [ { @@ -3696,9 +3681,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", "dev": true, "license": "MIT" }, @@ -3842,45 +3827,22 @@ "dev": true, "license": "MIT" }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz", + "integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==", "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.1" + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" }, "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" }, "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" + "node": ">=20" } }, "node_modules/cross-spawn": { @@ -4157,9 +4119,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.194", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", - "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==", + "version": "1.5.203", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.203.tgz", + "integrity": "sha512-uz4i0vLhfm6dLZWbz/iH88KNDV+ivj5+2SA+utpgjKaj9Q0iDLuwk6Idhe9BTxciHudyx6IvTvijhkPvFGUQ0g==", "dev": true, "license": "ISC" }, @@ -4184,9 +4146,9 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -4385,9 +4347,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4398,32 +4360,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -4450,20 +4412,20 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4860,24 +4822,6 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/eslint-rule-docs": { "version": "1.1.235", "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", @@ -5089,30 +5033,32 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" + "@jest/expect-utils": "30.0.5", + "@jest/get-type": "30.0.1", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/fast-check": { @@ -5792,17 +5738,6 @@ "dev": true, "license": "MIT" }, - "node_modules/https-pem": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/https-pem/-/https-pem-3.0.0.tgz", - "integrity": "sha512-JqYVRTpk1WeXziwBaTX6eyXod6Dt70d/kehtY3DR6ygl+11XgcksTjSl4NjZbNCKK3rpTB1qH9hnu75RSOFUWQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "selfsigned": "^2.0.1" - } - }, "node_modules/human-signals": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", @@ -6490,15 +6425,15 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "istanbul-lib-coverage": "^3.0.0" }, "engines": { "node": ">=10" @@ -6553,22 +6488,22 @@ } }, "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz", + "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" + "@jest/core": "30.0.5", + "@jest/types": "30.0.5", + "import-local": "^3.2.0", + "jest-cli": "30.0.5" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -6580,18 +6515,18 @@ } }, "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", + "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", "dev": true, "license": "MIT", "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", + "execa": "^5.1.1", + "jest-util": "30.0.5", "p-limit": "^3.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-changed-files/node_modules/execa": { @@ -6685,78 +6620,60 @@ } }, "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz", + "integrity": "sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.0.5", + "@jest/expect": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "chalk": "^4.0.0", + "chalk": "^4.1.2", "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.0.5", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-runtime": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", + "pretty-format": "30.0.5", + "pure-rand": "^7.0.0", "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.6" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-circus/node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz", + "integrity": "sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" + "@jest/core": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "yargs": "^17.7.2" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -6768,135 +6685,120 @@ } }, "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz", + "integrity": "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", + "@babel/core": "^7.27.4", + "@jest/get-type": "30.0.1", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.0.5", + "@jest/types": "30.0.5", + "babel-jest": "30.0.5", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.0.5", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.5", + "jest-runner": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "micromatch": "^4.0.8", "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", + "pretty-format": "30.0.5", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "@types/node": "*", + "esbuild-register": ">=3.4.0", "ts-node": ">=9.0.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "esbuild-register": { + "optional": true + }, "ts-node": { "optional": true } } }, - "node_modules/jest-config/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", + "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", "dev": true, "license": "MIT", "dependencies": { - "detect-newline": "^3.0.0" + "detect-newline": "^3.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz", + "integrity": "sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "jest-util": "30.0.5", + "pretty-format": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz", + "integrity": "sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-mock": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-get-type": { @@ -6910,95 +6812,94 @@ } }, "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz", + "integrity": "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", + "@jest/types": "30.0.5", "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "jest-worker": "30.0.5", + "micromatch": "^4.0.8", "walker": "^1.0.8" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "optionalDependencies": { - "fsevents": "^2.3.2" + "fsevents": "^2.3.3" } }, "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", + "integrity": "sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg==", "dev": true, "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.0.1", + "pretty-format": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", + "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.5", + "pretty-format": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.6" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-util": "^29.7.0" + "jest-util": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-pnp-resolver": { @@ -7020,169 +6921,148 @@ } }, "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz", + "integrity": "sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz", + "integrity": "sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw==", "dev": true, "license": "MIT", "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz", + "integrity": "sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/console": "30.0.5", + "@jest/environment": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "chalk": "^4.0.0", + "chalk": "^4.1.2", "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.5", + "jest-haste-map": "30.0.5", + "jest-leak-detector": "30.0.5", + "jest-message-util": "30.0.5", + "jest-resolve": "30.0.5", + "jest-runtime": "30.0.5", + "jest-util": "30.0.5", + "jest-watcher": "30.0.5", + "jest-worker": "30.0.5", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", + "integrity": "sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/globals": "30.0.5", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz", + "integrity": "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.0.5", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.5", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.5", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "pretty-format": "30.0.5", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-snapshot/node_modules/semver": { @@ -7199,39 +7079,52 @@ } }, "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.0.5", "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz", + "integrity": "sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.5", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "^29.7.0" + "pretty-format": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-validate/node_modules/camelcase": { @@ -7248,39 +7141,40 @@ } }, "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz", + "integrity": "sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" + "jest-util": "30.0.5", + "string-length": "^4.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", + "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", - "jest-util": "^29.7.0", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "supports-color": "^8.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-worker/node_modules/supports-color": { @@ -7409,16 +7303,6 @@ "node": ">=0.10.0" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -7755,9 +7639,9 @@ "license": "MIT" }, "node_modules/napi-postinstall": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.2.tgz", - "integrity": "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", "dev": true, "license": "MIT", "bin": { @@ -8504,18 +8388,18 @@ } }, "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/pretty-format/node_modules/ansi-styles": { @@ -8547,20 +8431,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8793,6 +8663,27 @@ "validate-npm-package-license": "^3.0.1" } }, + "node_modules/read-pkg/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/read-pkg/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -8882,22 +8773,19 @@ } }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8945,16 +8833,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -9046,13 +8924,12 @@ } }, "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-3.0.1.tgz", + "integrity": "sha512-6U6w6kSLrM9Zxo0D7mC7QdGS6ZZytMWBnj/vhF9p+dAHx6CwGezuRcO4VclTbrrI7mg7SD6zNiqXUuBHOVopNQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -9230,13 +9107,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -9298,9 +9168,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "dev": true, "license": "CC0-1.0" }, @@ -9693,6 +9563,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tapable": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", @@ -9762,11 +9648,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -9869,13 +9758,13 @@ } }, "node_modules/tsd": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.32.0.tgz", - "integrity": "sha512-R5lBZCbxGBowOcW0gpQaiIjGYrG5NmU+PfFDKcc3zbtzWjML1o/zAwzdDnS2ZheSlPu9GW51azpFqEPUBq9DoQ==", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.33.0.tgz", + "integrity": "sha512-/PQtykJFVw90QICG7zyPDMIyueOXKL7jOJVoX5pILnb3Ux+7QqynOxfVvarE+K+yi7BZyOSY4r+OZNWSWRiEwQ==", "dev": true, "license": "MIT", "dependencies": { - "@tsd/typescript": "~5.8.3", + "@tsd/typescript": "^5.9.2", "eslint-formatter-pretty": "^4.1.0", "globby": "^11.0.1", "jest-diff": "^29.0.3", @@ -9890,6 +9779,55 @@ "node": ">=14.16" } }, + "node_modules/tsd/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/tsd/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tsd/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/tsd/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/tsd/node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9900,6 +9838,21 @@ "node": ">=8" } }, + "node_modules/tsd/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -10033,9 +9986,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10047,16 +10000,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", - "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz", + "integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0" + "@typescript-eslint/eslint-plugin": "8.39.1", + "@typescript-eslint/parser": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10067,7 +10020,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/unbox-primitive": { @@ -10462,26 +10415,19 @@ "license": "ISC" }, "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "signal-exit": "^4.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/write-file-atomic/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -10522,9 +10468,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", "bin": { diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index 48948be80dd166..ef6fe2b902d370 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "7.13.0", + "version": "7.14.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { @@ -109,23 +109,23 @@ "devDependencies": { "@fastify/busboy": "3.1.1", "@matteo.collina/tspl": "^0.2.0", + "@metcoder95/https-pem": "^1.0.0", "@sinonjs/fake-timers": "^12.0.0", "@types/node": "^18.19.50", "abort-controller": "^3.0.0", "borp": "^0.20.0", "c8": "^10.0.0", - "cross-env": "^7.0.3", + "cross-env": "^10.0.0", "dns-packet": "^5.4.0", "esbuild": "^0.25.2", "eslint": "^9.9.0", "fast-check": "^4.1.1", - "https-pem": "^3.0.0", "husky": "^9.0.7", - "jest": "^29.0.2", + "jest": "^30.0.5", "neostandard": "^0.12.0", "node-forge": "^1.3.1", "proxy": "^2.1.1", - "tsd": "^0.32.0", + "tsd": "^0.33.0", "typescript": "^5.6.2", "ws": "^8.11.0" }, diff --git a/deps/undici/src/scripts/generate-pem.js b/deps/undici/src/scripts/generate-pem.js index 88ac5c8392f610..7401b03733b893 100644 --- a/deps/undici/src/scripts/generate-pem.js +++ b/deps/undici/src/scripts/generate-pem.js @@ -1,4 +1,4 @@ 'use strict' /* istanbul ignore file */ -require('https-pem/install') +require('@metcoder95/https-pem/install') diff --git a/deps/undici/src/types/eventsource.d.ts b/deps/undici/src/types/eventsource.d.ts index c85c4a14e586e4..081ca09aee97ff 100644 --- a/deps/undici/src/types/eventsource.d.ts +++ b/deps/undici/src/types/eventsource.d.ts @@ -56,6 +56,11 @@ export declare const EventSource: { } interface EventSourceInit { - withCredentials?: boolean, + withCredentials?: boolean + // @deprecated use `node.dispatcher` instead dispatcher?: Dispatcher + node?: { + dispatcher?: Dispatcher + reconnectionTime?: number + } } diff --git a/deps/undici/src/types/index.d.ts b/deps/undici/src/types/index.d.ts index f9035293a95037..be0bc289c5f48c 100644 --- a/deps/undici/src/types/index.d.ts +++ b/deps/undici/src/types/index.d.ts @@ -34,7 +34,9 @@ export * from './content-type' export * from './cache' export { Interceptable } from './mock-interceptor' -export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, SnapshotAgent, MockCallHistory, MockCallHistoryLog, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent, H2CClient } +declare function globalThisInstall (): void + +export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, SnapshotAgent, MockCallHistory, MockCallHistoryLog, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent, H2CClient, globalThisInstall as install } export default Undici declare namespace Undici { @@ -74,4 +76,5 @@ declare namespace Undici { MemoryCacheStore: typeof import('./cache-interceptor').default.MemoryCacheStore, SqliteCacheStore: typeof import('./cache-interceptor').default.SqliteCacheStore } + const install: typeof globalThisInstall } diff --git a/deps/undici/undici.js b/deps/undici/undici.js index 651a8cd6159569..39132374b7ac2e 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -5477,7 +5477,7 @@ var require_formdata = __commonJS({ __name(this, "FormData"); } #state = []; - constructor(form) { + constructor(form = void 0) { webidl.util.markAsUncloneable(this); if (form !== void 0) { throw webidl.errors.conversionFailed({ @@ -8782,7 +8782,6 @@ var require_proxy_agent = __commonJS({ "lib/dispatcher/proxy-agent.js"(exports2, module2) { "use strict"; var { kProxy, kClose, kDestroy, kDispatch } = require_symbols(); - var { URL: URL2 } = require("node:url"); var Agent = require_agent(); var Pool = require_pool(); var DispatcherBase = require_dispatcher_base(); @@ -8848,7 +8847,7 @@ var require_proxy_agent = __commonJS({ } = opts; opts.path = origin + path; if (!("host" in headers) && !("Host" in headers)) { - const { host } = new URL2(origin); + const { host } = new URL(origin); headers.host = host; } opts.headers = { ...this[kProxyHeaders], ...headers }; @@ -8866,7 +8865,7 @@ var require_proxy_agent = __commonJS({ __name(this, "ProxyAgent"); } constructor(opts) { - if (!opts || typeof opts === "object" && !(opts instanceof URL2) && !opts.uri) { + if (!opts || typeof opts === "object" && !(opts instanceof URL) && !opts.uri) { throw new InvalidArgumentError("Proxy uri is mandatory"); } const { clientFactory = defaultFactory } = opts; @@ -8895,7 +8894,7 @@ var require_proxy_agent = __commonJS({ this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }); const agentFactory = opts.factory || defaultAgentFactory; const factory = /* @__PURE__ */ __name((origin2, options) => { - const { protocol: protocol2 } = new URL2(origin2); + const { protocol: protocol2 } = new URL(origin2); if (!this[kTunnelProxy] && protocol2 === "http:" && this[kProxy].protocol === "http:") { return new Http1ProxyWrapper(this[kProxy].uri, { headers: this[kProxyHeaders], @@ -8956,7 +8955,7 @@ var require_proxy_agent = __commonJS({ const headers = buildHeaders(opts.headers); throwIfProxyAuthIsSent(headers); if (headers && !("host" in headers) && !("Host" in headers)) { - const { host } = new URL2(opts.origin); + const { host } = new URL(opts.origin); headers.host = host; } return this[kAgent].dispatch( @@ -8968,16 +8967,16 @@ var require_proxy_agent = __commonJS({ ); } /** - * @param {import('../types/proxy-agent').ProxyAgent.Options | string | URL} opts + * @param {import('../../types/proxy-agent').ProxyAgent.Options | string | URL} opts * @returns {URL} */ #getUrl(opts) { if (typeof opts === "string") { - return new URL2(opts); - } else if (opts instanceof URL2) { + return new URL(opts); + } else if (opts instanceof URL) { return opts; } else { - return new URL2(opts.uri); + return new URL(opts.uri); } } async [kClose]() { @@ -9642,7 +9641,7 @@ var require_response = __commonJS({ var { URLSerializer } = require_data_url(); var { kConstruct } = require_symbols(); var assert = require("node:assert"); - var { types } = require("node:util"); + var { isArrayBuffer } = nodeUtil.types; var textEncoder = new TextEncoder("utf-8"); var Response = class _Response { static { @@ -9769,6 +9768,9 @@ var require_response = __commonJS({ }); } const clonedResponse = cloneResponse(this.#state); + if (this.#state.body?.stream) { + streamRegistry.register(this, new WeakRef(this.#state.body.stream)); + } return fromInnerResponse(clonedResponse, getHeadersGuard(this.#headers)); } [nodeUtil.inspect.custom](depth, options) { @@ -9853,7 +9855,6 @@ var require_response = __commonJS({ const newResponse = makeResponse({ ...response, body: null }); if (response.body != null) { newResponse.body = cloneBody(response.body); - streamRegistry.register(newResponse, new WeakRef(response.body.stream)); } return newResponse; } @@ -9999,7 +10000,7 @@ var require_response = __commonJS({ if (webidl.is.Blob(V)) { return V; } - if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) { + if (ArrayBuffer.isView(V) || isArrayBuffer(V)) { return V; } if (webidl.is.FormData(V)) { @@ -11632,7 +11633,7 @@ var require_fetch = __commonJS({ fetchParams.controller.terminate(e); } }, "processBodyError"); - requestBody = async function* () { + requestBody = (async function* () { try { for await (const bytes of request.body.stream) { yield* processBodyChunk(bytes); @@ -11641,7 +11642,7 @@ var require_fetch = __commonJS({ } catch (err) { processBodyError(err); } - }(); + })(); } try { const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody }); @@ -13147,6 +13148,7 @@ var require_sender = __commonJS({ var require_websocket = __commonJS({ "lib/web/websocket/websocket.js"(exports2, module2) { "use strict"; + var { isArrayBuffer } = require("node:util/types"); var { webidl } = require_webidl(); var { URLSerializer } = require_data_url(); var { environmentSettingsObject } = require_util2(); @@ -13166,7 +13168,6 @@ var require_websocket = __commonJS({ var { ByteParser } = require_receiver(); var { kEnumerableProperty } = require_util(); var { getGlobalDispatcher: getGlobalDispatcher2 } = require_global2(); - var { types } = require("node:util"); var { ErrorEvent: ErrorEvent2, CloseEvent: CloseEvent2, createFastMessageEvent: createFastMessageEvent2 } = require_events(); var { SendQueue } = require_sender(); var { WebsocketFrameSend } = require_frame(); @@ -13306,7 +13307,7 @@ var require_websocket = __commonJS({ this.#sendQueue.add(buffer, () => { this.#bufferedAmount -= buffer.byteLength; }, sendHints.text); - } else if (types.isArrayBuffer(data)) { + } else if (isArrayBuffer(data)) { this.#bufferedAmount += data.byteLength; this.#sendQueue.add(data, () => { this.#bufferedAmount -= data.byteLength; @@ -13440,11 +13441,17 @@ var require_websocket = __commonJS({ } fireEvent("open", this); if (channels.open.hasSubscribers) { + const headers = response.headersList.entries; channels.open.publish({ address: response.socket.address(), protocol: this.#protocol, extensions: this.#extensions, - websocket: this + websocket: this, + handshakeResponse: { + status: response.status, + statusText: response.statusText, + headers + } }); } } @@ -13611,7 +13618,7 @@ var require_websocket = __commonJS({ if (webidl.is.Blob(V)) { return V; } - if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) { + if (ArrayBuffer.isView(V) || isArrayBuffer(V)) { return V; } } @@ -13951,10 +13958,10 @@ var require_eventsource = __commonJS({ } url = webidl.converters.USVString(url); eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict, prefix, "eventSourceInitDict"); - this.#dispatcher = eventSourceInitDict.dispatcher; + this.#dispatcher = eventSourceInitDict.node.dispatcher || eventSourceInitDict.dispatcher; this.#state = { lastEventId: "", - reconnectionTime: defaultReconnectionTime + reconnectionTime: eventSourceInitDict.node.reconnectionTime }; const settings = environmentSettingsObject; let urlRecord; @@ -14180,6 +14187,22 @@ var require_eventsource = __commonJS({ key: "dispatcher", // undici only converter: webidl.converters.any + }, + { + key: "node", + // undici only + converter: webidl.dictionaryConverter([ + { + key: "reconnectionTime", + converter: webidl.converters["unsigned long"], + defaultValue: /* @__PURE__ */ __name(() => defaultReconnectionTime, "defaultValue") + }, + { + key: "dispatcher", + converter: webidl.converters.any + } + ]), + defaultValue: /* @__PURE__ */ __name(() => ({}), "defaultValue") } ]); module2.exports = { diff --git a/src/undici_version.h b/src/undici_version.h index 9df0737f163f4a..37a129b9bbab23 100644 --- a/src/undici_version.h +++ b/src/undici_version.h @@ -2,5 +2,5 @@ // Refer to tools/dep_updaters/update-undici.sh #ifndef SRC_UNDICI_VERSION_H_ #define SRC_UNDICI_VERSION_H_ -#define UNDICI_VERSION "7.13.0" +#define UNDICI_VERSION "7.14.0" #endif // SRC_UNDICI_VERSION_H_ From 4276516781579695765b88b1e92b6b0bc0ea5bb1 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 19 Aug 2025 13:59:57 +0200 Subject: [PATCH 079/111] crypto: normalize RsaHashedKeyParams publicExponent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/nodejs/node/issues/59535 PR-URL: https://github.com/nodejs/node/pull/59538 Fixes: https://github.com/nodejs/node/issues/59534 Reviewed-By: Tobias Nießen Reviewed-By: Yagiz Nizipli Reviewed-By: Rafael Gonzaga --- lib/internal/crypto/util.js | 4 +++- test/parallel/test-webcrypto-keygen.js | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 685637ab7435df..3ea8059a3bf8c0 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -397,7 +397,9 @@ const kSupportedAlgorithms = createSupportedAlgorithms(kAlgorithmDefinitions); const simpleAlgorithmDictionaries = { AeadParams: { iv: 'BufferSource', additionalData: 'BufferSource' }, - RsaHashedKeyGenParams: { hash: 'HashAlgorithmIdentifier' }, + // publicExponent is not strictly a BufferSource but it is a Uint8Array that we normalize + // this way + RsaHashedKeyGenParams: { hash: 'HashAlgorithmIdentifier', publicExponent: 'BufferSource' }, EcKeyGenParams: {}, HmacKeyGenParams: { hash: 'HashAlgorithmIdentifier' }, RsaPssParams: {}, diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 95be384c16b046..1b9e4e01404d6c 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -311,14 +311,20 @@ if (hasOpenSSL(3, 5)) { assert.deepStrictEqual(privateKey.usages, privateUsages); assert.strictEqual(publicKey.algorithm.name, name); assert.strictEqual(publicKey.algorithm.modulusLength, modulusLength); - assert.deepStrictEqual(publicKey.algorithm.publicExponent, publicExponent); + assert(publicKey.algorithm.publicExponent instanceof Uint8Array); + assert.notStrictEqual(publicKey.algorithm.publicExponent, publicExponent); + assert(!Buffer.isBuffer(publicKey.algorithm.publicExponent)); + assert.deepStrictEqual(publicKey.algorithm.publicExponent, new Uint8Array(publicExponent)); assert.strictEqual( KeyObject.from(publicKey).asymmetricKeyDetails.publicExponent, bigIntArrayToUnsignedBigInt(publicExponent)); assert.strictEqual(publicKey.algorithm.hash.name, hash); assert.strictEqual(privateKey.algorithm.name, name); assert.strictEqual(privateKey.algorithm.modulusLength, modulusLength); - assert.deepStrictEqual(privateKey.algorithm.publicExponent, publicExponent); + assert(privateKey.algorithm.publicExponent instanceof Uint8Array); + assert.notStrictEqual(privateKey.algorithm.publicExponent, publicExponent); + assert(!Buffer.isBuffer(privateKey.algorithm.publicExponent)); + assert.deepStrictEqual(privateKey.algorithm.publicExponent, new Uint8Array(publicExponent)); assert.strictEqual( KeyObject.from(privateKey).asymmetricKeyDetails.publicExponent, bigIntArrayToUnsignedBigInt(publicExponent)); From 207ffbeb07c7ce264ea6a4dfa2daf1ea343fc443 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 19 Aug 2025 14:02:15 +0200 Subject: [PATCH 080/111] crypto: use CryptoKey internal slots in Web Cryptography MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59538 Fixes: https://github.com/nodejs/node/issues/59535 Fixes: https://github.com/nodejs/node/issues/59534 Reviewed-By: Tobias Nießen Reviewed-By: Yagiz Nizipli Reviewed-By: Rafael Gonzaga --- lib/internal/crypto/aes.js | 9 +-- lib/internal/crypto/cfrg.js | 3 +- lib/internal/crypto/diffiehellman.js | 10 ++-- lib/internal/crypto/ec.js | 3 +- lib/internal/crypto/keys.js | 28 ++++++---- lib/internal/crypto/mac.js | 3 +- lib/internal/crypto/ml_dsa.js | 8 ++- lib/internal/crypto/rsa.js | 18 +++--- lib/internal/crypto/webcrypto.js | 82 +++++++++++++++------------- lib/internal/util/comparisons.js | 11 ++-- 10 files changed, 98 insertions(+), 77 deletions(-) diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index b7d1abf4a85daf..ff2cc108c53e0a 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -47,6 +47,7 @@ const { InternalCryptoKey, SecretKeyObject, createSecretKey, + kAlgorithm, } = require('internal/crypto/keys'); const { @@ -108,7 +109,7 @@ function asyncAesCtrCipher(mode, key, data, algorithm) { mode, key[kKeyObject][kHandle], data, - getVariant('AES-CTR', key.algorithm.length), + getVariant('AES-CTR', key[kAlgorithm].length), algorithm.counter, algorithm.length)); } @@ -119,7 +120,7 @@ function asyncAesCbcCipher(mode, key, data, algorithm) { mode, key[kKeyObject][kHandle], data, - getVariant('AES-CBC', key.algorithm.length), + getVariant('AES-CBC', key[kAlgorithm].length), algorithm.iv)); } @@ -129,7 +130,7 @@ function asyncAesKwCipher(mode, key, data) { mode, key[kKeyObject][kHandle], data, - getVariant('AES-KW', key.algorithm.length))); + getVariant('AES-KW', key[kAlgorithm].length))); } function asyncAesGcmCipher(mode, key, data, algorithm) { @@ -166,7 +167,7 @@ function asyncAesGcmCipher(mode, key, data, algorithm) { mode, key[kKeyObject][kHandle], data, - getVariant('AES-GCM', key.algorithm.length), + getVariant('AES-GCM', key[kAlgorithm].length), algorithm.iv, tag, algorithm.additionalData)); diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index e8af5750a865fd..97272ab1672021 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -47,6 +47,7 @@ const { PublicKeyObject, createPrivateKey, createPublicKey, + kKeyType, } = require('internal/crypto/keys'); const generateKeyPair = promisify(_generateKeyPair); @@ -343,7 +344,7 @@ function eddsaSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key.type !== type) + if (key[kKeyType] !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); return jobPromise(() => new SignJob( diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 24ff71fca83f31..1712996f670dbe 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -51,6 +51,8 @@ const { const { KeyObject, + kAlgorithm, + kKeyType, } = require('internal/crypto/keys'); const { @@ -325,20 +327,20 @@ let masks; async function ecdhDeriveBits(algorithm, baseKey, length) { const { 'public': key } = algorithm; - if (baseKey.type !== 'private') { + if (baseKey[kKeyType] !== 'private') { throw lazyDOMException( 'baseKey must be a private key', 'InvalidAccessError'); } - if (key.algorithm.name !== baseKey.algorithm.name) { + if (key[kAlgorithm].name !== baseKey[kAlgorithm].name) { throw lazyDOMException( 'The public and private keys must be of the same type', 'InvalidAccessError'); } if ( - key.algorithm.name === 'ECDH' && - key.algorithm.namedCurve !== baseKey.algorithm.namedCurve + key[kAlgorithm].name === 'ECDH' && + key[kAlgorithm].namedCurve !== baseKey[kAlgorithm].namedCurve ) { throw lazyDOMException('Named curve mismatch', 'InvalidAccessError'); } diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index f4ea317b86ee73..52791412835300 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -41,6 +41,7 @@ const { PublicKeyObject, createPrivateKey, createPublicKey, + kKeyType, } = require('internal/crypto/keys'); const generateKeyPair = promisify(_generateKeyPair); @@ -284,7 +285,7 @@ function ecdsaSignVerify(key, data, { name, hash }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key.type !== type) + if (key[kKeyType] !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); const hashname = normalizeHashName(hash.name); diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index c34f36a277ea37..c99bc9cc1dd6dd 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -217,7 +217,7 @@ const { throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - if (result.usages.length === 0) { + if (result[kKeyUsages].length === 0) { throw lazyDOMException( `Usages cannot be empty when importing a ${result.type} key.`, 'SyntaxError'); @@ -309,7 +309,7 @@ const { throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - if (result.type === 'private' && result.usages.length === 0) { + if (result.type === 'private' && result[kKeyUsages].length === 0) { throw lazyDOMException( `Usages cannot be empty when importing a ${result.type} key.`, 'SyntaxError'); @@ -735,8 +735,8 @@ function prepareSecretKey(key, encoding, bufferOnly = false) { throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret'); return key[kHandle]; } else if (isCryptoKey(key)) { - if (key.type !== 'secret') - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret'); + if (key[kKeyType] !== 'secret') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key[kKeyType], 'secret'); return key[kKeyObject][kHandle]; } } @@ -785,7 +785,7 @@ function createPrivateKey(key) { } function isKeyObject(obj) { - return obj != null && obj[kKeyType] !== undefined; + return obj != null && obj[kKeyType] !== undefined && obj[kKeyObject] === undefined; } // Our implementation of CryptoKey is a simple wrapper around a KeyObject @@ -809,17 +809,21 @@ class CryptoKey { }; return `CryptoKey ${inspect({ - type: this.type, - extractable: this.extractable, - algorithm: this.algorithm, - usages: this.usages, + type: this[kKeyType], + extractable: this[kExtractable], + algorithm: this[kAlgorithm], + usages: this[kKeyUsages], }, opts)}`; } + get [kKeyType]() { + return this[kKeyObject].type; + } + get type() { if (!(this instanceof CryptoKey)) throw new ERR_INVALID_THIS('CryptoKey'); - return this[kKeyObject].type; + return this[kKeyType]; } get extractable() { @@ -1008,4 +1012,8 @@ module.exports = { isKeyObject, isCryptoKey, importGenericSecretKey, + kAlgorithm, + kExtractable, + kKeyType, + kKeyUsages, }; diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 4a78c45c70d84d..ed30a64ba239ea 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -36,6 +36,7 @@ const { InternalCryptoKey, SecretKeyObject, createSecretKey, + kAlgorithm, } = require('internal/crypto/keys'); const generateKey = promisify(_generateKey); @@ -161,7 +162,7 @@ function hmacSignVerify(key, data, algorithm, signature) { return jobPromise(() => new HmacJob( kCryptoJobAsync, mode, - normalizeHashName(key.algorithm.hash.name), + normalizeHashName(key[kAlgorithm].hash.name), key[kKeyObject][kHandle], data, signature)); diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 21dc95786b10e1..3dabe5743b9a99 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -51,6 +51,8 @@ const { PublicKeyObject, createPrivateKey, createPublicKey, + kAlgorithm, + kKeyType, } = require('internal/crypto/keys'); const generateKeyPair = promisify(_generateKeyPair); @@ -116,7 +118,7 @@ function mlDsaExportKey(key, format) { try { switch (format) { case kWebCryptoKeyFormatRaw: { - if (key.type === 'private') { + if (key[kKeyType] === 'private') { const { priv } = key[kKeyObject][kHandle].exportJwk({}, false); return Buffer.alloc(32, priv, 'base64url').buffer; } @@ -136,7 +138,7 @@ function mlDsaExportKey(key, format) { 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x00, 0x04, 0x22, 0x80, 0x20, ], 0); - switch (key.algorithm.name) { + switch (key[kAlgorithm].name) { case 'ML-DSA-44': buffer.set([0x11], 17); break; @@ -292,7 +294,7 @@ function mlDsaSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key.type !== type) + if (key[kKeyType] !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); return jobPromise(() => new SignJob( diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index dd0669df4ed032..ecb3759ba48354 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -50,6 +50,8 @@ const { PublicKeyObject, createPublicKey, createPrivateKey, + kAlgorithm, + kKeyType, } = require('internal/crypto/keys'); const { @@ -95,7 +97,7 @@ function rsaOaepCipher(mode, key, data, algorithm) { validateRsaOaepAlgorithm(algorithm); const type = mode === kWebCryptoCipherEncrypt ? 'public' : 'private'; - if (key.type !== type) { + if (key[kKeyType] !== type) { throw lazyDOMException( 'The requested operation is not valid for the provided key', 'InvalidAccessError'); @@ -107,7 +109,7 @@ function rsaOaepCipher(mode, key, data, algorithm) { key[kKeyObject][kHandle], data, kKeyVariantRSA_OAEP, - normalizeHashName(key.algorithm.hash.name), + normalizeHashName(key[kAlgorithm].hash.name), algorithm.label)); } @@ -201,7 +203,7 @@ function rsaExportKey(key, format) { kCryptoJobAsync, format, key[kKeyObject][kHandle], - kRsaVariants[key.algorithm.name])); + kRsaVariants[key[kAlgorithm].name])); } function rsaImportKey( @@ -329,16 +331,16 @@ function rsaSignVerify(key, data, { saltLength }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key.type !== type) + if (key[kKeyType] !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); return jobPromise(() => { - if (key.algorithm.name === 'RSA-PSS') { + if (key[kAlgorithm].name === 'RSA-PSS') { validateInt32( saltLength, 'algorithm.saltLength', 0, - MathCeil((key.algorithm.modulusLength - 1) / 8) - getDigestSizeInBytes(key.algorithm.hash.name) - 2); + MathCeil((key[kAlgorithm].modulusLength - 1) / 8) - getDigestSizeInBytes(key[kAlgorithm].hash.name) - 2); } return new SignJob( @@ -349,9 +351,9 @@ function rsaSignVerify(key, data, { saltLength }, signature) { undefined, undefined, data, - normalizeHashName(key.algorithm.hash.name), + normalizeHashName(key[kAlgorithm].hash.name), saltLength, - key.algorithm.name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined, + key[kAlgorithm].name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined, undefined, signature); }); diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 5961ee3e67dbf7..6c2407cdca3445 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -33,6 +33,10 @@ const { createPublicKey, CryptoKey, importGenericSecretKey, + kAlgorithm, + kKeyUsages, + kExtractable, + kKeyType, } = require('internal/crypto/keys'); const { @@ -175,9 +179,9 @@ async function generateKey( if ( (resultType === 'CryptoKey' && - (result.type === 'secret' || result.type === 'private') && - result.usages.length === 0) || - (resultType === 'CryptoKeyPair' && result.privateKey.usages.length === 0) + (result[kKeyType] === 'secret' || result[kKeyType] === 'private') && + result[kKeyUsages].length === 0) || + (resultType === 'CryptoKeyPair' && result.privateKey[kKeyUsages].length === 0) ) { throw lazyDOMException( 'Usages cannot be empty when creating a key.', @@ -209,12 +213,12 @@ async function deriveBits(algorithm, baseKey, length = null) { } algorithm = normalizeAlgorithm(algorithm, 'deriveBits'); - if (!ArrayPrototypeIncludes(baseKey.usages, 'deriveBits')) { + if (!ArrayPrototypeIncludes(baseKey[kKeyUsages], 'deriveBits')) { throw lazyDOMException( 'baseKey does not have deriveBits usage', 'InvalidAccessError'); } - if (baseKey.algorithm.name !== algorithm.name) + if (baseKey[kAlgorithm].name !== algorithm.name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); switch (algorithm.name) { case 'X25519': @@ -296,12 +300,12 @@ async function deriveKey( algorithm = normalizeAlgorithm(algorithm, 'deriveBits'); derivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm, 'importKey'); - if (!ArrayPrototypeIncludes(baseKey.usages, 'deriveKey')) { + if (!ArrayPrototypeIncludes(baseKey[kKeyUsages], 'deriveKey')) { throw lazyDOMException( 'baseKey does not have deriveKey usage', 'InvalidAccessError'); } - if (baseKey.algorithm.name !== algorithm.name) + if (baseKey[kAlgorithm].name !== algorithm.name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); const length = getKeyLength(normalizeAlgorithm(arguments[2], 'get key length')); @@ -335,7 +339,7 @@ async function deriveKey( } async function exportKeySpki(key) { - switch (key.algorithm.name) { + switch (key[kAlgorithm].name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': @@ -371,7 +375,7 @@ async function exportKeySpki(key) { } async function exportKeyPkcs8(key) { - switch (key.algorithm.name) { + switch (key[kAlgorithm].name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': @@ -407,7 +411,7 @@ async function exportKeyPkcs8(key) { } async function exportKeyRawPublic(key, format) { - switch (key.algorithm.name) { + switch (key[kAlgorithm].name) { case 'ECDSA': // Fall through case 'ECDH': @@ -440,7 +444,7 @@ async function exportKeyRawPublic(key, format) { } async function exportKeyRawSeed(key) { - switch (key.algorithm.name) { + switch (key[kAlgorithm].name) { case 'ML-DSA-44': // Fall through case 'ML-DSA-65': @@ -455,7 +459,7 @@ async function exportKeyRawSeed(key) { } async function exportKeyRawSecret(key, format) { - switch (key.algorithm.name) { + switch (key[kAlgorithm].name) { case 'AES-CTR': // Fall through case 'AES-CBC': @@ -478,27 +482,27 @@ async function exportKeyRawSecret(key, format) { async function exportKeyJWK(key) { const parameters = { - key_ops: key.usages, - ext: key.extractable, + key_ops: key[kKeyUsages], + ext: key[kExtractable], }; - switch (key.algorithm.name) { + switch (key[kAlgorithm].name) { case 'RSASSA-PKCS1-v1_5': { const alg = normalizeHashName( - key.algorithm.hash.name, + key[kAlgorithm].hash.name, normalizeHashName.kContextJwkRsa); if (alg) parameters.alg = alg; break; } case 'RSA-PSS': { const alg = normalizeHashName( - key.algorithm.hash.name, + key[kAlgorithm].hash.name, normalizeHashName.kContextJwkRsaPss); if (alg) parameters.alg = alg; break; } case 'RSA-OAEP': { const alg = normalizeHashName( - key.algorithm.hash.name, + key[kAlgorithm].hash.name, normalizeHashName.kContextJwkRsaOaep); if (alg) parameters.alg = alg; break; @@ -520,7 +524,7 @@ async function exportKeyJWK(key) { case 'Ed25519': // Fall through case 'Ed448': - parameters.alg = key.algorithm.name; + parameters.alg = key[kAlgorithm].name; break; case 'AES-CTR': // Fall through @@ -530,14 +534,14 @@ async function exportKeyJWK(key) { // Fall through case 'AES-KW': parameters.alg = require('internal/crypto/aes') - .getAlgorithmName(key.algorithm.name, key.algorithm.length); + .getAlgorithmName(key[kAlgorithm].name, key[kAlgorithm].length); break; case 'ChaCha20-Poly1305': parameters.alg = 'C20P'; break; case 'HMAC': { const alg = normalizeHashName( - key.algorithm.hash.name, + key[kAlgorithm].hash.name, normalizeHashName.kContextJwkHmac); if (alg) parameters.alg = alg; break; @@ -565,25 +569,25 @@ async function exportKey(format, key) { }); try { - normalizeAlgorithm(key.algorithm, 'exportKey'); + normalizeAlgorithm(key[kAlgorithm], 'exportKey'); } catch { throw lazyDOMException( - `${key.algorithm.name} key export is not supported`, 'NotSupportedError'); + `${key[kAlgorithm].name} key export is not supported`, 'NotSupportedError'); } - if (!key.extractable) + if (!key[kExtractable]) throw lazyDOMException('key is not extractable', 'InvalidAccessException'); let result; switch (format) { case 'spki': { - if (key.type === 'public') { + if (key[kKeyType] === 'public') { result = await exportKeySpki(key); } break; } case 'pkcs8': { - if (key.type === 'private') { + if (key[kKeyType] === 'private') { result = await exportKeyPkcs8(key); } break; @@ -593,27 +597,27 @@ async function exportKey(format, key) { break; } case 'raw-secret': { - if (key.type === 'secret') { + if (key[kKeyType] === 'secret') { result = await exportKeyRawSecret(key, format); } break; } case 'raw-public': { - if (key.type === 'public') { + if (key[kKeyType] === 'public') { result = await exportKeyRawPublic(key, format); } break; } case 'raw-seed': { - if (key.type === 'private') { + if (key[kKeyType] === 'private') { result = await exportKeyRawSeed(key); } break; } case 'raw': { - if (key.type === 'secret') { + if (key[kKeyType] === 'secret') { result = await exportKeyRawSecret(key, format); - } else if (key.type === 'public') { + } else if (key[kKeyType] === 'public') { result = await exportKeyRawPublic(key, format); } break; @@ -622,7 +626,7 @@ async function exportKey(format, key) { if (!result) { throw lazyDOMException( - `Unable to export ${key.algorithm.name} ${key.type} key using ${format} format`, + `Unable to export ${key[kAlgorithm].name} ${key[kKeyType]} key using ${format} format`, 'NotSupportedError'); } @@ -749,7 +753,7 @@ async function importKey( 'NotSupportedError'); } - if ((result.type === 'secret' || result.type === 'private') && result.usages.length === 0) { + if ((result.type === 'secret' || result.type === 'private') && result[kKeyUsages].length === 0) { throw lazyDOMException( `Usages cannot be empty when importing a ${result.type} key.`, 'SyntaxError'); @@ -897,8 +901,8 @@ function signVerify(algorithm, key, data, signature) { } algorithm = normalizeAlgorithm(algorithm, usage); - if (!ArrayPrototypeIncludes(key.usages, usage) || - algorithm.name !== key.algorithm.name) { + if (!ArrayPrototypeIncludes(key[kKeyUsages], usage) || + algorithm.name !== key[kAlgorithm].name) { throw lazyDOMException( `Unable to use this key to ${usage}`, 'InvalidAccessError'); @@ -987,8 +991,8 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { // in this case. Both Firefox and Chrome throw simple TypeErrors here. // The key algorithm and cipher algorithm must match, and the // key must have the proper usage. - if (key.algorithm.name !== algorithm.name || - !ArrayPrototypeIncludes(key.usages, op)) { + if (key[kAlgorithm].name !== algorithm.name || + !ArrayPrototypeIncludes(key[kKeyUsages], op)) { throw lazyDOMException( 'The requested operation is not valid for the provided key', 'InvalidAccessError'); @@ -1085,12 +1089,12 @@ async function getPublicKey(key, keyUsages) { context: '2nd argument', }); - if (key.type !== 'private') + if (key[kKeyType] !== 'private') throw lazyDOMException('key must be a private key', 'InvalidAccessError'); const keyObject = createPublicKey(key[kKeyObject]); - return keyObject.toCryptoKey(key.algorithm, true, keyUsages); + return keyObject.toCryptoKey(key[kAlgorithm], true, keyUsages); } // The SubtleCrypto and Crypto classes are defined as part of the diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index 178c6aceeff7d7..398d6b43494fe6 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -135,8 +135,6 @@ const kIsArray = 1; const kIsSet = 2; const kIsMap = 3; -let kKeyObject; - // Check if they have the same source and flags function areSimilarRegExps(a, b) { return a.source === b.source && @@ -395,11 +393,12 @@ function objectComparisonStart(val1, val2, mode, memos) { return false; } } else if (isCryptoKey(val1)) { - kKeyObject ??= require('internal/crypto/util').kKeyObject; + const { kKeyObject } = require('internal/crypto/util'); + const { kExtractable, kAlgorithm, kKeyUsages } = require('internal/crypto/keys'); if (!isCryptoKey(val2) || - val1.extractable !== val2.extractable || - !innerDeepEqual(val1.algorithm, val2.algorithm, mode, memos) || - !innerDeepEqual(val1.usages, val2.usages, mode, memos) || + val1[kExtractable] !== val2[kExtractable] || + !innerDeepEqual(val1[kAlgorithm], val2[kAlgorithm], mode, memos) || + !innerDeepEqual(val1[kKeyUsages], val2[kKeyUsages], mode, memos) || !innerDeepEqual(val1[kKeyObject], val2[kKeyObject], mode, memos) ) { return false; From 2d05c046db21adac40753ab8795a1ee28c69a669 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 19 Aug 2025 14:02:53 +0200 Subject: [PATCH 081/111] crypto: return cached copies from CryptoKey algorithm and usages getters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/nodejs/node/issues/59534 PR-URL: https://github.com/nodejs/node/pull/59538 Fixes: https://github.com/nodejs/node/issues/59535 Reviewed-By: Tobias Nießen Reviewed-By: Yagiz Nizipli Reviewed-By: Rafael Gonzaga --- lib/internal/crypto/keys.js | 13 ++++++++++-- .../test-webcrypto-internal-slots.mjs | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-webcrypto-internal-slots.mjs diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index c99bc9cc1dd6dd..fdaa93a7f8fb5b 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -1,6 +1,7 @@ 'use strict'; const { + ArrayFrom, ArrayPrototypeSlice, ObjectDefineProperties, ObjectDefineProperty, @@ -81,6 +82,8 @@ const kAlgorithm = Symbol('kAlgorithm'); const kExtractable = Symbol('kExtractable'); const kKeyType = Symbol('kKeyType'); const kKeyUsages = Symbol('kKeyUsages'); +const kCachedAlgorithm = Symbol('kCachedAlgorithm'); +const kCachedKeyUsages = Symbol('kCachedKeyUsages'); // Key input contexts. const kConsumePublic = 0; @@ -835,13 +838,19 @@ class CryptoKey { get algorithm() { if (!(this instanceof CryptoKey)) throw new ERR_INVALID_THIS('CryptoKey'); - return this[kAlgorithm]; + if (!this[kCachedAlgorithm]) { + this[kCachedAlgorithm] ??= { ...this[kAlgorithm] }; + this[kCachedAlgorithm].hash &&= { ...this[kCachedAlgorithm].hash }; + this[kCachedAlgorithm].publicExponent &&= new Uint8Array(this[kCachedAlgorithm].publicExponent); + } + return this[kCachedAlgorithm]; } get usages() { if (!(this instanceof CryptoKey)) throw new ERR_INVALID_THIS('CryptoKey'); - return this[kKeyUsages]; + this[kCachedKeyUsages] ??= ArrayFrom(this[kKeyUsages]); + return this[kCachedKeyUsages]; } } diff --git a/test/parallel/test-webcrypto-internal-slots.mjs b/test/parallel/test-webcrypto-internal-slots.mjs new file mode 100644 index 00000000000000..9b824167ba4553 --- /dev/null +++ b/test/parallel/test-webcrypto-internal-slots.mjs @@ -0,0 +1,21 @@ +import * as common from '../common/index.mjs'; + +if (!common.hasCrypto) + common.skip('missing crypto'); + +import * as assert from 'node:assert'; +import * as util from 'node:util'; + +const { subtle } = globalThis.crypto; + +const kp = await subtle.generateKey('Ed25519', true, ['sign', 'verify']); +assert.notStrictEqual(kp.publicKey.algorithm, kp.privateKey.algorithm); +assert.notStrictEqual(kp.publicKey.usages, kp.privateKey.usages); +kp.publicKey.algorithm.name = 'ed25519'; +assert.strictEqual(kp.publicKey.algorithm.name, 'ed25519'); +kp.publicKey.usages.push('foo'); +assert.ok(kp.publicKey.usages.includes('foo')); +assert.ok(util.inspect(kp.publicKey).includes("algorithm: { name: 'Ed25519' }")); +assert.ok(util.inspect(kp.publicKey).includes("usages: [ 'verify' ]")); + +await subtle.sign('Ed25519', kp.privateKey, Buffer.alloc(32)); From 4bf6ed0bf5914f026230d89333890d314f0d117c Mon Sep 17 00:00:00 2001 From: PhistucK Date: Thu, 21 Aug 2025 15:55:34 +0100 Subject: [PATCH 082/111] doc: fix typos in `environment_variables.md` PR-URL: https://github.com/nodejs/node/pull/59536 Reviewed-By: Antoine du Hamel Reviewed-By: Luigi Pinca --- doc/api/environment_variables.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/environment_variables.md b/doc/api/environment_variables.md index 4878eeaa9e6dcd..747d8313c4b7fa 100644 --- a/doc/api/environment_variables.md +++ b/doc/api/environment_variables.md @@ -96,10 +96,10 @@ For example: MY_VAR_B = ' my variable b ' ``` -will be treaded identically to: +will be treated identically to: ```text -MY_VAR_A = my variable +MY_VAR_A = my variable a MY_VAR_B = ' my variable b ' ``` From 0e8bc2c7ac2a8e3e9543ca77b092195b62e3a892 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 21 Aug 2025 11:38:09 -0800 Subject: [PATCH 083/111] test: rename test-net-server-drop-connections-in-cluster.js to -http- PR-URL: https://github.com/nodejs/node/pull/59532 Reviewed-By: Luigi Pinca Reviewed-By: Joyee Cheung --- ...cluster.js => test-http-server-drop-connections-in-cluster.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/parallel/{test-net-server-drop-connections-in-cluster.js => test-http-server-drop-connections-in-cluster.js} (100%) diff --git a/test/parallel/test-net-server-drop-connections-in-cluster.js b/test/parallel/test-http-server-drop-connections-in-cluster.js similarity index 100% rename from test/parallel/test-net-server-drop-connections-in-cluster.js rename to test/parallel/test-http-server-drop-connections-in-cluster.js From baa22893bb1a5d6d3fd76ea4cca06a6c9d8761ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=B1=EC=9A=B0=ED=98=84=20=7C=20Woohyun=20Sung?= <87058570+whsung0330@users.noreply.github.com> Date: Fri, 22 Aug 2025 05:39:21 +0900 Subject: [PATCH 084/111] typings: add missing URLBinding methods PR-URL: https://github.com/nodejs/node/pull/59468 Reviewed-By: Daeyeon Jeong Reviewed-By: Yagiz Nizipli Reviewed-By: Luigi Pinca --- typings/internalBinding/url.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typings/internalBinding/url.d.ts b/typings/internalBinding/url.d.ts index b2e1a92ade9141..5ad5932d091f33 100644 --- a/typings/internalBinding/url.d.ts +++ b/typings/internalBinding/url.d.ts @@ -10,4 +10,6 @@ export interface URLBinding { format(input: string, fragment?: boolean, unicode?: boolean, search?: boolean, auth?: boolean): string; parse(input: string, base?: string): string | false; update(input: string, actionType: typeof urlUpdateActions, value: string): string | false; + getOrigin(input: string): string; + pathToFileURL(input: string, isWindows: boolean, hostname?: string): string; } From cf84fffea524bc9fe78642a80f20f9afc468da3a Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Thu, 21 Aug 2025 19:30:30 -0400 Subject: [PATCH 085/111] doc: link to `TypedArray.from()` in signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59226 Reviewed-By: Luigi Pinca Reviewed-By: Ulises Gascón Reviewed-By: Tierney Cyren Reviewed-By: Zeyu "Alex" Yang Reviewed-By: Qingyu Deng --- doc/api/buffer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/buffer.md b/doc/api/buffer.md index c6c90b0bd29cce..70e32a6c998216 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -405,7 +405,7 @@ implementations. Specifically, the {TypedArray} variants accept a second argument that is a mapping function that is invoked on every element of the typed array: -* `TypedArray.from(source[, mapFn[, thisArg]])` +* [`TypedArray.from(source[, mapFn[, thisArg]])`][`TypedArray.from()`] The `Buffer.from()` method, however, does not support the use of a mapping function: From f78f47ca5a489e89e3fc499ff61241a88070f008 Mon Sep 17 00:00:00 2001 From: Pietro Marchini Date: Fri, 22 Aug 2025 11:24:31 +0200 Subject: [PATCH 086/111] test: support standalone env comment in tests PR-URL: https://github.com/nodejs/node/pull/59546 Reviewed-By: Marco Ippolito Reviewed-By: Chemi Atlow --- test/common/index.js | 60 ++++++++++++---------- test/parallel/test-parse-test-only-envs.js | 20 ++++++++ 2 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 test/parallel/test-parse-test-only-envs.js diff --git a/test/common/index.js b/test/common/index.js index 67323cd21fa846..0f06c480ff1556 100755 --- a/test/common/index.js +++ b/test/common/index.js @@ -116,34 +116,38 @@ if (process.argv.length === 2 && require('cluster').isPrimary && fs.existsSync(process.argv[1])) { const { flags, envs } = parseTestMetadata(); - for (const flag of flags) { - if (!process.execArgv.includes(flag) && - // If the binary is build without `intl` the inspect option is - // invalid. The test itself should handle this case. - (process.features.inspector || !flag.startsWith('--inspect'))) { - console.log( - 'NOTE: The test started as a child_process using these flags:', - inspect(flags), - 'And these environment variables:', - inspect(envs), - 'Use NODE_SKIP_FLAG_CHECK to run the test with the original flags.', - ); - const { spawnSync } = require('child_process'); - const args = [...flags, ...process.execArgv, ...process.argv.slice(1)]; - const options = { - encoding: 'utf8', - stdio: 'inherit', - env: { - ...process.env, - ...envs, - }, - }; - const result = spawnSync(process.execPath, args, options); - if (result.signal) { - process.kill(0, result.signal); - } else { - process.exit(result.status); - } + + const flagsTriggerSpawn = flags.some((flag) => ( + !process.execArgv.includes(flag) && + // If the binary is build without `intl` the inspect option is + // invalid. The test itself should handle this case. + (process.features.inspector || !flag.startsWith('--inspect')) + )); + const envsTriggerSpawn = Object.keys(envs).some((key) => process.env[key] !== envs[key]); + + if (flagsTriggerSpawn || envsTriggerSpawn) { + console.log( + 'NOTE: The test started as a child_process using these flags:', + inspect(flags), + 'And these environment variables:', + inspect(envs), + 'Use NODE_SKIP_FLAG_CHECK to run the test with the original flags.', + ); + const { spawnSync } = require('child_process'); + const args = [...flags, ...process.execArgv, ...process.argv.slice(1)]; + const options = { + encoding: 'utf8', + stdio: 'inherit', + env: { + ...process.env, + ...envs, + }, + }; + const result = spawnSync(process.execPath, args, options); + if (result.signal) { + process.kill(0, result.signal); + } else { + process.exit(result.status); } } } diff --git a/test/parallel/test-parse-test-only-envs.js b/test/parallel/test-parse-test-only-envs.js new file mode 100644 index 00000000000000..0eafec719325db --- /dev/null +++ b/test/parallel/test-parse-test-only-envs.js @@ -0,0 +1,20 @@ +'use strict'; + +// Env: A_SET_ENV_VAR=A_SET_ENV_VAR_VALUE B_SET_ENV_VAR=B_SET_ENV_VAR_VALUE + +require('../common'); +const assert = require('node:assert'); +const { describe, it } = require('node:test'); + + +// This test verifies that a test file that requires 'common' can set environment variables +// via comments in the test file, similar to how we set flags via comments. +// Ref: https://github.com/nodejs/node/issues/58179 +describe('env var via comment', () => { + it('should set env var A_SET_ENV_VAR', () => { + assert.strictEqual(process.env.A_SET_ENV_VAR, 'A_SET_ENV_VAR_VALUE'); + }); + it('should set env var B_SET_ENV_VAR', () => { + assert.strictEqual(process.env.B_SET_ENV_VAR, 'B_SET_ENV_VAR_VALUE'); + }); +}); From 99128d9244163d20486fe3d6478564cabe154c2d Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Fri, 22 Aug 2025 11:57:45 +0100 Subject: [PATCH 087/111] node-api: link to other programming language bindings PR-URL: https://github.com/nodejs/node/pull/59516 Refs: https://github.com/nodejs/node/pull/59498 Reviewed-By: Vladimir Morozov Reviewed-By: Luigi Pinca --- doc/api/n-api.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/doc/api/n-api.md b/doc/api/n-api.md index bbe0fd550e572f..6b83ca6f4c2ad8 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -33,23 +33,28 @@ properties: using `napi_get_last_error_info`. More information can be found in the error handling section [Error handling][]. +## Writing addons in various programming languages + Node-API is a C API that ensures ABI stability across Node.js versions -and different compiler levels. A C++ API can be easier to use. -To support using C++, the project maintains a -C++ wrapper module called [`node-addon-api`][]. -This wrapper provides an inlinable C++ API. Binaries built -with `node-addon-api` will depend on the symbols for the Node-API C-based -functions exported by Node.js. `node-addon-api` is a more -efficient way to write code that calls Node-API. Take, for example, the -following `node-addon-api` code. The first section shows the -`node-addon-api` code and the second section shows what actually gets -used in the addon. +and different compiler levels. With this stability guarantee, it is possible +to write addons in other programming languages on top of Node-API. Refer +to [language and engine bindings][] for more programming languages and engines +support details. + +[`node-addon-api`][] is the official C++ binding that provides a more efficient way to +write C++ code that calls Node-API. This wrapper is a header-only library that offers an inlinable C++ API. +Binaries built with `node-addon-api` will depend on the symbols of the Node-API +C-based functions exported by Node.js. The following code snippet is an example +of `node-addon-api`: ```cpp Object obj = Object::New(env); obj["foo"] = String::New(env, "bar"); ``` +The above `node-addon-api` C++ code is equivalent to the following C-based +Node-API code: + ```cpp napi_status status; napi_value object, string; @@ -72,8 +77,9 @@ if (status != napi_ok) { } ``` -The end result is that the addon only uses the exported C APIs. As a result, -it still gets the benefits of the ABI stability provided by the C API. +The end result is that the addon only uses the exported C APIs. Even though +the addon is written in C++, it still gets the benefits of the ABI stability +provided by the C Node-API. When using `node-addon-api` instead of the C APIs, start with the API [docs][] for `node-addon-api`. @@ -6887,6 +6893,7 @@ the add-on's file name during loading. [externals]: #napi_create_external [global scope]: globals.md [gyp-next]: https://github.com/nodejs/gyp-next +[language and engine bindings]: https://github.com/nodejs/abi-stable-node/blob/doc/node-api-engine-bindings.md [module scope]: modules.md#the-module-scope [node-gyp]: https://github.com/nodejs/node-gyp [node-pre-gyp]: https://github.com/mapbox/node-pre-gyp From 93a368df046fa35b641db1d645b5029fc588efa5 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 22 Aug 2025 13:07:50 +0200 Subject: [PATCH 088/111] src: use simdjson to parse --snapshot-config PR-URL: https://github.com/nodejs/node/pull/59473 Refs: https://github.com/nodejs/node/issues/59288 Reviewed-By: Daniel Lemire --- src/node_snapshotable.cc | 65 +++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index 9bdef1032e4dc9..c2e24b4645e790 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -11,7 +11,6 @@ #include "embedded_data.h" #include "encoding_binding.h" #include "env-inl.h" -#include "json_parser.h" #include "node_blob.h" #include "node_builtins.h" #include "node_contextify.h" @@ -27,6 +26,7 @@ #include "node_url.h" #include "node_v8.h" #include "node_v8_platform-inl.h" +#include "simdjson.h" #include "timers.h" #if HAVE_INSPECTOR @@ -909,32 +909,61 @@ std::optional ReadSnapshotConfig(const char* config_path) { return std::nullopt; } - JSONParser parser; - if (!parser.Parse(config_content)) { - FPrintF(stderr, "Cannot parse JSON from %s\n", config_path); - return std::nullopt; - } - SnapshotConfig result; - result.builder_script_path = parser.GetTopLevelStringField("builder"); - if (!result.builder_script_path.has_value()) { + + simdjson::ondemand::parser parser; + simdjson::ondemand::document document; + simdjson::ondemand::object main_object; + simdjson::error_code error = + parser.iterate(simdjson::pad(config_content)).get(document); + + if (!error) { + error = document.get_object().get(main_object); + } + if (error) { FPrintF(stderr, - "\"builder\" field of %s is not a non-empty string\n", - config_path); + "Cannot parse JSON from %s: %s\n", + config_path, + simdjson::error_message(error)); return std::nullopt; } - std::optional WithoutCodeCache = - parser.GetTopLevelBoolField("withoutCodeCache"); - if (!WithoutCodeCache.has_value()) { + for (auto field : main_object) { + std::string_view key; + if (field.unescaped_key().get(key)) { + FPrintF(stderr, "Cannot read key from %s\n", config_path); + return std::nullopt; + } + if (key == "builder") { + std::string builder_path; + if (field.value().get_string().get(builder_path) || + builder_path.empty()) { + FPrintF(stderr, + "\"builder\" field of %s is not a non-empty string\n", + config_path); + return std::nullopt; + } + result.builder_script_path = builder_path; + } else if (key == "withoutCodeCache") { + bool without_code_cache_value = false; + if (field.value().get_bool().get(without_code_cache_value)) { + FPrintF(stderr, + "\"withoutCodeCache\" field of %s is not a boolean\n", + config_path); + return std::nullopt; + } + if (without_code_cache_value) { + result.flags |= SnapshotFlags::kWithoutCodeCache; + } + } + } + + if (!result.builder_script_path.has_value()) { FPrintF(stderr, - "\"withoutCodeCache\" field of %s is not a boolean\n", + "\"builder\" field of %s is not a non-empty string\n", config_path); return std::nullopt; } - if (WithoutCodeCache.value()) { - result.flags |= SnapshotFlags::kWithoutCodeCache; - } return result; } From e2b6bdc640a0bc5aad7eb63b091856ae03a4f123 Mon Sep 17 00:00:00 2001 From: Edy Silva Date: Fri, 22 Aug 2025 09:56:58 -0300 Subject: [PATCH 089/111] sqlite: handle ?NNN parameters as positional PR-URL: https://github.com/nodejs/node/pull/59350 Reviewed-By: Zeyu "Alex" Yang --- src/node_sqlite.cc | 4 ++- test/parallel/test-sqlite-statement-sync.js | 30 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 195527830ddd38..5c0179e2ba92c5 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -1933,7 +1933,9 @@ bool StatementSync::BindParams(const FunctionCallbackInfo& args) { } for (int i = anon_start; i < args.Length(); ++i) { - while (sqlite3_bind_parameter_name(statement_, anon_idx) != nullptr) { + while (1) { + const char* param = sqlite3_bind_parameter_name(statement_, anon_idx); + if (param == nullptr || param[0] == '?') break; anon_idx++; } diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index 858a1486601763..04494a02c692a8 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -240,6 +240,36 @@ suite('StatementSync.prototype.run()', () => { stmt.run({ k: 3, v: 30 }), { changes: 1, lastInsertRowid: 3 } ); }); + + test('SQLite defaults unbound ?NNN parameters', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?1, ?3)'); + + t.assert.throws(() => { + stmt.run(1); + }, { + code: 'ERR_SQLITE_ERROR', + message: 'NOT NULL constraint failed: data.val', + errcode: 1299, + errstr: 'constraint failed', + }); + }); + + test('binds ?NNN params by position', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?1, ?2)'); + t.assert.deepStrictEqual(stmt.run(1, 2), { changes: 1, lastInsertRowid: 1 }); + }); }); suite('StatementSync.prototype.sourceSQL', () => { From 389a24bbff9c16a4e3a71774d6c2392c5ca0ae76 Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Fri, 22 Aug 2025 13:57:07 +0100 Subject: [PATCH 090/111] module: allow overriding linked requests for a ModuleWrap This allows overriding linked requests for a `ModuleWrap`. The `statusOverride` in `vm.SourceTextModule` could call `moduleWrap.link` a second time when `statusOverride` of `linking` is set to undefined. Overriding of linked requests should be no harm but better to be avoided. However, this will require a follow-up fix on `statusOverride` in `vm.SourceTextModule`. PR-URL: https://github.com/nodejs/node/pull/59527 Fixes: https://github.com/nodejs/node/issues/59480 Reviewed-By: Joyee Cheung Reviewed-By: Marco Ippolito --- src/module_wrap.cc | 10 --- .../test-vm-module-link-shared-deps.js | 82 +++++++++++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 test/parallel/test-vm-module-link-shared-deps.js diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 28843f6206e62a..72d910fa4a8144 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -604,7 +604,6 @@ void ModuleWrap::GetModuleRequests(const FunctionCallbackInfo& args) { // moduleWrap.link(moduleWraps) void ModuleWrap::Link(const FunctionCallbackInfo& args) { - Realm* realm = Realm::GetCurrent(args); Isolate* isolate = args.GetIsolate(); ModuleWrap* dependent; @@ -612,15 +611,6 @@ void ModuleWrap::Link(const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 1); - Local linked_requests = - args.This()->GetInternalField(kLinkedRequestsSlot); - if (linked_requests->IsValue() && - !linked_requests.As()->IsUndefined()) { - // If the module is already linked, we should not link it again. - THROW_ERR_VM_MODULE_LINK_FAILURE(realm->env(), "module is already linked"); - return; - } - Local requests = dependent->module_.Get(isolate)->GetModuleRequests(); Local modules = args[0].As(); diff --git a/test/parallel/test-vm-module-link-shared-deps.js b/test/parallel/test-vm-module-link-shared-deps.js new file mode 100644 index 00000000000000..4d4bae253a426d --- /dev/null +++ b/test/parallel/test-vm-module-link-shared-deps.js @@ -0,0 +1,82 @@ +// Flags: --experimental-vm-modules + +'use strict'; + +const common = require('../common'); +const vm = require('vm'); +const assert = require('assert'); + +// This test verifies that a module can be returned multiple +// times in the linker function in `module.link(linker)`. +// `module.link(linker)` should handle the race condition of +// `module.status` when the linker function is asynchronous. + +// Regression of https://github.com/nodejs/node/issues/59480 + +const sources = { + './index.js': ` + import foo from "./foo.js"; + import shared from "./shared.js"; + export default { + foo, + shared + }; + `, + './foo.js': ` + import shared from "./shared.js"; + export default { + name: "foo" + }; + `, + './shared.js': ` + export default { + name: "shared", + }; + `, +}; + +const moduleCache = new Map(); + +function getModuleInstance(identifier) { + let module = moduleCache.get(identifier); + + if (!module) { + module = new vm.SourceTextModule(sources[identifier], { + identifier, + }); + moduleCache.set(identifier, module); + } + + return module; +} + +async function esmImport(identifier) { + const module = getModuleInstance(identifier); + const requests = []; + + await module.link(async (specifier, referrer) => { + requests.push([specifier, referrer.identifier]); + // Use `Promise.resolve` to defer a tick to create a race condition on + // `module.status` when a module is being imported several times. + return Promise.resolve(getModuleInstance(specifier)); + }); + + await module.evaluate(); + return [module.namespace, requests]; +} + +async function test() { + const { 0: mod, 1: requests } = await esmImport('./index.js'); + assert.strictEqual(mod.default.foo.name, 'foo'); + assert.strictEqual(mod.default.shared.name, 'shared'); + + // Assert that there is no duplicated requests. + assert.deepStrictEqual(requests, [ + // [specifier, referrer] + ['./foo.js', './index.js'], + ['./shared.js', './index.js'], + ['./shared.js', './foo.js'], + ]); +} + +test().then(common.mustCall()); From b7383186c748db5cec9ece1cde5a67650db0717d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 22 Aug 2025 17:56:56 +0200 Subject: [PATCH 091/111] crypto: fix subtle.getPublicKey error for secret type key inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59558 Reviewed-By: Yagiz Nizipli Reviewed-By: Tobias Nießen --- lib/internal/crypto/webcrypto.js | 3 ++- test/parallel/test-webcrypto-get-public-key.mjs | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 6c2407cdca3445..2f6cd7d1b227ef 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1090,7 +1090,8 @@ async function getPublicKey(key, keyUsages) { }); if (key[kKeyType] !== 'private') - throw lazyDOMException('key must be a private key', 'InvalidAccessError'); + throw lazyDOMException('key must be a private key', + key[kKeyType] === 'secret' ? 'NotSupportedError' : 'InvalidAccessError'); const keyObject = createPublicKey(key[kKeyObject]); diff --git a/test/parallel/test-webcrypto-get-public-key.mjs b/test/parallel/test-webcrypto-get-public-key.mjs index dda8d7e8d58802..9764aabd0a887e 100644 --- a/test/parallel/test-webcrypto-get-public-key.mjs +++ b/test/parallel/test-webcrypto-get-public-key.mjs @@ -41,11 +41,16 @@ for await (const { privateKey } of [ name: 'SyntaxError', message: /Unsupported key usage/ }); + + await assert.rejects(() => subtle.getPublicKey(publicKey, publicKey.usages), { + name: 'InvalidAccessError', + message: 'key must be a private key' + }); } const secretKey = await subtle.generateKey( { name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']); await assert.rejects(() => subtle.getPublicKey(secretKey, ['encrypt', 'decrypt']), { - name: 'InvalidAccessError', + name: 'NotSupportedError', message: 'key must be a private key' }); From e3ad5cc2c6d47f7f967555df92f17af6a5c92ed5 Mon Sep 17 00:00:00 2001 From: Richard Lau Date: Fri, 22 Aug 2025 20:23:39 +0100 Subject: [PATCH 092/111] test: skip sea tests on Linux ppc64le MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Theses tests are failing when compiled with clang. Skip for now to avoid breaking the CI when we switch over to building with clang. PR-URL: https://github.com/nodejs/node/pull/59563 Refs: https://github.com/nodejs/node/issues/59561 Reviewed-By: Luigi Pinca Reviewed-By: Michaël Zasso Reviewed-By: Xuguang Mei Reviewed-By: Marco Ippolito Reviewed-By: Yagiz Nizipli --- test/sequential/sequential.status | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/sequential/sequential.status b/test/sequential/sequential.status index 81a152ffa0ce67..439fa2e3cd0f22 100644 --- a/test/sequential/sequential.status +++ b/test/sequential/sequential.status @@ -51,3 +51,17 @@ test-tls-psk-client: PASS, FLAKY [$arch==arm] # https://github.com/nodejs/node/issues/49933 test-watch-mode-inspect: SKIP + +[$system==linux && $arch==ppc64] +# https://github.com/nodejs/node/issues/59561 +test-single-executable-application: SKIP +test-single-executable-application-assets: SKIP +test-single-executable-application-assets-raw: SKIP +test-single-executable-application-disable-experimental-sea-warning: SKIP +test-single-executable-application-empty: SKIP +test-single-executable-application-exec-argv: SKIP +test-single-executable-application-exec-argv-empty: SKIP +test-single-executable-application-snapshot: SKIP +test-single-executable-application-snapshot-and-code-cache: SKIP +test-single-executable-application-snapshot-worker: SKIP +test-single-executable-application-use-code-cache: SKIP From b2b83837556ba52e5108e544c8647ffdfd27ad60 Mon Sep 17 00:00:00 2001 From: Sohyeon Kim Date: Sat, 23 Aug 2025 18:28:02 +0900 Subject: [PATCH 093/111] test: use mustSucceed in test-repl-tab-complete-import Refactor test/parallel/test-repl-tab-complete-import.js to use mustSucceed in places where no error is expected in the callback. This clarifies the intent of the tests and improves assertion accuracy. Refs: https://github.com/nodejs/node/pull/59204 PR-URL: https://github.com/nodejs/node/pull/59368 Reviewed-By: Zeyu "Alex" Yang Reviewed-By: Luigi Pinca --- .../parallel/test-repl-tab-complete-import.js | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/test/parallel/test-repl-tab-complete-import.js b/test/parallel/test-repl-tab-complete-import.js index f4ef408c89174c..3ce48ac45b1f1e 100644 --- a/test/parallel/test-repl-tab-complete-import.js +++ b/test/parallel/test-repl-tab-complete-import.js @@ -32,8 +32,7 @@ const testMe = repl.start({ testMe._domain.on('error', assert.ifError); // Tab complete provides built in libs for import() -testMe.complete('import(\'', common.mustCall((error, data) => { - assert.strictEqual(error, null); +testMe.complete('import(\'', common.mustSucceed((data) => { publicUnprefixedModules.forEach((lib) => { assert( data[0].includes(lib) && data[0].includes(`node:${lib}`), @@ -43,15 +42,14 @@ testMe.complete('import(\'', common.mustCall((error, data) => { const newModule = 'foobar'; assert(!builtinModules.includes(newModule)); repl.builtinModules.push(newModule); - testMe.complete('import(\'', common.mustCall((_, [modules]) => { + testMe.complete('import(\'', common.mustSucceed(([modules]) => { assert.strictEqual(data[0].length + 1, modules.length); assert(modules.includes(newModule) && !modules.includes(`node:${newModule}`)); })); })); -testMe.complete("import\t( 'n", common.mustCall((error, data) => { - assert.strictEqual(error, null); +testMe.complete("import\t( 'n", common.mustSucceed((data) => { assert.strictEqual(data.length, 2); assert.strictEqual(data[1], 'n'); const completions = data[0]; @@ -77,16 +75,14 @@ testMe.complete("import\t( 'n", common.mustCall((error, data) => { // Import calls should handle all types of quotation marks. for (const quotationMark of ["'", '"', '`']) { putIn.run(['.clear']); - testMe.complete('import(`@nodejs', common.mustCall((err, data) => { - assert.strictEqual(err, null); + testMe.complete('import(`@nodejs', common.mustSucceed((data) => { assert.deepStrictEqual(data, [expected, '@nodejs']); })); putIn.run(['.clear']); // Completions should not be greedy in case the quotation ends. const input = `import(${quotationMark}@nodejsscope${quotationMark}`; - testMe.complete(input, common.mustCall((err, data) => { - assert.strictEqual(err, null); + testMe.complete(input, common.mustSucceed((data) => { assert.deepStrictEqual(data, [[], undefined]); })); } @@ -96,8 +92,7 @@ testMe.complete("import\t( 'n", common.mustCall((error, data) => { putIn.run(['.clear']); // Completions should find modules and handle whitespace after the opening // bracket. - testMe.complete('import \t("no_ind', common.mustCall((err, data) => { - assert.strictEqual(err, null); + testMe.complete('import \t("no_ind', common.mustSucceed((data) => { assert.deepStrictEqual(data, [['no_index', 'no_index/'], 'no_ind']); })); } @@ -110,8 +105,7 @@ testMe.complete("import\t( 'n", common.mustCall((error, data) => { process.chdir(__dirname); ['import(\'.', 'import(".'].forEach((input) => { - testMe.complete(input, common.mustCall((err, data) => { - assert.strictEqual(err, null); + testMe.complete(input, common.mustSucceed((data) => { assert.strictEqual(data.length, 2); assert.strictEqual(data[1], '.'); assert.strictEqual(data[0].length, 2); @@ -121,16 +115,14 @@ testMe.complete("import\t( 'n", common.mustCall((error, data) => { }); ['import(\'..', 'import("..'].forEach((input) => { - testMe.complete(input, common.mustCall((err, data) => { - assert.strictEqual(err, null); + testMe.complete(input, common.mustSucceed((data) => { assert.deepStrictEqual(data, [['../'], '..']); })); }); ['./', './test-'].forEach((path) => { [`import('${path}`, `import("${path}`].forEach((input) => { - testMe.complete(input, common.mustCall((err, data) => { - assert.strictEqual(err, null); + testMe.complete(input, common.mustSucceed((data) => { assert.strictEqual(data.length, 2); assert.strictEqual(data[1], path); assert.ok(data[0].includes('./test-repl-tab-complete.js')); @@ -140,8 +132,7 @@ testMe.complete("import\t( 'n", common.mustCall((error, data) => { ['../parallel/', '../parallel/test-'].forEach((path) => { [`import('${path}`, `import("${path}`].forEach((input) => { - testMe.complete(input, common.mustCall((err, data) => { - assert.strictEqual(err, null); + testMe.complete(input, common.mustSucceed((data) => { assert.strictEqual(data.length, 2); assert.strictEqual(data[1], path); assert.ok(data[0].includes('../parallel/test-repl-tab-complete.js')); From 72937e514428c0bcd9ae7aade2c8b617e3b8247b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 23 Aug 2025 12:25:50 +0200 Subject: [PATCH 094/111] crypto: require HMAC key length with SHA-3 hashes in Web Cryptography MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59567 Reviewed-By: Antoine du Hamel Reviewed-By: Tobias Nießen --- lib/internal/crypto/util.js | 8 ++++--- lib/internal/crypto/webcrypto.js | 13 ++++++++++- test/fixtures/webcrypto/supports-sha3.mjs | 27 ++++++++++++++++++----- test/parallel/test-webcrypto-derivekey.js | 22 +++++++++++++----- test/parallel/test-webcrypto-keygen.js | 27 ++++++++++++----------- 5 files changed, 69 insertions(+), 28 deletions(-) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 3ea8059a3bf8c0..79bd5e46b6f7f2 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -629,11 +629,13 @@ function getBlockSize(name) { case 'SHA-512': return 1024; case 'SHA3-256': - return 1088; + // Fall through case 'SHA3-384': - return 832; + // Fall through case 'SHA3-512': - return 576; + // This interaction is not defined for now. + // https://github.com/WICG/webcrypto-modern-algos/issues/23 + throw lazyDOMException('Explicit algorithm length member is required', 'NotSupportedError'); } } diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 2f6cd7d1b227ef..0e4c7f340b2d57 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -9,6 +9,7 @@ const { ReflectConstruct, StringPrototypeRepeat, StringPrototypeSlice, + StringPrototypeStartsWith, SymbolToStringTag, } = primordials; @@ -1235,7 +1236,6 @@ function check(op, alg, length) { case 'sign': case 'verify': case 'digest': - case 'generateKey': case 'importKey': case 'exportKey': case 'wrapKey': @@ -1260,6 +1260,17 @@ function check(op, alg, length) { return true; } + case 'generateKey': { + if ( + normalizedAlgorithm.name === 'HMAC' && + normalizedAlgorithm.length === undefined && + StringPrototypeStartsWith(normalizedAlgorithm.hash.name, 'SHA3-') + ) { + return false; + } + + return true; + } default: { const assert = require('internal/assert'); assert.fail('Unreachable code'); diff --git a/test/fixtures/webcrypto/supports-sha3.mjs b/test/fixtures/webcrypto/supports-sha3.mjs index 0b7be66746ce64..db1aa0e2ac4c0e 100644 --- a/test/fixtures/webcrypto/supports-sha3.mjs +++ b/test/fixtures/webcrypto/supports-sha3.mjs @@ -19,16 +19,18 @@ export const vectors = { [!boringSSL, 'SHA3-512'], ], 'generateKey': [ - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [!boringSSL, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], - [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], + + // This interaction is not defined for now. + // https://github.com/WICG/webcrypto-modern-algos/issues/23 + [false, { name: 'HMAC', hash: 'SHA3-256' }], ], 'deriveKey': [ [!boringSSL, @@ -36,7 +38,7 @@ export const vectors = { { name: 'AES-CBC', length: 128 }], [!boringSSL, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, - { name: 'HMAC', hash: 'SHA3-256' }], + { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [false, { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 'HKDF'], @@ -45,14 +47,29 @@ export const vectors = { { name: 'AES-CBC', length: 128 }], [!boringSSL, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, - { name: 'HMAC', hash: 'SHA3-256' }], + { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [false, { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, 'HKDF'], [!boringSSL, { name: 'X25519', public: X25519.publicKey }, - { name: 'HMAC', hash: 'SHA3-256' }], + { name: 'HMAC', hash: 'SHA3-256', length: 256 }], [!boringSSL, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA3-256', length: 256 }], + + // This interaction is not defined for now. + // https://github.com/WICG/webcrypto-modern-algos/issues/23 + [false, + { name: 'HKDF', hash: 'SHA3-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, + { name: 'HMAC', hash: 'SHA3-256' }], + [false, + { name: 'PBKDF2', hash: 'SHA3-256', salt: Buffer.alloc(0), iterations: 1 }, + { name: 'HMAC', hash: 'SHA3-256' }], + [false, + { name: 'X25519', public: X25519.publicKey }, + { name: 'HMAC', hash: 'SHA3-256' }], + [false, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA3-256' }], ], diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index 7a319643fa5f7b..07d427e841c6b1 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -157,9 +157,14 @@ const { KeyObject } = require('crypto'); // Not long enough secret generated by ECDH [{ name: 'HMAC', hash: 'SHA-384' }, 'sign', 1024], [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], - [{ name: 'HMAC', hash: 'SHA3-256' }, 'sign', 1088], - [{ name: 'HMAC', hash: 'SHA3-384' }, 'sign', 832], - [{ name: 'HMAC', hash: 'SHA3-512' }, 'sign', 576], + [{ name: 'HMAC', hash: 'SHA3-256', length: 256 }, 'sign', 256], + [{ name: 'HMAC', hash: 'SHA3-384', length: 384 }, 'sign', 384], + [{ name: 'HMAC', hash: 'SHA3-512', length: 512 }, 'sign', 512], + // This interaction is not defined for now. + // https://github.com/WICG/webcrypto-modern-algos/issues/23 + // [{ name: 'HMAC', hash: 'SHA3-256' }, 'sign', 256], + // [{ name: 'HMAC', hash: 'SHA3-384' }, 'sign', 384], + // [{ name: 'HMAC', hash: 'SHA3-512' }, 'sign', 512], ]; (async () => { @@ -196,9 +201,14 @@ const { KeyObject } = require('crypto'); [{ name: 'HMAC', hash: 'SHA-256' }, 'sign', 512], [{ name: 'HMAC', hash: 'SHA-384' }, 'sign', 1024], [{ name: 'HMAC', hash: 'SHA-512' }, 'sign', 1024], - [{ name: 'HMAC', hash: 'SHA3-256' }, 'sign', 1088], - [{ name: 'HMAC', hash: 'SHA3-384' }, 'sign', 832], - [{ name: 'HMAC', hash: 'SHA3-512' }, 'sign', 576], + [{ name: 'HMAC', hash: 'SHA3-256', length: 256 }, 'sign', 256], + [{ name: 'HMAC', hash: 'SHA3-384', length: 384 }, 'sign', 384], + [{ name: 'HMAC', hash: 'SHA3-512', length: 512 }, 'sign', 512], + // This interaction is not defined for now. + // https://github.com/WICG/webcrypto-modern-algos/issues/23 + // [{ name: 'HMAC', hash: 'SHA3-256' }, 'sign', 256], + // [{ name: 'HMAC', hash: 'SHA3-384' }, 'sign', 384], + // [{ name: 'HMAC', hash: 'SHA3-512' }, 'sign', 512], ]; (async () => { diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 1b9e4e01404d6c..794581c0ecdba6 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -614,9 +614,6 @@ if (hasOpenSSL(3, 5)) { case 'SHA-256': length = 512; break; case 'SHA-384': length = 1024; break; case 'SHA-512': length = 1024; break; - case 'SHA3-256': length = 1088; break; - case 'SHA3-384': length = 832; break; - case 'SHA3-512': length = 576; break; } } @@ -642,20 +639,24 @@ if (hasOpenSSL(3, 5)) { } const kTests = [ - [ undefined, 'SHA-1', ['sign', 'verify']], - [ undefined, 'SHA-256', ['sign', 'verify']], - [ undefined, 'SHA-384', ['sign', 'verify']], - [ undefined, 'SHA-512', ['sign', 'verify']], - [ 128, 'SHA-256', ['sign', 'verify']], - [ 1024, 'SHA-512', ['sign', 'verify']], + [undefined, 'SHA-1', ['sign', 'verify']], + [undefined, 'SHA-256', ['sign', 'verify']], + [undefined, 'SHA-384', ['sign', 'verify']], + [undefined, 'SHA-512', ['sign', 'verify']], + [128, 'SHA-256', ['sign', 'verify']], + [1024, 'SHA-512', ['sign', 'verify']], ]; if (!process.features.openssl_is_boringssl) { kTests.push( - - [ undefined, 'SHA3-256', ['sign', 'verify']], - [ undefined, 'SHA3-384', ['sign', 'verify']], - [ undefined, 'SHA3-512', ['sign', 'verify']], + [256, 'SHA3-256', ['sign', 'verify']], + [384, 'SHA3-384', ['sign', 'verify']], + [512, 'SHA3-512', ['sign', 'verify']], + // This interaction is not defined for now. + // https://github.com/WICG/webcrypto-modern-algos/issues/23 + // [undefined, 'SHA3-256', ['sign', 'verify']], + // [undefined, 'SHA3-384', ['sign', 'verify']], + // [undefined, 'SHA3-512', ['sign', 'verify']], ); } else { common.printSkipMessage('Skipping unsupported SHA-3 test cases'); From 566fb04c82f59bf960496f1482873c31500bcdcb Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Sat, 23 Aug 2025 10:20:51 -0400 Subject: [PATCH 095/111] meta: update devcontainer to the latest schema PR-URL: https://github.com/nodejs/node/pull/54347 Reviewed-By: James M Snell --- .devcontainer.json | 25 +++++++++++++++++++++++++ .devcontainer/devcontainer.json | 18 ------------------ .gitignore | 3 +-- 3 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 .devcontainer.json delete mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 00000000000000..c0ae5292d6b572 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,25 @@ +{ + "name": "Node.js Core Developer Environment", + "runArgs": [ + "--platform=linux/amd64" + ], + "customizations": { + "vscode": { + "extensions": [ + "github.vscode-pull-request-github", + "ms-vsliveshare.vsliveshare", + "vscode-icons-team.vscode-icons", + "visualstudioexptteam.vscodeintellicode" + ], + "settings": { + "terminal.integrated.profiles.linux": { + "zsh (login)": { + "path": "zsh", + "args": ["-l"] + } + } + } + } + }, + "image": "nodejs/devcontainer:nightly" +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 75fda6e5b319b9..00000000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "Node.js Core Developer Environment", - "extensions": [ - "github.vscode-pull-request-github", - "ms-vsliveshare.vsliveshare", - "vscode-icons-team.vscode-icons", - "visualstudioexptteam.vscodeintellicode" - ], - "image": "nodejs/devcontainer:nightly", - "settings": { - "terminal.integrated.profiles.linux": { - "zsh (login)": { - "path": "zsh", - "args": ["-l"] - } - } - } -} diff --git a/.gitignore b/.gitignore index 60a3ff8b4a4cd8..90ebce4cb674ac 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,7 @@ .* # Exclude specific dotfiles that we want to track. !deps/**/.* -!.devcontainer/ -!.devcontainer/.devcontainer.json +!.devcontainer.json !test/fixtures/**/.* !.clang-format !.cpplint From 18a2ee5b6c00424c3c0feff080d0919457fbca70 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 21 Aug 2025 11:09:06 +0200 Subject: [PATCH 096/111] crypto: support ML-KEM in Web Cryptography MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59569 Reviewed-By: Tobias Nießen Reviewed-By: James M Snell --- deps/ncrypto/ncrypto.cc | 17 +- doc/api/webcrypto.md | 354 +++++++++++++--- lib/internal/crypto/keys.js | 10 +- lib/internal/crypto/ml_dsa.js | 11 +- lib/internal/crypto/ml_kem.js | 287 +++++++++++++ lib/internal/crypto/util.js | 30 ++ lib/internal/crypto/webcrypto.js | 386 +++++++++++++++++- lib/internal/crypto/webidl.js | 4 + src/crypto/crypto_keys.cc | 65 ++- src/crypto/crypto_keys.h | 9 +- test/fixtures/crypto/ml-kem.js | 49 +++ .../webcrypto/supports-modern-algorithms.mjs | 50 ++- .../test-webcrypto-encap-decap-ml-kem.js | 264 ++++++++++++ .../test-webcrypto-export-import-ml-dsa.js | 2 +- .../test-webcrypto-export-import-ml-kem.js | 315 ++++++++++++++ test/parallel/test-webcrypto-keygen.js | 45 ++ test/parallel/test-webcrypto-supports.mjs | 52 +++ tools/doc/type-parser.mjs | 2 + 18 files changed, 1852 insertions(+), 100 deletions(-) create mode 100644 lib/internal/crypto/ml_kem.js create mode 100644 test/fixtures/crypto/ml-kem.js create mode 100644 test/parallel/test-webcrypto-encap-decap-ml-kem.js create mode 100644 test/parallel/test-webcrypto-export-import-ml-kem.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 4b7a5bf5f28685..55395e31de09d1 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -2162,21 +2162,34 @@ DataPointer EVPKeyPointer::rawPublicKey() const { #if OPENSSL_WITH_PQC DataPointer EVPKeyPointer::rawSeed() const { if (!pkey_) return {}; + + // Determine seed length and parameter name based on key type + size_t seed_len; + const char* param_name; + switch (id()) { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: + seed_len = 32; // ML-DSA uses 32-byte seeds + param_name = OSSL_PKEY_PARAM_ML_DSA_SEED; + break; + case EVP_PKEY_ML_KEM_512: + case EVP_PKEY_ML_KEM_768: + case EVP_PKEY_ML_KEM_1024: + seed_len = 64; // ML-KEM uses 64-byte seeds + param_name = OSSL_PKEY_PARAM_ML_KEM_SEED; break; default: unreachable(); } - size_t seed_len = 32; if (auto data = DataPointer::Alloc(seed_len)) { const Buffer buf = data; size_t len = data.size(); + if (EVP_PKEY_get_octet_string_param( - get(), OSSL_PKEY_PARAM_ML_DSA_SEED, buf.data, len, &seed_len) != 1) + get(), param_name, buf.data, len, &seed_len) != 1) return {}; return data; } diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index 2ec2ba3598ef59..be38b6fcc9fc3b 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -2,6 +2,9 @@ + +> Stability: 1.1 - Active development + +* `decapsulationAlgorithm` {string|Algorithm} +* `decapsulationKey` {CryptoKey} +* `ciphertext` {ArrayBuffer|TypedArray|DataView|Buffer} +* Returns: {Promise} Fulfills with {ArrayBuffer} upon success. + +The algorithms currently supported include: + +* `'ML-KEM-512'`[^modern-algos] +* `'ML-KEM-768'`[^modern-algos] +* `'ML-KEM-1024'`[^modern-algos] + +### `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages)` + + + +> Stability: 1.1 - Active development + +* `decapsulationAlgorithm` {string|Algorithm} +* `decapsulationKey` {CryptoKey} +* `ciphertext` {ArrayBuffer|TypedArray|DataView|Buffer} +* `sharedKeyAlgorithm` {string|Algorithm|HmacImportParams|AesDerivedKeyParams} +* `extractable` {boolean} +* `usages` {string\[]} See [Key usages][]. +* Returns: {Promise} Fulfills with {CryptoKey} upon success. + +The algorithms currently supported include: + +* `'ML-KEM-512'`[^modern-algos] +* `'ML-KEM-768'`[^modern-algos] +* `'ML-KEM-1024'`[^modern-algos] + ### `subtle.decrypt(algorithm, key, data)` + +> Stability: 1.1 - Active development + +* `encapsulationAlgorithm` {string|Algorithm} +* `encapsulationKey` {CryptoKey} +* Returns: {Promise} Fulfills with {EncapsulatedBits} upon success. + +The algorithms currently supported include: + +* `'ML-KEM-512'`[^modern-algos] +* `'ML-KEM-768'`[^modern-algos] +* `'ML-KEM-1024'`[^modern-algos] + +### `subtle.encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKeyAlgorithm, extractable, usages)` + + + +> Stability: 1.1 - Active development + +* `encapsulationAlgorithm` {string|Algorithm} +* `encapsulationKey` {CryptoKey} +* `sharedKeyAlgorithm` {string|Algorithm|HmacImportParams|AesDerivedKeyParams} +* `extractable` {boolean} +* `usages` {string\[]} See [Key usages][]. +* Returns: {Promise} Fulfills with {EncapsulatedKey} upon success. + +The algorithms currently supported include: + +* `'ML-KEM-512'`[^modern-algos] +* `'ML-KEM-768'`[^modern-algos] +* `'ML-KEM-1024'`[^modern-algos] + ### `subtle.encrypt(algorithm, key, data)` + +#### `encapsulatedBits.ciphertext` + + + +* Type: {ArrayBuffer} + +#### `encapsulatedBits.sharedKey` + + + +* Type: {ArrayBuffer} + +### Class: `EncapsulatedKey` + + + +#### `encapsulatedKey.ciphertext` + + + +* Type: {ArrayBuffer} + +#### `encapsulatedKey.sharedKey` + + + +* Type: {CryptoKey} + ### Class: `HkdfParams` -* Type: {string} Must be `'AES-GCM'` or `'ChaCha20-Poly1305'`. +* Type: {string} Must be `'AES-GCM'`, `'AES-OCB'`, or `'ChaCha20-Poly1305'`. #### `aeadParams.tagLength` @@ -1514,8 +1541,7 @@ added: v15.0.0 added: v15.0.0 --> -* Type: {string} Must be one of `'AES-CBC'`, `'AES-CTR'`, `'AES-GCM'`, or - `'AES-KW'` +* Type: {string} Must be one of `'AES-CBC'`, `'AES-CTR'`, `'AES-GCM'`, `'AES-OCB'`, or `'AES-KW'` #### `aesDerivedKeyParams.length` @@ -2391,6 +2417,8 @@ The length (in bytes) of the random salt to use. [^modern-algos]: See [Modern Algorithms in the Web Cryptography API][] +[^openssl30]: Requires OpenSSL >= 3.0 + [^openssl35]: Requires OpenSSL >= 3.5 [JSON Web Key]: https://tools.ietf.org/html/rfc7517 diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index ff2cc108c53e0a..0abffe85c9881b 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -18,14 +18,17 @@ const { kKeyVariantAES_CBC_128, kKeyVariantAES_GCM_128, kKeyVariantAES_KW_128, + kKeyVariantAES_OCB_128, kKeyVariantAES_CTR_192, kKeyVariantAES_CBC_192, kKeyVariantAES_GCM_192, kKeyVariantAES_KW_192, + kKeyVariantAES_OCB_192, kKeyVariantAES_CTR_256, kKeyVariantAES_CBC_256, kKeyVariantAES_GCM_256, kKeyVariantAES_KW_256, + kKeyVariantAES_OCB_256, kWebCryptoCipherDecrypt, kWebCryptoCipherEncrypt, } = internalBinding('crypto'); @@ -62,6 +65,7 @@ function getAlgorithmName(name, length) { case 'AES-CTR': return `A${length}CTR`; case 'AES-GCM': return `A${length}GCM`; case 'AES-KW': return `A${length}KW`; + case 'AES-OCB': return `A${length}OCB`; } } @@ -100,6 +104,13 @@ function getVariant(name, length) { case 256: return kKeyVariantAES_KW_256; } break; + case 'AES-OCB': + switch (length) { + case 128: return kKeyVariantAES_OCB_128; + case 192: return kKeyVariantAES_OCB_192; + case 256: return kKeyVariantAES_OCB_256; + } + break; } } @@ -173,11 +184,49 @@ function asyncAesGcmCipher(mode, key, data, algorithm) { algorithm.additionalData)); } +function asyncAesOcbCipher(mode, key, data, algorithm) { + const { tagLength = 128 } = algorithm; + + const tagByteLength = tagLength / 8; + let tag; + switch (mode) { + case kWebCryptoCipherDecrypt: { + const slice = ArrayBufferIsView(data) ? + TypedArrayPrototypeSlice : ArrayBufferPrototypeSlice; + tag = slice(data, -tagByteLength); + + // Similar to GCM, OCB requires the tag to be present for decryption + if (tagByteLength > tag.byteLength) { + return PromiseReject(lazyDOMException( + 'The provided data is too small.', + 'OperationError')); + } + + data = slice(data, 0, -tagByteLength); + break; + } + case kWebCryptoCipherEncrypt: + tag = tagByteLength; + break; + } + + return jobPromise(() => new AESCipherJob( + kCryptoJobAsync, + mode, + key[kKeyObject][kHandle], + data, + getVariant('AES-OCB', key.algorithm.length), + algorithm.iv, + tag, + algorithm.additionalData)); +} + function aesCipher(mode, key, data, algorithm) { switch (algorithm.name) { case 'AES-CTR': return asyncAesCtrCipher(mode, key, data, algorithm); case 'AES-CBC': return asyncAesCbcCipher(mode, key, data, algorithm); case 'AES-GCM': return asyncAesGcmCipher(mode, key, data, algorithm); + case 'AES-OCB': return asyncAesOcbCipher(mode, key, data, algorithm); case 'AES-KW': return asyncAesKwCipher(mode, key, data); } } @@ -236,7 +285,11 @@ function aesImportKey( keyObject = keyData; break; } + case 'raw-secret': case 'raw': { + if (format === 'raw' && name === 'AES-OCB') { + return undefined; + } validateKeyLength(keyData.byteLength * 8); keyObject = createSecretKey(keyData); break; diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index bea4c4b142ae37..c8b3d870ac5225 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -199,6 +199,8 @@ const { case 'AES-GCM': // Fall through case 'AES-KW': + // Fall through + case 'AES-OCB': result = require('internal/crypto/aes') .aesImportKey(algorithm, 'KeyObject', this, extractable, keyUsages); break; diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index b33d36fc8d504a..98d638909ef0e3 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -39,6 +39,7 @@ const { EVP_PKEY_ML_KEM_512, EVP_PKEY_ML_KEM_768, EVP_PKEY_ML_KEM_1024, + kKeyVariantAES_OCB_128: hasAesOcbMode, } = internalBinding('crypto'); const { getOptionValue } = require('internal/options'); @@ -208,6 +209,14 @@ const kAlgorithmDefinitions = { 'wrapKey': null, 'unwrapKey': null, }, + 'AES-OCB': { + 'generateKey': 'AesKeyGenParams', + 'exportKey': null, + 'importKey': null, + 'encrypt': 'AeadParams', + 'decrypt': 'AeadParams', + 'get key length': 'AesDerivedKeyParams', + }, 'ChaCha20-Poly1305': { 'generateKey': null, 'exportKey': null, @@ -350,6 +359,7 @@ const kAlgorithmDefinitions = { // Conditionally supported algorithms const conditionalAlgorithms = { 'AES-KW': !process.features.openssl_is_boringssl, + 'AES-OCB': !!hasAesOcbMode, 'ChaCha20-Poly1305': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getCiphers(), 'chacha20-poly1305'), 'cSHAKE128': !process.features.openssl_is_boringssl || @@ -374,6 +384,7 @@ const conditionalAlgorithms = { // Experimental algorithms const experimentalAlgorithms = [ + 'AES-OCB', 'ChaCha20-Poly1305', 'cSHAKE128', 'cSHAKE256', diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index b08498683312ef..60f9de5fbfcb28 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -155,6 +155,8 @@ async function generateKey( // Fall through case 'AES-GCM': // Fall through + case 'AES-OCB': + // Fall through case 'AES-KW': resultType = 'CryptoKey'; result = await require('internal/crypto/aes') @@ -253,6 +255,7 @@ function getKeyLength({ name, length, hash }) { case 'AES-CTR': case 'AES-CBC': case 'AES-GCM': + case 'AES-OCB': case 'AES-KW': if (length !== 128 && length !== 192 && length !== 256) throw lazyDOMException('Invalid key length', 'OperationError'); @@ -510,6 +513,8 @@ async function exportKeyRawSecret(key, format) { // Fall through case 'HMAC': return key[kKeyObject][kHandle].export().buffer; + case 'AES-OCB': + // Fall through case 'ChaCha20-Poly1305': if (format === 'raw-secret') { return key[kKeyObject][kHandle].export().buffer; @@ -572,6 +577,8 @@ async function exportKeyJWK(key) { // Fall through case 'AES-GCM': // Fall through + case 'AES-OCB': + // Fall through case 'AES-KW': parameters.alg = require('internal/crypto/aes') .getAlgorithmName(key[kAlgorithm].name, key[kAlgorithm].length); @@ -758,7 +765,11 @@ async function importKey( case 'AES-GCM': // Fall through case 'AES-KW': - format = aliasKeyFormat(format); + // Fall through + case 'AES-OCB': + if (algorithm.name !== 'AES-OCB') { + format = aliasKeyFormat(format); + } result = require('internal/crypto/aes') .aesImportKey(algorithm, format, keyData, extractable, keyUsages); break; @@ -1060,6 +1071,8 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { case 'AES-CBC': // Fall through case 'AES-GCM': + // Fall through + case 'AES-OCB': return require('internal/crypto/aes') .aesCipher(mode, key, data, algorithm); case 'ChaCha20-Poly1305': diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index ed6a1fdc255521..1bec09b17437a0 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -699,6 +699,13 @@ converters.AeadParams = createDictionaryConverter( case 'aes-gcm': validateMaxBufferLength(V, 'algorithm.iv'); break; + case 'aes-ocb': + if (V.byteLength > 15) { + throw lazyDOMException( + 'AES-OCB algorithm.iv must be no more than 15 bytes', + 'OperationError'); + } + break; } }, required: true, @@ -723,6 +730,13 @@ converters.AeadParams = createDictionaryConverter( 'OperationError'); } break; + case 'aes-ocb': + if (!ArrayPrototypeIncludes([64, 96, 128], V)) { + throw lazyDOMException( + `${V} is not a valid AES-OCB tag length`, + 'OperationError'); + } + break; } }, }, diff --git a/src/crypto/crypto_aes.cc b/src/crypto/crypto_aes.cc index 48c2fbde65dc31..b5495e59737eb6 100644 --- a/src/crypto/crypto_aes.cc +++ b/src/crypto/crypto_aes.cc @@ -61,7 +61,8 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, return WebCryptoCipherStatus::FAILED; } - if (params.cipher.isGcmMode() && !ctx.setIvLength(params.iv.size())) { + if ((params.cipher.isGcmMode() || params.cipher.isOcbMode()) && + !ctx.setIvLength(params.iv.size())) { return WebCryptoCipherStatus::FAILED; } @@ -76,11 +77,20 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, size_t tag_len = 0; - if (params.cipher.isGcmMode()) { + if (params.cipher.isGcmMode() || params.cipher.isOcbMode()) { switch (cipher_mode) { case kWebCryptoCipherDecrypt: { // If in decrypt mode, the auth tag must be set in the params.tag. CHECK(params.tag); + + // For OCB mode, we need to set the auth tag length before setting the + // tag + if (params.cipher.isOcbMode()) { + if (!ctx.setAeadTagLength(params.tag.size())) { + return WebCryptoCipherStatus::FAILED; + } + } + ncrypto::Buffer buffer = { .data = params.tag.data(), .len = params.tag.size(), @@ -91,12 +101,19 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, break; } case kWebCryptoCipherEncrypt: { - // In decrypt mode, we grab the tag length here. We'll use it to + // In encrypt mode, we grab the tag length here. We'll use it to // ensure that that allocated buffer has enough room for both the // final block and the auth tag. Unlike our other AES-GCM implementation // in CipherBase, in WebCrypto, the auth tag is concatenated to the end // of the generated ciphertext and returned in the same ArrayBuffer. tag_len = params.length; + + // For OCB mode, we need to set the auth tag length + if (params.cipher.isOcbMode()) { + if (!ctx.setAeadTagLength(tag_len)) { + return WebCryptoCipherStatus::FAILED; + } + } break; } default: @@ -112,8 +129,8 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, .data = params.additional_data.data(), .len = params.additional_data.size(), }; - if (params.cipher.isGcmMode() && params.additional_data.size() && - !ctx.update(buffer, nullptr, &out_len)) { + if ((params.cipher.isGcmMode() || params.cipher.isOcbMode()) && + params.additional_data.size() && !ctx.update(buffer, nullptr, &out_len)) { return WebCryptoCipherStatus::FAILED; } @@ -147,9 +164,9 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, } total += out_len; - // If using AES_GCM, grab the generated auth tag and append + // If using AES_GCM or AES_OCB, grab the generated auth tag and append // it to the end of the ciphertext. - if (encrypt && params.cipher.isGcmMode()) { + if (encrypt && (params.cipher.isGcmMode() || params.cipher.isOcbMode())) { if (!ctx.getAeadTag(tag_len, ptr + total)) { return WebCryptoCipherStatus::FAILED; } @@ -492,7 +509,7 @@ Maybe AESCipherTraits::AdditionalConfig( if (!ValidateCounter(env, args[offset + 2], params)) { return Nothing(); } - } else if (params->cipher.isGcmMode()) { + } else if (params->cipher.isGcmMode() || params->cipher.isOcbMode()) { if (!ValidateAuthTag(env, mode, cipher_mode, args[offset + 2], params) || !ValidateAdditionalData(env, mode, args[offset + 3], params)) { return Nothing(); @@ -502,9 +519,18 @@ Maybe AESCipherTraits::AdditionalConfig( UseDefaultIV(params); } - if (params->iv.size() < static_cast(params->cipher.getIvLength())) { - THROW_ERR_CRYPTO_INVALID_IV(env); - return Nothing(); + // For OCB mode, allow variable IV lengths (1-15 bytes) + if (params->cipher.isOcbMode()) { + if (params->iv.size() == 0 || params->iv.size() > 15) { + THROW_ERR_CRYPTO_INVALID_IV(env); + return Nothing(); + } + } else { + // For other modes, check against the cipher's expected IV length + if (params->iv.size() < static_cast(params->cipher.getIvLength())) { + THROW_ERR_CRYPTO_INVALID_IV(env); + return Nothing(); + } } return JustVoid(); diff --git a/src/crypto/crypto_aes.h b/src/crypto/crypto_aes.h index 74cfdb80818287..401ef70a5eba9f 100644 --- a/src/crypto/crypto_aes.h +++ b/src/crypto/crypto_aes.h @@ -12,7 +12,7 @@ namespace node::crypto { constexpr unsigned kNoAuthTagLength = static_cast(-1); -#define VARIANTS(V) \ +#define VARIANTS_COMMON(V) \ V(CTR_128, AES_CTR_Cipher, ncrypto::Cipher::AES_128_CTR) \ V(CTR_192, AES_CTR_Cipher, ncrypto::Cipher::AES_192_CTR) \ V(CTR_256, AES_CTR_Cipher, ncrypto::Cipher::AES_256_CTR) \ @@ -26,6 +26,19 @@ constexpr unsigned kNoAuthTagLength = static_cast(-1); V(KW_192, AES_Cipher, ncrypto::Cipher::AES_192_KW) \ V(KW_256, AES_Cipher, ncrypto::Cipher::AES_256_KW) +#if OPENSSL_VERSION_MAJOR >= 3 +#define VARIANTS_OCB(V) \ + V(OCB_128, AES_Cipher, ncrypto::Cipher::AES_128_OCB) \ + V(OCB_192, AES_Cipher, ncrypto::Cipher::AES_192_OCB) \ + V(OCB_256, AES_Cipher, ncrypto::Cipher::AES_256_OCB) +#else +#define VARIANTS_OCB(V) +#endif + +#define VARIANTS(V) \ + VARIANTS_COMMON(V) \ + VARIANTS_OCB(V) + enum class AESKeyVariant { #define V(name, _, __) name, VARIANTS(V) diff --git a/test/fixtures/crypto/aes_ocb.js b/test/fixtures/crypto/aes_ocb.js new file mode 100644 index 00000000000000..add970d478e2da --- /dev/null +++ b/test/fixtures/crypto/aes_ocb.js @@ -0,0 +1,124 @@ +'use strict'; + +module.exports = function() { + const kPlaintext = + Buffer.from('546869732073706563696669636174696f6e206465736372696265' + + '732061204a6176615363726970742041504920666f722070657266' + + '6f726d696e672062617369632063727970746f6772617068696320' + + '6f7065726174696f6e7320696e20776562206170706c6963617469' + + '6f6e732c20737563682061732068617368696e672c207369676e61' + + '747572652067656e65726174696f6e20616e642076657269666963' + + '6174696f6e2c20616e6420656e6372797074696f6e20616e642064' + + '656372797074696f6e2e204164646974696f6e616c6c792c206974' + + '2064657363726962657320616e2041504920666f72206170706c69' + + '636174696f6e7320746f2067656e657261746520616e642f6f7220' + + '6d616e61676520746865206b6579696e67206d6174657269616c20' + + '6e656365737361727920746f20706572666f726d20746865736520' + + '6f7065726174696f6e732e205573657320666f7220746869732041' + + '50492072616e67652066726f6d2075736572206f72207365727669' + + '63652061757468656e7469636174696f6e2c20646f63756d656e74' + + '206f7220636f6465207369676e696e672c20616e64207468652063' + + '6f6e666964656e7469616c69747920616e6420696e746567726974' + + '79206f6620636f6d6d756e69636174696f6e732e', 'hex'); + + const kKeyBytes = { + '128': Buffer.from('dec0d4fcbf3c4741c892dabd1cd4c04e', 'hex'), + '256': Buffer.from('67693823fb1d58073f91ece9cc3af910e5532616a4d27b1' + + '3eb7b74d8000bbf30', 'hex') + } + + const iv = Buffer.from('3a92732aa6ea39bf3986e0c73fa920', 'hex'); + + const additionalData = Buffer.from( + '5468657265206172652037206675727468657220656469746f72696' + + '16c206e6f74657320696e2074686520646f63756d656e742e', 'hex'); + + const vectorData = { + '128': { + '64': { + ciphertext: Buffer.from('4680d176c2fa66ef4376bc013ca5435ebd27b260c1236ae0148eb84eb24869ec1f1ebba2ba5356a2ee36944e717f668ab180c94817058216930d0192f403652bd2b0f3adac6466a74a69a8676d8460e2d81811de0cf8c0ec0c1aea48d470d0b6818fffb30dcdba67ffcf4bcf62e241e853c04370014cbea9cd68de4b90f8e52b5d40e972df70104fb70a78ddff9e7eb6e0c528c52aca9738030a6ad253d042697de254a059d06606ce718e8c95afd35767d05640b11367c5de4be405dd0c0bbbff54c8adfdae259b6588a44af382b3c5a2dec4c91bc8c3c156ae4859bd95e1a12f13fd292e0e80de25267941b8c5974e53dcff3741211d9c9e312919283f625201b201bb208f341b792d50c26b3c5769107e28c694ee55396a92b8ef18f6aa5849e44f63da4ab7d6d27d0b7c0869be21c650049dba5c3691de3fdc0dc9cd9676857d35d924372487e87c5ce4d656f69ee0cd62edbd949db134f9850eb6f017d5ba1933e8a39e56822fbe6a35eb9590e28bd1bbd46217c2db14264518caad1929885c143d28f4274fb4a655de0e24b2f37f1351c4820cb5c4fe49e9433f28bc1a0ac63a52200ac0876471c4db9a7ef1852b679f8a1d9bd54e739ce642bdfca700ed162516a33798733b52b726376e10f840714109150c7afeb71c652970ea86', 'hex'), + tagWithAD: Buffer.from('cd1d3fa016ffebf8', 'hex'), + tagWithoutAD: Buffer.from('85917be096f12614', 'hex') + }, + '96': { + ciphertext: Buffer.from('165e7cb1789fc9cb1e9b81e48d2c30d22a019bce5ff79d45ea7adbcf585b7bfc015a5959c9c1478714f4621ee0675f785a1689f1a9254b76580d368cc4a02b18b3f2a8abb5173e2b9f27042af4c0daeae44d88679e7bb79cab48a8f100804c0dad11547c68f2ac0e9a74f1abdeaa7c95e12b97361c4217905f25a03d9a5f8982af979b7756768cd9b044cc928d25ccd56e4fc494e4f62d96aabf3a4bd4889478990e58dcc180c4a81aceaf93afbbcf866a47030d579a981e42d78fae1907df32fd6c8cb37e1bd12e9ac7e81636e411e1717dac7836cf35b2683dd055fd0032a37d048835ef977b381d282ebb4c743eb09126d37764bd177af48d40f0c50534484dfd23ca9d046be673f493a83f705bd3a7d6579814690ee936095f1d80175271f33832ce9d93fff24d4c4ac3fbfa5e12b57109a56fdd5fa302391fe561095dafa4e41ce8e6dc5aac6091aefd7ca3b694ff6301ffdbe02c0c2ce438101ee92a08f85f3b153aa3116a80bc7778040ed9ee8b408909fc6d86004f23798ae85d9b1957435c9f74becdc53b38a7f0b9ac3d515e17d0ca9f5874096db5fb234d0d45e8149f6d15e2d7d3138622fa6fa7eded639fb6929fcaacf03060ec9db0106e58a3fa45d9ab2f6a2b56eee39cfc8cb305901c8f612e24da4cd3b07d4cf966cdf1', 'hex'), + tagWithAD: Buffer.from('80b55f5111770c4fd51ae2a1', 'hex'), + tagWithoutAD: Buffer.from('c8391b119179c1a3c534f03b', 'hex') + }, + '128': { + ciphertext: Buffer.from('0e33334c6fd3cf8c371d06875f342d239832a94c43b2f721d8bd70b6d62e7ac34bfd2041b214cd77624e0330e0892abfd696577144205882a24a1f4f0234602503222558c8c0e7dd033c3888d7f747107d3b11ad3f4d2c6088a80413d12a83587503a7393022ec541b284b358fa1fe3236ae706cd49fb8e2d4216318e8659275d80616940d2f3762e672a19ece3f2ee918c4e99b173c544dfb3300a867564790a436967563fa2bd3240dbb4d370d9153110411d772ff7542651db8c38672cc0f0ceae4f24065dfc996dd8b8d915f1bce206878ac54fad4df8a8157af6f1a8dc0344f526cd6cc398e1f049af3af9334204c5025a653292c0db11985ab83acf1bce2879754c0684a40d7e64e1f062a5d586a7c8702f326119dec9b1d0d316f8ba93f63d07546ee796db70fa66738499126c3a4bdada811dfc698b96569fbfcb935059c9b80349ce2b5caf6def0f2f6ba0d8ebf3395bb1766cddbc93a946d9706342bc378cda55eaee8edb411314c73bb2c480dea05e2eab5f83d089624bc9884dd14ba714d62e15767f730782e37c519b608d8d4ccee98e6d4ba28171417753f72a3b476403ccd5f0bbc4d7021170c751f8d844ee58ce6d2558270333d26e14e184d07e46a22d9270258517c7d6fa55875642d07a74ae0056c41e2931ef08d3f', 'hex'), + tagWithAD: Buffer.from('cdee14358fba08d170fe5906ab34a56f', 'hex'), + tagWithoutAD: Buffer.from('856250750fb4c53d60d04b9cb18534ba', 'hex') + } + }, + '256': { + '64': { + ciphertext: Buffer.from('188ee89ebee501f6a1dd111aa09b00eb67e1b2c6e1f205737d7f47e2cca0b80c9408daccfe820ebeab75f290589dc2175039d60c891002dea5214237393a5672bf91403d69fe41122b666fdd4b796ca18eb84a219895a58ca91689758dfc578079f89f27016a3b3080d0d8177a5ed8fc4b6e0af40604eaf4d91125aeac6656277a429c120bd9a2fa73086eb93302e3cc4d31dd2433d2f07cfbf604e60e380b7f94fb9de182f9752664c57dcb5c9797a952cc8a27b88a582342747c84bdcaae7ddfce460ab681856432429c6cc6e3658929f5669d5088a123a9158b680a8601960b068ea5a7b8f9bac98c3b7399c8fb8067ecbf6313606afdc3d1528d048b6803e12bdb44b119a2107463d01db4bc2791df8f3d0761ce5b401f8e0383f279fe7b1af335b10b48bef05d0e91bdab9631fa79a67a03a4790b4b1d325be028f6beda26d68958811670c86050d05b745f399ac77ff97be3b91cff64fd15e5047b1698c2dffe6d3d7c6cb0c8b5956cee43e8ecf7bc22199bfd7d61178843f9554bc0db539e7a59001ba3c963f299a1d838b8629bddc7c5646145a86ce52077af7785d213c787640b2010424b73b7b786ac7ca946fff501fecd6793ac00e9fb10ce7fb3a6d7b277b7096c83c94a421747f63c43723dd098b144549edbaeda4536ded1', 'hex'), + tagWithAD: Buffer.from('dd2a37097d430860', 'hex'), + tagWithoutAD: Buffer.from('4aefbda08fd14436', 'hex') + }, + '96': { + ciphertext: Buffer.from('18da5c8e77a1fa3cbe6c510c594b905f027ba3b56e446f23bbc17d12f265f260799ec5531792b8ec5cfec2b660bef94760ff5e7a714947cb342a7e9a1e0f3085e7c1608ea9dd1c71507cf09a7683e855b664615702cb556553319b5bd0fcb600d5c3a3bb8c36d8014f30b85a2d26ac36e83cfe8cebb6f7118c3e5875597fa39efd269f5ea237edd60036a906ea592fbe2868455503e9b1928396a894fefe2321138e8f5df4fcc932f4f05f3c15cd16cd5da9d54399dd0a90448f12c54a288b1724b60dfb7a434e72e357ddb631c1038efe9331a5037eb98e61f2a69df51d17de14799df035b5e468782d95d58d0cfd68d69c1b1529b4ad191bbf6f4c10f8a85e2c4ac7c525b3ef258de9a2f9b6193d74ca6e0180333167de426039cf8490d15e7f8ea86bfd143f98f2bb8e5c32a17e19aa0370cc7cbad8cfaeb69be29b6a6a7c27354a9a0dae13258578c681d182855c917c300e96912d24a80db1824e719fd5bddafd839f67fe18f632a892e69e46e0bef56aa94ff26ab7943a4b7de052f5d24302feb3add1c481b019d0d97bd442bd7c0021721d13b9e471686409c7c69551b3977ed3505eff8213e5564d1afe6042dc1ea572aca30cd1b7540e954b2e6c0498ebd2526fb0fb5ffcf48c6ef23b385e99649fd49290e0ce4de6d0a57498b7', 'hex'), + tagWithAD: Buffer.from('06a22a5a7776797981c5ee6d', 'hex'), + tagWithoutAD: Buffer.from('9167a0f385e4352f4c97aa94', 'hex') + }, + '128': { + ciphertext: Buffer.from('4604fafe03ab3897dd54e41c5884cac385d575768a2eb1c4c21c8470636a62565309605c8a556c4f3837be13df6f02de9b5a1ebbedba07e34a1fe0cd037eefc5c324ddfd95a9d16eaebecafd5b93d3c8d9a2deadb079497437d120c1fb0ae4b4d60b9fe220486c54f49c23c00fc3ffb1d57022761315c51f609fb28d70e4de2479325851af9c71671b6507c81fdecc5e5b8335b9d78320b0a4dfdff2f4a0dc86401128fdbe5601491acc6b0876d72fa842e95b75626dde15e602593b82874ed9233ddc64b06c41ea25dcccd678eb720d10d1c85f17635aceef2f102706f6de89b6d0fe6dcd686677d0a682fa3bf781a1fdb13c506be5b1c46ead578c54161129dbe0763d897fde4bfaf87ba61c5cf6884bd1e75678c086aeb2fcf057faf14ec38492ecba850595fa5b84d66c07576486da9cff68dbd961872985b1094d23f9dc31dda35cbe68ee570323843374cb89e07d2f11adf3476e6bdc2b2525cffdffcb7ee58190b19d8601b73d175bd8ebda079c97ed36e77c09a7c1c5e48f57c1881b91e2b17b6a737c79a1528c90ffbc1504914677593b9f6eca64fad08cdd4d318cd7f163cdf325667e949d829bebcccd932481ef132b49ac156eef34947924c165ce64b9e1e1461bd8d3d1e4e928411b448faa5d7db7f8bddc4fdce1ea035d60', 'hex'), + tagWithAD: Buffer.from('a36db50a9235d475609287268818792c', 'hex'), + tagWithoutAD: Buffer.from('34a83fa360a79823adc0c3dfc8e64d20', 'hex') + } + } + }; + + const kKeyLengths = [128, 256]; + const kTagLengths = [64, 96, 128]; + + const passing = []; + kKeyLengths.forEach((keyLength) => { + kTagLengths.forEach((tagLength) => { + const byteCount = tagLength / 8; + const data = vectorData[keyLength][tagLength]; + + // With additional data + const result = new Uint8Array(data.ciphertext.byteLength + byteCount); + result.set(data.ciphertext, 0); + result.set(data.tagWithAD.slice(0, byteCount), data.ciphertext.byteLength); + passing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { name: 'AES-OCB', iv, additionalData, tagLength }, + plaintext: kPlaintext, + result + }); + + // Without additional data + const noadresult = new Uint8Array(data.ciphertext.byteLength + byteCount); + noadresult.set(data.ciphertext, 0); + noadresult.set(data.tagWithoutAD.slice(0, byteCount), data.ciphertext.byteLength); + passing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { name: 'AES-OCB', iv, tagLength }, + plaintext: kPlaintext, + result: noadresult + }); + }); + }); + + const failing = []; + kKeyLengths.forEach((keyLength) => { + [24, 48, 72, 95, 129].forEach((badTagLength) => { + failing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { + name: 'AES-OCB', + iv, + additionalData, + tagLength: badTagLength + }, + plaintext: kPlaintext, + result: vectorData[keyLength]['128'].ciphertext, + }); + }); + }); + + return { passing, failing, decryptionFailing: [] }; +}; diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 5b358852949d75..8d0df6c7014ffa 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -6,6 +6,7 @@ const pqc = hasOpenSSL(3, 5); const shake128 = crypto.getHashes().includes('shake128'); const shake256 = crypto.getHashes().includes('shake256'); const chacha = crypto.getCiphers().includes('chacha20-poly1305'); +const ocb = hasOpenSSL(3); export const vectors = { 'digest': [ @@ -35,6 +36,7 @@ export const vectors = { [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [chacha, 'ChaCha20-Poly1305'], + [ocb, { name: 'AES-OCB', length: 128 }], ], 'importKey': [ [pqc, 'ML-DSA-44'], @@ -44,6 +46,7 @@ export const vectors = { [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [chacha, 'ChaCha20-Poly1305'], + [ocb, { name: 'AES-OCB', length: 128 }], ], 'exportKey': [ [pqc, 'ML-DSA-44'], @@ -53,6 +56,7 @@ export const vectors = { [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [chacha, 'ChaCha20-Poly1305'], + [ocb, 'AES-OCB'], ], 'getPublicKey': [ [true, 'RSA-OAEP'], @@ -73,6 +77,7 @@ export const vectors = { [false, 'AES-CTR'], [false, 'AES-CBC'], [false, 'AES-GCM'], + [false, 'AES-OCB'], [false, 'AES-KW'], [false, 'ChaCha20-Poly1305'], ], @@ -82,6 +87,13 @@ export const vectors = { [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 64 }], [false, 'ChaCha20-Poly1305'], + [ocb, { name: 'AES-OCB', iv: Buffer.alloc(15) }], + [false, { name: 'AES-OCB', iv: Buffer.alloc(16) }], + [ocb, { name: 'AES-OCB', iv: Buffer.alloc(12), tagLength: 128 }], + [ocb, { name: 'AES-OCB', iv: Buffer.alloc(12), tagLength: 96 }], + [ocb, { name: 'AES-OCB', iv: Buffer.alloc(12), tagLength: 64 }], + [false, { name: 'AES-OCB', iv: Buffer.alloc(12), tagLength: 32 }], + [false, 'AES-OCB'], ], 'encapsulateBits': [ [pqc, 'ML-KEM-512'], diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index 590fef60dc9efe..0629f85b0fb538 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasOpenSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; function getDeriveKeyInfo(name, length, hash, ...usages) { @@ -34,7 +35,16 @@ if (!process.features.openssl_is_boringssl) { ['HMAC', 256, 'SHA3-512', 'sign', 'verify'], ); } else { - common.printSkipMessage('Skipping unsupported AES-KW test cases'); + common.printSkipMessage('Skipping unsupported test cases'); +} + +if (hasOpenSSL(3)) { + kDerivedKeyTypes.push( + ['AES-OCB', 128, undefined, 'encrypt', 'decrypt'], + ['AES-OCB', 256, undefined, 'encrypt', 'decrypt'], + ); +} else { + common.printSkipMessage('Skipping unsupported test cases'); } const kDerivedKeys = { @@ -464,7 +474,7 @@ async function testDeriveKey( true, usages); - const bits = await subtle.exportKey('raw', key); + const bits = await subtle.exportKey(key.algorithm.name === 'AES-OCB' ? 'raw-secret' : 'raw', key); assert.strictEqual( Buffer.from(bits).toString('hex'), diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js index 298f6d60698139..e03be277f089c7 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js @@ -5,6 +5,8 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasOpenSSL } = require('../common/crypto'); + const assert = require('assert'); const { subtle } = globalThis.crypto; @@ -12,8 +14,9 @@ async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { // Using a copy of plaintext to prevent tampering of the original plaintext = Buffer.from(plaintext); + const keyFormat = algorithm.name === 'AES-OCB' ? 'raw-secret' : 'raw'; const key = await subtle.importKey( - 'raw', + keyFormat, keyBuffer, { name: algorithm.name }, false, @@ -37,8 +40,9 @@ async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { } async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) { + const keyFormat = algorithm.name === 'AES-OCB' ? 'raw-secret' : 'raw'; const key = await subtle.importKey( - 'raw', + keyFormat, keyBuffer, { name: algorithm.name }, false, @@ -50,8 +54,9 @@ async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) { } async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) { + const keyFormat = algorithm.name === 'AES-OCB' ? 'raw-secret' : 'raw'; const key = await subtle.importKey( - 'raw', + keyFormat, keyBuffer, { name: algorithm.name }, false, @@ -66,8 +71,9 @@ async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) { async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) { assert.notStrictEqual(algorithm.name, alg); + const keyFormat = alg === 'AES-OCB' ? 'raw-secret' : 'raw'; const key = await subtle.importKey( - 'raw', + keyFormat, keyBuffer, { name: alg }, false, @@ -79,8 +85,9 @@ async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) { } async function testDecrypt({ keyBuffer, algorithm, result }) { + const keyFormat = algorithm.name === 'AES-OCB' ? 'raw-secret' : 'raw'; const key = await subtle.importKey( - 'raw', + keyFormat, keyBuffer, { name: algorithm.name }, false, @@ -202,6 +209,43 @@ async function testDecrypt({ keyBuffer, algorithm, result }) { })().then(common.mustCall()); } +// Test aes-ocb vectors +if (hasOpenSSL(3)) { + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_ocb')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-GCM')); + }); + + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /is not a valid AES-OCB tag length/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /is not a valid AES-OCB tag length/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + name: 'OperationError' + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + { (async function() { const secretKey = await subtle.generateKey( diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js index c21aa3c29d0afe..a00c7d214bad99 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasOpenSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; // This is only a partial test. The WebCrypto Web Platform Tests @@ -181,3 +182,32 @@ if (!process.features.openssl_is_boringssl) { test().then(common.mustCall()); } + +// Test Encrypt/Decrypt AES-OCB +if (hasOpenSSL(3)) { + const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); + + async function test() { + const key = await subtle.generateKey({ + name: 'AES-OCB', + length: 256 + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt( + { name: 'AES-OCB', iv }, key, buf, + ); + + const plaintext = await subtle.decrypt( + { name: 'AES-OCB', iv }, key, ciphertext, + ); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + } + + test().then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported AES-OCB test cases'); +} diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 9a1f3d115fad0a..604caa4b9f6654 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -173,6 +173,19 @@ if (!process.features.openssl_is_boringssl) { common.printSkipMessage('Skipping unsupported test cases'); } +if (hasOpenSSL(3)) { + vectors['AES-OCB'] = { + algorithm: { length: 256 }, + result: 'CryptoKey', + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey', + ], + }; +} + if (hasOpenSSL(3, 5)) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { vectors[name] = { diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 6a504692ec53ed..bd788ec4ed8828 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -59,6 +59,18 @@ if (!process.features.openssl_is_boringssl) { common.printSkipMessage('Skipping unsupported AES-KW test case'); } +if (hasOpenSSL(3)) { + kWrappingData['AES-OCB'] = { + generate: { length: 128 }, + wrap: { + iv: new Uint8Array(15), + additionalData: new Uint8Array(16), + tagLength: 128 + }, + pair: false + }; +} + function generateWrappingKeys() { return Promise.all(Object.keys(kWrappingData).map(async (name) => { const keys = await subtle.generateKey( From dfd4962e5ff0dda19a6a08c6d34d812be34a870d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Sun, 24 Aug 2025 13:07:37 +0200 Subject: [PATCH 101/111] src: enforce assumptions in FIXED_ONE_BYTE_STRING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These functions are both meant to be used with a null-terminated and thus non-empty sequence of `char`s. However, there is nothing stopping call sites from passing zero-length sequences, which would certainly not be null-terminated and also would cause an underflow in `N - 1`. Therefore, this commit - changes the size `N` of the array from `int` to `std::size_t`, - ensures that compilation will fail if `N = 0`, and - adds a runtime assertion that fails if the `N`-th `char` is not `\0`. Note that the runtime assertion should be eliminated by any optimizing compiler when given a string literal, which is how these functions are used for the most part (though not exclusively). PR-URL: https://github.com/nodejs/node/pull/58155 Reviewed-By: Daniel Lemire Reviewed-By: Darshan Sen Reviewed-By: James M Snell Reviewed-By: Gerhard Stöbich --- src/util.h | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/util.h b/src/util.h index 74edcdbc2d45d1..a5313506d80d7c 100644 --- a/src/util.h +++ b/src/util.h @@ -346,17 +346,19 @@ inline v8::Local OneByteString(v8::Isolate* isolate, std::string_view str); // Used to be a macro, hence the uppercase name. -template -inline v8::Local FIXED_ONE_BYTE_STRING( - v8::Isolate* isolate, - const char(&data)[N]) { +template + requires(N > 0) +inline v8::Local FIXED_ONE_BYTE_STRING(v8::Isolate* isolate, + const char (&data)[N]) { + CHECK_EQ(data[N - 1], '\0'); return OneByteString(isolate, data, N - 1); } template + requires(N > 0) inline v8::Local FIXED_ONE_BYTE_STRING( - v8::Isolate* isolate, - const std::array& arr) { + v8::Isolate* isolate, const std::array& arr) { + CHECK_EQ(arr[N - 1], '\0'); return OneByteString(isolate, arr.data(), N - 1); } From 8ebd4f4434f6e79f12cad9cd1af30123c6837db1 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Sun, 24 Aug 2025 17:57:38 -0300 Subject: [PATCH 102/111] benchmark: calibrate length of util.diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 500 + 1000 already cover the curve. 2000 doesn’t add new qualitative insight — it just extends the same curve further down (another ~3–4× slowdown). According to https://github.com/nodejs/performance/issues/186 this benchmark takes one minute to conclude a single run. This should fix it. PR-URL: https://github.com/nodejs/node/pull/59588 Reviewed-By: Luigi Pinca Reviewed-By: Chemi Atlow --- benchmark/util/diff.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/util/diff.js b/benchmark/util/diff.js index 3ec024c24df6d0..7e40b3cd30f63b 100644 --- a/benchmark/util/diff.js +++ b/benchmark/util/diff.js @@ -5,7 +5,7 @@ const common = require('../common'); const bench = common.createBenchmark(main, { n: [1e3], - length: [1e3, 2e3], + length: [500, 1000], scenario: ['identical', 'small-diff', 'medium-diff', 'large-diff'], }); From f5ece45b45428c6e76c587cf07c52a26e9f554c8 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Sun, 24 Aug 2025 18:19:33 -0300 Subject: [PATCH 103/111] benchmark: reduce readfile-permission-enabled config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There's no need to test all readfile config as permission model won't impact based on size. PR-URL: https://github.com/nodejs/node/pull/59589 Reviewed-By: Vinícius Lourenço Claro Cardoso Reviewed-By: Chemi Atlow --- benchmark/fs/readfile-permission-enabled.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark/fs/readfile-permission-enabled.js b/benchmark/fs/readfile-permission-enabled.js index c688e9eecb0e00..4d784ab343467a 100644 --- a/benchmark/fs/readfile-permission-enabled.js +++ b/benchmark/fs/readfile-permission-enabled.js @@ -12,8 +12,8 @@ const filename = tmpdir.resolve(`.removeme-benchmark-garbage-${process.pid}`); const bench = common.createBenchmark(main, { duration: [5], - encoding: ['', 'utf-8'], - len: [1024, 16 * 1024 * 1024], + encoding: ['utf-8'], + len: [1024], concurrent: [1, 10], }, { flags: [ From 40b217a2f92d733f2b7a25bfeebc3b3e7af06270 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 25 Aug 2025 12:35:13 +0200 Subject: [PATCH 104/111] doc: clarify experimental platform vulnerability policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new section to the threat model specifying that security vulnerabilities affecting only experimental platforms will not be accepted as valid security issues and will be treated as normal bugs. This clarifies that experimental OS/hardware combinations do not qualify for CVEs or bug bounty rewards, aligning with their limited testing and support infrastructure. Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/59591 Reviewed-By: Rafael Gonzaga Reviewed-By: James M Snell Reviewed-By: Michaël Zasso Reviewed-By: Jordan Harband --- SECURITY.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 0c7c1c3c3d313f..9862585a92391c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -102,6 +102,22 @@ vulnerability in the context of the Node.js threat model. In other words, it cannot assume that a trusted element (such as the operating system) has been compromised. +### Experimental platforms + +Node.js maintains a tier-based support system for operating systems and +hardware combinations (Tier 1, Tier 2, and Experimental). For platforms +classified as "Experimental" in the [supported platforms](BUILDING.md#supported-platforms) +documentation: + +* Security vulnerabilities that only affect experimental platforms will **not** be accepted as valid security issues. +* Any issues on experimental platforms will be treated as normal bugs. +* No CVEs will be issued for issues that only affect experimental platforms +* Bug bounty rewards are not available for experimental platform-specific issues + +This policy recognizes that experimental platforms may not compile, may not +pass the test suite, and do not have the same level of testing and support +infrastructure as Tier 1 and Tier 2 platforms. + Being able to cause the following through control of the elements that Node.js does not trust is considered a vulnerability: From 89fe63551e1abff2931d4c88c216584c7143b365 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 25 Aug 2025 13:35:46 +0200 Subject: [PATCH 105/111] crypto: load system CA certificates off thread When --use-system-ca is enabled, load the system CA certificates eagerly off the main thread to avoid blocking the main thread when the first TLS connection is made. PR-URL: https://github.com/nodejs/node/pull/59550 Refs: https://github.com/nodejs/node/issues/58990 Reviewed-By: Yagiz Nizipli Reviewed-By: James M Snell --- src/crypto/crypto_context.cc | 21 +++++++++++++++++++++ src/crypto/crypto_util.h | 1 + src/node.cc | 14 ++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index f59603891f289e..d5b175ac7172da 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -814,6 +814,23 @@ static std::vector& GetSystemStoreCACertificates() { return system_store_certs; } +static void LoadSystemCACertificates(void* data) { + GetSystemStoreCACertificates(); +} + +static uv_thread_t system_ca_thread; +static bool system_ca_thread_started = false; +int LoadSystemCACertificatesOffThread() { + // This is only run once during the initialization of the process, so + // it is safe to use a static thread here. + int r = + uv_thread_create(&system_ca_thread, LoadSystemCACertificates, nullptr); + if (r == 0) { + system_ca_thread_started = true; + } + return r; +} + static std::vector InitializeExtraCACertificates() { std::vector extra_certs; unsigned long err = LoadCertsFromFile( // NOLINT(runtime/int) @@ -925,6 +942,10 @@ void CleanupCachedRootCertificates() { X509_free(cert); } } + if (system_ca_thread_started) { + uv_thread_join(&system_ca_thread); + system_ca_thread_started = false; + } } void GetBundledRootCertificates(const FunctionCallbackInfo& args) { diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index d2620b40c8bc4b..7a89764581ddd2 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -45,6 +45,7 @@ void InitCryptoOnce(); void InitCrypto(v8::Local target); extern void UseExtraCaCerts(std::string_view file); +extern int LoadSystemCACertificatesOffThread(); void CleanupCachedRootCertificates(); int PasswordCallback(char* buf, int size, int rwflag, void* u); diff --git a/src/node.cc b/src/node.cc index 95585e7b16a830..b6f227535c951c 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1208,6 +1208,20 @@ InitializeOncePerProcessInternal(const std::vector& args, return result; } + if (per_process::cli_options->use_system_ca) { + // Load the system CA certificates eagerly off the main thread to avoid + // blocking the main thread when the first TLS connection is made. We + // don't need to wait for the thread to finish with code here, as + // GetSystemStoreCACertificates() has a function-local static and any + // actual user of it will wait for that to complete initialization. + int r = crypto::LoadSystemCACertificatesOffThread(); + if (r != 0) { + FPrintF( + stderr, + "Warning: Failed to load system CA certificates off thread: %s\n", + uv_strerror(r)); + } + } // Ensure CSPRNG is properly seeded. CHECK(ncrypto::CSPRNG(nullptr, 0)); From 352d63541a541a39affe603e48b0c58f7de68cbf Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 25 Aug 2025 13:35:56 +0200 Subject: [PATCH 106/111] sea: implement execArgvExtension This implements the execArgvExtension configuration field for SEA, which takes one of three string values to specify whether and how execution arguments can be extended for the SEA at run time: * `"none"`: No extension is allowed. Only the arguments specified in `execArgv` will be used, and the `NODE_OPTIONS` environment variable will be ignored. * `"env"`: _(Default)_ The `NODE_OPTIONS` environment variable can extend the execution arguments. This is the default behavior to maintain backward compatibility. * `"cli"`: The executable can be launched with `--node-options="--flag1 --flag2"`, and those flags will be parsed as execution arguments for Node.js instead of being passed to the user script. This allows using arguments that are not supported by the `NODE_OPTIONS` environment variable. PR-URL: https://github.com/nodejs/node/pull/59560 Fixes: https://github.com/nodejs/node/issues/55573 Fixes: https://github.com/nodejs/single-executable/issues/100 Refs: https://github.com/nodejs/node/issues/51688 Reviewed-By: Xuguang Mei Reviewed-By: James M Snell Reviewed-By: Darshan Sen --- doc/api/single-executable-applications.md | 37 +++++++++ src/node.cc | 12 ++- src/node_sea.cc | 79 +++++++++++++++++-- src/node_sea.h | 10 ++- test/fixtures/sea-exec-argv-extension-cli.js | 14 ++++ test/fixtures/sea-exec-argv-extension-env.js | 19 +++++ test/fixtures/sea-exec-argv-extension-none.js | 14 ++++ ...ble-application-exec-argv-extension-cli.js | 63 +++++++++++++++ ...ble-application-exec-argv-extension-env.js | 68 ++++++++++++++++ ...le-application-exec-argv-extension-none.js | 63 +++++++++++++++ 10 files changed, 372 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/sea-exec-argv-extension-cli.js create mode 100644 test/fixtures/sea-exec-argv-extension-env.js create mode 100644 test/fixtures/sea-exec-argv-extension-none.js create mode 100644 test/sequential/test-single-executable-application-exec-argv-extension-cli.js create mode 100644 test/sequential/test-single-executable-application-exec-argv-extension-env.js create mode 100644 test/sequential/test-single-executable-application-exec-argv-extension-none.js diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 5e12e985b6567c..41e4bd7383ef7f 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -180,6 +180,7 @@ The configuration currently reads the following top-level fields: "useSnapshot": false, // Default: false "useCodeCache": true, // Default: false "execArgv": ["--no-warnings", "--max-old-space-size=4096"], // Optional + "execArgvExtension": "env", // Default: "env", options: "none", "env", "cli" "assets": { // Optional "a.dat": "/path/to/a.dat", "b.txt": "/path/to/b.txt" @@ -314,6 +315,42 @@ similar to what would happen if the application is started with: node --no-warnings --max-old-space-size=2048 /path/to/bundled/script.js user-arg1 user-arg2 ``` +### Execution argument extension + +The `execArgvExtension` field controls how additional execution arguments can be +provided beyond those specified in the `execArgv` field. It accepts one of three string values: + +* `"none"`: No extension is allowed. Only the arguments specified in `execArgv` will be used, + and the `NODE_OPTIONS` environment variable will be ignored. +* `"env"`: _(Default)_ The `NODE_OPTIONS` environment variable can extend the execution arguments. + This is the default behavior to maintain backward compatibility. +* `"cli"`: The executable can be launched with `--node-options="--flag1 --flag2"`, and those flags + will be parsed as execution arguments for Node.js instead of being passed to the user script. + This allows using arguments that are not supported by the `NODE_OPTIONS` environment variable. + +For example, with `"execArgvExtension": "cli"`: + +```json +{ + "main": "/path/to/bundled/script.js", + "output": "/path/to/write/the/generated/blob.blob", + "execArgv": ["--no-warnings"], + "execArgvExtension": "cli" +} +``` + +The executable can be launched as: + +```console +./my-sea --node-options="--trace-exit" user-arg1 user-arg2 +``` + +This would be equivalent to running: + +```console +node --no-warnings --trace-exit /path/to/bundled/script.js user-arg1 user-arg2 +``` + ## In the injected main script ### Single-executable application API diff --git a/src/node.cc b/src/node.cc index b6f227535c951c..79c9dff69cf7f5 100644 --- a/src/node.cc +++ b/src/node.cc @@ -940,7 +940,17 @@ static ExitCode InitializeNodeWithArgsInternal( } #if !defined(NODE_WITHOUT_NODE_OPTIONS) - if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) { + bool should_parse_node_options = + !(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv); +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION + if (sea::IsSingleExecutable()) { + sea::SeaResource sea_resource = sea::FindSingleExecutableResource(); + if (sea_resource.exec_argv_extension != sea::SeaExecArgvExtension::kEnv) { + should_parse_node_options = false; + } + } +#endif + if (should_parse_node_options) { // NODE_OPTIONS environment variable is preferred over the file one. if (credentials::SafeGetenv("NODE_OPTIONS", &node_options) || !node_options.empty()) { diff --git a/src/node_sea.cc b/src/node_sea.cc index bcc49c149e2374..49071304262f10 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -7,6 +7,7 @@ #include "node_errors.h" #include "node_external_reference.h" #include "node_internals.h" +#include "node_options.h" #include "node_snapshot_builder.h" #include "node_union_bytes.h" #include "node_v8_platform-inl.h" @@ -86,6 +87,11 @@ size_t SeaSerializer::Write(const SeaResource& sea) { uint32_t flags = static_cast(sea.flags); Debug("Write SEA flags %x\n", flags); written_total += WriteArithmetic(flags); + + Debug("Write SEA resource exec argv extension %u\n", + static_cast(sea.exec_argv_extension)); + written_total += + WriteArithmetic(static_cast(sea.exec_argv_extension)); DCHECK_EQ(written_total, SeaResource::kHeaderSize); Debug("Write SEA code path %p, size=%zu\n", @@ -158,6 +164,11 @@ SeaResource SeaDeserializer::Read() { CHECK_EQ(magic, kMagic); SeaFlags flags(static_cast(ReadArithmetic())); Debug("Read SEA flags %x\n", static_cast(flags)); + + uint8_t extension_value = ReadArithmetic(); + SeaExecArgvExtension exec_argv_extension = + static_cast(extension_value); + Debug("Read SEA resource exec argv extension %u\n", extension_value); CHECK_EQ(read_total, SeaResource::kHeaderSize); std::string_view code_path = @@ -212,7 +223,13 @@ SeaResource SeaDeserializer::Read() { exec_argv.emplace_back(arg); } } - return {flags, code_path, code, code_cache, assets, exec_argv}; + return {flags, + exec_argv_extension, + code_path, + code, + code_cache, + assets, + exec_argv}; } std::string_view FindSingleExecutableBlob() { @@ -297,26 +314,55 @@ std::tuple FixupArgsForSEA(int argc, char** argv) { if (IsSingleExecutable()) { static std::vector new_argv; static std::vector exec_argv_storage; + static std::vector cli_extension_args; SeaResource sea_resource = FindSingleExecutableResource(); new_argv.clear(); exec_argv_storage.clear(); + cli_extension_args.clear(); + + // Handle CLI extension mode for --node-options + if (sea_resource.exec_argv_extension == SeaExecArgvExtension::kCli) { + // Extract --node-options and filter argv + for (int i = 1; i < argc; ++i) { + if (strncmp(argv[i], "--node-options=", 15) == 0) { + std::string node_options = argv[i] + 15; + std::vector errors; + cli_extension_args = ParseNodeOptionsEnvVar(node_options, &errors); + // Remove this argument by shifting the rest + for (int j = i; j < argc - 1; ++j) { + argv[j] = argv[j + 1]; + } + argc--; + i--; // Adjust index since we removed an element + } + } + } - // Reserve space for argv[0], exec argv, original argv, and nullptr - new_argv.reserve(argc + sea_resource.exec_argv.size() + 2); + // Reserve space for argv[0], exec argv, cli extension args, original argv, + // and nullptr + new_argv.reserve(argc + sea_resource.exec_argv.size() + + cli_extension_args.size() + 2); new_argv.emplace_back(argv[0]); // Insert exec argv from SEA config if (!sea_resource.exec_argv.empty()) { - exec_argv_storage.reserve(sea_resource.exec_argv.size()); + exec_argv_storage.reserve(sea_resource.exec_argv.size() + + cli_extension_args.size()); for (const auto& arg : sea_resource.exec_argv) { exec_argv_storage.emplace_back(arg); new_argv.emplace_back(exec_argv_storage.back().data()); } } - // Add actual run time arguments. + // Insert CLI extension args + for (const auto& arg : cli_extension_args) { + exec_argv_storage.emplace_back(arg); + new_argv.emplace_back(exec_argv_storage.back().data()); + } + + // Add actual run time arguments new_argv.insert(new_argv.end(), argv, argv + argc); new_argv.emplace_back(nullptr); argc = new_argv.size() - 1; @@ -332,6 +378,7 @@ struct SeaConfig { std::string main_path; std::string output_path; SeaFlags flags = SeaFlags::kDefault; + SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv; std::unordered_map assets; std::vector exec_argv; }; @@ -475,6 +522,27 @@ std::optional ParseSingleExecutableConfig( result.flags |= SeaFlags::kIncludeExecArgv; result.exec_argv = std::move(exec_argv); } + } else if (key == "execArgvExtension") { + std::string_view extension_str; + if (field.value().get_string().get(extension_str)) { + FPrintF(stderr, + "\"execArgvExtension\" field of %s is not a string\n", + config_path); + return std::nullopt; + } + if (extension_str == "none") { + result.exec_argv_extension = SeaExecArgvExtension::kNone; + } else if (extension_str == "env") { + result.exec_argv_extension = SeaExecArgvExtension::kEnv; + } else if (extension_str == "cli") { + result.exec_argv_extension = SeaExecArgvExtension::kCli; + } else { + FPrintF(stderr, + "\"execArgvExtension\" field of %s must be one of " + "\"none\", \"env\", or \"cli\"\n", + config_path); + return std::nullopt; + } } } @@ -674,6 +742,7 @@ ExitCode GenerateSingleExecutableBlob( } SeaResource sea{ config.flags, + config.exec_argv_extension, config.main_path, builds_snapshot_from_main ? std::string_view{snapshot_blob.data(), snapshot_blob.size()} diff --git a/src/node_sea.h b/src/node_sea.h index 5ba41064304fdf..686e283fd6441b 100644 --- a/src/node_sea.h +++ b/src/node_sea.h @@ -31,8 +31,15 @@ enum class SeaFlags : uint32_t { kIncludeExecArgv = 1 << 4, }; +enum class SeaExecArgvExtension : uint8_t { + kNone = 0, + kEnv = 1, + kCli = 2, +}; + struct SeaResource { SeaFlags flags = SeaFlags::kDefault; + SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv; std::string_view code_path; std::string_view main_code_or_snapshot; std::optional code_cache; @@ -42,7 +49,8 @@ struct SeaResource { bool use_snapshot() const; bool use_code_cache() const; - static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags); + static constexpr size_t kHeaderSize = + sizeof(kMagic) + sizeof(SeaFlags) + sizeof(SeaExecArgvExtension); }; bool IsSingleExecutable(); diff --git a/test/fixtures/sea-exec-argv-extension-cli.js b/test/fixtures/sea-exec-argv-extension-cli.js new file mode 100644 index 00000000000000..e9585483fcc21d --- /dev/null +++ b/test/fixtures/sea-exec-argv-extension-cli.js @@ -0,0 +1,14 @@ +const assert = require('assert'); + +console.log('process.argv:', JSON.stringify(process.argv)); +console.log('process.execArgv:', JSON.stringify(process.execArgv)); + +// Should have execArgv from SEA config + CLI --node-options +assert.deepStrictEqual(process.execArgv, ['--no-warnings', '--max-old-space-size=1024']); + +assert.deepStrictEqual(process.argv.slice(2), [ + 'user-arg1', + 'user-arg2' +]); + +console.log('execArgvExtension cli test passed'); diff --git a/test/fixtures/sea-exec-argv-extension-env.js b/test/fixtures/sea-exec-argv-extension-env.js new file mode 100644 index 00000000000000..1d706dfe7cfe11 --- /dev/null +++ b/test/fixtures/sea-exec-argv-extension-env.js @@ -0,0 +1,19 @@ +const assert = require('assert'); + +process.emitWarning('This warning should not be shown in the output', 'TestWarning'); + +console.log('process.argv:', JSON.stringify(process.argv)); +console.log('process.execArgv:', JSON.stringify(process.execArgv)); + +// Should have execArgv from SEA config. +// Note that flags from NODE_OPTIONS are not included in process.execArgv no matter it's +// an SEA or not, but we can test whether it works by checking that the warning emitted +// above was silenced. +assert.deepStrictEqual(process.execArgv, ['--no-warnings']); + +assert.deepStrictEqual(process.argv.slice(2), [ + 'user-arg1', + 'user-arg2' +]); + +console.log('execArgvExtension env test passed'); diff --git a/test/fixtures/sea-exec-argv-extension-none.js b/test/fixtures/sea-exec-argv-extension-none.js new file mode 100644 index 00000000000000..c089b065677091 --- /dev/null +++ b/test/fixtures/sea-exec-argv-extension-none.js @@ -0,0 +1,14 @@ +const assert = require('assert'); + +console.log('process.argv:', JSON.stringify(process.argv)); +console.log('process.execArgv:', JSON.stringify(process.execArgv)); + +// Should only have execArgv from SEA config, no NODE_OPTIONS +assert.deepStrictEqual(process.execArgv, ['--no-warnings']); + +assert.deepStrictEqual(process.argv.slice(2), [ + 'user-arg1', + 'user-arg2' +]); + +console.log('execArgvExtension none test passed'); diff --git a/test/sequential/test-single-executable-application-exec-argv-extension-cli.js b/test/sequential/test-single-executable-application-exec-argv-extension-cli.js new file mode 100644 index 00000000000000..81ff05d53ce7a4 --- /dev/null +++ b/test/sequential/test-single-executable-application-exec-argv-extension-cli.js @@ -0,0 +1,63 @@ +'use strict'; + +require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the execArgvExtension "cli" mode in single executable applications. + +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { copyFileSync, writeFileSync, existsSync } = require('fs'); +const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process'); +const { join } = require('path'); +const assert = require('assert'); + +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +tmpdir.refresh(); + +// Copy test fixture to working directory +copyFileSync(fixtures.path('sea-exec-argv-extension-cli.js'), tmpdir.resolve('sea.js')); + +writeFileSync(configFile, ` +{ + "main": "sea.js", + "output": "sea-prep.blob", + "disableExperimentalSEAWarning": true, + "execArgv": ["--no-warnings"], + "execArgvExtension": "cli" +} +`); + +spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { cwd: tmpdir.path }); + +assert(existsSync(seaPrepBlob)); + +generateSEA(outputFile, process.execPath, seaPrepBlob); + +// Test that --node-options works with execArgvExtension: "cli" +spawnSyncAndAssert( + outputFile, + ['--node-options=--max-old-space-size=1024', 'user-arg1', 'user-arg2'], + { + env: { + ...process.env, + NODE_OPTIONS: '--max-old-space-size=2048', // Should be ignored + COMMON_DIRECTORY: join(__dirname, '..', 'common'), + NODE_DEBUG_NATIVE: 'SEA', + } + }, + { + stdout: /execArgvExtension cli test passed/ + }); diff --git a/test/sequential/test-single-executable-application-exec-argv-extension-env.js b/test/sequential/test-single-executable-application-exec-argv-extension-env.js new file mode 100644 index 00000000000000..25d07bdc468f9a --- /dev/null +++ b/test/sequential/test-single-executable-application-exec-argv-extension-env.js @@ -0,0 +1,68 @@ +'use strict'; + +require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the execArgvExtension "env" mode (default) in single executable applications. + +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { copyFileSync, writeFileSync, existsSync } = require('fs'); +const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process'); +const { join } = require('path'); +const assert = require('assert'); + +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +tmpdir.refresh(); + +// Copy test fixture to working directory +copyFileSync(fixtures.path('sea-exec-argv-extension-env.js'), tmpdir.resolve('sea.js')); + +writeFileSync(configFile, ` +{ + "main": "sea.js", + "output": "sea-prep.blob", + "disableExperimentalSEAWarning": true, + "execArgv": ["--no-warnings"], + "execArgvExtension": "env" +} +`); + +spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { cwd: tmpdir.path }); + +assert(existsSync(seaPrepBlob)); + +generateSEA(outputFile, process.execPath, seaPrepBlob); + +// Test that NODE_OPTIONS works with execArgvExtension: "env" (default behavior) +spawnSyncAndAssert( + outputFile, + ['user-arg1', 'user-arg2'], + { + env: { + ...process.env, + NODE_OPTIONS: '--max-old-space-size=512', + COMMON_DIRECTORY: join(__dirname, '..', 'common'), + NODE_DEBUG_NATIVE: 'SEA', + } + }, + { + stdout: /execArgvExtension env test passed/, + stderr(output) { + assert.doesNotMatch(output, /This warning should not be shown in the output/); + return true; + }, + trim: true + }); diff --git a/test/sequential/test-single-executable-application-exec-argv-extension-none.js b/test/sequential/test-single-executable-application-exec-argv-extension-none.js new file mode 100644 index 00000000000000..272016f006f3f7 --- /dev/null +++ b/test/sequential/test-single-executable-application-exec-argv-extension-none.js @@ -0,0 +1,63 @@ +'use strict'; + +require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the execArgvExtension "none" mode in single executable applications. + +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { copyFileSync, writeFileSync, existsSync } = require('fs'); +const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process'); +const { join } = require('path'); +const assert = require('assert'); + +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +tmpdir.refresh(); + +// Copy test fixture to working directory +copyFileSync(fixtures.path('sea-exec-argv-extension-none.js'), tmpdir.resolve('sea.js')); + +writeFileSync(configFile, ` +{ + "main": "sea.js", + "output": "sea-prep.blob", + "disableExperimentalSEAWarning": true, + "execArgv": ["--no-warnings"], + "execArgvExtension": "none" +} +`); + +spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { cwd: tmpdir.path }); + +assert(existsSync(seaPrepBlob)); + +generateSEA(outputFile, process.execPath, seaPrepBlob); + +// Test that NODE_OPTIONS is ignored with execArgvExtension: "none" +spawnSyncAndAssert( + outputFile, + ['user-arg1', 'user-arg2'], + { + env: { + ...process.env, + NODE_OPTIONS: '--max-old-space-size=2048', + COMMON_DIRECTORY: join(__dirname, '..', 'common'), + NODE_DEBUG_NATIVE: 'SEA', + } + }, + { + stdout: /execArgvExtension none test passed/ + }); From 0fa22cbf7c3253572744d1fab12b9494c6be727a Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Mon, 25 Aug 2025 10:35:26 -0300 Subject: [PATCH 107/111] benchmark: calibrate config v8/serialize.js According to recent tests with `calibrate-n` on a dedicated Hetzner machine (4vCPUs | 16gb) the results got stable with n=1e5. The work on https://github.com/nodejs/performance/issues/186#issue-3326002531 has shown this benchmark file spends 2-minutes for a single run. This should improve things a bit. PR-URL: https://github.com/nodejs/node/pull/59586 Reviewed-By: Luigi Pinca Reviewed-By: James M Snell --- benchmark/v8/serialize.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark/v8/serialize.js b/benchmark/v8/serialize.js index c370da49ea7cca..1b9a240bd458e4 100644 --- a/benchmark/v8/serialize.js +++ b/benchmark/v8/serialize.js @@ -4,8 +4,8 @@ const common = require('../common.js'); const v8 = require('v8'); const bench = common.createBenchmark(main, { - len: [256, 1024 * 16, 1024 * 512], - n: [1e6], + len: [256, 1024 * 16], + n: [1e5], }); function main({ n, len }) { From b4f202c2f159b76434c7d2d6876a0d6049ab79ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Mon, 25 Aug 2025 16:58:10 +0100 Subject: [PATCH 108/111] doc: improve `sqlite.backup()` progress/fulfillment documentation PR-URL: https://github.com/nodejs/node/pull/59598 Refs: https://github.com/nodejs/node/pull/56253 Reviewed-By: Edy Silva Reviewed-By: Zeyu "Alex" Yang --- doc/api/sqlite.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index 0c6f9dc1adc578..d12b7622aaffe5 100644 --- a/doc/api/sqlite.md +++ b/doc/api/sqlite.md @@ -756,9 +756,11 @@ changes: * `target` {string} Name of the target database. This can be `'main'` (the default primary database) or any other database that have been added with [`ATTACH DATABASE`][] **Default:** `'main'`. * `rate` {number} Number of pages to be transmitted in each batch of the backup. **Default:** `100`. - * `progress` {Function} Callback function that will be called with the number of pages copied and the total number of - pages. -* Returns: {Promise} A promise that resolves when the backup is completed and rejects if an error occurs. + * `progress` {Function} An optional callback function that will be called after each backup step. The argument passed + to this callback is an {Object} with `remainingPages` and `totalPages` properties, describing the current progress + of the backup operation. +* Returns: {Promise} A promise that fulfills with the total number of backed-up pages upon completion, or rejects if an + error occurs. This method makes a database backup. This method abstracts the [`sqlite3_backup_init()`][], [`sqlite3_backup_step()`][] and [`sqlite3_backup_finish()`][] functions. From 4e02ea1c526aa6ea630a19208d477649bce2f7ad Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Tue, 26 Aug 2025 02:06:31 +0100 Subject: [PATCH 109/111] tools: update gyp-next to 0.20.3 PR-URL: https://github.com/nodejs/node/pull/59603 Reviewed-By: Rafael Gonzaga Reviewed-By: Luigi Pinca --- tools/gyp/CHANGELOG.md | 8 ++++++++ tools/gyp/pylib/gyp/generator/make.py | 4 ++-- tools/gyp/pylib/gyp/xcode_emulation.py | 11 ++++++----- tools/gyp/pyproject.toml | 2 +- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/tools/gyp/CHANGELOG.md b/tools/gyp/CHANGELOG.md index c1c1c5909dc729..7af27a435c409b 100644 --- a/tools/gyp/CHANGELOG.md +++ b/tools/gyp/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.20.3](https://github.com/nodejs/gyp-next/compare/v0.20.2...v0.20.3) (2025-08-20) + + +### Bug Fixes + +* compilation failure on the OpenHarmony platform ([#301](https://github.com/nodejs/gyp-next/issues/301)) ([0cf7a14](https://github.com/nodejs/gyp-next/commit/0cf7a142be06f686b8b42849791de902f177cf9f)) +* make xcode_emulation handle `xcodebuild` not in the `PATH` ([#303](https://github.com/nodejs/gyp-next/issues/303)) ([8224dee](https://github.com/nodejs/gyp-next/commit/8224deef984add7e7afe846cfb82c9d3fa6da1fb)) + ## [0.20.2](https://github.com/nodejs/gyp-next/compare/v0.20.1...v0.20.2) (2025-06-22) diff --git a/tools/gyp/pylib/gyp/generator/make.py b/tools/gyp/pylib/gyp/generator/make.py index 7118492e77f763..0ba1a8c4e1050e 100644 --- a/tools/gyp/pylib/gyp/generator/make.py +++ b/tools/gyp/pylib/gyp/generator/make.py @@ -1880,7 +1880,7 @@ def WriteTarget( self.flavor not in ("mac", "openbsd", "netbsd", "win") and not self.is_standalone_static_library ): - if self.flavor in ("linux", "android"): + if self.flavor in ("linux", "android", "openharmony"): self.WriteMakeRule( [self.output_binary], link_deps, @@ -1894,7 +1894,7 @@ def WriteTarget( part_of_all, postbuilds=postbuilds, ) - elif self.flavor in ("linux", "android"): + elif self.flavor in ("linux", "android", "openharmony"): self.WriteMakeRule( [self.output_binary], link_deps, diff --git a/tools/gyp/pylib/gyp/xcode_emulation.py b/tools/gyp/pylib/gyp/xcode_emulation.py index 0746865dc84b72..08e645c57d5cda 100644 --- a/tools/gyp/pylib/gyp/xcode_emulation.py +++ b/tools/gyp/pylib/gyp/xcode_emulation.py @@ -521,7 +521,7 @@ def _GetSdkVersionInfoItem(self, sdk, infoitem): # most sensible route and should still do the right thing. try: return GetStdoutQuiet(["xcrun", "--sdk", sdk, infoitem]) - except GypError: + except (GypError, OSError): pass def _SdkRoot(self, configname): @@ -1354,7 +1354,7 @@ def _DefaultSdkRoot(self): return default_sdk_root try: all_sdks = GetStdout(["xcodebuild", "-showsdks"]) - except GypError: + except (GypError, OSError): # If xcodebuild fails, there will be no valid SDKs return "" for line in all_sdks.splitlines(): @@ -1508,7 +1508,8 @@ def XcodeVersion(): raise GypError("xcodebuild returned unexpected results") version = version_list[0].split()[-1] # Last word on first line build = version_list[-1].split()[-1] # Last word on last line - except GypError: # Xcode not installed so look for XCode Command Line Tools + except (GypError, OSError): + # Xcode not installed so look for XCode Command Line Tools version = CLTVersion() # macOS Catalina returns 11.0.0.0.1.1567737322 if not version: raise GypError("No Xcode or CLT version detected!") @@ -1541,14 +1542,14 @@ def CLTVersion(): try: output = GetStdout(["/usr/sbin/pkgutil", "--pkg-info", key]) return re.search(regex, output).groupdict()["version"] - except GypError: + except (GypError, OSError): continue regex = re.compile(r"Command Line Tools for Xcode\s+(?P\S+)") try: output = GetStdout(["/usr/sbin/softwareupdate", "--history"]) return re.search(regex, output).groupdict()["version"] - except GypError: + except (GypError, OSError): return None diff --git a/tools/gyp/pyproject.toml b/tools/gyp/pyproject.toml index 62fb2bf8cadf76..b233d8504df687 100644 --- a/tools/gyp/pyproject.toml +++ b/tools/gyp/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "gyp-next" -version = "0.20.2" +version = "0.20.3" authors = [ { name="Node.js contributors", email="ryzokuken@disroot.org" }, ] From fe7176d7c6838fec4356e34ed67946873a7052c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20=C5=81=C4=85giewka?= Date: Mon, 18 Aug 2025 13:11:05 +0200 Subject: [PATCH 110/111] lib: do not modify prototype deprecated asyncResource (encore) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous attempt has missed one argument (`useEmitSync`), therefore it effectively did not work as intended. This change sets `useEmitSync` to `false` which is equivalent to previous behaviour of `undefined` and sets `modifyPrototype` to `false` as expected. PR-URL: https://github.com/nodejs/node/pull/59518 Refs: https://github.com/nodejs/node/issues/58218 Refs: https://github.com/nodejs/node/pull/59195 Reviewed-By: Rafael Gonzaga Reviewed-By: Michaël Zasso --- lib/async_hooks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 4216ee3f240c48..1eb00871e2ebe6 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -262,10 +262,10 @@ class AsyncResource { enumerable: true, get: deprecate(function() { return self; - }, 'The asyncResource property on bound functions is deprecated', 'DEP0172', false), + }, 'The asyncResource property on bound functions is deprecated', 'DEP0172', false, false), set: deprecate(function(val) { self = val; - }, 'The asyncResource property on bound functions is deprecated', 'DEP0172', false), + }, 'The asyncResource property on bound functions is deprecated', 'DEP0172', false, false), }, }); return bound; From d795edb56765d4ec743bcda5a0c1452bed74de0c Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Tue, 26 Aug 2025 07:12:58 +0100 Subject: [PATCH 111/111] 2025-08-27, Version 24.7.0 (Current) Notable changes: crypto: * update root certificates to NSS 3.114 (Node.js GitHub Bot) https://github.com/nodejs/node/pull/59571 * (SEMVER-MINOR) add AES-OCB Web Cryptography algorithm (Filip Skokan) https://github.com/nodejs/node/pull/59539 * (SEMVER-MINOR) support ML-KEM in Web Cryptography (Filip Skokan) https://github.com/nodejs/node/pull/59569 * (SEMVER-MINOR) support ML-KEM, DHKEM, and RSASVE key encapsulation mechanisms (Filip Skokan) https://github.com/nodejs/node/pull/59491 * (SEMVER-MINOR) add argon2() and argon2Sync() methods (Ranieri Althoff) https://github.com/nodejs/node/pull/50353 * (SEMVER-MINOR) support ML-DSA spki/pkcs8 key formats in Web Cryptography (Filip Skokan) https://github.com/nodejs/node/pull/59365 * (SEMVER-MINOR) add ChaCha20-Poly1305 Web Cryptography algorithm (Filip Skokan) https://github.com/nodejs/node/pull/59365 * (SEMVER-MINOR) add subtle.getPublicKey() utility function in Web Cryptography (Filip Skokan) https://github.com/nodejs/node/pull/59365 * (SEMVER-MINOR) add SHA-3 Web Cryptography digest algorithms (Filip Skokan) https://github.com/nodejs/node/pull/59365 * (SEMVER-MINOR) add SHAKE Web Cryptography digest algorithms (Filip Skokan) https://github.com/nodejs/node/pull/59365 * (SEMVER-MINOR) add SubtleCrypto.supports feature detection in Web Cryptography (Filip Skokan) https://github.com/nodejs/node/pull/59365 * (SEMVER-MINOR) support ML-DSA in Web Cryptography (Filip Skokan) https://github.com/nodejs/node/pull/59365 * (SEMVER-MINOR) support ML-KEM KeyObject (Filip Skokan) https://github.com/nodejs/node/pull/59461 http: * (SEMVER-MINOR) add Agent.agentKeepAliveTimeoutBuffer option (Haram Jeong) https://github.com/nodejs/node/pull/59315 http2: * (SEMVER-MINOR) add support for raw header arrays in h2Stream.respond() (Tim Perry) https://github.com/nodejs/node/pull/59455 sea: * (SEMVER-MINOR) support execArgv in sea config (Joyee Cheung) https://github.com/nodejs/node/pull/59314 stream: * (SEMVER-MINOR) add brotli support to CompressionStream and DecompressionStream (Matthew Aitken) https://github.com/nodejs/node/pull/59464 PR-URL: https://github.com/nodejs/node/pull/59629 --- CHANGELOG.md | 3 +- doc/api/cli.md | 2 +- doc/api/crypto.md | 14 +-- doc/api/errors.md | 2 +- doc/api/globals.md | 4 +- doc/api/http.md | 2 +- doc/api/http2.md | 2 +- doc/api/inspector.md | 6 +- doc/api/test.md | 2 +- doc/api/webcrypto.md | 108 ++++++++-------- doc/api/webstreams.md | 4 +- doc/changelogs/CHANGELOG_V24.md | 213 ++++++++++++++++++++++++++++++++ src/node_version.h | 6 +- 13 files changed, 291 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1983b2ddda791..843b312c289d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,8 @@ release. -24.6.0
+24.7.0
+24.6.0
24.5.0
24.4.1
24.4.0
diff --git a/doc/api/cli.md b/doc/api/cli.md index b9b04c34cb24a2..01735cafddf762 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -2605,7 +2605,7 @@ The destination for the corresponding test reporter. See the documentation on A path to a file allowing the test runner to persist the state of the test diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 21579cd719378c..bc0d6442872b9b 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2046,7 +2046,7 @@ Other key details might be exposed via this API using additional attributes. > Stability: 1.2 - Release candidate @@ -3059,7 +3059,7 @@ argon2('argon2id', parameters, (err, derivedKey) => { ### `crypto.argon2Sync(algorithm, parameters)` > Stability: 1.2 - Release candidate @@ -3744,7 +3744,7 @@ algorithm names. ### `crypto.decapsulate(key, ciphertext[, callback])` > Stability: 1.2 - Release candidate @@ -3804,7 +3804,7 @@ If the `callback` function is provided this function uses libuv's threadpool. ### `crypto.encapsulate(key[, callback])` > Stability: 1.2 - Release candidate @@ -3911,7 +3911,7 @@ underlying hash function. See [`crypto.createHmac()`][] for more information. Attempted to use KEM operations while Node.js was not compiled with diff --git a/doc/api/globals.md b/doc/api/globals.md index 9d146b2e97dd2e..fa612bb8d7d74f 100644 --- a/doc/api/globals.md +++ b/doc/api/globals.md @@ -330,7 +330,7 @@ with the [`--no-experimental-websocket`][] CLI flag. * `params` {Object} @@ -616,7 +616,7 @@ a WebSocket connection has been initiated. * `params` {Object} @@ -630,7 +630,7 @@ This event indicates that the WebSocket handshake response has been received. * `params` {Object} diff --git a/doc/api/test.md b/doc/api/test.md index d7edc32ae79eba..264a2f0a825542 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -1382,7 +1382,7 @@ added: - v18.9.0 - v16.19.0 changes: - - version: REPLACEME + - version: v24.7.0 pr-url: https://github.com/nodejs/node/pull/59443 description: Added a rerunFailuresFilePath option. - version: v23.0.0 diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index 1ea5ee0d5ce2fa..4e6594bfbe4db5 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -2,22 +2,22 @@ > Stability: 1.1 - Active development @@ -788,7 +788,7 @@ which can be used to detect whether a given algorithm identifier ### `subtle.decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext)` > Stability: 1.1 - Active development @@ -807,7 +807,7 @@ The algorithms currently supported include: ### `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages)` > Stability: 1.1 - Active development @@ -831,10 +831,10 @@ The algorithms currently supported include: @@ -950,10 +950,10 @@ The algorithms currently supported include: @@ -984,7 +984,7 @@ whose value is one of the above. ### `subtle.encapsulateBits(encapsulationAlgorithm, encapsulationKey)` > Stability: 1.1 - Active development @@ -1002,7 +1002,7 @@ The algorithms currently supported include: ### `subtle.encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKeyAlgorithm, extractable, usages)` > Stability: 1.1 - Active development @@ -1025,10 +1025,10 @@ The algorithms currently supported include: @@ -1057,13 +1057,13 @@ The algorithms currently supported include: > Stability: 1.1 - Active development @@ -1136,13 +1136,13 @@ Derives the public key from a given private key. @@ -1195,13 +1195,13 @@ The {CryptoKey} (secret key) generating algorithms supported include: @@ -1384,7 +1384,7 @@ The unwrapped key algorithms supported include: @@ -1668,13 +1668,13 @@ added: v15.0.0 ### Class: `ContextParams` #### `contextParams.name` * Type: {string} Must be `'ML-DSA-44'`[^modern-algos], `'ML-DSA-65'`[^modern-algos], or `'ML-DSA-87'`[^modern-algos]. @@ -1682,7 +1682,7 @@ added: REPLACEME #### `contextParams.context` * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} @@ -1695,13 +1695,13 @@ which is equivalent to not providing context at all. ### Class: `CShakeParams` #### `cShakeParams.customization` * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} @@ -1713,7 +1713,7 @@ which is equivalent to not providing customization at all. #### `cShakeParams.functionName` * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} @@ -1726,7 +1726,7 @@ which is equivalent to not providing functionName at all. #### `cShakeParams.length` * Type: {number} represents the requested output length in bits. @@ -1734,7 +1734,7 @@ added: REPLACEME #### `cShakeParams.name` * Type: {string} Must be `'cSHAKE128'`[^modern-algos] or `'cSHAKE256'`[^modern-algos] @@ -1777,7 +1777,7 @@ added: v15.0.0 @@ -1905,13 +1905,13 @@ which is equivalent to not providing context at all. ### Class: `EncapsulatedBits` #### `encapsulatedBits.ciphertext` * Type: {ArrayBuffer} @@ -1919,7 +1919,7 @@ added: REPLACEME #### `encapsulatedBits.sharedKey` * Type: {ArrayBuffer} @@ -1927,13 +1927,13 @@ added: REPLACEME ### Class: `EncapsulatedKey` #### `encapsulatedKey.ciphertext` * Type: {ArrayBuffer} @@ -1941,7 +1941,7 @@ added: REPLACEME #### `encapsulatedKey.sharedKey` * Type: {CryptoKey} @@ -1957,7 +1957,7 @@ added: v15.0.0 @@ -2020,7 +2020,7 @@ added: v15.0.0 @@ -2102,7 +2102,7 @@ added: v15.0.0 @@ -2167,7 +2167,7 @@ added: v15.0.0 @@ -2226,7 +2226,7 @@ added: v15.0.0 @@ -2308,7 +2308,7 @@ added: v15.0.0 diff --git a/doc/api/webstreams.md b/doc/api/webstreams.md index 374bf4139c5497..d38196ab0432fa 100644 --- a/doc/api/webstreams.md +++ b/doc/api/webstreams.md @@ -1480,7 +1480,7 @@ changes: