diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index b743f3f93a149e..67b5f66e4c3320 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -16,6 +16,7 @@ const { ObjectKeys, ObjectPrototypeHasOwnProperty, PromiseWithResolvers, + SafeMap, SafeSet, StringPrototypeToUpperCase, Symbol, @@ -453,8 +454,11 @@ const experimentalAlgorithms = [ ]; // Transform the algorithm definitions into the operation-keyed structure +// Also builds a parallel Map per operation +// for O(1) case-insensitive algorithm name lookup in normalizeAlgorithm. function createSupportedAlgorithms(algorithmDefs) { const result = {}; + const nameMap = {}; for (const { 0: algorithmName, 1: operations } of ObjectEntries(algorithmDefs)) { // Skip algorithms that are conditionally not supported @@ -465,6 +469,8 @@ function createSupportedAlgorithms(algorithmDefs) { for (const { 0: operation, 1: dict } of ObjectEntries(operations)) { result[operation] ||= {}; + nameMap[operation] ||= new SafeMap(); + nameMap[operation].set(StringPrototypeToUpperCase(algorithmName), algorithmName); // Add experimental warnings for experimental algorithms if (ArrayPrototypeIncludes(experimentalAlgorithms, algorithmName)) { @@ -482,10 +488,11 @@ function createSupportedAlgorithms(algorithmDefs) { } } - return result; + return { algorithms: result, nameMap }; } -const kSupportedAlgorithms = createSupportedAlgorithms(kAlgorithmDefinitions); +const { algorithms: kSupportedAlgorithms, nameMap: kAlgorithmNameMap } = + createSupportedAlgorithms(kAlgorithmDefinitions); const simpleAlgorithmDictionaries = { AesCbcParams: { iv: 'BufferSource' }, @@ -527,6 +534,12 @@ const simpleAlgorithmDictionaries = { TurboShakeParams: {}, }; +// Pre-compute ObjectKeys() for each dictionary entry at module init +// to avoid allocating a new keys array on every normalizeAlgorithm call. +for (const { 0: name, 1: types } of ObjectEntries(simpleAlgorithmDictionaries)) { + simpleAlgorithmDictionaries[name] = { keys: ObjectKeys(types), types }; +} + function validateMaxBufferLength(data, name, max = kMaxBufferLength) { if (data.byteLength > max) { throw lazyDOMException( @@ -537,6 +550,14 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) { let webidl; +// Keep this as a regular object. The WebIDL converters read and spread these +// options on the normalizeAlgorithm hot path, and a null-prototype object +// measurably regresses benchmark/misc/webcrypto-webidl normalizeAlgorithm-*. +const kNormalizeAlgorithmOpts = { + prefix: 'Failed to normalize algorithm', + context: 'passed algorithm', +}; + // https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm // adapted for Node.js from Deno's implementation // https://github.com/denoland/deno/blob/v1.29.1/ext/crypto/00_crypto.js#L195 @@ -549,29 +570,20 @@ function normalizeAlgorithm(algorithm, op) { // 1. const registeredAlgorithms = kSupportedAlgorithms[op]; // 2. 3. - const initialAlg = webidl.converters.Algorithm(algorithm, { - prefix: 'Failed to normalize algorithm', - context: 'passed algorithm', - }); + const initialAlg = webidl.converters.Algorithm(algorithm, + kNormalizeAlgorithmOpts); // 4. let algName = initialAlg.name; - // 5. - let desiredType; - for (const key in registeredAlgorithms) { - if (!ObjectPrototypeHasOwnProperty(registeredAlgorithms, key)) { - continue; - } - if ( - StringPrototypeToUpperCase(key) === StringPrototypeToUpperCase(algName) - ) { - algName = key; - desiredType = registeredAlgorithms[key]; - } - } - if (desiredType === undefined) + // 5. Case-insensitive lookup via pre-built Map (O(1) instead of O(n)). + const canonicalName = kAlgorithmNameMap[op]?.get( + StringPrototypeToUpperCase(algName)); + if (canonicalName === undefined) throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + algName = canonicalName; + const desiredType = registeredAlgorithms[algName]; + // Fast path everything below if the registered dictionary is null if (desiredType === null) return { name: algName }; @@ -579,39 +591,35 @@ function normalizeAlgorithm(algorithm, op) { // 6. const normalizedAlgorithm = webidl.converters[desiredType]( { __proto__: algorithm, name: algName }, - { - prefix: 'Failed to normalize algorithm', - context: 'passed algorithm', - }, + kNormalizeAlgorithmOpts, ); // 7. normalizedAlgorithm.name = algName; - // 9. - const dict = simpleAlgorithmDictionaries[desiredType]; - // 10. - const dictKeys = dict ? ObjectKeys(dict) : []; - for (let i = 0; i < dictKeys.length; i++) { - const member = dictKeys[i]; - if (!ObjectPrototypeHasOwnProperty(dict, member)) - continue; - const idlType = dict[member]; - const idlValue = normalizedAlgorithm[member]; - // 3. - if (idlType === 'BufferSource' && idlValue) { - const isView = ArrayBufferIsView(idlValue); - normalizedAlgorithm[member] = TypedArrayPrototypeSlice( - new Uint8Array( - isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue, - isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0, - isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue), - ), - ); - } else if (idlType === 'HashAlgorithmIdentifier') { - normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest'); - } else if (idlType === 'AlgorithmIdentifier') { - // This extension point is not used by any supported algorithm (yet?) - throw lazyDOMException('Not implemented.', 'NotSupportedError'); + // 9. 10. Pre-computed keys and types from simpleAlgorithmDictionaries. + const dictMeta = simpleAlgorithmDictionaries[desiredType]; + if (dictMeta) { + const { keys: dictKeys, types: dictTypes } = dictMeta; + for (let i = 0; i < dictKeys.length; i++) { + const member = dictKeys[i]; + const idlType = dictTypes[member]; + const idlValue = normalizedAlgorithm[member]; + // 3. + if (idlType === 'BufferSource' && idlValue) { + const isView = ArrayBufferIsView(idlValue); + normalizedAlgorithm[member] = TypedArrayPrototypeSlice( + new Uint8Array( + isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue, + isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0, + isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue), + ), + ); + } else if (idlType === 'HashAlgorithmIdentifier') { + normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest'); + } 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 a87edcb411d08e..18bea7df03880d 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -106,6 +106,22 @@ converters['sequence'] = createSequenceConverter(converters.KeyUsage); converters.HashAlgorithmIdentifier = converters.AlgorithmIdentifier; +/** + * Builds conversion options for Web Crypto integer members that use Web IDL + * [EnforceRange]. Keep this helper instead of spreading opts in each member + * converter so the hot dictionary paths allocate stable-shape objects. + * @param {object} opts Parent conversion options. + * @returns {object} + */ +function enforceRangeOptions(opts) { + return { + prefix: opts.prefix, + context: opts.context, + code: opts.code, + enforceRange: true, + }; +} + const dictAlgorithm = [ { key: 'name', @@ -121,8 +137,9 @@ converters.Algorithm = createDictionaryConverter( // converters.BigInteger = webidl.Uint8Array; converters.BigInteger = (V, opts = kEmptyObject) => { return webidl.Uint8Array(V, { - __proto__: null, - ...opts, + prefix: opts.prefix, + context: opts.context, + code: opts.code, allowResizable: true, allowShared: false, }); @@ -132,10 +149,12 @@ converters.BigInteger = (V, opts = kEmptyObject) => { // removing this altogether. converters.BufferSource = (V, opts = kEmptyObject) => { return webidl.BufferSource(V, { - __proto__: null, - ...opts, + prefix: opts.prefix, + context: opts.context, + code: opts.code, allowResizable: opts.allowResizable === undefined ? true : opts.allowResizable, + allowShared: opts.allowShared, }); }; @@ -143,7 +162,7 @@ const dictRsaKeyGenParams = [ { key: 'modulusLength', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), required: true, }, { @@ -221,7 +240,7 @@ converters.AesKeyGenParams = createDictionaryConverter( { key: 'length', converter: (V, opts) => - converters['unsigned short'](V, { ...opts, enforceRange: true }), + converters['unsigned short'](V, enforceRangeOptions(opts)), validator: AESLengthValidator, required: true, }, @@ -244,7 +263,7 @@ converters.RsaPssParams = createDictionaryConverter( { key: 'saltLength', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), required: true, }, ], @@ -288,7 +307,7 @@ for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], { key: 'length', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), validator: validateMacKeyLength(`${name}.length`, zeroError), }, ], @@ -370,7 +389,7 @@ converters.CShakeParams = createDictionaryConverter( { key: 'outputLength', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V, opts) => { // The Web Crypto spec allows for SHAKE output length that are not multiples of // 8. We don't. @@ -404,7 +423,7 @@ converters.Pbkdf2Params = createDictionaryConverter( { key: 'iterations', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V, dict) => { if (V === 0) throw lazyDOMException('iterations cannot be zero', 'OperationError'); @@ -427,7 +446,7 @@ converters.AesDerivedKeyParams = createDictionaryConverter( { key: 'length', converter: (V, opts) => - converters['unsigned short'](V, { ...opts, enforceRange: true }), + converters['unsigned short'](V, enforceRangeOptions(opts)), validator: AESLengthValidator, required: true, }, @@ -481,7 +500,7 @@ converters.AeadParams = createDictionaryConverter( { key: 'tagLength', converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), + converters.octet(V, enforceRangeOptions(opts)), validator: (V, dict) => { switch (StringPrototypeToLowerCase(dict.name)) { case 'chacha20-poly1305': @@ -524,7 +543,7 @@ converters.AesCtrParams = createDictionaryConverter( { key: 'length', converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), + converters.octet(V, enforceRangeOptions(opts)), validator: (V, dict) => { if (V === 0 || V > 128) throw lazyDOMException( @@ -600,7 +619,7 @@ converters.Argon2Params = createDictionaryConverter( { key: 'parallelism', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V, dict) => { if (V === 0 || V > MathPow(2, 24) - 1) { throw lazyDOMException( @@ -613,7 +632,7 @@ converters.Argon2Params = createDictionaryConverter( { key: 'memory', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V, dict) => { if (V < 8 * dict.parallelism) { throw lazyDOMException( @@ -626,7 +645,7 @@ converters.Argon2Params = createDictionaryConverter( { key: 'passes', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V) => { if (V === 0) { throw lazyDOMException('passes must be > 0', 'OperationError'); @@ -637,7 +656,7 @@ converters.Argon2Params = createDictionaryConverter( { key: 'version', converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), + converters.octet(V, enforceRangeOptions(opts)), validator: (V, dict) => { if (V !== 0x13) { throw lazyDOMException( @@ -676,7 +695,7 @@ for (const { 0: name, 1: zeroError } of [['KmacKeyGenParams', 'OperationError'], { key: 'length', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), validator: validateMacKeyLength(`${name}.length`, zeroError), }, ], @@ -690,7 +709,7 @@ converters.KmacParams = createDictionaryConverter( { key: 'outputLength', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V, opts) => { // The Web Crypto spec allows for KMAC output length that are not multiples of 8. We don't. if (V % 8) @@ -712,7 +731,7 @@ converters.KangarooTwelveParams = createDictionaryConverter( { key: 'outputLength', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V, opts) => { if (V === 0 || V % 8) throw lazyDOMException('Invalid KangarooTwelveParams outputLength', 'OperationError'); @@ -733,7 +752,7 @@ converters.TurboShakeParams = createDictionaryConverter( { key: 'outputLength', converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V, opts) => { if (V === 0 || V % 8) throw lazyDOMException('Invalid TurboShakeParams outputLength', 'OperationError'); @@ -743,7 +762,7 @@ converters.TurboShakeParams = createDictionaryConverter( { key: 'domainSeparation', converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), + converters.octet(V, enforceRangeOptions(opts)), validator: (V) => { if (V < 0x01 || V > 0x7F) { throw lazyDOMException( diff --git a/lib/internal/webidl.js b/lib/internal/webidl.js index f727ea84a40535..13575d4f730df7 100644 --- a/lib/internal/webidl.js +++ b/lib/internal/webidl.js @@ -109,6 +109,28 @@ function makeException(message, options = kEmptyObject) { ); } +/** + * Builds derived conversion options for nested converter calls and adjusted + * error codes. These objects are allocated on dictionary/sequence conversion + * hot paths, so keep their shape stable and avoid object spread and + * null-prototype objects. + * @param {ConversionOptions} options Parent conversion options. + * @param {string} [context] Replacement context. + * @param {string} [code] Replacement error code. + * @returns {ConversionOptions} + */ +function makeOptions(options, context = options.context, code = options.code) { + return { + prefix: options.prefix, + context, + code, + enforceRange: options.enforceRange, + clamp: options.clamp, + allowShared: options.allowShared, + allowResizable: options.allowResizable, + }; +} + /** * Returns the ECMAScript specification type of a JavaScript value. * @see https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values @@ -376,7 +398,7 @@ function convertToInt( if (integer < lowerBound || integer > upperBound) { throw makeException( `is outside the expected range of ${lowerBound} to ${upperBound}.`, - { __proto__: null, ...options, code: 'ERR_OUT_OF_RANGE' }); + makeOptions(options, options.context, 'ERR_OUT_OF_RANGE')); } return integer; @@ -416,7 +438,7 @@ function convertToInt( if (x < lowerBound || x > upperBound) { throw makeException( `is outside the expected range of ${lowerBound} to ${upperBound}.`, - { __proto__: null, ...options, code: 'ERR_OUT_OF_RANGE' }); + makeOptions(options, options.context, 'ERR_OUT_OF_RANGE')); } return x; @@ -593,7 +615,7 @@ function requiredArguments(length, required, options = kEmptyObject) { `${required} argument${ required === 1 ? '' : 's' } required, but only ${length} present.`, - { __proto__: null, ...options, context: '', code: 'ERR_MISSING_ARGS' }); + makeOptions(options, '', 'ERR_MISSING_ARGS')); } } @@ -615,7 +637,7 @@ function createEnumConverter(name, values) { if (!E.has(S)) { throw makeException( `'${S}' is not a valid enum value of type ${name}.`, - { __proto__: null, ...options, code: 'ERR_INVALID_ARG_VALUE' }); + makeOptions(options, options.context, 'ERR_INVALID_ARG_VALUE')); } // Step 3: return the matching enumeration value. @@ -715,11 +737,7 @@ function createDictionaryConverter( // Step 4.1.4.1: convert the JavaScript value to IDL. const idlMemberValue = converter( jsMemberValue, - { - __proto__: null, - ...options, - context: dictionaryMemberContext(key, options), - }, + makeOptions(options, dictionaryMemberContext(key, options)), ); // Validators are a Node.js extension after conversion. They let // consumers reject known unsupported values while dictionary @@ -736,7 +754,7 @@ function createDictionaryConverter( // Step 4.1.6: required missing members throw. throw makeException( missingDictionaryMemberMessage(dictionaryName, key), - { __proto__: null, ...options, code: 'ERR_MISSING_OPTION' }); + makeOptions(options, options.context, 'ERR_MISSING_OPTION')); } } } @@ -794,11 +812,10 @@ function createSequenceConverter(converter) { break; } // Step 3.3: convert next to an IDL value of type T. - const idlValue = converter(next.value, { - __proto__: null, - ...options, - context: sequenceElementContext(idlSequence.length, options), - }); + const idlValue = converter( + next.value, + makeOptions(options, sequenceElementContext(idlSequence.length, options)), + ); // Step 3.4: store the value and advance i. ArrayPrototypePush(idlSequence, idlValue); }