diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index c5bbaae90cf595..fd3f168435ddcb 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -2,6 +2,7 @@ const { SafeSet, + StringPrototypeToLowerCase, } = primordials; const { Buffer } = require('buffer'); @@ -332,7 +333,7 @@ function cfrgImportKey( return undefined; } - if (keyObject.asymmetricKeyType !== name.toLowerCase()) { + if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) { throw lazyDOMException('Invalid key type', 'DataError'); } diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index ebe3bfe3d17ca0..f4df51ae7cb6aa 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -2,6 +2,7 @@ const { SafeSet, + StringPrototypeToLowerCase, TypedArrayPrototypeGetBuffer, TypedArrayPrototypeSet, Uint8Array, @@ -276,7 +277,7 @@ function mlDsaImportKey( return undefined; } - if (keyObject.asymmetricKeyType !== name.toLowerCase()) { + if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) { throw lazyDOMException('Invalid key type', 'DataError'); } diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index f6eb76cef10b20..208913cae86c36 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -3,6 +3,7 @@ const { PromiseWithResolvers, SafeSet, + StringPrototypeToLowerCase, TypedArrayPrototypeGetBuffer, TypedArrayPrototypeSet, Uint8Array, @@ -209,7 +210,7 @@ function mlKemImportKey( return undefined; } - if (keyObject.asymmetricKeyType !== name.toLowerCase()) { + if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) { throw lazyDOMException('Invalid key type', 'DataError'); } diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 96abe6a04b28e1..ab10c53a655a60 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -15,7 +15,7 @@ const { ObjectEntries, ObjectKeys, ObjectPrototypeHasOwnProperty, - Promise, + PromiseWithResolvers, StringPrototypeToUpperCase, Symbol, TypedArrayPrototypeGetBuffer, @@ -656,15 +656,15 @@ function onDone(resolve, reject, err, result) { } function jobPromise(getJob) { - return new Promise((resolve, reject) => { - try { - const job = getJob(); - job.ondone = FunctionPrototypeBind(onDone, job, resolve, reject); - job.run(); - } catch (err) { - onDone(resolve, reject, err); - } - }); + const { promise, resolve, reject } = PromiseWithResolvers(); + try { + const job = getJob(); + job.ondone = FunctionPrototypeBind(onDone, job, resolve, reject); + job.run(); + } catch (err) { + onDone(resolve, reject, err); + } + return promise; } // In WebCrypto, the publicExponent option in RSA is represented as a diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index e72931e9c83cca..4162d84d426f53 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -195,12 +195,14 @@ converters.object = (V, opts) => { const isNonSharedArrayBuffer = isArrayBuffer; +/** + * @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) { - if ( - typeof V === 'string' ? - !StringPrototypeStartsWith(StringPrototypeToLowerCase(V), 'sha') : - V.name?.toLowerCase?.().startsWith('sha') === false - ) + 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'); } diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs new file mode 100644 index 00000000000000..b4fbedba5e3242 --- /dev/null +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -0,0 +1,95 @@ +import * as common from '../common/index.mjs'; + +if (!common.hasCrypto) common.skip('missing crypto'); + +// WebCrypto subtle methods must not leak intermediate values +// through Promise.prototype.then pollution. +// Regression test for https://github.com/nodejs/node/pull/61492 +// and https://github.com/nodejs/node/issues/59699. + +import { hasOpenSSL } from '../common/crypto.js'; + +const { subtle } = globalThis.crypto; + +Promise.prototype.then = common.mustNotCall('Promise.prototype.then'); + +await subtle.digest('SHA-256', new Uint8Array([1, 2, 3])); + +await subtle.generateKey({ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); + +await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']); + +const rawKey = globalThis.crypto.getRandomValues(new Uint8Array(32)); + +const importedKey = await subtle.importKey( + 'raw', rawKey, { name: 'AES-CBC', length: 256 }, false, ['encrypt', 'decrypt']); + +const exportableKey = await subtle.importKey( + 'raw', rawKey, { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); + +await subtle.exportKey('raw', exportableKey); + +const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); +const plaintext = new TextEncoder().encode('Hello, world!'); + +const ciphertext = await subtle.encrypt({ name: 'AES-CBC', iv }, importedKey, plaintext); + +await subtle.decrypt({ name: 'AES-CBC', iv }, importedKey, ciphertext); + +const signingKey = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']); + +const data = new TextEncoder().encode('test data'); + +const signature = await subtle.sign('HMAC', signingKey, data); + +await subtle.verify('HMAC', signingKey, signature, data); + +const pbkdf2Key = await subtle.importKey( + 'raw', rawKey, 'PBKDF2', false, ['deriveBits', 'deriveKey']); + +await subtle.deriveBits( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, 256); + +await subtle.deriveKey( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt']); + +const wrappingKey = await subtle.generateKey( + { name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']); + +const keyToWrap = await subtle.generateKey( + { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); + +const wrapped = await subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW'); + +await subtle.unwrapKey( + 'raw', wrapped, wrappingKey, 'AES-KW', + { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); + +const { privateKey } = await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']); + +await subtle.getPublicKey(privateKey, ['verify']); + +if (hasOpenSSL(3, 5)) { + const kemPair = await subtle.generateKey( + { name: 'ML-KEM-768' }, false, + ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits']); + + const { ciphertext: ct1 } = await subtle.encapsulateKey( + { name: 'ML-KEM-768' }, kemPair.publicKey, 'HKDF', false, ['deriveBits']); + + await subtle.decapsulateKey( + { name: 'ML-KEM-768' }, kemPair.privateKey, ct1, 'HKDF', false, ['deriveBits']); + + const { ciphertext: ct2 } = await subtle.encapsulateBits( + { name: 'ML-KEM-768' }, kemPair.publicKey); + + await subtle.decapsulateBits( + { name: 'ML-KEM-768' }, kemPair.privateKey, ct2); +}