From f82a8044e407fb0cecaaacbc7fed97f12fc8e34b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:12:55 +0200 Subject: [PATCH 01/29] crypto: use internal random buffer bounds Read buffer lengths and element sizes from internal slots in randomFillSync() and randomFill(). Shadowed properties can otherwise skip filling, change the selected range, or fail a native bounds check. Use the intrinsic byte length for the getRandomValues() quota check and keep its delegation to randomFillSync(). Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/random.js | 61 +++++++-- .../test-crypto-randomfill-properties.js | 120 ++++++++++++++++++ ...st-webcrypto-getrandomvalues-properties.js | 31 +++++ 3 files changed, 201 insertions(+), 11 deletions(-) create mode 100644 test/parallel/test-crypto-randomfill-properties.js create mode 100644 test/parallel/test-webcrypto-getrandomvalues-properties.js diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index 919ad68f5617..10bd2627b0bb 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -10,6 +10,7 @@ const { BigInt, BigIntPrototypeToString, DataView, + DataViewPrototypeGetByteLength, DataViewPrototypeGetUint8, DateNow, FunctionPrototypeBind, @@ -21,6 +22,10 @@ const { StringFromCharCodeApply, StringPrototypePadStart, TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetLength, + TypedArrayPrototypeGetSymbolToStringTag, + Uint8Array, } = primordials; const { @@ -58,6 +63,8 @@ const { const { isArrayBufferView, isAnyArrayBuffer, + isDataView, + isSharedArrayBuffer, isTypedArray, isFloat16Array, isFloat32Array, @@ -69,6 +76,35 @@ const { FastBuffer } = require('internal/buffer'); const kMaxInt32 = 2 ** 31 - 1; const kMaxPossibleLength = MathMin(kMaxLength, kMaxInt32); +function getByteLength(buf) { + if (isArrayBufferView(buf)) { + return isDataView(buf) ? + DataViewPrototypeGetByteLength(buf) : TypedArrayPrototypeGetByteLength(buf); + } + if (isSharedArrayBuffer(buf)) + return TypedArrayPrototypeGetByteLength(new Uint8Array(buf)); + return ArrayBufferPrototypeGetByteLength(buf); +} + +function getElementSize(buf) { + switch (TypedArrayPrototypeGetSymbolToStringTag(buf)) { + case 'Int16Array': + case 'Uint16Array': + case 'Float16Array': + return 2; + case 'Int32Array': + case 'Uint32Array': + case 'Float32Array': + return 4; + case 'Float64Array': + case 'BigInt64Array': + case 'BigUint64Array': + return 8; + default: + return 1; + } +} + function assertOffset(offset, elementSize, length) { validateNumber(offset, 'offset'); offset *= elementSize; @@ -125,14 +161,15 @@ function randomFillSync(buf, offset = 0, size) { buf); } - const elementSize = buf.BYTES_PER_ELEMENT || 1; + const elementSize = getElementSize(buf); + const byteLength = getByteLength(buf); - offset = assertOffset(offset, elementSize, buf.byteLength); + offset = assertOffset(offset, elementSize, byteLength); if (size === undefined) { - size = buf.byteLength - offset; + size = byteLength - offset; } else { - size = assertSize(size, elementSize, offset, buf.byteLength); + size = assertSize(size, elementSize, offset, byteLength); } if (size === 0) @@ -159,26 +196,27 @@ function randomFill(buf, offset, size, callback) { buf); } - const elementSize = buf.BYTES_PER_ELEMENT || 1; + const elementSize = getElementSize(buf); if (typeof offset === 'function') { callback = offset; offset = 0; // Size is a length here, assertSize() call turns it into a number of bytes - size = buf.length; + size = isTypedArray(buf) ? TypedArrayPrototypeGetLength(buf) : undefined; } else if (typeof size === 'function') { callback = size; - size = (buf.length ?? buf.byteLength) - offset; + size = (isTypedArray(buf) ? TypedArrayPrototypeGetLength(buf) : getByteLength(buf)) - offset; } else { validateFunction(callback, 'callback'); } - offset = assertOffset(offset, elementSize, buf.byteLength); + const byteLength = getByteLength(buf); + offset = assertOffset(offset, elementSize, byteLength); if (size === undefined) { - size = buf.byteLength - offset; + size = byteLength - offset; } else { - size = assertSize(size, elementSize, offset, buf.byteLength); + size = assertSize(size, elementSize, offset, byteLength); } if (size === 0) { @@ -328,7 +366,8 @@ function getRandomValues(data) { 'The data argument must be an integer-type TypedArray', 'TypeMismatchError'); } - if (data.byteLength > 65536) { + const byteLength = TypedArrayPrototypeGetByteLength(data); + if (byteLength > 65536) { const { QuotaExceededError } = internalBinding('messaging'); throw new QuotaExceededError( 'The requested length exceeds 65,536 bytes'); diff --git a/test/parallel/test-crypto-randomfill-properties.js b/test/parallel/test-crypto-randomfill-properties.js new file mode 100644 index 000000000000..07066b5d462f --- /dev/null +++ b/test/parallel/test-crypto-randomfill-properties.js @@ -0,0 +1,120 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { randomFill, randomFillSync } = require('crypto'); + +const typedArrays = [ + Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, + Int32Array, Uint32Array, Float32Array, Float64Array, + BigInt64Array, BigUint64Array, +]; +if (globalThis.Float16Array !== undefined) + typedArrays.push(globalThis.Float16Array); + +const factories = []; +for (const Backing of [ArrayBuffer, SharedArrayBuffer]) { + for (const Type of [...typedArrays, Buffer, DataView]) { + const elementSize = Type.BYTES_PER_ELEMENT || 1; + factories.push({ + name: `${Type.name} on ${Backing.name}`, + elementSize, + make(length = 64) { + const backing = new Backing(length + 64); + const input = Type === Buffer ? Buffer.from(backing, 32, length) : + new Type(backing, 32, length / elementSize); + return { input, backing, bytes: new Uint8Array(backing), start: 32, length }; + }, + }); + } + factories.push({ + name: Backing.name, + elementSize: 1, + make(length = 64) { + const input = new Backing(length); + return { input, backing: input, bytes: new Uint8Array(input), start: 0, length }; + }, + }); +} + +function shadowMetadata(target, mode) { + const values = { + byteLength: mode === 'zero' ? 0 : 4096, + length: mode === 'zero' ? 0 : 4096, + BYTES_PER_ELEMENT: mode === 'zero' ? 0 : 4096, + buffer: new ArrayBuffer(0), + byteOffset: mode === 'zero' ? 0 : 4096, + [Symbol.toStringTag]: 'NotAnArrayBufferView', + }; + for (const key of Reflect.ownKeys(values)) { + Object.defineProperty(target, key, mode === 'getters' ? + { get: common.mustNotCall(`Unexpected ${String(key)} getter`) } : + { value: values[key] }); + } +} + +function checkFilled({ input, bytes, start }, result, offset, size, label) { + assert.strictEqual(result, input, label); + assert(bytes.subarray(0, start + offset).every((byte) => byte === 0), label); + assert(bytes.subarray(start + offset + size).every((byte) => byte === 0), label); + // Every nonempty test range contains at least 32 random bytes. Detect a + // no-op without assuming that any particular generated byte is nonzero. + if (size !== 0) + assert(bytes.subarray(start + offset, start + offset + size).some((byte) => byte !== 0), label); +} + +for (const factory of factories) { + const { elementSize } = factory; + const ranges = [ + { args: [], offset: 0, size: 64 }, + { args: [16 / elementSize], offset: 16, size: 48 }, + { args: [16 / elementSize, 32 / elementSize], offset: 16, size: 32 }, + { args: [16 / elementSize, undefined], offset: 16, size: 48 }, + { args: [64 / elementSize], offset: 64, size: 0 }, + ]; + for (const mode of ['getters', 'zero', 'inflated']) { + for (const { args, offset, size } of ranges) { + for (const async of [false, true]) { + const value = factory.make(); + shadowMetadata(value.input, mode); + if (value.backing !== value.input) + shadowMetadata(value.backing, mode); + const label = `${factory.name}, ${mode}, ${async ? 'async' : 'sync'}, ${args}`; + if (async) { + assert.strictEqual(randomFill(value.input, ...args, common.mustSucceed((result) => { + checkFilled(value, result, offset, size, label); + })), undefined); + } else { + checkFilled(value, randomFillSync(value.input, ...args), offset, size, label); + } + } + } + } + + // Inflated public bounds must not let an invalid range reach the native job. + // Keep the real element width here so each range exceeds the actual buffer. + const units = 64 / elementSize; + for (const args of [[units + 1], [0, units + 1], [units, 1]]) { + const { input } = factory.make(); + Object.defineProperties(input, { + byteLength: { value: 4096 }, + length: { value: 4096 }, + }); + assert.throws(() => randomFillSync(input, ...args), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => randomFill(input, ...args, common.mustNotCall()), { + code: 'ERR_OUT_OF_RANGE', + }); + } + + for (const args of [[], [0], [0, 0], [0, undefined]]) { + const value = factory.make(0); + shadowMetadata(value.input, 'inflated'); + checkFilled(value, randomFillSync(value.input, ...args), 0, 0, factory.name); + assert.strictEqual(randomFill(value.input, ...args, common.mustSucceed((result) => { + checkFilled(value, result, 0, 0, factory.name); + })), undefined); + } +} diff --git a/test/parallel/test-webcrypto-getrandomvalues-properties.js b/test/parallel/test-webcrypto-getrandomvalues-properties.js new file mode 100644 index 000000000000..c9aca5c9276f --- /dev/null +++ b/test/parallel/test-webcrypto-getrandomvalues-properties.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const webcrypto = globalThis.crypto; + +for (const Type of [Uint8Array, Uint16Array, Uint32Array, BigUint64Array]) { + const storage = new Uint8Array(128); + const view = new Type(storage.buffer, 32, 32 / Type.BYTES_PER_ELEMENT); + for (const name of ['buffer', 'byteOffset', 'byteLength', 'BYTES_PER_ELEMENT']) { + Object.defineProperty(view, name, { get: common.mustNotCall() }); + Object.defineProperty(storage.buffer, name, { get: common.mustNotCall() }); + } + assert.strictEqual(webcrypto.getRandomValues(view), view); + assert(storage.subarray(32, 64).some((byte) => byte !== 0)); + assert(storage.subarray(0, 32).every((byte) => byte === 0)); + assert(storage.subarray(64).every((byte) => byte === 0)); +} + +const oversized = new Uint8Array(65537); +Object.defineProperty(oversized, 'byteLength', { value: 0 }); +assert.throws(() => webcrypto.getRandomValues(oversized), { + name: 'QuotaExceededError', +}); + +const empty = new Uint8Array(0); +Object.defineProperty(empty, 'byteLength', { value: 65537 }); +assert.strictEqual(webcrypto.getRandomValues(empty), empty); From 1cbd1186506b81d86f3cfd3500e015ee9a2ff9b0 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:12:55 +0200 Subject: [PATCH 02/29] crypto: match algorithm names as ASCII Reject non-ASCII equivalents of registered Web Crypto algorithm names. Skip the character scan when the input already uses the canonical name. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/util.js | 10 ++++++++ .../test-webcrypto-algorithm-name-ascii.js | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 test/parallel/test-webcrypto-algorithm-name-ascii.js diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 8ac5046b7404..8c4a2d272b18 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -22,6 +22,7 @@ const { PromiseWithResolvers, SafeMap, SafeSet, + StringPrototypeCharCodeAt, StringPrototypeToUpperCase, Symbol, TypedArrayPrototypeGetBuffer, @@ -802,6 +803,15 @@ function normalizeAlgorithm(algorithm, op) { if (canonicalName === undefined) throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + // Registered names are ASCII. Only check characters when case folding was + // needed, so Unicode characters such as U+017F cannot match ASCII names. + if (algName !== canonicalName) { + for (let i = 0; i < algName.length; i++) { + if (StringPrototypeCharCodeAt(algName, i) > 0x7f) + throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + } + } + algName = canonicalName; const desiredType = registeredAlgorithms[algName]; diff --git a/test/parallel/test-webcrypto-algorithm-name-ascii.js b/test/parallel/test-webcrypto-algorithm-name-ascii.js new file mode 100644 index 000000000000..a1b33830884e --- /dev/null +++ b/test/parallel/test-webcrypto-algorithm-name-ascii.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const data = new Uint8Array(0); + assert.deepStrictEqual(await subtle.digest('sha-256', data), + await subtle.digest('SHA-256', data)); + for (const name of ['\u017fha-256', 'SHA-256\u0131']) { + await assert.rejects(subtle.digest(name, data), { name: 'NotSupportedError' }); + assert.strictEqual(SubtleCrypto.supports('digest', name), false); + } + await assert.rejects(subtle.generateKey({ name: 'AE\u017f-GCM', length: 128 }, + false, ['encrypt']), + { name: 'NotSupportedError' }); + await assert.rejects(subtle.importKey('raw-secret', data, 'Argon2\u0131d', + false, ['deriveBits']), + { name: 'NotSupportedError' }); +})().then(common.mustCall()); From 131cc414ec4da1ab411347021254d311e094e18c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:12:55 +0200 Subject: [PATCH 03/29] crypto: reject short AES-KW inputs Enforce the minimum key-wrap input lengths before the empty-input shortcut, including the integrity-check block when unwrapping. Signed-off-by: Filip Skokan Assisted-by: Codex --- src/crypto/crypto_aes.cc | 9 +++- .../test-webcrypto-aes-kw-short-input.js | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-webcrypto-aes-kw-short-input.js diff --git a/src/crypto/crypto_aes.cc b/src/crypto/crypto_aes.cc index bea8f5b24be5..e46a84188f0f 100644 --- a/src/crypto/crypto_aes.cc +++ b/src/crypto/crypto_aes.cc @@ -47,6 +47,13 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, ByteSource* out) { CHECK_EQ(key_data.GetKeyType(), kKeyTypeSecret); + const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt; + // AES-KW requires at least two 64-bit plaintext blocks, plus the + // 64-bit integrity check value when unwrapping. + if (params.cipher.isWrapMode() && in.size() < (encrypt ? 16u : 24u)) { + return WebCryptoCipherStatus::FAILED; + } + auto ctx = CipherCtxPointer::New(); if (!ctx) { return WebCryptoCipherStatus::FAILED; @@ -56,8 +63,6 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, ctx.setAllowWrap(); } - const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt; - if (!ctx.init(params.cipher, encrypt)) { // Cipher init failed return WebCryptoCipherStatus::FAILED; diff --git a/test/parallel/test-webcrypto-aes-kw-short-input.js b/test/parallel/test-webcrypto-aes-kw-short-input.js new file mode 100644 index 000000000000..09fe0268ab9b --- /dev/null +++ b/test/parallel/test-webcrypto-aes-kw-short-input.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { getFips } = require('crypto'); +const { hasOpenSSL } = require('../common/crypto'); +const { subtle } = globalThis.crypto; + +(async () => { + const keyToWrap = await subtle.importKey( + 'raw', new Uint8Array(16), 'AES-GCM', true, ['encrypt']); + let emptyKey; + if (hasOpenSSL(3) && getFips() !== 1) { + emptyKey = await subtle.importKey( + 'raw-secret', new Uint8Array(0), 'KMAC128', true, ['sign']); + } + + for (const length of [128, 192, 256]) { + const wrappingKey = await subtle.generateKey( + { name: 'AES-KW', length }, false, ['wrapKey', 'unwrapKey']); + + for (const byteLength of [0, 8, 16, 23]) { + // HKDF accepts an empty key, so the unwrap operation must reject + // before attempting to import the plaintext as a key. + await assert.rejects(subtle.unwrapKey( + 'raw', new Uint8Array(byteLength), wrappingKey, 'AES-KW', + 'HKDF', false, ['deriveBits']), { name: 'OperationError' }); + } + + if (emptyKey !== undefined) { + await assert.rejects(subtle.wrapKey( + 'raw-secret', emptyKey, wrappingKey, 'AES-KW'), + { name: 'OperationError' }); + } + + const wrapped = await subtle.wrapKey( + 'raw', keyToWrap, wrappingKey, 'AES-KW'); + assert.strictEqual(wrapped.byteLength, 24); + const unwrapped = await subtle.unwrapKey( + 'raw', wrapped, wrappingKey, 'AES-KW', 'AES-GCM', true, ['encrypt']); + assert.deepStrictEqual( + new Uint8Array(await subtle.exportKey('raw', unwrapped)), + new Uint8Array(16)); + } +})().then(common.mustCall()); From 0e286f02553e077eba5f9686079b01f804329ae8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:13:37 +0200 Subject: [PATCH 04/29] crypto: isolate cipher job errors Use only errors captured by the cipher job when converting its result. Unrelated errors on the event loop thread must not turn a successful operation into a failure or trigger an assertion. Signed-off-by: Filip Skokan Assisted-by: Codex --- src/crypto/crypto_cipher.h | 7 +-- .../test-webcrypto-cipher-unrelated-errors.js | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-webcrypto-cipher-unrelated-errors.js diff --git a/src/crypto/crypto_cipher.h b/src/crypto/crypto_cipher.h index f6deb774ea36..8817171a2be4 100644 --- a/src/crypto/crypto_cipher.h +++ b/src/crypto/crypto_cipher.h @@ -219,6 +219,7 @@ class CipherJob final : public CryptoJob { WebCryptoCipherMode cipher_mode() const { return cipher_mode_; } void DoThreadPoolWork() override { + ncrypto::ClearErrorOnReturn clear_error_on_return; const WebCryptoCipherStatus status = CipherTraits::DoCipher( AsyncWrap::env(), @@ -253,11 +254,7 @@ class CipherJob final : public CryptoJob { Environment* env = AsyncWrap::env(); CryptoErrorStore* errors = CryptoJob::errors(); - if (errors->Empty()) - errors->Capture(); - - if (out_.size() > 0 || errors->Empty()) { - CHECK(errors->Empty()); + if (errors->Empty()) { *err = v8::Undefined(env->isolate()); *result = out_.ToArrayBuffer(env); if (result->IsEmpty()) { diff --git a/test/parallel/test-webcrypto-cipher-unrelated-errors.js b/test/parallel/test-webcrypto-cipher-unrelated-errors.js new file mode 100644 index 000000000000..258733b0440e --- /dev/null +++ b/test/parallel/test-webcrypto-cipher-unrelated-errors.js @@ -0,0 +1,45 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { scrypt, scryptSync } = require('crypto'); +const { subtle } = globalThis.crypto; + +if (typeof scryptSync !== 'function') + common.skip('no scrypt support'); + +function failScrypt(sync) { + // The failed parameter check leaves an error on the main thread's + // OpenSSL error queue. Cipher jobs must only use their own errors. + assert.throws(() => { + if (sync) { + scryptSync('password', 'salt', 64, { N: 2 ** 17 }); + } else { + scrypt('password', 'salt', 64, { N: 2 ** 17 }, common.mustNotCall()); + } + }, { code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS' }); +} + +(async () => { + const key = await subtle.importKey( + 'raw', new Uint8Array(16), 'AES-GCM', false, ['encrypt', 'decrypt']); + const algorithm = { name: 'AES-GCM', iv: new Uint8Array(12) }; + + for (const sync of [true, false]) { + for (const byteLength of [0, 16]) { + const plaintext = new Uint8Array(byteLength); + failScrypt(sync); + const ciphertext = await subtle.encrypt(algorithm, key, plaintext); + assert.strictEqual(ciphertext.byteLength, byteLength + 16); + + failScrypt(sync); + assert.deepStrictEqual( + new Uint8Array(await subtle.decrypt(algorithm, key, ciphertext)), + plaintext); + } + } +})().then(common.mustCall()); From 5d64d76d8cd2f2a634c6de324ff7208d9d962ab2 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:14:40 +0200 Subject: [PATCH 05/29] crypto: convert getRandomValues views Apply Web IDL view conversion before checking the integer array type, including shared and resizable backing-buffer restrictions. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/webcrypto.js | 3 ++- lib/internal/webidl.js | 8 ++++++++ .../test-webcrypto-random-backing-buffer.js | 20 +++++++++++++++++++ test/parallel/test-webcrypto-random.js | 5 ++++- 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-webcrypto-random-backing-buffer.js diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index caeca416836d..c0a50a1b4be1 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -2027,7 +2027,8 @@ function getRandomValues(array) { const prefix = "Failed to execute 'getRandomValues' on 'Crypto'"; webidl.requiredArguments(arguments.length, 1, { prefix }); - return ReflectApply(_getRandomValues, this, arguments); + array = convertSubtleArgument(prefix, 'ArrayBufferView', array, 0); + return _getRandomValues(array); } ObjectDefineProperties( diff --git a/lib/internal/webidl.js b/lib/internal/webidl.js index ee0388b70e97..88f4205a1c05 100644 --- a/lib/internal/webidl.js +++ b/lib/internal/webidl.js @@ -904,6 +904,14 @@ function getViewedArrayBuffer(V) { TypedArrayPrototypeGetBuffer(V) : DataViewPrototypeGetBuffer(V); } +converters.ArrayBufferView = (V, options = kEmptyObject) => { + if (!ArrayBufferIsView(V)) { + throw makeException('is not an ArrayBufferView.', options); + } + validateBufferSourceBacking(getViewedArrayBuffer(V), options); + return V; +}; + /** * Validates [AllowShared] and [AllowResizable] backing-store constraints. * @param {ArrayBuffer|SharedArrayBuffer} buffer Backing buffer. diff --git a/test/parallel/test-webcrypto-random-backing-buffer.js b/test/parallel/test-webcrypto-random-backing-buffer.js new file mode 100644 index 000000000000..607b5b3e5093 --- /dev/null +++ b/test/parallel/test-webcrypto-random-backing-buffer.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +for (const buffer of [ + new SharedArrayBuffer(8), + new SharedArrayBuffer(8, { maxByteLength: 16 }), + new ArrayBuffer(8, { maxByteLength: 16 }), +]) { + for (const Type of [Uint8Array, Float32Array, DataView]) { + assert.throws(() => crypto.getRandomValues(new Type(buffer)), TypeError); + } +} + +assert.throws(() => crypto.getRandomValues(new Proxy(new Uint8Array(8), {})), + TypeError); diff --git a/test/parallel/test-webcrypto-random.js b/test/parallel/test-webcrypto-random.js index 20639d2b8e1d..abc0f86f671b 100644 --- a/test/parallel/test-webcrypto-random.js +++ b/test/parallel/test-webcrypto-random.js @@ -9,8 +9,11 @@ const { Buffer } = require('buffer'); const assert = require('assert'); const { crypto } = globalThis; +for (const value of [undefined, null, '', 1, {}, [], new ArrayBuffer(1)]) { + assert.throws(() => crypto.getRandomValues(value), TypeError); +} + [ - undefined, null, '', 1, {}, [], new Float32Array(1), new Float64Array(1), new DataView(new ArrayBuffer(1)), From bd27c232294fcf9556db0f050c21689473d062a4 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:14:41 +0200 Subject: [PATCH 06/29] crypto: validate normalized hash names Check the normalized SHA algorithm instead of reading the caller hash name before conversion. This prevents changing getters from bypassing the supported hash check. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/util.js | 8 +++- lib/internal/crypto/webidl.js | 19 -------- .../test-webcrypto-hash-normalization.js | 47 +++++++++++++++++++ 3 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 test/parallel/test-webcrypto-hash-normalization.js diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 8c4a2d272b18..05da8442ba24 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -23,6 +23,7 @@ const { SafeMap, SafeSet, StringPrototypeCharCodeAt, + StringPrototypeStartsWith, StringPrototypeToUpperCase, Symbol, TypedArrayPrototypeGetBuffer, @@ -841,7 +842,12 @@ function normalizeAlgorithm(algorithm, op) { getBufferSourceBytes(idlValue), ); } else if (idlType === 'HashAlgorithmIdentifier') { - normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest'); + const hash = normalizeAlgorithm(idlValue, 'digest'); + if (!StringPrototypeStartsWith(hash.name, 'SHA')) { + throw lazyDOMException( + `Only SHA hashes are supported in ${desiredType}`, 'NotSupportedError'); + } + normalizedAlgorithm[member] = hash; } else if (idlType === 'AlgorithmIdentifier') { // This extension point is not used by any supported algorithm (yet?) throw lazyDOMException('Not implemented.', 'NotSupportedError'); diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 73b3af6a7f52..5b3fc8d605b1 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -7,7 +7,6 @@ const { ObjectPrototypeHasOwnProperty, StringPrototypeCharCodeAt, StringPrototypeSplit, - StringPrototypeStartsWith, StringPrototypeToLowerCase, } = primordials; @@ -67,18 +66,6 @@ function namedCurveValidator(V, dict) { const converters = { __proto__: null, ...webidl }; -/** - * @param {string | object} V - The hash algorithm identifier (string or object). - * @param {string} label - The dictionary name for the error message. - */ -function ensureSHA(V, label) { - const name = typeof V === 'string' ? V : V.name; - if (typeof name !== 'string' || - !StringPrototypeStartsWith(StringPrototypeToLowerCase(name), 'sha')) - throw lazyDOMException( - `Only SHA hashes are supported in ${label}`, 'NotSupportedError'); -} - converters.AlgorithmIdentifier = (V, opts) => { // Union for (object or DOMString) if (type(V) === 'Object') { @@ -200,7 +187,6 @@ converters.RsaHashedKeyGenParams = createDictionaryConverter( { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'RsaHashedKeyGenParams'), required: true, }, ], @@ -213,7 +199,6 @@ converters.RsaHashedImportParams = createDictionaryConverter( { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'RsaHashedImportParams'), required: true, }, ], @@ -342,7 +327,6 @@ converters.EcdsaParams = createDictionaryConverter( { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'EcdsaParams'), required: true, }, ], @@ -368,7 +352,6 @@ for (let i = 0; i < kHmacDictionaries.length; i++) { { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, name), required: true, }, { @@ -432,7 +415,6 @@ converters.HkdfParams = createDictionaryConverter( { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'HkdfParams'), required: true, }, { @@ -500,7 +482,6 @@ converters.Pbkdf2Params = createDictionaryConverter( { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'Pbkdf2Params'), required: true, }, ], diff --git a/test/parallel/test-webcrypto-hash-normalization.js b/test/parallel/test-webcrypto-hash-normalization.js new file mode 100644 index 000000000000..c9c09ef1b8d3 --- /dev/null +++ b/test/parallel/test-webcrypto-hash-normalization.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const data = new Uint8Array(32); + let reads = 0; + const key = await subtle.importKey('raw', data, { + name: 'HMAC', + hash: { + get name() { + reads++; + return reads === 1 ? 'SHA-384' : 'SHA-256'; + }, + }, + }, false, ['sign']); + assert.strictEqual(reads, 1); + assert.strictEqual(key.algorithm.hash.name, 'SHA-384'); + assert.strictEqual((await subtle.sign('HMAC', key, data)).byteLength, 48); + + const converted = await subtle.importKey('raw', data, { + name: 'HMAC', + hash: { name: { toString() { return 'SHA-256'; } } }, + }, false, ['sign']); + assert.strictEqual(converted.algorithm.hash.name, 'SHA-256'); + + await assert.rejects(subtle.importKey('raw', data, { + name: 'HMAC', hash: {}, + }, false, ['sign']), TypeError); + const cshakeAvailable = SubtleCrypto.supports('digest', { + name: 'cSHAKE128', outputLength: 256, + }); + await assert.rejects(subtle.importKey('raw', data, { + name: 'HMAC', hash: 'cSHAKE128', + }, false, ['sign']), cshakeAvailable ? TypeError : { name: 'NotSupportedError' }); + await assert.rejects(subtle.importKey('raw', data, { + name: 'HMAC', hash: 'MD5', length: -1, + }, false, ['sign']), TypeError); + await assert.rejects(subtle.importKey('raw', data, { + name: 'HMAC', hash: { name: 'cSHAKE128', outputLength: 256 }, + }, false, ['sign']), { name: 'NotSupportedError' }); +})().then(common.mustCall()); From 7129683f3155533e84af4cc58ed793b83094cfe7 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:14:41 +0200 Subject: [PATCH 07/29] crypto: allow short AES-GCM IVs Allow nonempty GCM IVs shorter than the default length and let the underlying cipher enforce its supported range. Signed-off-by: Filip Skokan Assisted-by: Codex --- src/crypto/crypto_aes.cc | 9 +++- .../test-webcrypto-aes-gcm-iv-length.js | 42 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-webcrypto-aes-gcm-iv-length.js diff --git a/src/crypto/crypto_aes.cc b/src/crypto/crypto_aes.cc index e46a84188f0f..ea869a8eef12 100644 --- a/src/crypto/crypto_aes.cc +++ b/src/crypto/crypto_aes.cc @@ -573,8 +573,13 @@ Maybe AESCipherTraits::AdditionalConfig( UseDefaultIV(params); } - // For OCB mode, allow variable IV lengths (1-15 bytes) - if (params->cipher.isOcbMode()) { + if (params->cipher.isGcmMode()) { + if (params->iv.size() == 0) { + THROW_ERR_CRYPTO_INVALID_IV(env); + return Nothing(); + } + } else if (params->cipher.isOcbMode()) { + // For OCB mode, allow variable IV lengths (1-15 bytes). if (params->iv.size() == 0 || params->iv.size() > 15) { THROW_ERR_CRYPTO_INVALID_IV(env); return Nothing(); diff --git a/test/parallel/test-webcrypto-aes-gcm-iv-length.js b/test/parallel/test-webcrypto-aes-gcm-iv-length.js new file mode 100644 index 000000000000..a4243bd0930b --- /dev/null +++ b/test/parallel/test-webcrypto-aes-gcm-iv-length.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { createCipheriv } = require('crypto'); +const { subtle } = globalThis.crypto; + +(async () => { + const plaintext = Buffer.from('AES-GCM with a variable-length IV'); + const additionalData = Buffer.from('additional data'); + + for (const length of [128, 192, 256]) { + const keyBytes = Buffer.alloc(length / 8); + const key = await subtle.importKey( + 'raw', keyBytes, 'AES-GCM', false, ['encrypt', 'decrypt']); + + for (const ivLength of [1, 8, 11, 12, 16, 128]) { + const iv = Buffer.alloc(ivLength, 1); + const algorithm = { name: 'AES-GCM', iv, additionalData }; + const cipher = createCipheriv(`aes-${length}-gcm`, keyBytes, iv); + cipher.setAAD(additionalData); + const expected = Buffer.concat([ + cipher.update(plaintext), cipher.final(), cipher.getAuthTag(), + ]); + + assert.deepStrictEqual( + Buffer.from(await subtle.encrypt(algorithm, key, plaintext)), expected); + assert.deepStrictEqual( + Buffer.from(await subtle.decrypt(algorithm, key, expected)), plaintext); + } + + const algorithm = { name: 'AES-GCM', iv: new Uint8Array(0) }; + await assert.rejects(subtle.encrypt(algorithm, key, plaintext), + { name: 'OperationError' }); + await assert.rejects(subtle.decrypt(algorithm, key, new Uint8Array(16)), + { name: 'OperationError' }); + } +})().then(common.mustCall()); From 3d8975bd697b9464b358268de0e8ce09d7a1e0c2 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:15:29 +0200 Subject: [PATCH 08/29] crypto: allow unbound SubtleCrypto.supports Static Web IDL operations do not require an interface receiver. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/webcrypto.js | 1 - test/parallel/test-webcrypto-constructors.js | 2 +- test/parallel/test-webcrypto-supports-receiver.js | 14 ++++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-webcrypto-supports-receiver.js diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index c0a50a1b4be1..4ab482a4307a 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1759,7 +1759,6 @@ class SubtleCrypto { // Implements https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-supports static supports(operation, algorithm, lengthOrAdditionalAlgorithm = null) { emitExperimentalWarning('The supports Web Crypto API method'); - if (this !== SubtleCrypto) throw new ERR_INVALID_THIS('SubtleCrypto constructor'); webidl ??= require('internal/crypto/webidl'); const prefix = "Failed to execute 'supports' on 'SubtleCrypto'"; webidl.requiredArguments(arguments.length, 2, { prefix }); diff --git a/test/parallel/test-webcrypto-constructors.js b/test/parallel/test-webcrypto-constructors.js index 3d13b6c92bba..64e1a6ecbbbe 100644 --- a/test/parallel/test-webcrypto-constructors.js +++ b/test/parallel/test-webcrypto-constructors.js @@ -142,7 +142,7 @@ const notSubtle = Reflect.construct(function() {}, [], SubtleCrypto); // Test SubtleCrypto.supports { assert.throws(() => SubtleCrypto.supports.call(undefined), { - name: 'TypeError', code: 'ERR_INVALID_THIS', + name: 'TypeError', code: 'ERR_MISSING_ARGS', }); } diff --git a/test/parallel/test-webcrypto-supports-receiver.js b/test/parallel/test-webcrypto-supports-receiver.js new file mode 100644 index 000000000000..8fa8ed0d4058 --- /dev/null +++ b/test/parallel/test-webcrypto-supports-receiver.js @@ -0,0 +1,14 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { supports } = SubtleCrypto; + +assert.strictEqual(supports('digest', 'SHA-256'), true); +for (const receiver of [undefined, null, {}, globalThis.crypto.subtle]) { + assert.strictEqual(supports.call(receiver, 'digest', 'SHA-256'), true); + assert.strictEqual(supports.call(receiver, 'digest', 'unknown'), false); +} From 7f417a051f9471a3267bab7da35fc8255008245b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:16:42 +0200 Subject: [PATCH 09/29] crypto: resolve supports overloads by type Convert the third argument before checking the operation, and apply additional-algorithm checks only for that overload. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/webcrypto.js | 68 ++++++------------- test/fixtures/webcrypto/supports-level-2.mjs | 4 +- .../test-webcrypto-supports-overloads.js | 36 ++++++++++ 3 files changed, 59 insertions(+), 49 deletions(-) create mode 100644 test/parallel/test-webcrypto-supports-overloads.js diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 4ab482a4307a..847017529dc7 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1772,6 +1772,22 @@ class SubtleCrypto { context: '2nd argument', }); + let length = null; + let additionalAlgorithm; + if (lengthOrAdditionalAlgorithm === null || + typeof lengthOrAdditionalAlgorithm === 'number') { + if (lengthOrAdditionalAlgorithm !== null) { + length = webidl.converters['unsigned long'](lengthOrAdditionalAlgorithm, { + prefix, + context: '3rd argument', + enforceRange: true, + }); + } + } else { + additionalAlgorithm = webidl.converters.AlgorithmIdentifier( + lengthOrAdditionalAlgorithm, { prefix, context: '3rd argument' }); + } + switch (operation) { case 'decapsulateBits': case 'decapsulateKey': @@ -1795,17 +1811,7 @@ class SubtleCrypto { return false; } - let length; - let additionalAlgorithm; - if (operation === 'deriveKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier( - lengthOrAdditionalAlgorithm, - { - prefix, - context: '3rd argument', - }, - ); - + if (additionalAlgorithm !== undefined && operation === 'deriveKey') { if (!check('importKey', additionalAlgorithm)) { return false; } @@ -1817,39 +1823,14 @@ class SubtleCrypto { } operation = 'deriveBits'; - } else if (operation === 'wrapKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier( - lengthOrAdditionalAlgorithm, - { - prefix, - context: '3rd argument', - }, - ); - + } else if (additionalAlgorithm !== undefined && operation === 'wrapKey') { if (!check('exportKey', additionalAlgorithm)) { return false; } - } else if (operation === 'unwrapKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier( - lengthOrAdditionalAlgorithm, - { - prefix, - context: '3rd argument', - }, - ); - + } else if (additionalAlgorithm !== undefined && operation === 'unwrapKey') { if (!check('importKey', additionalAlgorithm)) { return false; } - } else if (operation === 'deriveBits') { - length = lengthOrAdditionalAlgorithm; - if (length !== null) { - length = webidl.converters['unsigned long'](length, { - prefix, - context: '3rd argument', - enforceRange: true, - }); - } } else if (operation === 'getPublicKey') { let normalizedAlgorithm; try { @@ -1870,15 +1851,8 @@ class SubtleCrypto { default: return false; } - } else if (operation === 'encapsulateKey' || operation === 'decapsulateKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier( - lengthOrAdditionalAlgorithm, - { - prefix, - context: '3rd argument', - }, - ); - + } else if (additionalAlgorithm !== undefined && + (operation === 'encapsulateKey' || operation === 'decapsulateKey')) { let sharedKeyLength; let normalizedAdditionalAlgorithm; try { diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index dacaa9070025..2e2014bb5f14 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -268,12 +268,12 @@ export const vectors = { [hasX25519, 'X25519'], ], 'wrapKey': [ - [false, 'AES-KW'], + [true, 'AES-KW'], [true, 'AES-KW', 'AES-CTR'], [true, 'AES-KW', 'HMAC'], ], 'unwrapKey': [ - [false, 'AES-KW'], + [true, 'AES-KW'], [true, 'AES-KW', 'AES-CTR'], ], 'unsupported operation': [ diff --git a/test/parallel/test-webcrypto-supports-overloads.js b/test/parallel/test-webcrypto-supports-overloads.js new file mode 100644 index 000000000000..d59bf24fa607 --- /dev/null +++ b/test/parallel/test-webcrypto-supports-overloads.js @@ -0,0 +1,36 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { supports } = SubtleCrypto; + +for (const operation of ['wrapKey', 'unwrapKey']) { + for (const length of [undefined, null, 0, 128]) { + assert.strictEqual(supports(operation, 'AES-KW', length), true); + } + assert.strictEqual(supports(operation, 'AES-KW'), true); +} + +for (const operation of ['digest', 'unknown', 'wrapKey', 'deriveBits']) { + for (const value of [-1, NaN, Infinity, 2 ** 32, Symbol()]) { + assert.throws(() => supports(operation, 'SHA-256', value), TypeError); + } +} + +const hkdf = { + name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(), info: new Uint8Array(), +}; +assert.strictEqual(supports('deriveBits', hkdf, 128), true); +for (const value of ['128', 'AES-GCM', {}, true, 128n]) { + assert.strictEqual(supports('deriveBits', hkdf, value), false); +} +assert.strictEqual(supports('deriveKey', hkdf), false); +assert.strictEqual(supports('deriveKey', hkdf, { name: 'AES-GCM', length: 128 }), true); + +if (supports('encapsulateBits', 'ML-KEM-768')) { + assert.strictEqual(supports('encapsulateKey', 'ML-KEM-768'), true); + assert.strictEqual(supports('decapsulateKey', 'ML-KEM-768', 128), true); +} From 9b6c75ee1cab548cafbfcbfad0288707c39235bc Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:16:43 +0200 Subject: [PATCH 10/29] crypto: copy detached parameters as empty Treat detached BufferSource parameters as empty byte sequences during algorithm normalization, including detached DataViews. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/util.js | 46 +++++++++++++++---- .../test-webcrypto-detached-parameters.js | 33 +++++++++++++ 2 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 test/parallel/test-webcrypto-detached-parameters.js diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 05da8442ba24..b045696bd11b 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -3,6 +3,7 @@ const { ArrayBufferIsView, ArrayBufferPrototypeGetByteLength, + ArrayBufferPrototypeGetDetached, ArrayPrototypeIncludes, ArrayPrototypePush, ArrayPrototypeSlice, @@ -107,6 +108,7 @@ const { const { isDataView, + isArrayBuffer, isArrayBufferView, isAnyArrayBuffer, isPromise, @@ -874,19 +876,43 @@ function getDataViewOrTypedArrayByteLength(V) { } function getBufferSourceByteLength(V) { - return ArrayBufferIsView(V) ? - getDataViewOrTypedArrayByteLength(V) : - ArrayBufferPrototypeGetByteLength(V); + // ArrayBuffer and typed array lengths already become zero when detached. + if (!ArrayBufferIsView(V)) + return ArrayBufferPrototypeGetByteLength(V); + if (!isDataView(V)) + return TypedArrayPrototypeGetByteLength(V); + const buffer = DataViewPrototypeGetBuffer(V); + if (isArrayBuffer(buffer) && ArrayBufferPrototypeGetDetached(buffer)) + return 0; + return DataViewPrototypeGetByteLength(V); } function getBufferSourceBytes(V) { - return ArrayBufferIsView(V) ? - new Uint8Array( - getDataViewOrTypedArrayBuffer(V), - getDataViewOrTypedArrayByteOffset(V), - getDataViewOrTypedArrayByteLength(V), - ) : - new Uint8Array(V, 0, ArrayBufferPrototypeGetByteLength(V)); + if (!ArrayBufferIsView(V)) { + const length = ArrayBufferPrototypeGetByteLength(V); + if (length === 0 && ArrayBufferPrototypeGetDetached(V)) + return new Uint8Array(0); + return new Uint8Array(V, 0, length); + } + + let buffer; + let offset; + let length; + if (isDataView(V)) { + buffer = DataViewPrototypeGetBuffer(V); + if (isArrayBuffer(buffer) && ArrayBufferPrototypeGetDetached(buffer)) + return new Uint8Array(0); + offset = DataViewPrototypeGetByteOffset(V); + length = DataViewPrototypeGetByteLength(V); + } else { + buffer = TypedArrayPrototypeGetBuffer(V); + offset = TypedArrayPrototypeGetByteOffset(V); + length = TypedArrayPrototypeGetByteLength(V); + if (length === 0 && isArrayBuffer(buffer) && + ArrayBufferPrototypeGetDetached(buffer)) + return new Uint8Array(0); + } + return new Uint8Array(buffer, offset, length); } function getOptionalByteLength(V) { diff --git a/test/parallel/test-webcrypto-detached-parameters.js b/test/parallel/test-webcrypto-detached-parameters.js new file mode 100644 index 000000000000..cf8b34cc9dc4 --- /dev/null +++ b/test/parallel/test-webcrypto-detached-parameters.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +function detached(kind) { + const buffer = new ArrayBuffer(16); + const value = kind === 'ArrayBuffer' ? buffer : + kind === 'DataView' ? new DataView(buffer) : new Uint8Array(buffer); + buffer.transfer(); + return value; +} + +(async () => { + const empty = new Uint8Array(0); + const input = new Uint8Array(16); + const key = await subtle.importKey('raw', input, 'HKDF', false, ['deriveBits']); + const algorithm = { name: 'HKDF', hash: 'SHA-256', salt: empty, info: empty }; + const expected = await subtle.deriveBits(algorithm, key, 256); + const digest = await subtle.digest('SHA-256', empty); + for (const kind of ['ArrayBuffer', 'Uint8Array', 'DataView']) { + for (const member of ['salt', 'info']) { + assert.deepStrictEqual(await subtle.deriveBits({ + ...algorithm, [member]: detached(kind), + }, key, 256), expected); + } + assert.deepStrictEqual(await subtle.digest('SHA-256', detached(kind)), digest); + } +})().then(common.mustCall()); From a45414a1d6ace776220d87e4a88de39d931ce6e6 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:18:26 +0200 Subject: [PATCH 11/29] crypto: convert importKey data as a union Choose the BufferSource or JsonWebKey branch from the value, then check the requested format after algorithm normalization. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/webcrypto.js | 11 +++++- test/parallel/test-webcrypto-import-union.js | 35 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-webcrypto-import-union.js diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 847017529dc7..73ae08fa1e12 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1,6 +1,7 @@ 'use strict'; const { + ArrayBufferIsView, ArrayIsArray, ArrayPrototypeSlice, FunctionPrototypeCall, @@ -39,6 +40,7 @@ const { const { codes: { ERR_ILLEGAL_CONSTRUCTOR, + ERR_INVALID_ARG_TYPE, ERR_INVALID_THIS, }, } = require('internal/errors'); @@ -85,6 +87,7 @@ const { } = require('internal/crypto/random'); const { + isArrayBuffer, isPromise, } = require('internal/util/types'); @@ -1114,7 +1117,8 @@ function importKeyImpl( const prefix = prepareSubtleMethod(this, 'importKey', arguments.length, 5); let i = 0; format = convertSubtleArgument(prefix, 'KeyFormat', format, i++); - const type = format === 'jwk' ? 'JsonWebKey' : 'BufferSource'; + const type = ArrayBufferIsView(keyData) || isArrayBuffer(keyData) ? + 'BufferSource' : 'JsonWebKey'; keyData = convertSubtleArgument(prefix, type, keyData, i++); algorithm = convertSubtleArgument( prefix, 'AlgorithmIdentifier', algorithm, i++); @@ -1124,6 +1128,11 @@ function importKeyImpl( const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'importKey'); + if ((format === 'jwk') !== (type === 'JsonWebKey')) { + throw new ERR_INVALID_ARG_TYPE( + 'keyData', format === 'jwk' ? 'JsonWebKey' : 'BufferSource', keyData); + } + return FunctionPrototypeCall( importKeySync, this, diff --git a/test/parallel/test-webcrypto-import-union.js b/test/parallel/test-webcrypto-import-union.js new file mode 100644 index 000000000000..3391ba5c8dc9 --- /dev/null +++ b/test/parallel/test-webcrypto-import-union.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; +const jwk = { kty: 'oct', k: 'AAAAAAAAAAAAAAAAAAAAAA' }; + +(async () => { + for (const data of [new ArrayBuffer(16), new Uint8Array(16), + new DataView(new ArrayBuffer(16)), Buffer.alloc(16)]) { + await assert.rejects(subtle.importKey('jwk', data, 'AES-GCM', true, ['encrypt']), + TypeError); + Object.assign(data, jwk); + await assert.rejects(subtle.importKey('jwk', data, 'AES-GCM', true, ['encrypt']), + TypeError); + await assert.rejects(subtle.importKey('jwk', data, 'unknown', true, ['encrypt']), + { name: 'NotSupportedError' }); + } + await assert.rejects(subtle.importKey('raw', {}, 'unknown', true, ['encrypt']), + { name: 'NotSupportedError' }); + await assert.rejects(subtle.importKey('raw', {}, 'AES-GCM', true, ['encrypt']), + TypeError); + for (const data of [null, new SharedArrayBuffer(16)]) { + await assert.rejects(subtle.importKey('jwk', data, 'AES-GCM', true, ['encrypt']), + { name: 'DataError' }); + } + assert.strictEqual((await subtle.importKey('jwk', jwk, 'AES-GCM', true, + ['encrypt'])).type, 'secret'); + await assert.rejects(subtle.importKey('raw', { + get kty() { throw new Error('converted JWK'); }, + }, 'unknown', true, ['encrypt']), { message: 'converted JWK' }); +})().then(common.mustCall()); From 677404d53fb8def83c67919d3368ca90f6ff24fb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:18:27 +0200 Subject: [PATCH 12/29] crypto: copy parameters without array species Allocate byte copies directly so typed-array species cannot replace normalized parameters or bit-truncated key material. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/util.js | 15 ++++---- .../test-webcrypto-parameter-species.js | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 test/parallel/test-webcrypto-parameter-species.js diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index b045696bd11b..34e09b8b6ed7 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -12,6 +12,7 @@ const { DataViewPrototypeGetByteLength, DataViewPrototypeGetByteOffset, MathFloor, + MathMin, Number, ObjectDefineProperty, ObjectEntries, @@ -31,7 +32,6 @@ const { TypedArrayPrototypeGetByteLength, TypedArrayPrototypeGetByteOffset, TypedArrayPrototypeGetLength, - TypedArrayPrototypeSlice, Uint8Array, } = primordials; @@ -757,14 +757,11 @@ function truncateToBitLength(length, bytes) { new Uint8Array( getDataViewOrTypedArrayBuffer(bytes), getDataViewOrTypedArrayByteOffset(bytes), - getDataViewOrTypedArrayByteLength(bytes), + MathMin(lengthBytes, getDataViewOrTypedArrayByteLength(bytes)), ) : - new Uint8Array(bytes, 0, ArrayBufferPrototypeGetByteLength(bytes)); - const result = TypedArrayPrototypeSlice( - byteView, - 0, - lengthBytes, - ); + new Uint8Array(bytes, 0, + MathMin(lengthBytes, ArrayBufferPrototypeGetByteLength(bytes))); + const result = new Uint8Array(byteView); const remainder = length % 8; if (remainder !== 0) @@ -840,7 +837,7 @@ function normalizeAlgorithm(algorithm, op) { const idlValue = normalizedAlgorithm[member]; // 3. if (idlType === 'BufferSource' && idlValue) { - normalizedAlgorithm[member] = TypedArrayPrototypeSlice( + normalizedAlgorithm[member] = new Uint8Array( getBufferSourceBytes(idlValue), ); } else if (idlType === 'HashAlgorithmIdentifier') { diff --git a/test/parallel/test-webcrypto-parameter-species.js b/test/parallel/test-webcrypto-parameter-species.js new file mode 100644 index 000000000000..2a02eb8a2810 --- /dev/null +++ b/test/parallel/test-webcrypto-parameter-species.js @@ -0,0 +1,36 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +function withSpecies(callback) { + Object.defineProperty(Uint8Array, Symbol.species, { + configurable: true, + value: Float64Array, + }); + try { + return callback(); + } finally { + delete Uint8Array[Symbol.species]; + } +} + +(async () => { + const key = await subtle.importKey('raw', new Uint8Array(16), + 'AES-GCM', false, ['encrypt']); + const algorithm = { name: 'AES-GCM', iv: new Uint8Array(12).fill(1) }; + const data = new Uint8Array(16); + const expected = await subtle.encrypt(algorithm, key, data); + assert.deepStrictEqual(await withSpecies(() => subtle.encrypt(algorithm, key, data)), expected); + + const hmac = { name: 'HMAC', hash: 'SHA-256', length: 100 }; + const secret = new Uint8Array(13).fill(1); + const normal = await subtle.importKey('raw', secret, hmac, true, ['sign']); + const tampered = await withSpecies(() => subtle.importKey('raw', secret, hmac, true, ['sign'])); + assert.deepStrictEqual(await subtle.exportKey('raw', tampered), await subtle.exportKey('raw', normal)); + assert.deepStrictEqual(await subtle.sign('HMAC', tampered, data), await subtle.sign('HMAC', normal, data)); +})().then(common.mustCall()); From 4f7f16fb8c026ba7c7645f6b0034d5c800b45110 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:18:28 +0200 Subject: [PATCH 13/29] crypto: check RSA JWK alg with SHA-3 hashes Reject a supplied JWK alg when no matching identifier exists for the requested RSA algorithm and hash. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/rsa.js | 2 +- .../test-webcrypto-rsa-jwk-sha3-alg.js | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-webcrypto-rsa-jwk-sha3-alg.js diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 27b23087d563..81ca67234296 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -202,7 +202,7 @@ function rsaImportKey( algorithm.name === 'RSA-PSS' ? normalizeHashName.kContextJwkRsaPss : normalizeHashName.kContextJwkRsaOaep); - if (expected && keyData.alg !== expected) + if (keyData.alg !== expected) throw lazyDOMException( 'JWK "alg" does not match the requested algorithm', 'DataError'); diff --git a/test/parallel/test-webcrypto-rsa-jwk-sha3-alg.js b/test/parallel/test-webcrypto-rsa-jwk-sha3-alg.js new file mode 100644 index 000000000000..88bc64194e6b --- /dev/null +++ b/test/parallel/test-webcrypto-rsa-jwk-sha3-alg.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { isBoringSSL } = require('../common/crypto'); +if (isBoringSSL) + common.skip('missing SHA-3'); + +const assert = require('assert'); +const { createPrivateKey, createPublicKey } = require('crypto'); +const fixtures = require('../common/fixtures'); +const { subtle } = globalThis.crypto; + +(async () => { + const privateKey = createPrivateKey(fixtures.readKey('rsa_private_2048.pem')); + const privateJwk = privateKey.export({ format: 'jwk' }); + const publicJwk = createPublicKey(privateKey).export({ format: 'jwk' }); + + for (const name of ['RSA-PSS', 'RSASSA-PKCS1-v1_5', 'RSA-OAEP']) { + for (const hash of ['SHA3-256', 'SHA3-384', 'SHA3-512']) { + for (const jwk of [publicJwk, privateJwk]) { + const usages = name === 'RSA-OAEP' ? [jwk.d ? 'decrypt' : 'encrypt'] : + [jwk.d ? 'sign' : 'verify']; + const algorithm = { name, hash }; + // There is no JWK alg identifier for RSA with SHA-3. Omitting alg is + // valid, but an identifier for SHA-2 or an unknown identifier is not. + const key = await subtle.importKey('jwk', jwk, algorithm, true, usages); + const exported = await subtle.exportKey('jwk', key); + assert.strictEqual(Object.hasOwn(exported, 'alg'), false); + const imported = await subtle.importKey('jwk', exported, algorithm, true, usages); + assert.deepStrictEqual(imported.algorithm, key.algorithm); + assert.deepStrictEqual(await subtle.exportKey('jwk', imported), exported); + for (const alg of ['RS256', 'PS256', 'RSA-OAEP-256', 'unknown']) { + await assert.rejects(subtle.importKey( + 'jwk', { ...jwk, alg }, algorithm, true, usages), { name: 'DataError' }); + } + } + } + } +})().then(common.mustCall()); From 191d52a410e58ac6e854df1fa96c634e9b7a254e Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:19:32 +0200 Subject: [PATCH 14/29] crypto: reject duplicate unknown key usages Check all JWK key_ops entries for duplicates before ignoring unknown usage names. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/util.js | 7 ++++--- .../test-webcrypto-key-ops-duplicates.js | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-webcrypto-key-ops-duplicates.js diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 34e09b8b6ed7..537870f25e8d 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -1220,15 +1220,16 @@ function validateKeyOps(keyOps, usagesSet) { if (keyOps === undefined) return; validateArray(keyOps, 'keyData.key_ops'); let keyOpsMask = 0; + const seen = new SafeSet(); for (let n = 0; n < keyOps.length; n++) { const op = keyOps[n]; + if (seen.has(op)) + throw lazyDOMException('Duplicate key operation', 'DataError'); + seen.add(op); const opMask = kUsageMasks[op]; // Skipping unknown key ops if (opMask === undefined) continue; - // Have we seen it already? if so, error - if (keyOpsMask & opMask) - throw lazyDOMException('Duplicate key operation', 'DataError'); keyOpsMask |= opMask; // TODO(@jasnell): RFC7517 section 4.3 strong recommends validating diff --git a/test/parallel/test-webcrypto-key-ops-duplicates.js b/test/parallel/test-webcrypto-key-ops-duplicates.js new file mode 100644 index 000000000000..4a666a1caa47 --- /dev/null +++ b/test/parallel/test-webcrypto-key-ops-duplicates.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const algorithm = { name: 'HMAC', hash: 'SHA-256' }; + const jwk = { kty: 'oct', k: 'AAAAAAAAAAAAAAAAAAAAAA' }; + await subtle.importKey('jwk', { ...jwk, key_ops: ['sign', 'future'] }, + algorithm, false, ['sign']); + for (const key_ops of [['sign', 'sign'], ['sign', 'future', 'future']]) { + await assert.rejects(subtle.importKey('jwk', { ...jwk, key_ops }, + algorithm, false, ['sign']), + { name: 'DataError' }); + } +})().then(common.mustCall()); From 2ae6b2072d07120b7e3442a7d59a3bb05586bb22 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:27:08 +0200 Subject: [PATCH 15/29] crypto: handle PBKDF2 iteration limits Report OperationError for unsupported iteration counts and return an empty result before invoking the backend when no output is requested. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/pbkdf2.js | 13 +++++++++--- lib/internal/crypto/webcrypto.js | 2 +- lib/internal/crypto/webidl.js | 10 --------- test/parallel/test-webcrypto-derivebits.js | 2 +- .../test-webcrypto-pbkdf2-iteration-limits.js | 21 +++++++++++++++++++ 5 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 test/parallel/test-webcrypto-pbkdf2-iteration-limits.js diff --git a/lib/internal/crypto/pbkdf2.js b/lib/internal/crypto/pbkdf2.js index f42ce3bbd133..b9f6b59c85bd 100644 --- a/lib/internal/crypto/pbkdf2.js +++ b/lib/internal/crypto/pbkdf2.js @@ -16,6 +16,7 @@ const { } = internalBinding('crypto'); const { + isInt32, validateFunction, validateInt32, validateString, @@ -98,7 +99,7 @@ function check(password, salt, iterations, keylen, digest) { return { password, salt, iterations, keylen, digest }; } -function validatePbkdf2DeriveBitsLength(length) { +function validatePbkdf2DeriveBits({ iterations }, length) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); @@ -107,10 +108,16 @@ function validatePbkdf2DeriveBitsLength(length) { 'length must be a multiple of 8', 'OperationError'); } + if (iterations === 0) + throw lazyDOMException('iterations cannot be zero', 'OperationError'); + if (length !== 0 && !isInt32(iterations)) { + throw lazyDOMException( + 'iterations exceeds the implementation limit', 'OperationError'); + } } function pbkdf2DeriveBits(algorithm, baseKey, length) { - validatePbkdf2DeriveBitsLength(length); + validatePbkdf2DeriveBits(algorithm, length); const { iterations, hash, salt } = algorithm; if (length === 0) @@ -129,5 +136,5 @@ module.exports = { pbkdf2, pbkdf2Sync, pbkdf2DeriveBits, - validatePbkdf2DeriveBitsLength, + validatePbkdf2DeriveBits, }; diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 73ae08fa1e12..ae45d8a9ec24 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1937,7 +1937,7 @@ function check(op, alg, length) { } if (normalizedAlgorithm.name === 'PBKDF2') { - require('internal/crypto/pbkdf2').validatePbkdf2DeriveBitsLength(length); + require('internal/crypto/pbkdf2').validatePbkdf2DeriveBits(normalizedAlgorithm, length); } if (StringPrototypeStartsWith(normalizedAlgorithm.name, 'Argon2')) { diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 5b3fc8d605b1..1d35ce69f56d 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -14,7 +14,6 @@ const { lazyDOMException, } = require('internal/util'); const { - isInt32, isUint32, } = require('internal/validators'); const { @@ -468,15 +467,6 @@ converters.Pbkdf2Params = createDictionaryConverter( key: 'iterations', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), - validator: (V, dict) => { - if (V === 0) - throw lazyDOMException('iterations cannot be zero', 'OperationError'); - if (!isInt32(V)) { - throw lazyDOMException( - 'iterations exceeds the implementation limit', - 'NotSupportedError'); - } - }, required: true, }, { diff --git a/test/parallel/test-webcrypto-derivebits.js b/test/parallel/test-webcrypto-derivebits.js index 99de1e6ba6d7..ee4de1c26a29 100644 --- a/test/parallel/test-webcrypto-derivebits.js +++ b/test/parallel/test-webcrypto-derivebits.js @@ -136,7 +136,7 @@ const rejectsXCurves = hasFIPS(3, 5); salt: new Uint8Array([2]), iterations: 2 ** 31, }, key, 8), - { name: 'NotSupportedError' }); + { name: 'OperationError' }); } test().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-pbkdf2-iteration-limits.js b/test/parallel/test-webcrypto-pbkdf2-iteration-limits.js new file mode 100644 index 000000000000..168ee83a6411 --- /dev/null +++ b/test/parallel/test-webcrypto-pbkdf2-iteration-limits.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const key = await subtle.importKey('raw', new Uint8Array(16), 'PBKDF2', false, ['deriveBits']); + for (const iterations of [2 ** 31, 2 ** 32 - 1]) { + const algorithm = { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array(16), iterations }; + assert.strictEqual((await subtle.deriveBits(algorithm, key, 0)).byteLength, 0); + assert.strictEqual(SubtleCrypto.supports('deriveBits', algorithm, 0), true); + assert.strictEqual(SubtleCrypto.supports('deriveBits', algorithm, 8), false); + await assert.rejects(subtle.deriveBits(algorithm, key, 8), { name: 'OperationError' }); + } + const algorithm = { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array(16), iterations: 0 }; + await assert.rejects(subtle.deriveBits(algorithm, key, 0), { name: 'OperationError' }); +})().then(common.mustCall()); From e937421ca77b74ff11cef1f252a4b0b01028a5f3 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:23:31 +0200 Subject: [PATCH 16/29] crypto: separate conversion from validation Convert algorithm dictionaries on the original receiver, with name read once. Validate normalized parameters in their operation steps after the method-level key checks and generation usage checks. This also makes Argon2 validation use converted parallelism and keeps later dictionary conversion errors ahead of semantic parameter errors. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/aes.js | 7 +- lib/internal/crypto/argon2.js | 2 + lib/internal/crypto/cfrg.js | 3 + lib/internal/crypto/chacha20_poly1305.js | 2 + lib/internal/crypto/diffiehellman.js | 2 + lib/internal/crypto/ec.js | 3 + lib/internal/crypto/hash.js | 2 + lib/internal/crypto/hkdf.js | 2 + lib/internal/crypto/mac.js | 15 ++-- lib/internal/crypto/ml_dsa.js | 3 + lib/internal/crypto/rsa.js | 2 + lib/internal/crypto/util.js | 15 +++- lib/internal/crypto/webcrypto.js | 10 ++- lib/internal/crypto/webidl.js | 89 ++++++++++++++----- .../test-webcrypto-crypto-job-mode.js | 12 +-- test/parallel/test-webcrypto-fips-refresh.js | 37 ++++++++ .../test-webcrypto-normalization-boundary.js | 79 ++++++++++++++++ test/parallel/test-webcrypto-supports-fips.js | 10 ++- test/parallel/test-webcrypto-webidl.js | 10 ++- 19 files changed, 258 insertions(+), 47 deletions(-) create mode 100644 test/parallel/test-webcrypto-fips-refresh.js create mode 100644 test/parallel/test-webcrypto-normalization-boundary.js diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index 3280ddff8459..dd95748dcab4 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -25,6 +25,7 @@ const { getUsagesMask, jobPromise, getBufferSourceByteLength, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -173,6 +174,7 @@ function asyncAesOcbCipher(mode, key, data, algorithm) { } function aesCipher(mode, key, data, algorithm) { + validateAlgorithm(algorithm, 'encrypt'); switch (algorithm.name) { case 'AES-CTR': return asyncAesCtrCipher(mode, key, data, algorithm); case 'AES-CBC': return asyncAesCbcCipher(mode, key, data, algorithm); @@ -185,8 +187,9 @@ function aesCipher(mode, key, data, algorithm) { function aesGenerateKey(algorithm, extractable, usages) { const { name, length } = algorithm; - const usagesSet = validateUsagesNotEmpty( - validateKeyUsages(usages, kUsages[name], name)); + const usagesSet = validateKeyUsages(usages, kUsages[name], name); + validateAlgorithm(algorithm, 'generateKey'); + validateUsagesNotEmpty(usagesSet); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, diff --git a/lib/internal/crypto/argon2.js b/lib/internal/crypto/argon2.js index bf9eaedd85a6..844f9dc94a3e 100644 --- a/lib/internal/crypto/argon2.js +++ b/lib/internal/crypto/argon2.js @@ -29,6 +29,7 @@ const { const { getArrayBufferOrView, jobPromise, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -203,6 +204,7 @@ function validateArgon2DeriveBitsLength(length) { function argon2DeriveBits(algorithm, baseKey, length) { validateArgon2DeriveBitsLength(length); + validateAlgorithm(algorithm, 'deriveBits'); const type = { '__proto__': null, diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 3152362caa8b..2e4bf9a82755 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -23,6 +23,7 @@ const { getUsagesMask, jobPromise, toUsagesSet, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -184,6 +185,8 @@ function eddsaSignVerify(key, data, algorithm, signature) { if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + validateAlgorithm(algorithm, mode === kSignJobModeSign ? 'sign' : 'verify'); + return jobPromise(() => new SignJob( kCryptoJobWebCrypto, mode, diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 45071cded217..32bbcfbf0b74 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -9,6 +9,7 @@ const { const { getUsagesMask, jobPromise, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -36,6 +37,7 @@ function validateKeyLength(length) { } function c20pCipher(mode, key, data, algorithm) { + validateAlgorithm(algorithm, 'encrypt'); return jobPromise(() => new ChaCha20Poly1305CipherJob( kCryptoJobWebCrypto, mode, diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 5bf9d5115427..63a9451b3cc0 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -61,6 +61,7 @@ const { numBitsToBytes, toBuf, truncateToBitLength, + validateAlgorithm, kHandle, } = require('internal/crypto/util'); @@ -332,6 +333,7 @@ function diffieHellman(options, callback) { // The ecdhDeriveBits function is part of the Web Crypto API and serves both // deriveKeys and deriveBits functions. function ecdhDeriveBits(algorithm, baseKey, length) { + validateAlgorithm(algorithm, 'deriveBits'); const { 'public': key } = algorithm; if (getCryptoKeyType(baseKey) !== 'private') { diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 0f1e0555202d..6f566569a258 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -31,6 +31,7 @@ const { getUsagesMask, jobPromise, normalizeHashName, + validateAlgorithm, kNamedCurveAliases, toUsagesSet, } = require('internal/crypto/util'); @@ -72,6 +73,7 @@ function ecGenerateKey(algorithm, extractable, usages) { const { name, namedCurve } = algorithm; const allowedUsages = kUsages[name]; const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); + validateAlgorithm(algorithm, 'generateKey'); const keyAlgorithm = { name, namedCurve }; const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); @@ -139,6 +141,7 @@ function ecImportKey( usages, ) { const { name, namedCurve } = algorithm; + validateAlgorithm(algorithm, 'importKey'); let handle; const allowedUsages = kUsages[name]; diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 69bdcd38c17c..12588b572633 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -27,6 +27,7 @@ const { normalizeHashName, numBitsToBytes, truncateToBitLength, + validateAlgorithm, validateMaxBufferLength, kHandle, getCachedHashId, @@ -240,6 +241,7 @@ Hmac.prototype._transform = Hash.prototype._transform; function asyncDigest(algorithm, data) { validateMaxBufferLength(data, 'data'); + validateAlgorithm(algorithm, 'digest'); switch (algorithm.name) { case 'SHA-1': diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index 8673f9b46817..bf8a296bc4ba 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -29,6 +29,7 @@ const { normalizeHashName, toBuf, validateByteSource, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -182,6 +183,7 @@ function validateHkdfDeriveBitsLength(length, hash) { function hkdfDeriveBits(algorithm, baseKey, length) { const { hash, salt, info } = algorithm; validateHkdfDeriveBitsLength(length, hash); + validateAlgorithm(algorithm, 'deriveBits'); if (length === 0) return PromiseResolve(new ArrayBuffer(0)); diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 3297297abd59..742e1ebe5908 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -21,6 +21,7 @@ const { numBitsToBytes, truncateToBitLength, validateKmacKeyLength, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -71,11 +72,12 @@ function hmacGenerateKey(algorithm, extractable, usages) { const { hash, name, - length = getBlockSize(hash.name), } = algorithm; - const usageSet = validateUsagesNotEmpty( - validateKeyUsages(usages, kUsages, name)); + const usageSet = validateKeyUsages(usages, kUsages, name); + validateAlgorithm(algorithm, 'generateKey'); + const { length = getBlockSize(hash.name) } = algorithm; + validateUsagesNotEmpty(usageSet); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -95,8 +97,9 @@ function kmacGenerateKey(algorithm, extractable, usages) { }[name], } = algorithm; - const usageSet = validateUsagesNotEmpty( - validateKeyUsages(usages, kUsages, name)); + const usageSet = validateKeyUsages(usages, kUsages, name); + validateAlgorithm(algorithm, 'generateKey'); + validateUsagesNotEmpty(usageSet); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -114,6 +117,7 @@ function macImportKey( usages, ) { const isHmac = algorithm.name === 'HMAC'; + validateAlgorithm(algorithm, 'importKey'); const usagesSet = validateKeyUsages( usages, kUsages, algorithm.name); let handle; @@ -181,6 +185,7 @@ function hmacSignVerify(key, data, algorithm, signature) { } function kmacSignVerify(key, data, algorithm, signature) { + validateAlgorithm(algorithm, 'sign'); const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; return jobPromise(() => new KmacJob( kCryptoJobWebCrypto, diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 71238c4726f0..581e9beb5d0f 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -25,6 +25,7 @@ const { getUsagesMask, jobPromise, toUsagesSet, + validateAlgorithm, getBufferSourceByteLength, } = require('internal/crypto/util'); @@ -197,6 +198,8 @@ function mlDsaSignVerify(key, data, algorithm, signature) { if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + validateAlgorithm(algorithm, mode === kSignJobModeSign ? 'sign' : 'verify'); + return jobPromise(() => new SignJob( kCryptoJobWebCrypto, mode, diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 81ca67234296..56c58bf12627 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -31,6 +31,7 @@ const { getUsagesMask, jobPromise, normalizeHashName, + validateAlgorithm, validateMaxBufferLength, toUsagesSet, } = require('internal/crypto/util'); @@ -110,6 +111,7 @@ function rsaKeyGenerate( const allowedUsages = kUsages[name]; const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); + validateAlgorithm(algorithm, 'generateKey'); const publicExponentConverted = bigIntArrayToUnsignedInt(publicExponent); const keyAlgorithm = { diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 537870f25e8d..a9ad6dfb62ff 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -820,8 +820,8 @@ function normalizeAlgorithm(algorithm, op) { return { name: algName }; // 6. - const normalizedAlgorithm = webidl.converters[desiredType]( - { __proto__: algorithm, name: algName }, + const normalizedAlgorithm = webidl.algorithmConverters[desiredType]( + algorithm, kNormalizeAlgorithmOpts, ); // 7. @@ -857,6 +857,16 @@ function normalizeAlgorithm(algorithm, op) { return normalizedAlgorithm; } +function validateAlgorithm(algorithm, op) { + // HMAC's get key length operation handles zero with a TypeError, while + // HmacImportParams otherwise uses the import operation's DataError. + if (op === 'get key length' && algorithm.name === 'HMAC') + return; + webidl ??= require('internal/crypto/webidl'); + const desiredType = getSupportedAlgorithms().algorithms[op]?.[algorithm.name]; + webidl.validators[desiredType]?.(algorithm); +} + function getDataViewOrTypedArrayBuffer(V) { return isDataView(V) ? DataViewPrototypeGetBuffer(V) : TypedArrayPrototypeGetBuffer(V); @@ -1283,6 +1293,7 @@ module.exports = { hasAnyNotIn, validateByteSource, validateKeyOps, + validateAlgorithm, jobPromise, jobPromiseThen, cleanupWebCryptoResult, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index ae45d8a9ec24..7777774a217d 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -41,6 +41,7 @@ const { codes: { ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, ERR_INVALID_THIS, }, } = require('internal/errors'); @@ -70,6 +71,7 @@ const { normalizeHashName, numBitsToBytes, prepareWebCryptoResult, + validateAlgorithm, validateMaxBufferLength, getOptionalByteLength, } = require('internal/crypto/util'); @@ -319,7 +321,9 @@ function deriveBitsImpl(algorithm, baseKey, length = null) { } } -function getKeyLength({ name, length, hash }) { +function getKeyLength(algorithm) { + validateAlgorithm(algorithm, 'get key length'); + const { name, length, hash } = algorithm; switch (name) { case 'AES-CTR': case 'AES-CBC': @@ -331,6 +335,8 @@ function getKeyLength({ name, length, hash }) { return length; case 'HMAC': + if (length === 0) + throw new ERR_INVALID_ARG_VALUE('algorithm.length', length, 'must not be zero'); if (length === undefined) { return getBlockSize(hash?.name); } @@ -1869,6 +1875,7 @@ class SubtleCrypto { normalizeAlgorithm(algorithm, 'get shared key length'); sharedKeyLength = getSharedKeyLength(normalizedAlgorithm); normalizedAdditionalAlgorithm = normalizeAlgorithm(additionalAlgorithm, 'importKey'); + validateAlgorithm(normalizedAdditionalAlgorithm, 'importKey'); } catch { return false; } @@ -1897,6 +1904,7 @@ function check(op, alg, length) { let normalizedAlgorithm; try { normalizedAlgorithm = normalizeAlgorithm(alg, op); + validateAlgorithm(normalizedAlgorithm, op); } catch { if (op === 'wrapKey') { return check('encrypt', alg); diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 1d35ce69f56d..4addaf62bccf 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -2,9 +2,12 @@ const { ArrayPrototypeIncludes, + ArrayPrototypePush, + ArrayPrototypeToSorted, MathPow, NumberParseInt, ObjectPrototypeHasOwnProperty, + ObjectSetPrototypeOf, StringPrototypeCharCodeAt, StringPrototypeSplit, StringPrototypeToLowerCase, @@ -133,6 +136,41 @@ const dictAlgorithm = [ converters.Algorithm = createDictionaryConverter( 'Algorithm', dictAlgorithm); +const validators = { __proto__: null }; +const algorithmConverters = { __proto__: null }; + +// Algorithm.name was converted when selecting the registered dictionary. +// Convert the remaining members on the original object, and keep operation +// validation separate from Web IDL conversion. +function createAlgorithmDictionaryConverter(name, dictionaries) { + const members = []; + const fullMembers = [dictAlgorithm]; + const checks = []; + for (let i = 1; i < dictionaries.length; i++) { + const dictionary = []; + const sorted = ArrayPrototypeToSorted(dictionaries[i], (a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + for (let j = 0; j < sorted.length; j++) { + const member = sorted[j]; + if (ObjectPrototypeHasOwnProperty(member, 'validator')) { + ArrayPrototypePush(checks, { key: member.key, validator: member.validator }); + } + ArrayPrototypePush(dictionary, { ...member, validator: undefined }); + } + ArrayPrototypePush(members, dictionary); + ArrayPrototypePush(fullMembers, dictionary); + } + validators[name] = (algorithm) => { + for (let i = 0; i < checks.length; i++) { + const check = checks[i]; + const value = algorithm[check.key]; + if (value !== undefined) + check.validator(value, algorithm); + } + }; + algorithmConverters[name] = createDictionaryConverter(name, members); + return createDictionaryConverter(name, fullMembers); +} + converters.BigInteger = webidl.Uint8Array; const dictRsaKeyGenParams = [ @@ -172,13 +210,13 @@ const dictRsaKeyGenParams = [ }, ]; -converters.RsaKeyGenParams = createDictionaryConverter( +converters.RsaKeyGenParams = createAlgorithmDictionaryConverter( 'RsaKeyGenParams', [ dictAlgorithm, dictRsaKeyGenParams, ]); -converters.RsaHashedKeyGenParams = createDictionaryConverter( +converters.RsaHashedKeyGenParams = createAlgorithmDictionaryConverter( 'RsaHashedKeyGenParams', [ dictAlgorithm, dictRsaKeyGenParams, @@ -191,7 +229,7 @@ converters.RsaHashedKeyGenParams = createDictionaryConverter( ], ]); -converters.RsaHashedImportParams = createDictionaryConverter( +converters.RsaHashedImportParams = createAlgorithmDictionaryConverter( 'RsaHashedImportParams', [ dictAlgorithm, [ @@ -205,7 +243,7 @@ converters.RsaHashedImportParams = createDictionaryConverter( converters.NamedCurve = converters.DOMString; -converters.EcKeyImportParams = createDictionaryConverter( +converters.EcKeyImportParams = createAlgorithmDictionaryConverter( 'EcKeyImportParams', [ dictAlgorithm, [ @@ -218,7 +256,7 @@ converters.EcKeyImportParams = createDictionaryConverter( ], ]); -converters.EcKeyGenParams = createDictionaryConverter( +converters.EcKeyGenParams = createAlgorithmDictionaryConverter( 'EcKeyGenParams', [ dictAlgorithm, [ @@ -231,7 +269,7 @@ converters.EcKeyGenParams = createDictionaryConverter( ], ]); -converters.AesKeyGenParams = createDictionaryConverter( +converters.AesKeyGenParams = createAlgorithmDictionaryConverter( 'AesKeyGenParams', [ dictAlgorithm, [ @@ -295,7 +333,7 @@ function validateCShakeCustomization(V) { validateMaxBufferLength(V, 'CShakeParams.customization', 512); } -converters.RsaPssParams = createDictionaryConverter( +converters.RsaPssParams = createAlgorithmDictionaryConverter( 'RsaPssParams', [ dictAlgorithm, [ @@ -308,7 +346,7 @@ converters.RsaPssParams = createDictionaryConverter( ], ]); -converters.RsaOaepParams = createDictionaryConverter( +converters.RsaOaepParams = createAlgorithmDictionaryConverter( 'RsaOaepParams', [ dictAlgorithm, [ @@ -319,7 +357,7 @@ converters.RsaOaepParams = createDictionaryConverter( ], ]); -converters.EcdsaParams = createDictionaryConverter( +converters.EcdsaParams = createAlgorithmDictionaryConverter( 'EcdsaParams', [ dictAlgorithm, [ @@ -344,7 +382,7 @@ const kHmacDictionaries = [ ]; for (let i = 0; i < kHmacDictionaries.length; i++) { const { 0: name, 1: zeroError } = kHmacDictionaries[i]; - converters[name] = createDictionaryConverter( + converters[name] = createAlgorithmDictionaryConverter( name, [ dictAlgorithm, [ @@ -407,7 +445,7 @@ converters.JsonWebKey = createDictionaryConverter( simpleDomStringKey('priv'), ]); -converters.HkdfParams = createDictionaryConverter( +converters.HkdfParams = createAlgorithmDictionaryConverter( 'HkdfParams', [ dictAlgorithm, [ @@ -430,7 +468,7 @@ converters.HkdfParams = createDictionaryConverter( ], ]); -converters.CShakeParams = createDictionaryConverter( +converters.CShakeParams = createAlgorithmDictionaryConverter( 'CShakeParams', [ dictAlgorithm, [ @@ -454,7 +492,7 @@ converters.CShakeParams = createDictionaryConverter( ], ]); -converters.Pbkdf2Params = createDictionaryConverter( +converters.Pbkdf2Params = createAlgorithmDictionaryConverter( 'Pbkdf2Params', [ dictAlgorithm, [ @@ -477,7 +515,7 @@ converters.Pbkdf2Params = createDictionaryConverter( ], ]); -converters.AesDerivedKeyParams = createDictionaryConverter( +converters.AesDerivedKeyParams = createAlgorithmDictionaryConverter( 'AesDerivedKeyParams', [ dictAlgorithm, [ @@ -491,7 +529,7 @@ converters.AesDerivedKeyParams = createDictionaryConverter( ], ]); -converters.AesCbcParams = createDictionaryConverter( +converters.AesCbcParams = createAlgorithmDictionaryConverter( 'AesCbcParams', [ dictAlgorithm, [ @@ -504,7 +542,7 @@ converters.AesCbcParams = createDictionaryConverter( ], ]); -converters.AeadParams = createDictionaryConverter( +converters.AeadParams = createAlgorithmDictionaryConverter( 'AeadParams', [ dictAlgorithm, [ @@ -568,7 +606,7 @@ converters.AeadParams = createDictionaryConverter( ], ]); -converters.AesCtrParams = createDictionaryConverter( +converters.AesCtrParams = createAlgorithmDictionaryConverter( 'AesCtrParams', [ dictAlgorithm, [ @@ -596,7 +634,7 @@ converters.AesCtrParams = createDictionaryConverter( converters.CryptoKey = createInterfaceConverter( 'CryptoKey', isCryptoKey); -converters.EcdhKeyDeriveParams = createDictionaryConverter( +converters.EcdhKeyDeriveParams = createAlgorithmDictionaryConverter( 'EcdhKeyDeriveParams', [ dictAlgorithm, [ @@ -618,7 +656,7 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter( ], ]); -converters.ContextParams = createDictionaryConverter( +converters.ContextParams = createAlgorithmDictionaryConverter( 'ContextParams', [ dictAlgorithm, [ @@ -652,7 +690,7 @@ converters.ContextParams = createDictionaryConverter( ], ]); -converters.Argon2Params = createDictionaryConverter( +converters.Argon2Params = createAlgorithmDictionaryConverter( 'Argon2Params', [ dictAlgorithm, [ @@ -729,7 +767,7 @@ converters.Argon2Params = createDictionaryConverter( const kKmacDictionaries = ['KmacKeyGenParams', 'KmacImportParams']; for (let i = 0; i < kKmacDictionaries.length; i++) { const name = kKmacDictionaries[i]; - converters[name] = createDictionaryConverter( + converters[name] = createAlgorithmDictionaryConverter( name, [ dictAlgorithm, [ @@ -743,7 +781,7 @@ for (let i = 0; i < kKmacDictionaries.length; i++) { ]); } -converters.KmacParams = createDictionaryConverter( +converters.KmacParams = createAlgorithmDictionaryConverter( 'KmacParams', [ dictAlgorithm, [ @@ -766,7 +804,7 @@ converters.KmacParams = createDictionaryConverter( ], ]); -converters.KangarooTwelveParams = createDictionaryConverter( +converters.KangarooTwelveParams = createAlgorithmDictionaryConverter( 'KangarooTwelveParams', [ dictAlgorithm, [ @@ -788,7 +826,7 @@ converters.KangarooTwelveParams = createDictionaryConverter( ], ]); -converters.TurboShakeParams = createDictionaryConverter( +converters.TurboShakeParams = createAlgorithmDictionaryConverter( 'TurboShakeParams', [ dictAlgorithm, [ @@ -818,6 +856,9 @@ converters.TurboShakeParams = createDictionaryConverter( ]); module.exports = { + // Spread into fast-property objects before detaching their prototypes. + algorithmConverters: ObjectSetPrototypeOf({ ...algorithmConverters }, null), converters, requiredArguments, + validators: ObjectSetPrototypeOf({ ...validators }, null), }; diff --git a/test/parallel/test-webcrypto-crypto-job-mode.js b/test/parallel/test-webcrypto-crypto-job-mode.js index 5f5f1761a4c7..c050ad5c26bf 100644 --- a/test/parallel/test-webcrypto-crypto-job-mode.js +++ b/test/parallel/test-webcrypto-crypto-job-mode.js @@ -15,10 +15,8 @@ const { } = require('internal/crypto/keys'); const { getUsagesMask, + jobPromise, } = require('internal/crypto/util'); -const { - aesCipher, -} = require('internal/crypto/aes'); const { AESCipherJob, @@ -135,11 +133,13 @@ async function withObjectPrototypeSetters(names, fn) { Buffer.alloc(15)), /Invalid initialization vector/); - const promise = aesCipher( + const promise = jobPromise(() => new AESCipherJob( + kCryptoJobWebCrypto, kWebCryptoCipherEncrypt, - key, + getCryptoKeyHandle(key), Buffer.alloc(16), - { name: 'AES-CBC', iv: Buffer.alloc(15) }); + kKeyVariantAES_CBC_128, + Buffer.alloc(15))); assert.strictEqual(Object.getPrototypeOf(promise), Promise.prototype); await assert.rejects(promise, (err) => { diff --git a/test/parallel/test-webcrypto-fips-refresh.js b/test/parallel/test-webcrypto-fips-refresh.js new file mode 100644 index 000000000000..71459099356c --- /dev/null +++ b/test/parallel/test-webcrypto-fips-refresh.js @@ -0,0 +1,37 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { getFips, setFips } = require('crypto'); +const { internalBinding } = require('internal/test/binding'); +const { getOptionValue } = require('internal/options'); +if (!internalBinding('crypto').testFipsCrypto()) + common.skip('requires an active FIPS provider'); +if (getOptionValue('--force-fips')) + common.skip('FIPS mode cannot be changed when forced'); + +const initial = getFips(); +try { + for (const fips of [false, true, false]) { + setFips(fips); + assert.strictEqual(SubtleCrypto.supports('digest', { + name: 'TurboSHAKE128', outputLength: 128, + }), !fips); + assert.strictEqual(SubtleCrypto.supports('digest', { + name: 'KT128', outputLength: 128, + }), !fips); + assert.strictEqual(SubtleCrypto.supports('digest', { + name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array(1), + }), !fips); + assert.strictEqual(SubtleCrypto.supports('generateKey', { + name: 'RSA-PSS', hash: 'SHA-256', modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + }), !fips); + } +} finally { + setFips(initial); +} diff --git a/test/parallel/test-webcrypto-normalization-boundary.js b/test/parallel/test-webcrypto-normalization-boundary.js new file mode 100644 index 000000000000..20102c32e6a5 --- /dev/null +++ b/test/parallel/test-webcrypto-normalization-boundary.js @@ -0,0 +1,79 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const data = new Uint8Array(16); + const key = await subtle.importKey('raw', data, 'AES-CBC', false, ['encrypt']); + class Parameters { + #iv = new Uint8Array(16); + get name() { return 'AES-CBC'; } + get iv() { return this.#iv; } + } + await subtle.encrypt(new Parameters(), key, data); + + const reads = []; + const algorithm = new Proxy({ name: 'AES-CBC', iv: data }, { + get: common.mustCall((target, member, receiver) => { + assert.strictEqual(receiver, algorithm); + reads.push(member); + return Reflect.get(target, member, receiver); + }, 2), + }); + await subtle.encrypt(algorithm, key, data); + assert.deepStrictEqual(reads, ['name', 'iv']); + + const decryptKey = await subtle.importKey('raw', data, 'AES-CBC', false, ['decrypt']); + await assert.rejects(subtle.encrypt({ name: 'AES-CBC', iv: new Uint8Array(8) }, + decryptKey, data), { name: 'InvalidAccessError' }); + await assert.rejects(subtle.generateKey({ name: 'AES-CBC', length: 100 }, false, ['sign']), + { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey({ name: 'HMAC', hash: 'SHA-256', length: 0 }, false, ['encrypt']), + { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey({ name: 'AES-CBC', length: 64 }, false, []), + { name: 'OperationError' }); + await assert.rejects(subtle.generateKey({ name: 'HMAC', hash: 'SHA-256', length: 0 }, false, []), + { name: 'OperationError' }); + if (SubtleCrypto.supports('digest', 'SHA3-256')) { + const hmac = { name: 'HMAC', hash: 'SHA3-256' }; + await assert.rejects(subtle.generateKey(hmac, false, ['encrypt']), { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey(hmac, false, []), { name: 'NotSupportedError' }); + } + await assert.rejects(subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-999' }, false, ['encrypt']), + { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-999' }, false, []), + { name: 'NotSupportedError' }); + await assert.rejects(subtle.generateKey({ + name: 'RSA-PSS', modulusLength: 0, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', + }, false, []), { name: 'OperationError' }); + await assert.rejects(subtle.encrypt({ name: 'AES-CTR', counter: new Uint8Array(8), length: NaN }, + key, data), TypeError); + assert.strictEqual(SubtleCrypto.supports('encrypt', { name: 'AES-CBC', iv: new Uint8Array(8) }), false); + + const hkdf = { name: 'HKDF', hash: 'SHA-256', salt: data, info: data }; + const base = await subtle.importKey('raw', data, 'HKDF', false, ['deriveKey']); + const noDeriveKey = await subtle.importKey('raw', data, 'HKDF', false, ['deriveBits']); + const hmac = { name: 'HMAC', hash: 'SHA-256', length: 0 }; + await assert.rejects(subtle.deriveKey(hkdf, base, hmac, false, ['sign']), TypeError); + await assert.rejects(subtle.deriveKey(hkdf, noDeriveKey, hmac, false, ['sign']), + { name: 'InvalidAccessError' }); + await assert.rejects(subtle.deriveKey(hkdf, noDeriveKey, { name: 'AES-GCM', length: 100 }, false, ['encrypt']), + { name: 'InvalidAccessError' }); + + if (SubtleCrypto.supports('importKey', 'Argon2id')) { + const base = await subtle.importKey('raw-secret', data, 'Argon2id', false, ['deriveBits']); + const parameters = { name: 'Argon2id', nonce: data, passes: 1, memory: 8, parallelism: 1 }; + const expected = await subtle.deriveBits(parameters, base, 256); + let conversions = 0; + const parallelism = { valueOf() { conversions++; return 1.5; } }; + assert.deepStrictEqual(await subtle.deriveBits({ ...parameters, parallelism }, base, 256), expected); + assert.strictEqual(conversions, 1); + assert.strictEqual(SubtleCrypto.supports('deriveBits', { ...parameters, parallelism: 1.5 }, 256), true); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-supports-fips.js b/test/parallel/test-webcrypto-supports-fips.js index ae5d1d4b32d6..e08551c65f35 100644 --- a/test/parallel/test-webcrypto-supports-fips.js +++ b/test/parallel/test-webcrypto-supports-fips.js @@ -50,21 +50,23 @@ async function check() { } } } + const hashes = crypto.getHashes(); + const hashError = { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }; const rsa = { name: 'RSA-PSS', modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }; assert.strictEqual(SubtleCrypto.supports('generateKey', rsa), !fips); if (fips) { - assert.throws(() => normalizeAlgorithm(rsa, 'generateKey'), { + // Hash normalization precedes the RSA operation's modulus validation. + const error = hashes.includes('sha256') ? { name: 'OperationError', message: 'algorithm.modulusLength must be at least 2048', - }); + } : hashError; + await assert.rejects(subtle.generateKey(rsa, true, ['sign']), error); } else { assert.strictEqual(normalizeAlgorithm(rsa, 'generateKey').modulusLength, 1024); } - const hashes = crypto.getHashes(); const salt = new Uint8Array(16); - const hashError = { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }; for (const [name, alias] of [ ['SHA-1', 'sha1'], ['SHA-256', 'sha256'], ['SHA-384', 'sha384'], ['SHA-512', 'sha512'], ['SHA3-256', 'sha3-256'], ['SHA3-384', 'sha3-384'], ['SHA3-512', 'sha3-512'], diff --git a/test/parallel/test-webcrypto-webidl.js b/test/parallel/test-webcrypto-webidl.js index 3dcc4e7ca863..643343a77f46 100644 --- a/test/parallel/test-webcrypto-webidl.js +++ b/test/parallel/test-webcrypto-webidl.js @@ -643,7 +643,9 @@ function assertJsonWebKey(actual, expected) { }); } - assert.throws(() => converters.Argon2Params({ ...good, passes: 0 }, opts), { + const zeroPasses = converters.Argon2Params({ ...good, passes: 0 }, opts); + assert.strictEqual(zeroPasses.passes, 0); + assert.throws(() => webidl.validators.Argon2Params(zeroPasses), { name: 'OperationError', message: 'passes must be > 0', }); @@ -728,11 +730,13 @@ function assertJsonWebKey(actual, expected) { }; assertIdlDictionary(converters.Argon2Params({ ...good, filtered: 'out' }, opts), good); - assert.throws(() => converters.Argon2Params({ + const excessiveParallelism = converters.Argon2Params({ ...good, parallelism: maxParallelism + 1, memory: 8 * (maxParallelism + 1), - }, opts), { + }, opts); + assert.strictEqual(excessiveParallelism.parallelism, maxParallelism + 1); + assert.throws(() => webidl.validators.Argon2Params(excessiveParallelism), { name: 'OperationError', message: 'parallelism must be > 0 and <= 16777215', }); From 36bb4f617187c77490dc66c55043d1c5fac8dae3 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:25:22 +0200 Subject: [PATCH 17/29] crypto: copy derived bits without species Use the byte-copy helper for truncated ECDH results so ArrayBuffer species cannot replace or resize the returned key material. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/diffiehellman.js | 8 ++---- .../test-webcrypto-derived-bits-species.js | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 test/parallel/test-webcrypto-derived-bits-species.js diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 63a9451b3cc0..46659419c8d6 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -2,7 +2,6 @@ const { ArrayBufferPrototypeGetByteLength, - ArrayBufferPrototypeSlice, FunctionPrototypeCall, ObjectDefineProperty, TypedArrayPrototypeGetBuffer, @@ -381,11 +380,8 @@ function ecdhDeriveBits(algorithm, baseKey, length) { if (byteLength < sliceLength) throw lazyDOMException('derived bit length is too small', 'OperationError'); - if (length % 8 === 0) { - if (byteLength === sliceLength) - return bits; - return ArrayBufferPrototypeSlice(bits, 0, sliceLength); - } + if (length % 8 === 0 && byteLength === sliceLength) + return bits; return TypedArrayPrototypeGetBuffer(truncateToBitLength(length, bits)); }); diff --git a/test/parallel/test-webcrypto-derived-bits-species.js b/test/parallel/test-webcrypto-derived-bits-species.js new file mode 100644 index 000000000000..491dc0df8771 --- /dev/null +++ b/test/parallel/test-webcrypto-derived-bits-species.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const keys = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits']); + const algorithm = { name: 'ECDH', public: keys.publicKey }; + const expected = await subtle.deriveBits(algorithm, keys.privateKey, 128); + const species = Object.getOwnPropertyDescriptor(ArrayBuffer, Symbol.species); + Object.defineProperty(ArrayBuffer, Symbol.species, { + configurable: true, + get: common.mustNotCall(), + }); + try { + const actual = await subtle.deriveBits(algorithm, keys.privateKey, 128); + assert.deepStrictEqual(actual, expected); + assert.strictEqual(Object.getPrototypeOf(actual), ArrayBuffer.prototype); + } finally { + Object.defineProperty(ArrayBuffer, Symbol.species, species); + } +})().then(common.mustCall()); From 716fc4e9d14f51fb4f68edb39aecc82cb1d52c75 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:27:08 +0200 Subject: [PATCH 18/29] crypto: minimize RSA public exponent metadata Omit leading zero octets from generated CryptoKey publicExponent values, matching the BigInteger representation. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/rsa.js | 10 ++++++- .../test-webcrypto-rsa-exponent-length.js | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-webcrypto-rsa-exponent-length.js diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 56c58bf12627..767c5e145072 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -2,6 +2,8 @@ const { TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteOffset, + TypedArrayPrototypeGetLength, Uint8Array, } = primordials; @@ -113,11 +115,17 @@ function rsaKeyGenerate( const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); validateAlgorithm(algorithm, 'generateKey'); const publicExponentConverted = bigIntArrayToUnsignedInt(publicExponent); + let firstNonzeroByte = 0; + while (publicExponent[firstNonzeroByte] === 0) + firstNonzeroByte++; const keyAlgorithm = { name, modulusLength, - publicExponent, + publicExponent: new Uint8Array( + TypedArrayPrototypeGetBuffer(publicExponent), + TypedArrayPrototypeGetByteOffset(publicExponent) + firstNonzeroByte, + TypedArrayPrototypeGetLength(publicExponent) - firstNonzeroByte), hash, }; diff --git a/test/parallel/test-webcrypto-rsa-exponent-length.js b/test/parallel/test-webcrypto-rsa-exponent-length.js new file mode 100644 index 000000000000..c227154c7a1b --- /dev/null +++ b/test/parallel/test-webcrypto-rsa-exponent-length.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { getFips } = require('crypto'); +const { subtle } = globalThis.crypto; + +(async () => { + const exponents = getFips() ? [[1, 0, 1]] : [[3], [1, 0, 1]]; + for (const name of ['RSA-PSS', 'RSASSA-PKCS1-v1_5', 'RSA-OAEP']) { + const usages = name === 'RSA-OAEP' ? ['encrypt', 'decrypt'] : ['sign', 'verify']; + for (const exponent of exponents) { + const publicExponent = new Uint8Array([0, 0, 0, 0, ...exponent]); + const pair = await subtle.generateKey({ + name, modulusLength: 2048, publicExponent, hash: 'SHA-256', + }, true, usages); + for (const key of [pair.publicKey, pair.privateKey]) { + assert.deepStrictEqual(key.algorithm.publicExponent, new Uint8Array(exponent)); + } + assert.deepStrictEqual(publicExponent, new Uint8Array([0, 0, 0, 0, ...exponent])); + } + } +})().then(common.mustCall()); From 02d6adfea37068f0c5ca7dcdbc176dd8ca93bace Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:29:27 +0200 Subject: [PATCH 19/29] crypto: create key usage arrays without species Build public usages and JWK key_ops from the native usage mask so array species cannot replace or mutate internal usage arrays. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/kem_hybrids.js | 6 ++-- lib/internal/crypto/keys.js | 5 ++-- lib/internal/crypto/webcrypto.js | 5 ++-- .../parallel/test-webcrypto-usages-species.js | 30 +++++++++++++++++++ 4 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 test/parallel/test-webcrypto-usages-species.js diff --git a/lib/internal/crypto/kem_hybrids.js b/lib/internal/crypto/kem_hybrids.js index 58fcb6e8fd1a..d95a651b61b7 100644 --- a/lib/internal/crypto/kem_hybrids.js +++ b/lib/internal/crypto/kem_hybrids.js @@ -1,7 +1,6 @@ 'use strict'; const { - ArrayPrototypeSlice, BigInt, PromiseWithResolvers, SafeSet, @@ -24,7 +23,7 @@ const { getCryptoKeySeedData, getCryptoKeySecondaryHandle, getCryptoKeyType, - getCryptoKeyUsages, + getCryptoKeyUsagesMask, InternalCryptoKey, } = require('internal/crypto/keys'); @@ -51,6 +50,7 @@ const { const { getBufferSourceBytes, getUsagesMask, + getUsagesFromMask, jobPromise, jobPromiseThen, resolveWebCryptoResult, @@ -860,7 +860,7 @@ function exportJwkKey(key, rawEncapsulationKey, config) { kty: 'AKP', alg: config.name, pub: base64urlEncode(rawEncapsulationKey), - key_ops: ArrayPrototypeSlice(getCryptoKeyUsages(key), 0), + key_ops: getUsagesFromMask(getCryptoKeyUsagesMask(key)), ext: getCryptoKeyExtractable(key), }; if (getCryptoKeyType(key) === 'private') diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index f54ba68f6705..cdbf163ec081 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -2,7 +2,6 @@ const { ArrayPrototypeMap, - ArrayPrototypeSlice, ObjectDefineProperties, ObjectPrototypeHasOwnProperty, ObjectSetPrototypeOf, @@ -1224,7 +1223,7 @@ const { type: getCryptoKeyType(this), extractable: getCryptoKeyExtractable(this), algorithm: cloneAlgorithm(getCryptoKeyAlgorithm(this)), - usages: ArrayPrototypeSlice(getCryptoKeyUsages(this), 0), + usages: getUsagesFromMask(getCryptoKeyUsagesMask(this)), }, opts)}`; } @@ -1258,7 +1257,7 @@ const { const slots = getSlots(this); let cached = slots[kSlotClonedUsages]; if (cached === undefined) { - cached = ArrayPrototypeSlice(getCryptoKeyUsagesFromSlots(slots), 0); + cached = getUsagesFromMask(slots[kSlotUsagesMask]); slots[kSlotClonedUsages] = cached; } return cached; diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 7777774a217d..2f44d726026b 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -3,7 +3,6 @@ const { ArrayBufferIsView, ArrayIsArray, - ArrayPrototypeSlice, FunctionPrototypeCall, JSONParse, JSONStringify, @@ -52,7 +51,6 @@ const { getCryptoKeyExtractable, getCryptoKeyHandle, getCryptoKeyType, - getCryptoKeyUsages, getCryptoKeyUsagesMask, hasCryptoKeyUsage, importGenericSecretKey, @@ -66,6 +64,7 @@ const { const { cleanupWebCryptoResult, getBlockSize, + getUsagesFromMask, jobPromiseThen, normalizeAlgorithm, normalizeHashName, @@ -801,7 +800,7 @@ function exportKeyJWK(key) { // `parameters` before native export populates key material. Delete it for // algorithms without a JWK alg value to keep the expected shape. const parameters = { - key_ops: ArrayPrototypeSlice(getCryptoKeyUsages(key), 0), + key_ops: getUsagesFromMask(getCryptoKeyUsagesMask(key)), ext: getCryptoKeyExtractable(key), alg, }; diff --git a/test/parallel/test-webcrypto-usages-species.js b/test/parallel/test-webcrypto-usages-species.js new file mode 100644 index 000000000000..2e1aed7ec42f --- /dev/null +++ b/test/parallel/test-webcrypto-usages-species.js @@ -0,0 +1,30 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const key = await subtle.importKey('raw', new Uint8Array(16), 'AES-GCM', true, + ['encrypt', 'decrypt']); + const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, 'constructor'); + let usages; + let jwk; + try { + Object.defineProperty(Array.prototype, 'constructor', { get: common.mustNotCall() }); + usages = key.usages; + jwk = await subtle.exportKey('jwk', key); + } finally { + Object.defineProperty(Array.prototype, 'constructor', descriptor); + } + assert.strictEqual(Object.getPrototypeOf(usages), Array.prototype); + assert.strictEqual(key.usages, usages); + assert.deepStrictEqual(usages, ['encrypt', 'decrypt']); + assert.deepStrictEqual(jwk.key_ops, usages); + usages.push('sign'); + assert.deepStrictEqual((await subtle.exportKey('jwk', key)).key_ops, + ['encrypt', 'decrypt']); +})().then(common.mustCall()); From 6e9984ce043cd39b6e49ee75aaa91c03c03ed69d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:29:27 +0200 Subject: [PATCH 20/29] crypto: derive Argon2 without worker threads Keep Argon2 lane count independent from backend worker availability. When the OpenSSL thread pool is unavailable, compute lanes serially. Signed-off-by: Filip Skokan Assisted-by: Codex --- deps/ncrypto/ncrypto.cc | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 83b3eb9d6749..fe99e84d7fac 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -2937,7 +2937,9 @@ DataPointer argon2(const Buffer& pass, // per-context. It inherits no configuration, so availability is checked // against the default context, otherwise Argon2 works in FIPS mode. DeleteFnPtr ctx; - if (lanes > 1) { + uint32_t threads = lanes == 0 ? 0 : 1; + if (lanes > 1 && (OSSL_get_thread_support_flags() & + OSSL_THREAD_SUPPORT_FLAG_DEFAULT_SPAWN) != 0) { if (!KDF::Fetch(algorithm.data())) { return {}; } @@ -2947,8 +2949,13 @@ DataPointer argon2(const Buffer& pass, return {}; } - if (OSSL_set_max_threads(ctx.get(), lanes) != 1) { - return {}; + MarkPopErrorOnReturn mark_pop_error_on_return; + if (OSSL_set_max_threads(ctx.get(), lanes) == 1) { + threads = lanes; + } else { + // Lane count is an Argon2 input; worker threads are only an + // optimization. Compute the same lanes serially if unavailable. + ctx.reset(); } } @@ -2966,7 +2973,8 @@ DataPointer argon2(const Buffer& pass, 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_THREADS, &threads)); params.push_back( OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &lanes)); params.push_back( From baaf5fc00ac91fa8f9d0c5d689b037c457c91ca6 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:29:27 +0200 Subject: [PATCH 21/29] crypto: report actual RSA modulus lengths Use the generated key's modulus size for both CryptoKey algorithm objects. Preserve successful backend generation when it rounds the requested size, keeping metadata consistent across export and cloning. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/rsa.js | 11 ++++- .../test-webcrypto-rsa-modulus-length.js | 47 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-webcrypto-rsa-modulus-length.js diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 767c5e145072..8fa3ecdff1b8 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -32,6 +32,7 @@ const { bigIntArrayToUnsignedInt, getUsagesMask, jobPromise, + jobPromiseThen, normalizeHashName, validateAlgorithm, validateMaxBufferLength, @@ -132,7 +133,7 @@ function rsaKeyGenerate( const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); validateUsagesNotEmpty(keyUsages.private); - return jobPromise(() => new RsaKeyPairGenJob( + return jobPromiseThen(jobPromise(() => new RsaKeyPairGenJob( kCryptoJobWebCrypto, kKeyVariantRSA_SSA_PKCS1_v1_5, modulusLength, @@ -140,7 +141,13 @@ function rsaKeyGenerate( keyAlgorithm, getUsagesMask(keyUsages.public), getUsagesMask(keyUsages.private), - extractable)); + extractable)), (result) => { + const { modulusLength: actualModulusLength } = + getCryptoKeyHandle(result.publicKey).keyDetail({ __proto__: null }); + getCryptoKeyAlgorithm(result.publicKey).modulusLength = actualModulusLength; + getCryptoKeyAlgorithm(result.privateKey).modulusLength = actualModulusLength; + return result; + }); } function rsaExportKey(key, format) { diff --git a/test/parallel/test-webcrypto-rsa-modulus-length.js b/test/parallel/test-webcrypto-rsa-modulus-length.js new file mode 100644 index 000000000000..72032ed1a443 --- /dev/null +++ b/test/parallel/test-webcrypto-rsa-modulus-length.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { generateKeyPairSync, KeyObject } = require('crypto'); +const { subtle } = globalThis.crypto; + +(async () => { + for (const name of ['RSA-PSS', 'RSASSA-PKCS1-v1_5', 'RSA-OAEP']) { + const usages = name === 'RSA-OAEP' ? ['encrypt', 'decrypt'] : ['sign', 'verify']; + for (const modulusLength of [1025, 2048, 2049]) { + let pair; + try { + pair = await subtle.generateKey({ + name, modulusLength, hash: 'SHA-256', publicExponent: new Uint8Array([1, 0, 1]), + }, true, usages); + } catch (err) { + if (modulusLength === 2048 || err.name !== 'OperationError') + throw err; + // Only reject sizes the backend also rejects. Some backends round + // the requested size, which must still produce a usable CryptoKey. + assert.throws(() => generateKeyPairSync('rsa', { modulusLength }), + { name: 'Error' }); + continue; + } + const actualModulusLength = + KeyObject.from(pair.publicKey).asymmetricKeyDetails.modulusLength; + for (const key of [pair.publicKey, pair.privateKey]) { + assert.strictEqual(key.algorithm.modulusLength, actualModulusLength); + assert.strictEqual( + KeyObject.from(key).asymmetricKeyDetails.modulusLength, actualModulusLength); + assert.deepStrictEqual(structuredClone(key).algorithm, key.algorithm); + + const format = key.type === 'public' ? 'spki' : 'pkcs8'; + const imported = await subtle.importKey( + format, await subtle.exportKey(format, key), + { name, hash: 'SHA-256' }, true, key.usages); + assert.deepStrictEqual(imported.algorithm, key.algorithm); + } + const publicKey = await subtle.getPublicKey(pair.privateKey, pair.publicKey.usages); + assert.deepStrictEqual(publicKey.algorithm, pair.publicKey.algorithm); + } + } +})().then(common.mustCall()); From ac0931cfd8a3ae05f8f3e4ad88a15bbbdb5581c3 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:32:12 +0200 Subject: [PATCH 22/29] crypto: split hybrid keys without species Create byte views directly when splitting hybrid KEM keys and seeds, so typed-array species cannot alter the component key material. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/kem_hybrids.js | 16 ++++++--- .../test-webcrypto-hybrid-kem-species.js | 36 +++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-webcrypto-hybrid-kem-species.js diff --git a/lib/internal/crypto/kem_hybrids.js b/lib/internal/crypto/kem_hybrids.js index d95a651b61b7..aa8fe27efed9 100644 --- a/lib/internal/crypto/kem_hybrids.js +++ b/lib/internal/crypto/kem_hybrids.js @@ -8,9 +8,9 @@ const { TypedArrayOf, TypedArrayPrototypeGetBuffer, TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, TypedArrayPrototypeGetLength, TypedArrayPrototypeSet, - TypedArrayPrototypeSubarray, Uint8Array, } = primordials; @@ -111,6 +111,12 @@ const kEncapsulationUsages = ['encapsulateKey', 'encapsulateBits']; const kDecapsulationUsages = ['decapsulateKey', 'decapsulateBits']; const kUsages = createKeyUsages(kEncapsulationUsages, kDecapsulationUsages); +function getByteView(data, start, end = TypedArrayPrototypeGetByteLength(data)) { + return new Uint8Array(TypedArrayPrototypeGetBuffer(data), + TypedArrayPrototypeGetByteOffset(data) + start, + end - start); +} + /** * Adds lengths that are the concatenation of the PQ KEM component and the * traditional group component. @@ -289,8 +295,8 @@ function validateLength(data, length) { function splitAt(data, offset) { return { __proto__: null, - head: TypedArrayPrototypeSubarray(data, 0, offset), - tail: TypedArrayPrototypeSubarray(data, offset), + head: getByteView(data, 0, offset), + tail: getByteView(data, offset), }; } @@ -353,7 +359,7 @@ function randomScalar(seed, config) { offset + groupScalarLength <= TypedArrayPrototypeGetLength(seed); offset += groupScalarLength) { const scalar = copyBytes( - TypedArrayPrototypeSubarray(seed, offset, offset + groupScalarLength)); + getByteView(seed, offset, offset + groupScalarLength)); const value = os2ip(scalar); if (value !== 0n && value < groupOrder) return scalar; @@ -761,7 +767,7 @@ function combineSharedSecret( config) { // C2PRICombiner(ss_PQ, ss_T, ct_T, ek_T, Label). // https://www.ietf.org/archive/id/draft-irtf-cfrg-hybrid-kems-12.html#section-5.1.3 - const encapsulationKeyT = TypedArrayPrototypeSubarray( + const encapsulationKeyT = getByteView( encapsulationKey, config.kemPqEncapsulationKeyLength); const inputLength = diff --git a/test/parallel/test-webcrypto-hybrid-kem-species.js b/test/parallel/test-webcrypto-hybrid-kem-species.js new file mode 100644 index 000000000000..0521e0b7fc4b --- /dev/null +++ b/test/parallel/test-webcrypto-hybrid-kem-species.js @@ -0,0 +1,36 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + for (const name of ['MLKEM768-P256', 'MLKEM768-X25519', 'MLKEM1024-P384']) { + if (!SubtleCrypto.supports('generateKey', name)) continue; + const { privateKey, publicKey } = await subtle.generateKey( + name, true, ['encapsulateBits', 'decapsulateBits']); + const seed = await subtle.exportKey('raw-seed', privateKey); + const publicBytes = await subtle.exportKey('raw-public', publicKey); + const descriptor = Object.getOwnPropertyDescriptor(Uint8Array, Symbol.species); + try { + Object.defineProperty(Uint8Array, Symbol.species, { + configurable: true, get: common.mustNotCall(), + }); + const imported = await subtle.importKey('raw-seed', seed, name, true, + ['decapsulateBits']); + assert.deepStrictEqual(await subtle.exportKey('raw-seed', imported), seed); + const publicImported = await subtle.importKey('raw-public', publicBytes, name, + true, ['encapsulateBits']); + assert.deepStrictEqual(await subtle.exportKey('raw-public', publicImported), publicBytes); + } finally { + if (descriptor) { + Object.defineProperty(Uint8Array, Symbol.species, descriptor); + } else { + delete Uint8Array[Symbol.species]; + } + } + } +})().then(common.mustCall()); From a95665bce1c4430cf4e080afb548d816ea533c21 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:36:59 +0200 Subject: [PATCH 23/29] crypto: use the public CryptoKey prototype Give generated, imported, converted, and transferred keys the public interface prototype directly, without an intermediate internal object. Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/crypto/keys.js | 8 +++--- .../test-webcrypto-cryptokey-brand-check.js | 12 ++++----- test/parallel/test-webcrypto-key-prototype.js | 25 +++++++++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 test/parallel/test-webcrypto-key-prototype.js diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index cdbf163ec081..4b62919a0bbc 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -1139,10 +1139,9 @@ function getKeyObjectAsymmetricKeyDetails(key) { // KEM-only seed data) lives // on a C++ class, NativeCryptoKey, created by createCryptoKeyClass. // InternalCryptoKey is the only constructor we expose to internal -// code; it extends NativeCryptoKey to get that storage and then has -// its prototype spliced so the chain visible to user code is: -// instance -> InternalCryptoKey.prototype -// -> CryptoKey.prototype +// code; it extends NativeCryptoKey to get that storage and gives each +// instance the public interface prototype: +// instance -> CryptoKey.prototype // -> Object.prototype // // Normal construction caches all native internal slots in a private class @@ -1285,6 +1284,7 @@ const { extractable, secondaryHandle, seedData); + ObjectSetPrototypeOf(this, CryptoKey.prototype); if (algorithm !== undefined) { this.#slots = [ handle.getKeyType(), diff --git a/test/parallel/test-webcrypto-cryptokey-brand-check.js b/test/parallel/test-webcrypto-cryptokey-brand-check.js index 9174aebe8f05..bb32dc840186 100644 --- a/test/parallel/test-webcrypto-cryptokey-brand-check.js +++ b/test/parallel/test-webcrypto-cryptokey-brand-check.js @@ -38,11 +38,11 @@ const { subtle } = globalThis.crypto; } assert.strictEqual(isCryptoKey(key), true); assert.strictEqual(Object.hasOwn(CryptoKey, 'getSlots'), false); - const internalProto = Object.getPrototypeOf(key); - assert.strictEqual(Object.hasOwn(internalProto, 'getSlots'), false); - assert.strictEqual('getSlots' in internalProto, false); - assert.strictEqual(internalProto.constructor, CryptoKey); - assert.strictEqual(Object.getPrototypeOf(internalProto), CryptoKey.prototype); + const keyPrototype = Object.getPrototypeOf(key); + assert.strictEqual(Object.hasOwn(keyPrototype, 'getSlots'), false); + assert.strictEqual('getSlots' in keyPrototype, false); + assert.strictEqual(keyPrototype.constructor, CryptoKey); + assert.strictEqual(keyPrototype, CryptoKey.prototype); const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; const invalidArgType = { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' }; @@ -93,7 +93,7 @@ const { subtle } = globalThis.crypto; await assertInvalidReceiver(receiver); } - // Prototype spoofing with InternalCryptoKey.prototype must not pass + // Prototype spoofing with CryptoKey.prototype must not pass // util.types.isCryptoKey(). const spoofed = {}; Object.setPrototypeOf(spoofed, Object.getPrototypeOf(key)); diff --git a/test/parallel/test-webcrypto-key-prototype.js b/test/parallel/test-webcrypto-key-prototype.js new file mode 100644 index 000000000000..61c23ae93839 --- /dev/null +++ b/test/parallel/test-webcrypto-key-prototype.js @@ -0,0 +1,25 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { createSecretKey } = require('crypto'); +const { subtle } = globalThis.crypto; + +function check(key) { + assert.strictEqual(Object.getPrototypeOf(key), CryptoKey.prototype); + assert.strictEqual(Object.getPrototypeOf(structuredClone(key)), CryptoKey.prototype); + assert(key instanceof CryptoKey); +} + +(async () => { + check(await subtle.generateKey({ name: 'AES-GCM', length: 128 }, true, ['encrypt'])); + check(await subtle.importKey('raw', new Uint8Array(16), 'AES-GCM', true, ['encrypt'])); + const pair = await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, + true, ['sign', 'verify']); + check(pair.publicKey); + check(pair.privateKey); + check(createSecretKey(new Uint8Array(16)).toCryptoKey('AES-GCM', true, ['encrypt'])); +})().then(common.mustCall()); From 3e45b42b8b06f951b3bd795d65c874387bce6b31 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:36:59 +0200 Subject: [PATCH 24/29] crypto: include EC public keys in PKCS8 exports Export Web Crypto EC private keys from a clone with the public point included, even when the imported encoding omitted it. Preserve the original KeyObject encoding state. Signed-off-by: Filip Skokan Assisted-by: Codex --- deps/ncrypto/ncrypto.cc | 18 +++++++ deps/ncrypto/ncrypto.h | 1 + lib/internal/crypto/ec.js | 2 +- src/crypto/crypto_keys.cc | 21 ++++++++ src/crypto/crypto_keys.h | 2 + .../test-webcrypto-ec-pkcs8-public-key.js | 49 +++++++++++++++++++ typings/internalBinding/crypto.d.ts | 1 + 7 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-webcrypto-ec-pkcs8-public-key.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index fe99e84d7fac..52f841422c77 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -7384,6 +7384,24 @@ int Ec::getCurve() const { return EC_GROUP_get_curve_name(getGroup()); } +BIOPointer Ec::ExportPrivatePkcs8(const EVPKeyPointer& key) { + MarkPopErrorOnReturn mark_pop_error_on_return; + if (!key || !key.isA(KeyAlgorithm::EC)) return {}; + auto ec = ECKeyPointer(key).clone(); + if (!ec) return {}; +#if NCRYPTO_USE_LEGACY_KEY_TYPES + // Decoding an ECPrivateKey without publicKey reconstructs the public point + // but retains a flag that omits it from subsequent encodings. + EC_KEY_set_enc_flags(ec.get(), + EC_KEY_get_enc_flags(ec.get()) & ~EC_PKEY_NO_PUBKEY); +#endif + auto export_key = EVPKeyPointer::New(); + if (!export_key || !export_key.set(ec)) return {}; + auto encoded = export_key.writePrivateKey({}); + if (!encoded) return {}; + return std::move(encoded.value); +} + DataPointer Ec::TryExportPublic(const EVPKeyPointer& key, point_conversion_form_t form) { if (!key || form != POINT_CONVERSION_UNCOMPRESSED) return {}; diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 4649c5143724..30dccfc0c79f 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -778,6 +778,7 @@ class Ec final { static DataPointer TryExportPublic(const EVPKeyPointer& key, point_conversion_form_t form); static DataPointer ExportPrivate(const EVPKeyPointer& key); + static BIOPointer ExportPrivatePkcs8(const EVPKeyPointer& key); static bool GetKeyComponents(const EVPKeyPointer& key, BignumPointer* x, BignumPointer* y, diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 6f566569a258..bb857be74c84 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -121,7 +121,7 @@ function ecExportKey(key, format) { } case kWebCryptoKeyFormatPKCS8: { return TypedArrayPrototypeGetBuffer( - handle.export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); + handle.exportECPrivatePkcs8()); } default: return undefined; diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index f787c97eb7f8..580f9ac009d6 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -1144,6 +1144,8 @@ Local KeyObjectHandle::Initialize(Environment* env) { isolate, templ, "exportECPublicRaw", ExportECPublicRaw); SetProtoMethodNoSideEffect( isolate, templ, "exportECPrivateRaw", ExportECPrivateRaw); + SetProtoMethodNoSideEffect( + isolate, templ, "exportECPrivatePkcs8", ExportECPrivatePkcs8); SetProtoMethod(isolate, templ, "keyDetail", GetKeyDetail); SetProtoMethod(isolate, templ, "equals", Equals); @@ -1167,6 +1169,7 @@ void KeyObjectHandle::RegisterExternalReferences( registry->Register(RawSeed); registry->Register(ExportECPublicRaw); registry->Register(ExportECPrivateRaw); + registry->Register(ExportECPrivatePkcs8); registry->Register(GetKeyDetail); registry->Register(Equals); } @@ -1583,6 +1586,24 @@ void KeyObjectHandle::ExportECPrivateRaw( .FromMaybe(Local())); } +void KeyObjectHandle::ExportECPrivatePkcs8( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); + const KeyObjectData& data = key->Data(); + CHECK_EQ(data.GetKeyType(), kKeyTypePrivate); + Mutex::ScopedLock lock(data.mutex()); + auto encoded = ncrypto::Ec::ExportPrivatePkcs8(data.GetAsymmetricKey()); + if (!encoded) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to export EC private key"); + } + const EVPKeyPointer::PrivateKeyEncodingConfig config; + args.GetReturnValue().Set( + ToV8Value(env, encoded, config).FromMaybe(Local())); +} + void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); KeyObjectHandle* key; diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index 9f3b1cce091c..cb7209d0835d 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -181,6 +181,8 @@ class KeyObjectHandle : public BaseObject { const v8::FunctionCallbackInfo& args); static void ExportECPrivateRaw( const v8::FunctionCallbackInfo& args); + static void ExportECPrivatePkcs8( + const v8::FunctionCallbackInfo& args); static void RawSeed(const v8::FunctionCallbackInfo& args); v8::MaybeLocal ExportSecretKey() const; diff --git a/test/parallel/test-webcrypto-ec-pkcs8-public-key.js b/test/parallel/test-webcrypto-ec-pkcs8-public-key.js new file mode 100644 index 000000000000..e88ac6243251 --- /dev/null +++ b/test/parallel/test-webcrypto-ec-pkcs8-public-key.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { createPrivateKey, KeyObject } = require('crypto'); +const fixtures = require('../common/fixtures'); +const { subtle } = globalThis.crypto; + +function der(tag, ...parts) { + const body = Buffer.concat(parts); + const length = body.length < 128 ? [body.length] : [0x81, body.length]; + return Buffer.concat([Buffer.from([tag, ...length]), body]); +} + +(async () => { + for (const [curve, oid] of [ + ['p256', '06082a8648ce3d030107'], + ['p384', '06052b81040022'], + ['p521', '06052b81040023'], + ]) { + const privateKey = createPrivateKey(fixtures.readKey(`ec_${curve}_private.pem`)); + const expected = privateKey.export({ type: 'pkcs8', format: 'der' }); + const jwk = privateKey.export({ format: 'jwk' }); + const curveOid = Buffer.from(oid, 'hex'); + const algorithmIdentifier = der( + 0x30, Buffer.from('06072a8648ce3d0201', 'hex'), curveOid); + + for (const includeParameters of [false, true]) { + const ecPrivateKey = der( + 0x30, Buffer.from('020101', 'hex'), der(0x04, Buffer.from(jwk.d, 'base64url')), + includeParameters ? der(0xa0, curveOid) : Buffer.alloc(0)); + const privateOnly = der( + 0x30, Buffer.from('020100', 'hex'), algorithmIdentifier, der(0x04, ecPrivateKey)); + for (const name of ['ECDSA', 'ECDH']) { + const key = await subtle.importKey( + 'pkcs8', privateOnly, { name, namedCurve: jwk.crv }, true, + name === 'ECDSA' ? ['sign'] : ['deriveBits']); + const original = KeyObject.from(key).export({ type: 'pkcs8', format: 'der' }); + const actual = Buffer.from(await subtle.exportKey('pkcs8', key)); + assert.deepStrictEqual(actual, expected); + assert.deepStrictEqual( + KeyObject.from(key).export({ type: 'pkcs8', format: 'der' }), original); + } + } + } +})().then(common.mustCall()); diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index 306e57383029..af6455f886fa 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -583,6 +583,7 @@ declare namespace InternalCryptoBinding { getAsymmetricKeyType(): string | undefined; getSymmetricKeySize(): number; checkEcKeyData(): boolean; + exportECPrivatePkcs8(): Buffer; } interface NativeKeyObject { From 1f701b0349653739f6d56afed60934edcbc2f174 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:44:50 +0200 Subject: [PATCH 25/29] crypto: map JWK export failures to OperationError Map inaccessible key material to the Web Crypto error type, including seedless ML-DSA and ML-KEM keys converted from KeyObjects. Assisted-by: Codex Signed-off-by: Filip Skokan --- lib/internal/crypto/webcrypto.js | 8 ++++- .../test-webcrypto-seedless-jwk-export.js | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-webcrypto-seedless-jwk-export.js diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 2f44d726026b..6585baa6fc51 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -806,7 +806,13 @@ function exportKeyJWK(key) { }; if (alg === undefined) delete parameters.alg; - return getCryptoKeyHandle(key).exportJwk(parameters, true); + try { + return getCryptoKeyHandle(key).exportJwk(parameters, true); + } catch (err) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); + } } function exportKeySync(format, key) { diff --git a/test/parallel/test-webcrypto-seedless-jwk-export.js b/test/parallel/test-webcrypto-seedless-jwk-export.js new file mode 100644 index 000000000000..1dcad8c64da3 --- /dev/null +++ b/test/parallel/test-webcrypto-seedless-jwk-export.js @@ -0,0 +1,36 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const { hasOpenSSL, isBoringSSL } = require('../common/crypto'); +if (isBoringSSL || !hasOpenSSL(3, 5)) + common.skip('requires OpenSSL 3.5 or later'); + +const assert = require('assert'); +const { createPrivateKey } = require('crypto'); +const fixtures = require('../common/fixtures'); +const { subtle } = globalThis.crypto; + +(async () => { + const algorithm = { name: 'AES-GCM', iv: new Uint8Array(12) }; + const wrappingKey = await subtle.generateKey({ name: 'AES-GCM', length: 128 }, + false, ['wrapKey']); + for (const [name, usage] of [ + ['ML-KEM-512', 'decapsulateBits'], + ['ML-KEM-768', 'decapsulateBits'], + ['ML-KEM-1024', 'decapsulateBits'], + ['ML-DSA-44', 'sign'], + ['ML-DSA-65', 'sign'], + ['ML-DSA-87', 'sign'], + ]) { + const file = `${name.toLowerCase().replaceAll('-', '_')}_private_priv_only.pem`; + const key = createPrivateKey(fixtures.readKey(file)).toCryptoKey(name, true, [usage]); + await assert.rejects(subtle.exportKey('jwk', key), { + name: 'OperationError', constructor: DOMException, + }); + await assert.rejects(subtle.wrapKey('jwk', key, wrappingKey, algorithm), { + name: 'OperationError', constructor: DOMException, + }); + } +})().then(common.mustCall()); From 36d932dc2e0cb56e46adc3274bb1baef45c4d603 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 23:18:41 +0200 Subject: [PATCH 26/29] crypto: check EC coordinate conversion results Propagate coordinate conversion failures instead of importing an oversized JWK coordinate as zero. Keep equivalent short and zero-padded integer encodings accepted by the native decoder. Signed-off-by: Filip Skokan Assisted-by: Codex --- deps/ncrypto/ncrypto.cc | 12 +++-- .../test-crypto-ec-jwk-coordinates.js | 53 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 test/parallel/test-crypto-ec-jwk-coordinates.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 52f841422c77..44b74e13fd2b 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -5942,8 +5942,10 @@ bool ECKeyPointer::setPublicKeyRaw(const BignumPointer& x, if (!buf) return false; unsigned char* ptr = static_cast(buf.get()); ptr[0] = POINT_CONVERSION_UNCOMPRESSED; - x.encodePaddedInto(ptr + 1, field_len); - y.encodePaddedInto(ptr + 1 + field_len, field_len); + if (x.encodePaddedInto(ptr + 1, field_len) != field_len || + y.encodePaddedInto(ptr + 1 + field_len, field_len) != field_len) { + return false; + } auto point = ECPointPointer::New(group); if (!point) return false; @@ -6171,8 +6173,10 @@ bool ECKeyPointer::setPublicKeyRaw(const BignumPointer& x, if (!buf) return false; unsigned char* ptr = static_cast(buf.get()); ptr[0] = POINT_CONVERSION_UNCOMPRESSED; - x.encodePaddedInto(ptr + 1, field_len); - y.encodePaddedInto(ptr + 1 + field_len, field_len); + if (x.encodePaddedInto(ptr + 1, field_len) != field_len || + y.encodePaddedInto(ptr + 1 + field_len, field_len) != field_len) { + return false; + } auto point = ECPointPointer::New(group_.get()); if (!point || !point.setFromBuffer({ptr, uncompressed_len}, group_.get())) { diff --git a/test/parallel/test-crypto-ec-jwk-coordinates.js b/test/parallel/test-crypto-ec-jwk-coordinates.js new file mode 100644 index 000000000000..8ae3d79e6a3d --- /dev/null +++ b/test/parallel/test-crypto-ec-jwk-coordinates.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { createPublicKey, subtle } = require('crypto'); + +// This is the valid P-256 point (0, sqrt(b)). A failed conversion of an +// oversized x coordinate must not silently replace it with zero. +const jwk = { + kty: 'EC', + crv: 'P-256', + x: Buffer.alloc(32).toString('base64url'), + y: 'ZkhceA4vg9ckM71dhKBrtlQcKvMdrocXKL-FahdPk_Q', +}; + +(async () => { + for (const field of ['x', 'y']) { + const invalid = { ...jwk, [field]: Buffer.alloc(33, 1).toString('base64url') }; + assert.throws(() => createPublicKey({ key: invalid, format: 'jwk' }), { + code: 'ERR_CRYPTO_INVALID_JWK', + }); + for (const name of ['ECDSA', 'ECDH']) { + await assert.rejects(subtle.importKey( + 'jwk', invalid, { name, namedCurve: 'P-256' }, true, + name === 'ECDSA' ? ['verify'] : []), { name: 'DataError' }); + } + } + + // Equivalent integer encodings remain accepted by the existing decoder. + for (const encoded of [ + jwk, + { ...jwk, x: Buffer.alloc(1).toString('base64url') }, + { + ...jwk, + x: Buffer.alloc(33).toString('base64url'), + y: Buffer.concat([Buffer.alloc(1), Buffer.from(jwk.y, 'base64url')]).toString('base64url'), + }, + ]) { + const publicKey = createPublicKey({ key: encoded, format: 'jwk' }); + assert.deepStrictEqual(publicKey.export({ format: 'jwk' }), jwk); + for (const name of ['ECDSA', 'ECDH']) { + const key = await subtle.importKey( + 'jwk', encoded, { name, namedCurve: 'P-256' }, true, + name === 'ECDSA' ? ['verify'] : []); + const exported = await subtle.exportKey('jwk', key); + assert.strictEqual(exported.x, jwk.x); + assert.strictEqual(exported.y, jwk.y); + } + } +})().then(common.mustCall()); From 4a5c46f90d39edaa3bec65444655ad51416b0f9f Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 23 Sep 2026 11:11:50 +0200 Subject: [PATCH 27/29] benchmark: cover Web Crypto conversion costs Signed-off-by: Filip Skokan Assisted-by: Codex --- benchmark/crypto/webcrypto-export.js | 47 +++++++++++ benchmark/misc/webcrypto-util.js | 117 +++++++++++++++++++++++++++ benchmark/misc/webcrypto-webidl.js | 59 +++++++++++++- 3 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 benchmark/crypto/webcrypto-export.js create mode 100644 benchmark/misc/webcrypto-util.js diff --git a/benchmark/crypto/webcrypto-export.js b/benchmark/crypto/webcrypto-export.js new file mode 100644 index 000000000000..4d96b54ec74d --- /dev/null +++ b/benchmark/crypto/webcrypto-export.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common.js'); +const fixtures = require('../../test/common/fixtures.js'); +const { createPrivateKey, createPublicKey, subtle } = require('node:crypto'); + +const bench = common.createBenchmark(main, { + keyType: ['hmac', 'aes-gcm', 'ecdsa-private', 'rsa-private', 'rsa-public'], + n: [1e5], +}); + +async function createKey(keyType) { + switch (keyType) { + case 'hmac': + return subtle.importKey( + 'raw', new Uint8Array(32), { name: 'HMAC', hash: 'SHA-256' }, + true, ['sign', 'verify']); + case 'aes-gcm': + return subtle.importKey( + 'raw', new Uint8Array(32), 'AES-GCM', + true, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']); + case 'ecdsa-private': + return createPrivateKey(fixtures.readKey('ec_p256_private.pem')) + .toCryptoKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign']); + case 'rsa-private': + return createPrivateKey(fixtures.readKey('rsa_private_2048.pem')) + .toCryptoKey({ name: 'RSA-PSS', hash: 'SHA-256' }, true, ['sign']); + case 'rsa-public': + return createPublicKey(fixtures.readKey('rsa_private_2048.pem')) + .toCryptoKey({ name: 'RSA-PSS', hash: 'SHA-256' }, true, ['verify']); + default: + throw new Error(`Unsupported key type: ${keyType}`); + } +} + +async function main({ n, keyType }) { + const key = await createKey(keyType); + let result; + + bench.start(); + for (let i = 0; i < n; i++) + result = await subtle.exportKey('jwk', key); + bench.end(n); + + if (result.kty === undefined) + throw new Error('Missing key type'); +} diff --git a/benchmark/misc/webcrypto-util.js b/benchmark/misc/webcrypto-util.js new file mode 100644 index 000000000000..581602ab347f --- /dev/null +++ b/benchmark/misc/webcrypto-util.js @@ -0,0 +1,117 @@ +'use strict'; + +const common = require('../common.js'); + +const inputs = [ + 'arraybuffer', + 'uint8array', + 'dataview', + 'buffer', + 'sharedview', + 'empty-arraybuffer', + 'empty-uint8array', + 'empty-dataview', + 'detached-arraybuffer', + 'detached-uint8array', + 'detached-dataview', +]; + +const bench = common.createBenchmark(main, { + op: [ + ...inputs.flatMap((input) => [`byteLength:${input}`, `bytes:${input}`]), + 'truncate:arraybuffer:100', + 'truncate:arraybuffer:128', + 'truncate:uint8array:100', + 'truncate:uint8array:128', + 'usages:0', + 'usages:1', + 'usages:2', + 'usages:4', + ], + n: [1e6], +}, { flags: ['--expose-internals'] }); + +function createInput(input) { + const empty = input.startsWith('empty-'); + const detached = input.startsWith('detached-'); + const type = input.replace(/^(empty|detached)-/, ''); + const size = empty ? 0 : 32; + const buffer = new ArrayBuffer(size + 16); + let value; + switch (type) { + case 'arraybuffer': + value = new ArrayBuffer(size); + break; + case 'uint8array': + value = new Uint8Array(buffer, 8, size); + break; + case 'dataview': + value = new DataView(buffer, 8, size); + break; + case 'buffer': + value = Buffer.from(buffer, 8, size); + break; + case 'sharedview': + value = new Uint8Array(new SharedArrayBuffer(size)); + break; + default: + throw new Error(`Unsupported input: ${input}`); + } + if (detached) { + const backing = type === 'arraybuffer' ? value : buffer; + structuredClone(backing, { transfer: [backing] }); + } + return value; +} + +function main({ n, op }) { + const { + getBufferSourceByteLength, + getBufferSourceBytes, + getUsagesFromMask, + getUsagesMask, + truncateToBitLength, + } = require('internal/crypto/util'); + const [operation, input, length] = op.split(':'); + let run; + switch (operation) { + case 'byteLength': { + const value = createInput(input); + run = () => getBufferSourceByteLength(value); + break; + } + case 'bytes': { + const value = createInput(input); + run = () => getBufferSourceBytes(value); + break; + } + case 'truncate': { + const value = createInput(input); + const bits = Number(length); + run = () => truncateToBitLength(bits, value); + break; + } + case 'usages': { + const usages = { + 0: [], + 1: ['sign'], + 2: ['sign', 'verify'], + 4: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], + }; + const mask = getUsagesMask(usages[input]); + run = () => getUsagesFromMask(mask); + break; + } + default: + throw new Error(`Unsupported operation: ${operation}`); + } + + let result; + bench.start(); + for (let i = 0; i < n; i++) + result = run(); + bench.end(n); + + if (result === undefined) + throw new Error('Missing benchmark result'); +} diff --git a/benchmark/misc/webcrypto-webidl.js b/benchmark/misc/webcrypto-webidl.js index 0f6275ed0951..318ea7982071 100644 --- a/benchmark/misc/webcrypto-webidl.js +++ b/benchmark/misc/webcrypto-webidl.js @@ -6,6 +6,13 @@ const bench = common.createBenchmark(main, { op: [ 'normalizeAlgorithm-string', 'normalizeAlgorithm-dict', + 'normalizeAlgorithm-validate-aes-gcm', + 'normalizeAlgorithm-validate-aes-cbc', + 'normalizeAlgorithm-validate-aes-ctr', + 'normalizeAlgorithm-validate-aes-generate', + 'normalizeAlgorithm-validate-hkdf', + 'normalizeAlgorithm-validate-hmac', + 'normalizeAlgorithm-validate-rsa', 'webidl-dict', 'webidl-algorithm-identifier-string', 'webidl-algorithm-identifier-object', @@ -17,7 +24,7 @@ const bench = common.createBenchmark(main, { }, { flags: ['--expose-internals'] }); function main({ n, op }) { - const { normalizeAlgorithm } = require('internal/crypto/util'); + const { normalizeAlgorithm, validateAlgorithm } = require('internal/crypto/util'); switch (op) { case 'normalizeAlgorithm-string': { @@ -37,6 +44,54 @@ function main({ n, op }) { bench.end(n); break; } + case 'normalizeAlgorithm-validate-aes-gcm': + case 'normalizeAlgorithm-validate-aes-cbc': + case 'normalizeAlgorithm-validate-aes-ctr': + case 'normalizeAlgorithm-validate-aes-generate': + case 'normalizeAlgorithm-validate-hkdf': + case 'normalizeAlgorithm-validate-hmac': + case 'normalizeAlgorithm-validate-rsa': { + const cases = { + 'aes-gcm': [ + { name: 'AES-GCM', iv: new Uint8Array(12), tagLength: 128 }, + 'encrypt', + ], + 'aes-cbc': [ + { name: 'AES-CBC', iv: new Uint8Array(16) }, + 'encrypt', + ], + 'aes-ctr': [ + { name: 'AES-CTR', counter: new Uint8Array(16), length: 64 }, + 'encrypt', + ], + 'aes-generate': [{ name: 'AES-GCM', length: 256 }, 'generateKey'], + 'hkdf': [ + { + name: 'HKDF', hash: 'SHA-256', + salt: new Uint8Array(32), info: new Uint8Array(32), + }, + 'deriveBits', + ], + 'hmac': [{ name: 'HMAC', hash: 'SHA-256', length: 256 }, 'importKey'], + 'rsa': [ + { + name: 'RSA-PSS', hash: 'SHA-256', modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + }, + 'generateKey', + ], + }; + const name = op.slice('normalizeAlgorithm-validate-'.length); + const [input, operation] = cases[name]; + bench.start(); + for (let i = 0; i < n; i++) { + const normalized = normalizeAlgorithm(input, operation); + // Older revisions validate inside normalizeAlgorithm. + validateAlgorithm?.(normalized, operation); + } + bench.end(n); + break; + } case 'webidl-dict': { // WebIDL dictionary converter in isolation. const webidl = require('internal/crypto/webidl'); @@ -85,7 +140,7 @@ function main({ n, op }) { break; } case 'webidl-dict-ensure-sha': { - // Exercises ensureSHA on a hash member. + // Converts a dictionary containing a hash identifier. const webidl = require('internal/crypto/webidl'); const input = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }; const opts = { prefix: 'test', context: 'test' }; From a2164160d321f666002051187b10b1a7177998c7 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 23 Sep 2026 13:34:11 +0200 Subject: [PATCH 28/29] crypto: use backend cSHAKE and KMAC Require cSHAKE and KMAC output lengths and KMAC key lengths to be multiples of 8 bits. KMAC keys must be at least 32 bits. Share these restrictions between operations and supports. Use OpenSSL's KMAC provider for all supported inputs and its cSHAKE implementation for non-empty function names or customization strings. Keep using SHAKE when both cSHAKE parameters are empty. Remove the custom Keccak framing, partial-bit handling, and short-key fallback. Document the OpenSSL 4.0 requirement for non-empty cSHAKE parameters and reject customization strings containing null bytes. Keep the documented 512-byte customization limit and let backend failures reach callers. Signed-off-by: Filip Skokan Assisted-by: Codex --- doc/api/webcrypto.md | 27 +- lib/internal/crypto/hash.js | 28 +- lib/internal/crypto/mac.js | 1 - lib/internal/crypto/util.js | 2 +- lib/internal/crypto/webcrypto.js | 12 +- lib/internal/crypto/webidl.js | 25 +- src/crypto/crypto_hash.cc | 353 ------------------ src/crypto/crypto_hash.h | 68 ---- src/crypto/crypto_kmac.cc | 97 +---- src/crypto/crypto_kmac.h | 3 +- test/fixtures/crypto/kmac.js | 55 --- .../webcrypto/supports-modern-algorithms.mjs | 48 ++- .../test-crypto-key-objects-to-crypto-key.js | 15 +- .../test-webcrypto-aes-kw-short-input.js | 16 +- test/parallel/test-webcrypto-derivekey.js | 18 +- test/parallel/test-webcrypto-digest.js | 100 +++-- test/parallel/test-webcrypto-export-import.js | 82 ++-- .../test-webcrypto-fips-exceptions.mjs | 72 ++-- test/parallel/test-webcrypto-fips-refresh.js | 6 +- .../test-webcrypto-keccak-byte-alignment.js | 90 +++++ test/parallel/test-webcrypto-keygen-kmac.js | 18 +- .../test-webcrypto-prototype-pollution.mjs | 9 +- .../test-webcrypto-sign-verify-kmac.js | 126 ++----- test/parallel/test-webcrypto-wrap-unwrap.js | 38 +- typings/internalBinding/crypto.d.ts | 13 - 25 files changed, 367 insertions(+), 955 deletions(-) create mode 100644 test/parallel/test-webcrypto-keccak-byte-alignment.js diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index f61255812678..609ebf003796 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -1842,6 +1842,9 @@ changes: description: Renamed `cShakeParams.length` to `cShakeParams.outputLength`. --> +When both `functionName` and `customization` are empty or `undefined`, cSHAKE is +equivalent to plain SHAKE. + #### `cShakeParams.name` -* Type: {number} represents the requested output length in bits. +* Type: {number} represents the requested output length in bits. Must be a + multiple of 8. #### `cShakeParams.functionName` @@ -1875,9 +1879,10 @@ changes: * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} The `functionName` member represents the NIST function-name byte string used to -domain-separate functions built on top of cSHAKE. Accepted values are: +domain-separate functions built on top of cSHAKE. Non-empty values require +OpenSSL 4.0 or later. Accepted values are: -* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE +* empty or `undefined` * the ASCII byte sequence `'KMAC'` * the ASCII byte sequence `'TupleHash'` * the ASCII byte sequence `'ParallelHash'` @@ -1896,11 +1901,11 @@ changes: * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} -The `customization` member represents the customization data. Accepted -values are: +The `customization` member represents the customization data. Non-empty values +require OpenSSL 4.0 or later. Accepted values are: -* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE -* up to 512 bytes of arbitrary data +* empty or `undefined` +* up to 512 bytes of data without null bytes ### Class: `EcdhKeyDeriveParams` @@ -2360,7 +2365,7 @@ added: v24.8.0 * Type: {number} The optional number of bits in the KMAC key. This is optional and should -be omitted for most cases. +be omitted for most cases. The key length must be at least 32 and a multiple of 8. #### `kmacImportParams.name` @@ -2410,7 +2415,8 @@ added: v24.8.0 The number of bits to generate for the KMAC key. If omitted, the length will be determined by the KMAC algorithm used. -This is optional and should be omitted for most cases. +This is optional and should be omitted for most cases. Must be at least 32 and a +multiple of 8. #### `kmacKeyGenParams.name` @@ -2448,7 +2454,8 @@ added: - v24.15.0 --> -* Type: {number} represents the requested output length in bits. +* Type: {number} represents the requested output length in bits. Must be a + multiple of 8. #### `kmacParams.customization` diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 12588b572633..97bed5aab4a3 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -5,12 +5,10 @@ const { ObjectSetPrototypeOf, StringPrototypeToLowerCase, Symbol, - TypedArrayPrototypeGetBuffer, TypedArrayPrototypeIncludes, } = primordials; const { - CShakeJob, Hash: _Hash, HashJob, Hmac: _Hmac, @@ -23,10 +21,7 @@ const { const { getStringOption, jobPromise, - jobPromiseThen, normalizeHashName, - numBitsToBytes, - truncateToBitLength, validateAlgorithm, validateMaxBufferLength, kHandle, @@ -42,7 +37,6 @@ const { } = require('internal/crypto/keys'); const { - lazyDOMException, normalizeEncoding, encodingsMap, getDeprecationWarningEmitter, @@ -267,30 +261,20 @@ function asyncDigest(algorithm, data) { const outputLength = algorithm.outputLength; if (getOptionalByteLength(algorithm.functionName) || getOptionalByteLength(algorithm.customization)) { - if (CShakeJob === undefined) { - throw lazyDOMException( - 'Non-empty CShakeParams functionName or customization is not supported', - 'NotSupportedError'); - } - - return jobPromise(() => new CShakeJob( + return jobPromise(() => new HashJob( kCryptoJobWebCrypto, - algorithm.name, + StringPrototypeToLowerCase(algorithm.name), data, + outputLength, algorithm.functionName, - algorithm.customization, - outputLength)); + algorithm.customization)); } - const bits = jobPromise(() => new HashJob( + return jobPromise(() => new HashJob( kCryptoJobWebCrypto, normalizeHashName(algorithm.name), data, - numBitsToBytes(outputLength) * 8)); - if (outputLength % 8 === 0) - return bits; - return jobPromiseThen(bits, (bits) => - TypedArrayPrototypeGetBuffer(truncateToBitLength(outputLength, bits))); + outputLength)); } case 'TurboSHAKE128': // Fall through diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 742e1ebe5908..ed2be2b7fdba 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -193,7 +193,6 @@ function kmacSignVerify(key, data, algorithm, signature) { getCryptoKeyHandle(key), algorithm.name, algorithm.customization, - getCryptoKeyAlgorithm(key).length, algorithm.outputLength, data, signature)); diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index a9ad6dfb62ff..d1f1cdc9b4f2 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -729,7 +729,7 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) { } function validateKmacKeyLength(length) { - if ((length < 32 || length % 8) && isFips()) + if (length < 32 || length % 8 !== 0) throw lazyDOMException('Invalid key length', 'NotSupportedError'); } diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 6585baa6fc51..7a2ff063fa1a 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -23,7 +23,6 @@ const { } = primordials; const { - CShakeJob, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatSPKI, @@ -72,7 +71,6 @@ const { prepareWebCryptoResult, validateAlgorithm, validateMaxBufferLength, - getOptionalByteLength, } = require('internal/crypto/util'); const { @@ -1923,15 +1921,7 @@ function check(op, alg, length) { } switch (op) { - case 'digest': { - if ((normalizedAlgorithm.name === 'cSHAKE128' || - normalizedAlgorithm.name === 'cSHAKE256') && - (getOptionalByteLength(normalizedAlgorithm.functionName) || - getOptionalByteLength(normalizedAlgorithm.customization))) { - return CShakeJob !== undefined; - } - return true; - } + case 'digest': case 'decapsulate': case 'decrypt': case 'encapsulate': diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 4addaf62bccf..33705dbb9c33 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -11,14 +11,12 @@ const { StringPrototypeCharCodeAt, StringPrototypeSplit, StringPrototypeToLowerCase, + TypedArrayPrototypeIncludes, } = primordials; const { lazyDOMException, } = require('internal/util'); -const { - isUint32, -} = require('internal/validators'); const { getCryptoKeyAlgorithm, getCryptoKeyType, @@ -29,9 +27,9 @@ const { validateMaxBufferLength, getBufferSourceByteLength, getBufferSourceBytes, + getHashes, isFips, kNamedCurveAliases, - numBitsToBytes, validateKmacKeyLength, } = require('internal/crypto/util'); const { @@ -293,20 +291,20 @@ function validateZeroLength(parameterName) { } function validateCShakeOutputLength(V) { - if (!isUint32(numBitsToBytes(V) * 8)) { + if (V % 8 !== 0) { throw lazyDOMException( 'Invalid CShakeParams outputLength', - 'OperationError'); + 'NotSupportedError'); } } const kCShakeFunctionNames = ['KMAC', 'TupleHash', 'ParallelHash']; -function validateCShakeFunctionName(V) { +function validateCShakeFunctionName(V, dict) { const length = getBufferSourceByteLength(V); if (length === 0) return; - if (!isFips()) { + if (ArrayPrototypeIncludes(getHashes(), StringPrototypeToLowerCase(dict.name))) { const bytes = getBufferSourceBytes(V); for (let i = 0; i < kCShakeFunctionNames.length; i++) { const functionName = kCShakeFunctionNames[i]; @@ -325,12 +323,17 @@ function validateCShakeFunctionName(V) { 'NotSupportedError'); } -function validateCShakeCustomization(V) { - if (isFips() && getBufferSourceByteLength(V) !== 0) +function validateCShakeCustomization(V, dict) { + if (getBufferSourceByteLength(V) === 0) return; + if (!ArrayPrototypeIncludes(getHashes(), StringPrototypeToLowerCase(dict.name))) throw lazyDOMException( 'Unsupported CShakeParams customization', 'NotSupportedError'); validateMaxBufferLength(V, 'CShakeParams.customization', 512); + if (TypedArrayPrototypeIncludes(getBufferSourceBytes(V), 0)) + throw lazyDOMException( + 'Unsupported CShakeParams customization', + 'NotSupportedError'); } converters.RsaPssParams = createAlgorithmDictionaryConverter( @@ -790,7 +793,7 @@ converters.KmacParams = createAlgorithmDictionaryConverter( converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V) => { - if ((V === 0 || V % 8) && isFips()) + if (V % 8 !== 0 || (V === 0 && isFips())) throw lazyDOMException( 'Invalid KmacParams outputLength', 'NotSupportedError'); diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 976c921fee94..24d49a6d3774 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -7,21 +7,15 @@ #include "threadpoolwork-inl.h" #include "v8.h" -#if OPENSSL_WITH_EVP_MAC -#include #include -#endif #if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK #include #endif #include -#include #include #include -#include -#include #include #include @@ -588,9 +582,6 @@ void Hash::Initialize(Environment* env, Local target) { SetMethodNoSideEffect(context, target, "oneShotDigest", OneShotDigest); HashJob::Initialize(env, target); -#if OPENSSL_WITH_EVP_MAC - CShakeJob::Initialize(env, target); -#endif } void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { @@ -602,9 +593,6 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(OneShotDigest); HashJob::RegisterExternalReferences(registry); -#if OPENSSL_WITH_EVP_MAC - CShakeJob::RegisterExternalReferences(registry); -#endif } // new Hash(algorithm, xofLen, algorithmId, algorithmCache[, functionName, @@ -895,346 +883,5 @@ bool HashTraits::DeriveBits(Environment* env, return true; } -#if OPENSSL_WITH_EVP_MAC -namespace { - -static constexpr std::array kEmptyString = {}; -static constexpr size_t kKeccakKmac128Rate = 168; -static constexpr size_t kKeccakKmac256Rate = 136; -static constexpr size_t kMaxCShakeCustomizationSize = 512; - -struct EncodedLength { - std::array data; - size_t size; -}; - -struct EncodedStringInput { - const void* data; - size_t byte_length; - size_t bit_length; -}; - -struct KeccakKmacXof { - ncrypto::EVPMDCtxPointer ctx; - size_t rate; -}; - -size_t EncodedLengthSize(size_t value) { - size_t size = 1; - size_t remaining = value; - while (remaining >>= CHAR_BIT) size++; - return size + 1; -} - -bool AddSize(size_t a, size_t b, size_t* out) { - if (a > std::numeric_limits::max() - b) return false; - *out = a + b; - return true; -} - -EncodedLength EncodeLength(size_t value, bool left) { - const size_t value_size = EncodedLengthSize(value) - 1; - EncodedLength encoded = {{}, value_size + 1}; - - if (left) encoded.data[0] = static_cast(value_size); - for (size_t n = 0; n < value_size; n++) { - const size_t shift = CHAR_BIT * (value_size - n - 1); - encoded.data[(left ? 1 : 0) + n] = - static_cast(value >> shift); - } - if (!left) encoded.data[value_size] = static_cast(value_size); - - return encoded; -} - -bool DigestUpdate(ncrypto::EVPMDCtxPointer* ctx, - const void* data, - size_t size) { - if (size == 0) return true; - return ctx->digestUpdate(ncrypto::Buffer{ - .data = data, - .len = size, - }); -} - -bool DigestUpdateZeros(ncrypto::EVPMDCtxPointer* ctx, size_t size) { - static constexpr std::array zeros = {}; - while (size > 0) { - const size_t chunk = std::min(size, zeros.size()); - if (!DigestUpdate(ctx, zeros.data(), chunk)) return false; - size -= chunk; - } - return true; -} - -bool EncodedStringSize(size_t byte_length, size_t bit_length, size_t* size) { - return AddSize(EncodedLengthSize(bit_length), byte_length, size); -} - -bool ByteLengthToBitLength(size_t byte_length, size_t* bit_length) { - if (byte_length > std::numeric_limits::max() / CHAR_BIT) { - return false; - } - *bit_length = byte_length * CHAR_BIT; - return true; -} - -KeccakKmacXof NewKeccakKmacXof(bool use_128_bits) { - // OpenSSL 3.x exposes the cSHAKE/KMAC suffix primitive as KECCAK-KMAC-*. - const char* digest_name = use_128_bits ? OSSL_DIGEST_NAME_KECCAK_KMAC128 - : OSSL_DIGEST_NAME_KECCAK_KMAC256; - auto digest = std::unique_ptr{ - EVP_MD_fetch(nullptr, digest_name, nullptr), EVP_MD_free}; - if (!digest) return {}; - - auto ctx = ncrypto::EVPMDCtxPointer::New(); - if (!ctx.digestInit(digest.get())) return {}; - - return { - .ctx = std::move(ctx), - .rate = use_128_bits ? kKeccakKmac128Rate : kKeccakKmac256Rate, - }; -} - -bool ToEncodedStringInput(const void* data, - size_t byte_length, - EncodedStringInput* input) { - if (byte_length > 0 && data == nullptr) return false; - - size_t bit_length; - if (!ByteLengthToBitLength(byte_length, &bit_length)) return false; - - *input = { - .data = byte_length == 0 ? kEmptyString.data() : data, - .byte_length = byte_length, - .bit_length = bit_length, - }; - return true; -} - -bool DigestUpdateEncodedLength(ncrypto::EVPMDCtxPointer* ctx, - size_t value, - bool left) { - const EncodedLength encoded = EncodeLength(value, left); - return DigestUpdate(ctx, encoded.data.data(), encoded.size); -} - -bool DigestUpdateEncodedString(ncrypto::EVPMDCtxPointer* ctx, - const void* data, - size_t byte_length, - size_t bit_length) { - return DigestUpdateEncodedLength(ctx, bit_length, true) && - DigestUpdate(ctx, data, byte_length); -} - -bool DigestUpdateBytepad(ncrypto::EVPMDCtxPointer* ctx, - size_t width, - const void* data, - size_t byte_length, - size_t bit_length, - const void* data2 = nullptr, - size_t byte_length2 = 0, - size_t bit_length2 = 0) { - if (width == 0) return false; - - size_t encoded_size; - size_t written = EncodedLengthSize(width); - if (!EncodedStringSize(byte_length, bit_length, &encoded_size) || - !AddSize(written, encoded_size, &written)) { - return false; - } - if (data2 != nullptr) { - if (!EncodedStringSize(byte_length2, bit_length2, &encoded_size) || - !AddSize(written, encoded_size, &written)) { - return false; - } - } - - size_t padded_size; - if (!AddSize(written, width - 1, &padded_size)) return false; - padded_size = padded_size / width * width; - DCHECK_GE(padded_size, written); - const size_t padding = padded_size - written; - - return DigestUpdateEncodedLength(ctx, width, true) && - DigestUpdateEncodedString(ctx, data, byte_length, bit_length) && - (data2 == nullptr || - DigestUpdateEncodedString(ctx, data2, byte_length2, bit_length2)) && - DigestUpdateZeros(ctx, padding); -} - -} // namespace - -CShakeConfig::CShakeConfig(CShakeConfig&& other) noexcept - : in(std::move(other.in)), - function_name(std::move(other.function_name)), - customization(std::move(other.customization)), - variant(other.variant), - length(other.length) {} - -CShakeConfig& CShakeConfig::operator=(CShakeConfig&& other) noexcept { - if (&other == this) return *this; - this->~CShakeConfig(); - return *new (this) CShakeConfig(std::move(other)); -} - -void CShakeConfig::MemoryInfo(MemoryTracker* tracker) const { - tracker->TraitTrackInline(in, "in"); - tracker->TraitTrackInline(function_name, "function_name"); - tracker->TraitTrackInline(customization, "customization"); -} - -MaybeLocal CShakeTraits::EncodeOutput(Environment* env, - const CShakeConfig& params, - ByteSource* out) { - return out->ToArrayBuffer(env); -} - -Maybe CShakeTraits::AdditionalConfig( - CryptoJobMode mode, - const FunctionCallbackInfo& args, - unsigned int offset, - CShakeConfig* params) { - Environment* env = Environment::GetCurrent(args); - - if (IsFipsEnabled()) { - THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env); - return Nothing(); - } - - CHECK(args[offset]->IsString()); // Algorithm name - Utf8Value algorithm_name(env->isolate(), args[offset]); - std::string_view algorithm_str = algorithm_name.ToStringView(); - - if (algorithm_str == "cSHAKE128") { - params->variant = CShakeVariant::CSHAKE128; - } else if (algorithm_str == "cSHAKE256") { - params->variant = CShakeVariant::CSHAKE256; - } else { - UNREACHABLE(); - } - - ArrayBufferOrViewContents data(args[offset + 1]); - if (!data.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "data is too big"); - return Nothing(); - } - params->in = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); - - if (!args[offset + 2]->IsUndefined()) { - ArrayBufferOrViewContents function_name(args[offset + 2]); - if (!function_name.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "functionName is too big"); - return Nothing(); - } - params->function_name = IsCryptoJobAsync(mode) - ? function_name.ToCopy() - : function_name.ToByteSource(); - } - - if (!args[offset + 3]->IsUndefined()) { - ArrayBufferOrViewContents customization(args[offset + 3]); - if (!customization.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); - return Nothing(); - } - params->customization = IsCryptoJobAsync(mode) - ? customization.ToCopy() - : customization.ToByteSource(); - } - - CHECK(args[offset + 4]->IsUint32()); // Length - params->length = args[offset + 4].As()->Value(); - - return JustVoid(); -} - -bool CShakeTraits::DeriveBits(Environment* env, - const CShakeConfig& params, - ByteSource* out, - CryptoJobMode mode, - CryptoErrorStore*) { - CShakeParams cshake_params = { - .variant = params.variant, - .function_name_data = params.function_name.data(), - .function_name_size = params.function_name.size(), - .customization_data = params.customization.data(), - .customization_size = params.customization.size(), - .bytepad_input = nullptr, - .input_data = params.in.data(), - .input_size = params.in.size(), - .append_output_length = false, - .length = params.length, - }; - return DeriveCShakeBits(cshake_params, out); -} - -bool DeriveCShakeBits(const CShakeParams& params, ByteSource* out) { - if (params.customization_size > kMaxCShakeCustomizationSize) { - return false; - } - - if (params.length == 0) { - *out = ByteSource(); - return true; - } - - auto xof = NewKeccakKmacXof(params.variant == CShakeVariant::CSHAKE128); - if (!xof.ctx) return false; - auto ctx = std::move(xof.ctx); - - EncodedStringInput function_name; - EncodedStringInput customization; - if (!ToEncodedStringInput(params.function_name_data, - params.function_name_size, - &function_name) || - !ToEncodedStringInput(params.customization_data, - params.customization_size, - &customization)) { - return false; - } - - if (!DigestUpdateBytepad(&ctx, - xof.rate, - function_name.data, - function_name.byte_length, - function_name.bit_length, - customization.data, - customization.byte_length, - customization.bit_length)) { - return false; - } - - if (params.bytepad_input != nullptr && - !DigestUpdateBytepad(&ctx, - xof.rate, - params.bytepad_input->data, - params.bytepad_input->byte_length, - params.bytepad_input->bit_length)) { - return false; - } - - if (!DigestUpdate(&ctx, params.input_data, params.input_size)) { - return false; - } - - if (params.append_output_length && - !DigestUpdateEncodedLength(&ctx, params.length, false)) { - return false; - } - - const size_t length_bytes = - NumBitsToBytes(static_cast(params.length)); - auto data = ctx.digestFinal(length_bytes); - if (!data) [[unlikely]] - return false; - - DCHECK(!data.isSecure()); - *out = ByteSource::Allocated(data.release()); - if (params.length % CHAR_BIT != 0) TruncateToBitLength(params.length, out); - return true; -} -#endif // OPENSSL_WITH_EVP_MAC - } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h index 146e9cf55b52..1cdfa2054e41 100644 --- a/src/crypto/crypto_hash.h +++ b/src/crypto/crypto_hash.h @@ -86,74 +86,6 @@ struct HashTraits final { using HashJob = DeriveBitsJob; -#if OPENSSL_WITH_EVP_MAC -enum class CShakeVariant { CSHAKE128, CSHAKE256 }; - -struct CShakeBytepadInput final { - const void* data; - size_t byte_length; - size_t bit_length; -}; - -struct CShakeParams final { - CShakeVariant variant; - const void* function_name_data; - size_t function_name_size; - const void* customization_data; - size_t customization_size; - const CShakeBytepadInput* bytepad_input; - const void* input_data; - size_t input_size; - bool append_output_length; - uint32_t length; // Output length in bits -}; - -bool DeriveCShakeBits(const CShakeParams& params, ByteSource* out); - -struct CShakeConfig final : public MemoryRetainer { - ByteSource in; - ByteSource function_name; - ByteSource customization; - CShakeVariant variant; - uint32_t length; // Output length in bits - - CShakeConfig() = default; - - explicit CShakeConfig(CShakeConfig&& other) noexcept; - - CShakeConfig& operator=(CShakeConfig&& other) noexcept; - - void MemoryInfo(MemoryTracker* tracker) const override; - SET_MEMORY_INFO_NAME(CShakeConfig) - SET_SELF_SIZE(CShakeConfig) -}; - -struct CShakeTraits final { - using AdditionalParameters = CShakeConfig; - static constexpr const char* JobName = "CShakeJob"; - static constexpr AsyncWrap::ProviderType Provider = - AsyncWrap::PROVIDER_HASHREQUEST; - - static v8::Maybe AdditionalConfig( - CryptoJobMode mode, - const v8::FunctionCallbackInfo& args, - unsigned int offset, - CShakeConfig* params); - - static bool DeriveBits(Environment* env, - const CShakeConfig& params, - ByteSource* out, - CryptoJobMode mode, - CryptoErrorStore* errors); - - static v8::MaybeLocal EncodeOutput(Environment* env, - const CShakeConfig& params, - ByteSource* out); -}; - -using CShakeJob = DeriveBitsJob; -#endif // OPENSSL_WITH_EVP_MAC - } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index 7bdbece96277..f7a994b9e125 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -1,14 +1,11 @@ #include "crypto/crypto_kmac.h" #include "async_wrap-inl.h" -#include "crypto/crypto_hash.h" #include "node_internals.h" #include "threadpoolwork-inl.h" #if OPENSSL_WITH_EVP_MAC #include #include -#include -#include #include #include "crypto/crypto_keys.h" #include "crypto/crypto_sig.h" @@ -26,7 +23,6 @@ using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; -using v8::Number; using v8::Object; using v8::Uint32; using v8::Value; @@ -38,7 +34,6 @@ KmacConfig::KmacConfig(KmacConfig&& other) noexcept signature(std::move(other.signature)), customization(std::move(other.customization)), variant(other.variant), - key_length(other.key_length), length(other.length) {} KmacConfig& KmacConfig::operator=(KmacConfig&& other) noexcept { @@ -96,27 +91,18 @@ Maybe KmacTraits::AdditionalConfig( } // If undefined, params->customization remains uninitialized (size 0). - CHECK(args[offset + 4]->IsNumber()); // Key length - double key_length = args[offset + 4].As()->Value(); - if (!(key_length >= 0) || - key_length > static_cast(std::numeric_limits::max())) { - THROW_ERR_OUT_OF_RANGE(env, "key length is too big"); - return Nothing(); - } - params->key_length = static_cast(key_length); + CHECK(args[offset + 4]->IsUint32()); // Length + params->length = args[offset + 4].As()->Value(); - CHECK(args[offset + 5]->IsUint32()); // Length - params->length = args[offset + 5].As()->Value(); - - ArrayBufferOrViewContents data(args[offset + 6]); + ArrayBufferOrViewContents data(args[offset + 5]); if (!data.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); - if (!args[offset + 7]->IsUndefined()) { - ArrayBufferOrViewContents signature(args[offset + 7]); + if (!args[offset + 6]->IsUndefined()) { + ArrayBufferOrViewContents signature(args[offset + 6]); if (!signature.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); return Nothing(); @@ -128,86 +114,18 @@ Maybe KmacTraits::AdditionalConfig( return JustVoid(); } -namespace { - -static constexpr std::array kKmacFunctionName = { - 'K', 'M', 'A', 'C'}; -static constexpr size_t kKmacMinOpenSSLKeySize = 4; -// Keep the bit-aware path within OpenSSL's KMAC provider limits. -static constexpr size_t kKmacMaxOpenSSLKeySize = 512; -static constexpr size_t kKmacMaxOpenSSLCustomizationSize = 512; -static constexpr size_t kKmacMaxOpenSSLOutputSize = 0xffffff / CHAR_BIT; - -bool KmacParamsWithinOpenSSLLimits(const KmacConfig& params, - size_t key_size, - size_t length_bytes) { - return key_size <= kKmacMaxOpenSSLKeySize && - NumBitsToBytes(params.key_length) <= kKmacMaxOpenSSLKeySize && - params.customization.size() <= kKmacMaxOpenSSLCustomizationSize && - length_bytes <= kKmacMaxOpenSSLOutputSize; -} - -bool DeriveBitsWithCShake(const KmacConfig& params, - const void* key_data, - size_t key_size, - ByteSource* out) { - if (IsFipsEnabled()) return false; - - const size_t key_length_bytes = NumBitsToBytes(params.key_length); - if (key_size < key_length_bytes) return false; - - CShakeBytepadInput key_input = { - .data = key_data, - .byte_length = key_length_bytes, - .bit_length = params.key_length, - }; - CShakeParams cshake_params = { - .variant = params.variant == KmacVariant::KMAC128 - ? CShakeVariant::CSHAKE128 - : CShakeVariant::CSHAKE256, - .function_name_data = kKmacFunctionName.data(), - .function_name_size = kKmacFunctionName.size(), - .customization_data = params.customization.data(), - .customization_size = params.customization.size(), - .bytepad_input = &key_input, - .input_data = params.data.data(), - .input_size = params.data.size(), - .append_output_length = true, - .length = params.length, - }; - return DeriveCShakeBits(cshake_params, out); -} - -} // namespace - bool KmacTraits::DeriveBits(Environment* env, const KmacConfig& params, ByteSource* out, CryptoJobMode mode, CryptoErrorStore*) { - const bool truncate_to_bit_length = params.length % CHAR_BIT != 0; - const size_t length_bytes = - NumBitsToBytes(static_cast(params.length)); + if (params.length % CHAR_BIT != 0) return false; + const size_t length_bytes = params.length / CHAR_BIT; // Get the key data. const void* key_data = params.key.GetSymmetricKey(); size_t key_size = params.key.GetSymmetricKeySize(); - if (!KmacParamsWithinOpenSSLLimits(params, key_size, length_bytes)) { - return false; - } - - if (params.length == 0) { - *out = ByteSource(); - return true; - } - - // OpenSSL's EVP_MAC provider rejects KMAC keys shorter than 4 bytes. - if (params.length % CHAR_BIT != 0 || params.key_length % CHAR_BIT != 0 || - key_size < kKmacMinOpenSSLKeySize) { - return DeriveBitsWithCShake(params, key_data, key_size, out); - } - // Fetch the KMAC algorithm auto mac = EVPMacPointer::Fetch((params.variant == KmacVariant::KMAC128) ? OSSL_MAC_NAME_KMAC128 @@ -261,7 +179,6 @@ bool KmacTraits::DeriveBits(Environment* env, auto buffer = result.release(); *out = ByteSource::Allocated(buffer.data, buffer.len); - if (truncate_to_bit_length) TruncateToBitLength(params.length, out); return true; } diff --git a/src/crypto/crypto_kmac.h b/src/crypto/crypto_kmac.h index 703b03c2c882..4f8fe27d2ff4 100644 --- a/src/crypto/crypto_kmac.h +++ b/src/crypto/crypto_kmac.h @@ -21,8 +21,7 @@ struct KmacConfig final : public MemoryRetainer { ByteSource signature; ByteSource customization; KmacVariant variant; - size_t key_length; // Key length in bits - uint32_t length; // Output length in bits + uint32_t length; // Output length in bits KmacConfig() = default; diff --git a/test/fixtures/crypto/kmac.js b/test/fixtures/crypto/kmac.js index ed265bb9c9f3..cc1870af2bb0 100644 --- a/test/fixtures/crypto/kmac.js +++ b/test/fixtures/crypto/kmac.js @@ -114,61 +114,6 @@ module.exports = function() { 0x76, 0xfc, 0x89, 0x65, ]), }, - { - // KMAC128 with a short key, generated with OpenSSL's KECCAK-KMAC128 - // digest over independently encoded NIST SP 800-185 framing. - algorithm: 'KMAC128', - key: Buffer.from([0x00, 0x01, 0x02]), - data: Buffer.from([0x01, 0x02, 0x03]), - customization: Buffer.from('Node.js'), - outputLength: 256, - expected: Buffer.from([ - 0xfb, 0x7c, 0xbb, 0xa2, 0xa1, 0x0d, 0x1a, 0x87, 0x9a, 0x9f, 0x96, 0x8c, - 0x58, 0x9d, 0x2a, 0xfe, 0x4a, 0x9b, 0xbf, 0x03, 0x7e, 0x85, 0x2c, 0xac, - 0x05, 0xdd, 0x78, 0x5b, 0x78, 0xd6, 0x57, 0x1c, - ]), - }, - { - // KMAC256 with a short key, generated with OpenSSL's KECCAK-KMAC256 - // digest over independently encoded NIST SP 800-185 framing. - algorithm: 'KMAC256', - key: Buffer.from([0x00, 0x01, 0x02]), - data: Buffer.from([0x01, 0x02, 0x03]), - customization: Buffer.from('Node.js'), - outputLength: 512, - expected: Buffer.from([ - 0x2c, 0xcf, 0x20, 0xde, 0xd8, 0xc9, 0x6d, 0xb0, 0x5f, 0x15, 0xe0, 0xb3, - 0xce, 0x5d, 0xf0, 0x45, 0xc7, 0xd7, 0x4e, 0xfe, 0x18, 0xee, 0x36, 0xa8, - 0xe4, 0x9a, 0x37, 0xfb, 0xc2, 0xb1, 0xbb, 0xfd, 0xad, 0xf5, 0xb7, 0x89, - 0xd3, 0xa4, 0xbc, 0xb3, 0xa8, 0x28, 0x8e, 0x9f, 0x25, 0xe6, 0x8d, 0x5b, - 0x4a, 0x01, 0x0b, 0x90, 0xae, 0x6d, 0x2b, 0xfc, 0xf1, 0xb6, 0xbb, 0x82, - 0x34, 0x8b, 0x51, 0xd9, - ]), - }, - { - // KMAC128 with a non-byte-aligned output length. The second byte has its - // unused low bits cleared after squeezing a 9-bit result. - algorithm: 'KMAC128', - key: Buffer.from([0x00, 0x01, 0x02, 0x03]), - data: Buffer.from([0x01, 0x02, 0x03]), - customization: undefined, - outputLength: 9, - expected: Buffer.from([0x63, 0x80]), - }, - { - // KMAC128 with a non-byte-aligned key length. The raw key is already - // truncated to 25 bits, matching WebCrypto import semantics. - algorithm: 'KMAC128', - key: Buffer.from([0xff, 0xff, 0xff, 0x80]), - keyLength: 25, - data: Buffer.from([0x01, 0x02, 0x03]), - customization: undefined, - outputLength: 128, - expected: Buffer.from([ - 0x25, 0xea, 0xc7, 0x06, 0x82, 0x47, 0x7e, 0x3c, 0x9b, 0xf0, 0xf1, 0x51, - 0x87, 0x46, 0x40, 0x0c, - ]), - }, ]; return vectors; diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index b01ebce3e5a5..8d58397783ed 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -11,6 +11,8 @@ const pqc = hasOpenSSL(3, 5) || boringSSL; const argon2 = hasOpenSSL(3, 2) && !fips; const shake128 = crypto.getHashes().includes('shake128'); const shake256 = crypto.getHashes().includes('shake256'); +const cshake128 = crypto.getHashes().includes('cshake128'); +const cshake256 = crypto.getHashes().includes('cshake256'); const sha3 = crypto.getHashes().includes('sha3-256'); const ocb = hasOpenSSL(3) && crypto.getCiphers().includes('aes-128-ocb'); const kmac = hasOpenSSL(3) && crypto.getMacs().includes('kmac128'); @@ -22,19 +24,21 @@ export const vectors = { [false, 'cSHAKE128'], [shake128, { name: 'cSHAKE128', outputLength: 128 }], [shake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [shake128 && kmac && !fips, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }], + [cshake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }], [false, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('SHAKE') }], - [shake128 && kmac && !fips, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], + [cshake128, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1, 1) }], + [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(513) }], - [shake128, { name: 'cSHAKE128', outputLength: 127 }], + [false, { name: 'cSHAKE128', outputLength: 127 }], [false, 'cSHAKE256'], [shake256, { name: 'cSHAKE256', outputLength: 256 }], [shake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [shake256 && kmac && !fips, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }], + [cshake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }], [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('SHAKE') }], - [shake256 && kmac && !fips, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], + [cshake256, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1, 1) }], + [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(513) }], - [shake256, { name: 'cSHAKE256', outputLength: 255 }], + [false, { name: 'cSHAKE256', outputLength: 255 }], [false, 'TurboSHAKE128'], [!fips, { name: 'TurboSHAKE128', outputLength: 128 }], [!fips, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x07 }], @@ -86,9 +90,9 @@ export const vectors = { [false, 'KMAC128'], [false, 'KMAC256'], [kmac, { name: 'KMAC128', outputLength: 256 }], - [kmac && !fips, { name: 'KMAC128', outputLength: 255 }], + [false, { name: 'KMAC128', outputLength: 255 }], [kmac, { name: 'KMAC256', outputLength: 256 }], - [kmac && !fips, { name: 'KMAC256', outputLength: 255 }], + [false, { name: 'KMAC256', outputLength: 255 }], ], 'generateKey': [ [pqc, 'ML-DSA-44'], @@ -109,10 +113,14 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [kmac && !fips, { name: 'KMAC128', length: 0 }], - [kmac && !fips, { name: 'KMAC256', length: 0 }], - [kmac && !fips, { name: 'KMAC128', length: 1 }], - [kmac && !fips, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC128', length: 0 }], + [false, { name: 'KMAC256', length: 0 }], + [false, { name: 'KMAC128', length: 1 }], + [false, { name: 'KMAC128', length: 24 }], + [kmac, { name: 'KMAC128', length: 32 }], + [false, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC256', length: 24 }], + [kmac, { name: 'KMAC256', length: 32 }], ], 'importKey': [ [pqc, 'ML-DSA-44'], @@ -133,10 +141,14 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [kmac && !fips, { name: 'KMAC128', length: 0 }], - [kmac && !fips, { name: 'KMAC256', length: 0 }], - [kmac && !fips, { name: 'KMAC128', length: 1 }], - [kmac && !fips, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC128', length: 0 }], + [false, { name: 'KMAC256', length: 0 }], + [false, { name: 'KMAC128', length: 1 }], + [false, { name: 'KMAC128', length: 24 }], + [kmac, { name: 'KMAC128', length: 32 }], + [false, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC256', length: 24 }], + [kmac, { name: 'KMAC256', length: 32 }], ], 'exportKey': [ [pqc, 'ML-DSA-44'], @@ -251,7 +263,7 @@ export const vectors = { [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 249 }], - [pqc && kmac && !fips, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [hybridKems, 'MLKEM768-P256', 'HKDF'], [hybridKems, 'MLKEM768-X25519', 'HKDF'], [hybridKems, 'MLKEM1024-P384', 'HKDF'], @@ -283,7 +295,7 @@ export const vectors = { [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 249 }], - [pqc && kmac && !fips, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [hybridKems, 'MLKEM768-P256', 'HKDF'], [hybridKems, 'MLKEM768-X25519', 'HKDF'], [hybridKems, 'MLKEM1024-P384', 'HKDF'], 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 ba41eb64eb28..a5430df9e0fc 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -14,7 +14,6 @@ const { } = require('crypto'); const { hasFIPS } = require('../common/crypto'); const { kSupportedAlgorithms } = require('internal/crypto/util'); -const fips = hasFIPS(); const rejectsXCurves = hasFIPS(3, 5); const hashes = Object.keys(kSupportedAlgorithms.digest).filter((name) => { @@ -139,19 +138,11 @@ function genericSecretVectors(name) { ]; } -function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { +function macInvalid(algorithm, invalidLengthMessage, isKmac = false) { const key = createSecretKey(randomBytes(32)); const usages = ['sign', 'verify']; - if (allowZeroKey && !fips) { - const zeroKey = createSecretKey(Buffer.alloc(0)) - .toCryptoKey(algorithm, true, usages); - assert.strictEqual(zeroKey.algorithm.length, 0); - - const explicitZeroKey = createSecretKey(Buffer.alloc(0)) - .toCryptoKey({ ...algorithm, length: 0 }, true, usages); - assert.strictEqual(explicitZeroKey.algorithm.length, 0); - } else if (allowZeroKey) { + if (isKmac) { for (const zeroAlgorithm of [algorithm, { ...algorithm, length: 0 }]) { assert.throws(() => { createSecretKey(Buffer.alloc(0)) @@ -177,7 +168,7 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { assert.throws( () => key.toCryptoKey({ ...algorithm, length: 0 }, true, usages), - allowZeroKey && fips ? { + isKmac ? { name: 'NotSupportedError', message: 'Invalid key length', } : { diff --git a/test/parallel/test-webcrypto-aes-kw-short-input.js b/test/parallel/test-webcrypto-aes-kw-short-input.js index 09fe0268ab9b..e11e19360de9 100644 --- a/test/parallel/test-webcrypto-aes-kw-short-input.js +++ b/test/parallel/test-webcrypto-aes-kw-short-input.js @@ -6,18 +6,13 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { getFips } = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; (async () => { const keyToWrap = await subtle.importKey( 'raw', new Uint8Array(16), 'AES-GCM', true, ['encrypt']); - let emptyKey; - if (hasOpenSSL(3) && getFips() !== 1) { - emptyKey = await subtle.importKey( - 'raw-secret', new Uint8Array(0), 'KMAC128', true, ['sign']); - } + const shortKey = await subtle.importKey( + 'raw', new Uint8Array(8), { name: 'HMAC', hash: 'SHA-256' }, true, ['sign']); for (const length of [128, 192, 256]) { const wrappingKey = await subtle.generateKey( @@ -31,11 +26,8 @@ const { subtle } = globalThis.crypto; 'HKDF', false, ['deriveBits']), { name: 'OperationError' }); } - if (emptyKey !== undefined) { - await assert.rejects(subtle.wrapKey( - 'raw-secret', emptyKey, wrappingKey, 'AES-KW'), - { name: 'OperationError' }); - } + await assert.rejects(subtle.wrapKey( + 'raw', shortKey, wrappingKey, 'AES-KW'), { name: 'OperationError' }); const wrapped = await subtle.wrapKey( 'raw', keyToWrap, wrappingKey, 'AES-KW'); diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index e1b663fa30ff..318ca8d2500d 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -284,7 +284,7 @@ const fips4 = hasFIPS(4); })().then(common.mustCall()); } -if (hasOpenSSL(3) && !hasFIPS()) { +if (hasOpenSSL(3)) { (async () => { const derivedKeyAlgorithm = { name: 'KMAC128', length: 0 }; const usages = ['sign']; @@ -314,19 +314,9 @@ if (hasOpenSSL(3) && !hasFIPS()) { baseKeyAlgorithm, false, ['deriveKey']); - const derived = await subtle.deriveKey( - algorithm, - baseKey, - derivedKeyAlgorithm, - false, - usages); - assert.strictEqual(derived.algorithm.length, 0); - - const signature = subtle.sign({ - name: 'KMAC128', - outputLength: 256, - }, derived, new Uint8Array()); - assert.strictEqual((await signature).byteLength, 32); + await assert.rejects( + subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, false, usages), + { name: 'NotSupportedError', message: 'Invalid key length' }); } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 47a56a912d69..89bd86bd9284 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -9,8 +9,7 @@ const assert = require('assert'); const { Buffer } = require('buffer'); const { subtle } = globalThis.crypto; const { createHash, getHashes } = require('crypto'); -const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); -const fips = hasFIPS(); +const { isBoringSSL } = require('../common/crypto'); const kTests = [ ['SHA-1', ['sha1'], 160], @@ -265,16 +264,16 @@ if (getHashes().includes('shake128')) { new Uint8Array(0), ); - const digest = await subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)); - assert.strictEqual(digest.byteLength, 1); - assert.strictEqual(new Uint8Array(digest)[0] & 0b00000001, 0); + await assert.rejects( + subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)), + { name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength' }); await assert.rejects( subtle.digest( { name: 'cSHAKE128', outputLength: 0xffffffff }, Buffer.alloc(1)), { - name: 'OperationError', + name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength', }); @@ -291,22 +290,53 @@ if (getHashes().includes('shake128')) { message: 'Unsupported CShakeParams functionName', }); - if (fips) return; - - await assert.rejects( - subtle.digest( - { - name: 'cSHAKE128', + for (const name of ['cSHAKE128', 'cSHAKE256']) { + const supported = getHashes().includes(name.toLowerCase()); + assert.deepStrictEqual( + await subtle.digest({ + name, outputLength: 256, - customization: Buffer.alloc(513), - }, - Buffer.alloc(1)), - { - name: 'OperationError', - message: 'CShakeParams.customization must be at most 512 bytes', - }); - - if (!hasOpenSSL(3)) return; + functionName: Buffer.alloc(0), + customization: Buffer.alloc(0), + }, Buffer.alloc(1)), + await subtle.digest({ name, outputLength: 256 }, Buffer.alloc(1))); + + await assert.rejects( + subtle.digest({ + name, + outputLength: 256, + customization: Buffer.alloc(513, 1), + }, Buffer.alloc(1)), + supported ? { + name: 'OperationError', + message: 'CShakeParams.customization must be at most 512 bytes', + } : { + name: 'NotSupportedError', + message: 'Unsupported CShakeParams customization', + }); + + await assert.rejects( + subtle.digest({ + name, + outputLength: 256, + customization: Buffer.from([0x61, 0x00, 0x62]), + }, Buffer.alloc(1)), + { name: 'NotSupportedError', message: 'Unsupported CShakeParams customization' }); + + for (const params of [ + { functionName: Buffer.from('KMAC') }, + { customization: Buffer.from('Node.js') }, + ]) { + const algorithm = { name, outputLength: 256, ...params }; + if (supported) { + assert.strictEqual((await subtle.digest(algorithm, Buffer.alloc(1))).byteLength, 32); + } else { + await assert.rejects(subtle.digest(algorithm, Buffer.alloc(1)), { + name: 'NotSupportedError', + }); + } + } + } const nistCShakeShortInput = Buffer.from('00010203', 'hex'); const nistCShakeLongInput = @@ -400,19 +430,23 @@ if (getHashes().includes('shake128')) { 'ca6f88db415829', }, ]) { - assert.strictEqual( - Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), - expected); + if (getHashes().includes(algorithm.name.toLowerCase())) { + assert.strictEqual( + Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), + expected); + } else { + await assert.rejects(subtle.digest(algorithm, data), { + name: 'NotSupportedError', + }); + } } - const truncated = Buffer.from(await subtle.digest( - { ...nistCShakeSample1.algorithm, outputLength: 255 }, - nistCShakeSample1.data)); - const expected = Buffer.from(nistCShakeSample1.expected, 'hex'); - assert.strictEqual(truncated.byteLength, expected.byteLength); - assert.deepStrictEqual( - truncated.subarray(0, 31), expected.subarray(0, 31)); - assert.strictEqual(truncated[31] & 0b00000001, 0); - assert.strictEqual(truncated[31] | 0b00000001, expected[31]); + if (getHashes().includes('cshake128')) { + await assert.rejects( + subtle.digest( + { ...nistCShakeSample1.algorithm, outputLength: 255 }, + nistCShakeSample1.data), + { name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength' }); + } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index 385ae8c65e9d..63fdde05f1ef 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -285,69 +285,35 @@ if (hasOpenSSL(3)) { [/* empty usages */]), { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' }); - { - if (getFips() !== 1) { - const importedZeroImplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array(), - name, - true, - ['sign', 'verify']); - const importedZeroImplicitRaw = - await subtle.exportKey('raw-secret', importedZeroImplicit); - assert.strictEqual(importedZeroImplicit.algorithm.length, 0); - assert.strictEqual(importedZeroImplicitRaw.byteLength, 0); + for (const algorithm of [name, { name, length: 0 }]) { + await assert.rejects( + subtle.importKey( + 'raw-secret', new Uint8Array(), algorithm, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); + } - const importedZeroExplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array(), - { name, length: 0 }, - true, - ['sign', 'verify']); - const importedZeroExplicitRaw = - await subtle.exportKey('raw-secret', importedZeroExplicit); - assert.strictEqual(importedZeroExplicit.algorithm.length, 0); - assert.strictEqual(importedZeroExplicitRaw.byteLength, 0); - - await assert.rejects( - subtle.importKey( - 'raw-secret', - new Uint8Array([0xff]), - { name, length: 0 }, - true, - ['sign', 'verify']), - { name: 'DataError', message: 'Invalid key length' }); - - const generated = await subtle.generateKey( - { name, length: 9 }, - true, - ['sign', 'verify']); - const generatedRaw = await subtle.exportKey('raw-secret', generated); - assert.strictEqual(generated.algorithm.length, 9); - assert.strictEqual(generatedRaw.byteLength, 2); - assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 0 }, + true, + ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); + + await assert.rejects( + subtle.generateKey({ name, length: 9 }, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); - const importedExplicit = await subtle.importKey( + for (const byteLength of [1, 2]) { + await assert.rejects( + subtle.importKey( 'raw-secret', - new Uint8Array([0xff, 0xff]), + new Uint8Array(byteLength).fill(0xff), { name, length: 9 }, true, - ['sign', 'verify']); - const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit); - assert.strictEqual(importedExplicit.algorithm.length, 9); - assert.deepStrictEqual( - new Uint8Array(importedExplicitRaw), - new Uint8Array([0xff, 0x80])); - - await assert.rejects( - subtle.importKey( - 'raw-secret', - new Uint8Array([0xff]), - { name, length: 9 }, - true, - ['sign', 'verify']), - { name: 'DataError', message: 'Invalid key length' }); - } + ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); } } diff --git a/test/parallel/test-webcrypto-fips-exceptions.mjs b/test/parallel/test-webcrypto-fips-exceptions.mjs index ecc0f3c6989d..b45740dc508e 100644 --- a/test/parallel/test-webcrypto-fips-exceptions.mjs +++ b/test/parallel/test-webcrypto-fips-exceptions.mjs @@ -12,10 +12,10 @@ if (!hasFIPS(3)) common.skip('requires OpenSSL >= 3 in FIPS mode'); const require = createRequire(import.meta.url); +const { getHashes } = require('node:crypto'); const { internalBinding } = require('internal/test/binding'); const { getCryptoKeyHandle } = require('internal/crypto/keys'); const { - CShakeJob, KangarooTwelveJob, KmacJob, TurboShakeJob, @@ -52,13 +52,6 @@ for (const createJob of [ kCryptoJobWebCrypto, 'TurboSHAKE128', 0x1f, 16, data), () => new KangarooTwelveJob( kCryptoJobWebCrypto, 'KT128', undefined, 16, data), - () => new CShakeJob( - kCryptoJobWebCrypto, - 'cSHAKE128', - data, - Buffer.from('KMAC'), - undefined, - 128), ]) { assert.throws(createJob, { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', @@ -66,48 +59,34 @@ for (const createJob of [ }); } -const emptyCShake = { - name: 'cSHAKE128', - outputLength: 256, - customization: data, - functionName: data, -}; -assert.strictEqual(SubtleCrypto.supports('digest', emptyCShake), true); - -for (const length of [1, 513]) { - const algorithm = { - name: 'cSHAKE128', +for (const name of ['cSHAKE128', 'cSHAKE256']) { + const emptyCShake = { + name, outputLength: 256, - customization: new Uint8Array(length), + customization: data, + functionName: data, }; - await assertFipsException( - 'digest', - algorithm, - () => subtle.digest(algorithm, data), - 'Unsupported CShakeParams customization'); + assert.strictEqual(SubtleCrypto.supports('digest', emptyCShake), true); + assert.strictEqual((await subtle.digest(emptyCShake, data)).byteLength, 32); + + for (const params of [ + { customization: Buffer.from('Node.js') }, + { functionName: Buffer.from('KMAC') }, + { functionName: Buffer.from('KMAC'), customization: Buffer.from('Node.js') }, + ]) { + const algorithm = { name, outputLength: 256, ...params }; + const supported = getHashes().includes(name.toLowerCase()); + assert.strictEqual(SubtleCrypto.supports('digest', algorithm), supported); + if (supported) { + assert.strictEqual((await subtle.digest(algorithm, data)).byteLength, 32); + } else { + await assert.rejects(subtle.digest(algorithm, data), { + name: 'NotSupportedError', + }); + } + } } -const functionName = { - name: 'cSHAKE256', - outputLength: 256, - functionName: Buffer.from('KMAC'), -}; -await assertFipsException( - 'digest', - functionName, - () => subtle.digest(functionName, data), - 'Unsupported CShakeParams functionName'); - -const bothCShakeParams = { - ...functionName, - customization: new Uint8Array(1), -}; -await assertFipsException( - 'digest', - bothCShakeParams, - () => subtle.digest(bothCShakeParams, data), - 'Unsupported CShakeParams customization'); - for (const length of [0, 24, 33]) { const algorithm = { name: 'KMAC128', length }; await assertFipsException( @@ -170,7 +149,6 @@ await assert.rejects( getCryptoKeyHandle(key), 'KMAC128', undefined, - 32, 9, data, undefined).run(), diff --git a/test/parallel/test-webcrypto-fips-refresh.js b/test/parallel/test-webcrypto-fips-refresh.js index 71459099356c..4eff258b72b9 100644 --- a/test/parallel/test-webcrypto-fips-refresh.js +++ b/test/parallel/test-webcrypto-fips-refresh.js @@ -6,7 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { getFips, setFips } = require('crypto'); +const { getFips, getHashes, setFips } = require('crypto'); const { internalBinding } = require('internal/test/binding'); const { getOptionValue } = require('internal/options'); if (!internalBinding('crypto').testFipsCrypto()) @@ -25,8 +25,8 @@ try { name: 'KT128', outputLength: 128, }), !fips); assert.strictEqual(SubtleCrypto.supports('digest', { - name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array(1), - }), !fips); + name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array([1]), + }), getHashes().includes('cshake128')); assert.strictEqual(SubtleCrypto.supports('generateKey', { name: 'RSA-PSS', hash: 'SHA-256', modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), diff --git a/test/parallel/test-webcrypto-keccak-byte-alignment.js b/test/parallel/test-webcrypto-keccak-byte-alignment.js new file mode 100644 index 000000000000..ce3201210eb6 --- /dev/null +++ b/test/parallel/test-webcrypto-keccak-byte-alignment.js @@ -0,0 +1,90 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { createSecretKey, subtle } = require('crypto'); +const { SubtleCrypto } = globalThis; +const unsupported = { name: 'NotSupportedError' }; + +(async () => { + const data = new Uint8Array([1, 2, 3]); + for (const name of ['cSHAKE128', 'cSHAKE256']) { + if (!SubtleCrypto.supports('digest', { name, outputLength: 128 })) + continue; + + for (const customization of [undefined, new Uint8Array([1])]) { + if (!SubtleCrypto.supports('digest', { name, outputLength: 128, customization })) + continue; + + for (let remainder = 1; remainder < 8; remainder++) { + const algorithm = { name, outputLength: 128 + remainder, customization }; + assert.strictEqual(SubtleCrypto.supports('digest', algorithm), false); + await assert.rejects(subtle.digest(algorithm, data), unsupported); + } + + const digest = await subtle.digest({ name, outputLength: 128, customization }, data); + assert.strictEqual(digest.byteLength, 16); + } + + // Check the uint32 boundary without allocating a large digest. + assert.strictEqual(SubtleCrypto.supports('digest', { name, outputLength: 0xfffffff8 }), true); + for (const outputLength of [0xfffffff9, 0xffffffff]) { + assert.strictEqual(SubtleCrypto.supports('digest', { name, outputLength }), false); + await assert.rejects(subtle.digest({ name, outputLength }, data), unsupported); + } + } + + const baseKey = await subtle.importKey('raw-secret', data, 'HKDF', false, ['deriveKey']); + const derivation = { name: 'HKDF', hash: 'SHA-256', salt: data, info: data }; + const raw = Buffer.alloc(32, 1); + const secretKey = createSecretKey(raw); + + for (const name of ['KMAC128', 'KMAC256']) { + if (!SubtleCrypto.supports('importKey', name)) + continue; + + const key = await subtle.importKey('raw-secret', raw, name, false, ['sign', 'verify']); + for (let remainder = 1; remainder < 8; remainder++) { + const algorithm = { name, length: 248 + remainder }; + assert.strictEqual(SubtleCrypto.supports('generateKey', algorithm), false); + assert.strictEqual(SubtleCrypto.supports('importKey', algorithm), false); + assert.strictEqual(SubtleCrypto.supports('deriveKey', derivation, algorithm), false); + await assert.rejects(subtle.generateKey(algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.importKey('raw-secret', raw, algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.importKey( + 'jwk', { kty: 'oct', k: raw.toString('base64url') }, algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.deriveKey(derivation, baseKey, algorithm, false, ['sign']), unsupported); + assert.throws(() => secretKey.toCryptoKey(algorithm, false, ['sign']), unsupported); + + const params = { name, outputLength: 248 + remainder }; + assert.strictEqual(SubtleCrypto.supports('sign', params), false); + assert.strictEqual(SubtleCrypto.supports('verify', params), false); + await assert.rejects(subtle.sign(params, key, data), unsupported); + await assert.rejects(subtle.verify(params, key, raw, data), unsupported); + } + } + + if (SubtleCrypto.supports('generateKey', 'ML-KEM-768')) { + const { publicKey, privateKey } = await subtle.generateKey( + 'ML-KEM-768', false, ['encapsulateBits', 'encapsulateKey', 'decapsulateKey']); + const { ciphertext } = await subtle.encapsulateBits('ML-KEM-768', publicKey); + for (const name of ['KMAC128', 'KMAC256']) { + if (!SubtleCrypto.supports('importKey', name)) + continue; + + const algorithm = { name, length: 255 }; + assert.strictEqual(SubtleCrypto.supports('encapsulateKey', 'ML-KEM-768', algorithm), false); + assert.strictEqual(SubtleCrypto.supports('decapsulateKey', 'ML-KEM-768', algorithm), false); + await assert.rejects(subtle.encapsulateKey( + 'ML-KEM-768', publicKey, algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.decapsulateKey( + 'ML-KEM-768', privateKey, ciphertext, algorithm, false, ['sign']), unsupported); + + assert.strictEqual(SubtleCrypto.supports('encapsulateKey', 'ML-KEM-768', { name, length: 256 }), true); + assert.strictEqual(SubtleCrypto.supports('decapsulateKey', 'ML-KEM-768', { name, length: 256 }), true); + } + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-keygen-kmac.js b/test/parallel/test-webcrypto-keygen-kmac.js index 33716095751f..999e70df6a60 100644 --- a/test/parallel/test-webcrypto-keygen-kmac.js +++ b/test/parallel/test-webcrypto-keygen-kmac.js @@ -5,7 +5,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasFIPS, hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL >= 3'); @@ -13,7 +13,6 @@ if (!hasOpenSSL(3)) const assert = require('assert'); const { types: { isCryptoKey } } = require('util'); const { subtle } = globalThis.crypto; -const fips = hasFIPS(); const usages = ['sign', 'verify']; @@ -23,9 +22,6 @@ async function test(name, length) { if (length !== undefined) algorithm.length = length; - if (fips && length !== undefined && - (length < 32 || length % 8 !== 0)) return; - const generatedKey = await subtle.generateKey(algorithm, true, usages); assert(generatedKey); @@ -45,12 +41,10 @@ async function test(name, length) { } const kTests = [ - ['KMAC128', 0], ['KMAC128', 32], ['KMAC128', 128], ['KMAC128', 256], ['KMAC128'], - ['KMAC256', 0], ['KMAC256', 32], ['KMAC256', 128], ['KMAC256', 256], @@ -60,3 +54,13 @@ const kTests = [ const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); + +(async () => { + for (const name of ['KMAC128', 'KMAC256']) { + for (const length of [0, 8, 16, 24]) { + await assert.rejects( + subtle.generateKey({ name, length }, true, usages), + { name: 'NotSupportedError', message: 'Invalid key length' }); + } + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-prototype-pollution.mjs b/test/parallel/test-webcrypto-prototype-pollution.mjs index 9f0b41ac10bb..f485cb076f11 100644 --- a/test/parallel/test-webcrypto-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-prototype-pollution.mjs @@ -13,8 +13,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const require = createRequire(import.meta.url); const { kSupportedAlgorithms } = require('internal/crypto/util'); -const { getFips } = require('node:crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { getFips, getHashes } = require('node:crypto'); const { subtle } = globalThis.crypto; const TypedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype); @@ -134,15 +133,15 @@ if (supports('digest', 'cSHAKE128')) { message: /Unsupported CShakeParams functionName/, }))); - // asyncDigest() picks the cSHAKE job over plain SHAKE on a non-empty + // asyncDigest() picks cSHAKE over plain SHAKE on a non-empty // customization. - if (hasOpenSSL(3)) { + { const algorithm = { name: 'cSHAKE128', outputLength: 256, customization: new Uint8Array([1, 2, 3]), }; - if (getFips() === 1) { + if (!getHashes().includes('cshake128')) { await withPoisoned(poisonTypedArrayByteLength(0), common.mustCall(() => assert.rejects(subtle.digest(algorithm, data), { name: 'NotSupportedError', diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index ac0b738bcd57..5829ab3685c0 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -5,38 +5,16 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasFIPS, hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL >= 3'); const assert = require('assert'); const { subtle } = globalThis.crypto; -const fips = hasFIPS(); -const fips4 = hasFIPS(4); const vectors = require('../fixtures/crypto/kmac')(); -function isFipsProviderUnsupported(err) { - return err.name === 'OperationError' && - err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED'; -} - -function usesNonFipsImplementation({ key, keyLength, outputLength }) { - const keyLengthInBits = keyLength ?? key.byteLength * 8; - return outputLength === 0 || - outputLength % 8 !== 0 || - keyLengthInBits < 32 || - keyLengthInBits % 8 !== 0; -} - -function isFips4Incompatible({ key, keyLength, outputLength }) { - const keyLengthInBits = keyLength ?? key.byteLength * 8; - return keyLengthInBits < 128 || - keyLengthInBits % 8 !== 0 || - outputLength % 8 !== 0; -} - async function testVerify({ algorithm, key, keyLength, @@ -215,17 +193,8 @@ async function testSign({ algorithm, const variations = []; for (const vector of vectors) { - if (fips && usesNonFipsImplementation(vector)) continue; - - if (fips4 && isFips4Incompatible(vector)) { - variations.push(assert.rejects( - testVerify(vector), isFipsProviderUnsupported)); - variations.push(assert.rejects( - testSign(vector), isFipsProviderUnsupported)); - } else { - variations.push(testVerify(vector)); - variations.push(testSign(vector)); - } + variations.push(testVerify(vector)); + variations.push(testSign(vector)); } await Promise.all(variations); @@ -240,87 +209,54 @@ async function testSign({ algorithm, ['sign', 'verify']); const algorithm = { name: 'KMAC128', - outputLength: fips ? 16 : 9, + outputLength: 16, customization: new Uint8Array(), }; const data = new Uint8Array([1, 2, 3]); const signature = await subtle.sign(algorithm, key, data); assert.strictEqual(signature.byteLength, 2); - if (!fips) - assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); assert(await subtle.verify(algorithm, key, signature, data)); - if (fips) { - const signature128 = await subtle.sign({ - ...algorithm, - outputLength: 128, - }, key, data); - assert.strictEqual(signature128.byteLength, 16); - assert(await subtle.verify({ - ...algorithm, - outputLength: 128, - }, key, signature128, data)); - } else { - const signature16 = new Uint8Array(await subtle.sign({ - ...algorithm, - outputLength: 16, - }, key, data)); - signature16[1] &= 0b10000000; - assert.notDeepStrictEqual(new Uint8Array(signature), signature16); - } + const signature128 = await subtle.sign({ + ...algorithm, + outputLength: 128, + }, key, data); + assert.strictEqual(signature128.byteLength, 16); + assert(await subtle.verify({ + ...algorithm, + outputLength: 128, + }, key, signature128, data)); const invalidSignature = new Uint8Array(signature); - if (fips) - invalidSignature[0] ^= 0b00000001; - else - invalidSignature[1] |= 0b00000001; + invalidSignature[0] ^= 0b00000001; assert(!(await subtle.verify(algorithm, key, invalidSignature, data))); - if (!fips) { - const nonByteKey = await subtle.importKey( + const nonByteOutput = { ...algorithm, outputLength: 9 }; + await assert.rejects( + subtle.sign(nonByteOutput, key, data), + { name: 'NotSupportedError', message: 'Invalid KmacParams outputLength' }); + await assert.rejects( + subtle.verify(nonByteOutput, key, signature, data), + { name: 'NotSupportedError', message: 'Invalid KmacParams outputLength' }); + + await assert.rejects( + subtle.importKey( 'raw-secret', new Uint8Array([0xff, 0xff, 0xff, 0xff]), { name: 'KMAC128', length: 25 }, false, - ['sign', 'verify']); - const nonByteKeySignature = subtle.sign({ - ...algorithm, - outputLength: 16, - }, nonByteKey, data); - const result = await nonByteKeySignature; - assert.strictEqual(result.byteLength, 2); - assert(await subtle.verify({ - ...algorithm, - outputLength: 16, - }, nonByteKey, result, data)); - } + ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); })().then(common.mustCall()); (async function() { - if (fips) return; - - const data = new Uint8Array([1, 2, 3]); - for (const name of ['KMAC128', 'KMAC256']) { - for (const keyData of [ - new Uint8Array(), - new Uint8Array([1]), - new Uint8Array([1, 2, 3]), - ]) { - const key = await subtle.importKey( - 'raw-secret', - keyData, - { name }, - true, - ['sign', 'verify']); - assert.strictEqual(key.algorithm.length, keyData.byteLength * 8); - - const algorithm = { name, outputLength: 256 }; - const signature = subtle.sign(algorithm, key, data); - const result = await signature; - assert.strictEqual(result.byteLength, 32); - assert(await subtle.verify(algorithm, key, result, data)); + for (const byteLength of [0, 1, 2, 3]) { + await assert.rejects( + subtle.importKey( + 'raw-secret', new Uint8Array(byteLength), name, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); } } })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index b899e4f712d1..1f8d45f02c24 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -485,20 +485,30 @@ async function testNonByteLengthWrapUnwrap({ }); if (hasOpenSSL(3) && getFips() !== 1) { - const kmacAlgorithm = { name: 'KMAC128' }; - const kmacKey = await subtle.importKey( - 'raw-secret', - new Uint8Array([0xff, 0xff]), - { ...kmacAlgorithm, length: 9 }, - true, - ['sign', 'verify']); - await testNonByteLengthWrapUnwrap({ - key: kmacKey, - formats: ['raw-secret', 'jwk'], - rawFormat: 'raw-secret', - explicitAlgorithm: { ...kmacAlgorithm, length: 9 }, - implicitAlgorithm: kmacAlgorithm, - }); + for (const name of ['KMAC128', 'KMAC256']) { + const keyData = new Uint8Array(32).fill(0xff); + const kmacKey = await subtle.importKey( + 'raw-secret', keyData, name, true, ['sign', 'verify']); + const wrappingKey = await subtle.generateKey( + { name: 'AES-GCM', length: 128 }, true, ['wrapKey', 'unwrapKey']); + + for (const [i, format] of ['raw-secret', 'jwk'].entries()) { + const wrapAlgorithm = { name: 'AES-GCM', iv: new Uint8Array(12).fill(i) }; + const wrapped = await subtle.wrapKey(format, kmacKey, wrappingKey, wrapAlgorithm); + await assert.rejects( + subtle.unwrapKey( + format, wrapped, wrappingKey, wrapAlgorithm, + { name, length: 255 }, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); + + const unwrapped = await subtle.unwrapKey( + format, wrapped, wrappingKey, wrapAlgorithm, + { name, length: 256 }, true, ['sign', 'verify']); + assert.strictEqual(unwrapped.algorithm.length, 256); + assert.deepStrictEqual( + new Uint8Array(await subtle.exportKey('raw-secret', unwrapped)), keyData); + } + } } })().then(common.mustCall()); diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index af6455f886fa..e12016ec9161 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -182,17 +182,6 @@ declare namespace InternalCryptoBinding { ): CryptoJobWebCrypto; } - interface CShakeJobConstructor { - new( - mode: CryptoJobWebCryptoMode, - algorithm: string, - data: ByteSource, - functionName: OptionalByteSource, - customization: OptionalByteSource, - outputLength: number, - ): CryptoJobWebCrypto; - } - interface ChaCha20Poly1305CipherJobConstructor { new( mode: CryptoJobWebCryptoMode, @@ -375,7 +364,6 @@ declare namespace InternalCryptoBinding { key: KeyObjectHandle, algorithm: string, customization: OptionalByteSource, - keyLength: number, outputLength: number, data: ByteSource, ...signature: MacJobSignatureArgs @@ -818,7 +806,6 @@ declare namespace InternalCryptoBinding { export interface CryptoBinding { AESCipherJob: InternalCryptoBinding.AESCipherJobConstructor; Argon2Job: InternalCryptoBinding.Argon2JobConstructor; - CShakeJob?: InternalCryptoBinding.CShakeJobConstructor; ChaCha20Poly1305CipherJob: InternalCryptoBinding.ChaCha20Poly1305CipherJobConstructor; CheckPrimeJob: InternalCryptoBinding.CheckPrimeJobConstructor; DHBitsJob: InternalCryptoBinding.DHBitsJobConstructor; From 4f26a356c6d00ffb4b46849a79311e7badfc3461 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 23 Sep 2026 14:38:13 +0200 Subject: [PATCH 29/29] crypto: verify empty KMAC outputs Compare empty KMAC outputs with empty signatures as equal, as required by the verification algorithm. Skip CRYPTO_memcmp for zero-length buffers while retaining the constant-time comparison for other MACs. Signed-off-by: Filip Skokan Assisted-by: Codex --- src/crypto/crypto_kmac.cc | 7 ++-- .../test-webcrypto-kmac-empty-output.js | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-webcrypto-kmac-empty-output.js diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index f7a994b9e125..40b39990737f 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -191,9 +191,10 @@ MaybeLocal KmacTraits::EncodeOutput(Environment* env, case SignConfiguration::Mode::Verify: return Boolean::New( env->isolate(), - out->size() > 0 && out->size() == params.signature.size() && - CRYPTO_memcmp( - out->data(), params.signature.data(), out->size()) == 0); + out->size() == params.signature.size() && + (out->size() == 0 || + CRYPTO_memcmp( + out->data(), params.signature.data(), out->size()) == 0)); } UNREACHABLE(); } diff --git a/test/parallel/test-webcrypto-kmac-empty-output.js b/test/parallel/test-webcrypto-kmac-empty-output.js new file mode 100644 index 000000000000..fe88f7ab7692 --- /dev/null +++ b/test/parallel/test-webcrypto-kmac-empty-output.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); + +if (!hasOpenSSL(3)) + common.skip('requires OpenSSL >= 3'); +if (hasFIPS()) + common.skip('empty KMAC output is not supported in FIPS mode'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const data = new Uint8Array([1, 2, 3]); + + for (const name of ['KMAC128', 'KMAC256']) { + const key = await subtle.importKey( + 'raw-secret', new Uint8Array(32), name, false, ['sign', 'verify']); + const otherKey = await subtle.importKey( + 'raw-secret', new Uint8Array(32).fill(1), name, false, ['verify']); + const algorithm = { name, outputLength: 0 }; + const signature = await subtle.sign(algorithm, key, data); + + assert.strictEqual(signature.byteLength, 0); + assert.strictEqual(await subtle.verify(algorithm, key, signature, data), true); + assert.strictEqual(await subtle.verify(algorithm, key, new Uint8Array([0]), data), false); + assert.strictEqual(await subtle.verify( + { name, outputLength: 256 }, key, signature, data), false); + + // Empty MACs compare equal even when the inputs differ. + assert.strictEqual(await subtle.verify( + algorithm, key, signature, new Uint8Array([4, 5, 6])), true); + assert.strictEqual(await subtle.verify(algorithm, otherKey, signature, data), true); + assert.strictEqual(await subtle.verify( + { ...algorithm, customization: new Uint8Array([1]) }, key, signature, data), true); + } +})().then(common.mustCall());