diff --git a/benchmark/crypto/webcrypto-webidl.js b/benchmark/crypto/webcrypto-webidl.js new file mode 100644 index 00000000000000..d5848440974bce --- /dev/null +++ b/benchmark/crypto/webcrypto-webidl.js @@ -0,0 +1,113 @@ +'use strict'; + +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + op: [ + 'normalizeAlgorithm-string', + 'normalizeAlgorithm-dict', + 'webidl-dict', + 'webidl-algorithm-identifier-string', + 'webidl-algorithm-identifier-object', + 'webidl-dict-enforce-range', + 'webidl-dict-ensure-sha', + 'webidl-dict-null', + ], + n: [1e6], +}, { flags: ['--expose-internals'] }); + +function main({ n, op }) { + const { normalizeAlgorithm } = require('internal/crypto/util'); + + switch (op) { + case 'normalizeAlgorithm-string': { + // String shortcut + null dictionary (cheapest path). + bench.start(); + for (let i = 0; i < n; i++) + normalizeAlgorithm('SHA-256', 'digest'); + bench.end(n); + break; + } + case 'normalizeAlgorithm-dict': { + // Object input with a dictionary type (no BufferSource members). + const alg = { name: 'ECDSA', hash: 'SHA-256' }; + bench.start(); + for (let i = 0; i < n; i++) + normalizeAlgorithm(alg, 'sign'); + bench.end(n); + break; + } + case 'webidl-dict': { + // WebIDL dictionary converter in isolation. + const webidl = require('internal/crypto/webidl'); + const input = { name: 'AES-GCM', iv: new Uint8Array(12) }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.AeadParams(input, opts); + bench.end(n); + break; + } + case 'webidl-algorithm-identifier-string': { + // Exercises converters.AlgorithmIdentifier string path (isObjectType + // fast-reject + DOMString conversion). + const webidl = require('internal/crypto/webidl'); + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.AlgorithmIdentifier('SHA-256', opts); + bench.end(n); + break; + } + case 'webidl-algorithm-identifier-object': { + // Exercises converters.AlgorithmIdentifier object path (isObjectType + // fast-accept, no dictionary walk). + const webidl = require('internal/crypto/webidl'); + const input = { name: 'SHA-256' }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.AlgorithmIdentifier(input, opts); + bench.end(n); + break; + } + case 'webidl-dict-enforce-range': { + // Exercises shared enforceRange integer wrappers (RsaKeyGenParams has + // both modulusLength: unsigned long and publicExponent: BigInteger). + const webidl = require('internal/crypto/webidl'); + const input = { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.RsaKeyGenParams(input, opts); + bench.end(n); + break; + } + case 'webidl-dict-ensure-sha': { + // Exercises ensureSHA on a hash member (no lowercase allocation). + const webidl = require('internal/crypto/webidl'); + const input = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.RsaHashedImportParams(input, opts); + bench.end(n); + break; + } + case 'webidl-dict-null': { + // Exercises the null/undefined fast path in createDictionaryConverter + // (JsonWebKey has no required members). + const webidl = require('internal/crypto/webidl'); + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.JsonWebKey(undefined, opts); + bench.end(n); + break; + } + } +} diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 70e1027946190a..35df723a91534d 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -16,6 +16,7 @@ const { ObjectKeys, ObjectPrototypeHasOwnProperty, PromiseWithResolvers, + SafeMap, StringPrototypeToUpperCase, Symbol, TypedArrayPrototypeGetBuffer, @@ -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,12 +488,14 @@ function createSupportedAlgorithms(algorithmDefs) { } } - return result; + return { algorithms: result, nameMap }; } -const kSupportedAlgorithms = createSupportedAlgorithms(kAlgorithmDefinitions); +const { algorithms: kSupportedAlgorithms, nameMap: kAlgorithmNameMap } = + createSupportedAlgorithms(kAlgorithmDefinitions); const simpleAlgorithmDictionaries = { + __proto__: null, AesCbcParams: { iv: 'BufferSource' }, AesCtrParams: { counter: 'BufferSource' }, AeadParams: { iv: 'BufferSource', additionalData: 'BufferSource' }, @@ -527,6 +535,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) { if (data.byteLength > kMaxBufferLength) { throw lazyDOMException( @@ -537,6 +551,11 @@ function validateMaxBufferLength(data, name) { let webidl; +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 +568,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 +589,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 4f371955a73bfd..ffc9890342e193 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -13,7 +13,6 @@ const { ArrayBufferIsView, ArrayPrototypeIncludes, - ArrayPrototypePush, ArrayPrototypeSort, MathPow, MathTrunc, @@ -67,32 +66,9 @@ function toNumber(value, opts = kEmptyObject) { } } -function type(V) { - if (V === null) - return 'Null'; - - switch (typeof V) { - case 'undefined': - return 'Undefined'; - case 'boolean': - return 'Boolean'; - case 'number': - return 'Number'; - case 'string': - return 'String'; - case 'symbol': - return 'Symbol'; - case 'bigint': - return 'BigInt'; - case 'object': // Fall through - case 'function': // Fall through - default: - // Per ES spec, typeof returns an implementation-defined value that is not - // any of the existing ones for uncallable non-standard exotic objects. - // Yet Type() which the Web IDL spec depends on returns Object for such - // cases. So treat the default case as an object. - return 'Object'; - } +// Fast check for the WebIDL Object type: a non-null object or a function. +function isObjectType(V) { + return V !== null && (typeof V === 'object' || typeof V === 'function'); } const integerPart = MathTrunc; @@ -171,6 +147,23 @@ converters.octet = createIntegerConversion(8); converters['unsigned short'] = createIntegerConversion(16); converters['unsigned long'] = createIntegerConversion(32); +// Shared helpers for `[EnforceRange] unsigned short/long/octet` dictionary +// members. Dictionary converters always call members with a freshly allocated +// `{ __proto__: null, prefix, context }` options object, so it is safe to +// mutate it to set `enforceRange` instead of spreading it into a new object. +const enforceRangeOctet = (V, opts) => { + opts.enforceRange = true; + return converters.octet(V, opts); +}; +const enforceRangeUnsignedShort = (V, opts) => { + opts.enforceRange = true; + return converters['unsigned short'](V, opts); +}; +const enforceRangeUnsignedLong = (V, opts) => { + opts.enforceRange = true; + return converters['unsigned long'](V, opts); +}; + converters.DOMString = function(V, opts = kEmptyObject) { if (typeof V === 'string') { return V; @@ -184,7 +177,7 @@ converters.DOMString = function(V, opts = kEmptyObject) { }; converters.object = (V, opts) => { - if (type(V) !== 'Object') { + if (!isObjectType(V)) { throw makeException( 'is not an object.', opts); @@ -264,7 +257,7 @@ function createDictionaryConverter(name, dictionaries) { if (member.required) { hasRequiredKey = true; } - ArrayPrototypePush(allMembers, member); + allMembers[i] = member; } ArrayPrototypeSort(allMembers, (a, b) => { if (a.key === b.key) { @@ -272,52 +265,51 @@ function createDictionaryConverter(name, dictionaries) { } return a.key < b.key ? -1 : 1; }); + const membersLength = allMembers.length; return function(V, opts = kEmptyObject) { - const typeV = type(V); - switch (typeV) { - case 'Undefined': - case 'Null': - case 'Object': - break; - default: - throw makeException( - 'can not be converted to a dictionary', - opts); + let isNullish; + if (V === undefined || V === null) { + isNullish = true; + } else if (typeof V === 'object' || typeof V === 'function') { + isNullish = false; + } else { + throw makeException( + 'can not be converted to a dictionary', + opts); } - const esDict = V; + const idlDict = {}; // Fast path null and undefined. - if (V == null && !hasRequiredKey) { + if (isNullish && !hasRequiredKey) { return idlDict; } - for (const member of new SafeArrayIterator(allMembers)) { + const prefix = opts.prefix; + const outerContext = opts.context; + + for (let i = 0; i < membersLength; i++) { + const member = allMembers[i]; const key = member.key; - let esMemberValue; - if (typeV === 'Undefined' || typeV === 'Null') { - esMemberValue = undefined; - } else { - esMemberValue = esDict[key]; - } + const esMemberValue = isNullish ? undefined : V[key]; if (esMemberValue !== undefined) { - const context = `'${key}' of '${name}'${ - opts.context ? ` (${opts.context})` : '' - }`; + const context = outerContext ? + `'${key}' of '${name}' (${outerContext})` : + `'${key}' of '${name}'`; const idlMemberValue = member.converter(esMemberValue, { __proto__: null, - ...opts, + prefix, context, }); - member.validator?.(idlMemberValue, esDict); + member.validator?.(idlMemberValue, V); setOwnProperty(idlDict, key, idlMemberValue); } else if (member.required) { throw makeException( `can not be converted to '${name}' because '${key}' is required in '${name}'.`, - { __proto__: null, ...opts, code: 'ERR_MISSING_OPTION' }); + { __proto__: null, prefix, context: outerContext, code: 'ERR_MISSING_OPTION' }); } } @@ -338,8 +330,8 @@ function createInterfaceConverter(name, prototype) { converters.AlgorithmIdentifier = (V, opts) => { // Union for (object or DOMString) - if (type(V) === 'Object') { - return converters.object(V, opts); + if (isObjectType(V)) { + return V; } return converters.DOMString(V, opts); }; @@ -391,8 +383,7 @@ const dictRsaKeyGenParams = [ ...new SafeArrayIterator(dictAlgorithm), { key: 'modulusLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, required: true, }, { @@ -456,8 +447,7 @@ converters.AesKeyGenParams = createDictionaryConverter( ...new SafeArrayIterator(dictAlgorithm), { key: 'length', - converter: (V, opts) => - converters['unsigned short'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedShort, validator: AESLengthValidator, required: true, }, @@ -477,8 +467,7 @@ converters.RsaPssParams = createDictionaryConverter( ...new SafeArrayIterator(dictAlgorithm), { key: 'saltLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, required: true, }, ]); @@ -515,8 +504,7 @@ for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], }, { key: 'length', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, validator: validateMacKeyLength(`${name}.length`, zeroError), }, ]); @@ -592,8 +580,7 @@ converters.CShakeParams = createDictionaryConverter( ...new SafeArrayIterator(dictAlgorithm), { key: 'outputLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, validator: (V, opts) => { // The Web Crypto spec allows for SHAKE output length that are not multiples of // 8. We don't. @@ -625,8 +612,7 @@ converters.Pbkdf2Params = createDictionaryConverter( }, { key: 'iterations', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, validator: (V, dict) => { if (V === 0) throw lazyDOMException('iterations cannot be zero', 'OperationError'); @@ -645,8 +631,7 @@ converters.AesDerivedKeyParams = createDictionaryConverter( ...new SafeArrayIterator(dictAlgorithm), { key: 'length', - converter: (V, opts) => - converters['unsigned short'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedShort, validator: AESLengthValidator, required: true, }, @@ -690,8 +675,7 @@ converters.AeadParams = createDictionaryConverter( }, { key: 'tagLength', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), + converter: enforceRangeOctet, validator: (V, dict) => { switch (StringPrototypeToLowerCase(dict.name)) { case 'chacha20-poly1305': @@ -736,8 +720,7 @@ converters.AesCtrParams = createDictionaryConverter( }, { key: 'length', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), + converter: enforceRangeOctet, validator: (V, dict) => { if (V === 0 || V > 128) throw lazyDOMException( @@ -801,8 +784,7 @@ converters.Argon2Params = createDictionaryConverter( }, { key: 'parallelism', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, validator: (V, dict) => { if (V === 0 || V > MathPow(2, 24) - 1) { throw lazyDOMException( @@ -814,8 +796,7 @@ converters.Argon2Params = createDictionaryConverter( }, { key: 'memory', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, validator: (V, dict) => { if (V < 8 * dict.parallelism) { throw lazyDOMException( @@ -827,14 +808,12 @@ converters.Argon2Params = createDictionaryConverter( }, { key: 'passes', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, required: true, }, { key: 'version', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), + converter: enforceRangeOctet, validator: (V, dict) => { if (V !== 0x13) { throw lazyDOMException( @@ -870,8 +849,7 @@ for (const { 0: name, 1: zeroError } of [['KmacKeyGenParams', 'OperationError'], ...new SafeArrayIterator(dictAlgorithm), { key: 'length', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, validator: validateMacKeyLength(`${name}.length`, zeroError), }, ]); @@ -882,8 +860,7 @@ converters.KmacParams = createDictionaryConverter( ...new SafeArrayIterator(dictAlgorithm), { key: 'outputLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, validator: (V, opts) => { // The Web Crypto spec allows for KMAC output length that are not multiples of 8. We don't. if (V % 8) @@ -902,8 +879,7 @@ converters.KangarooTwelveParams = createDictionaryConverter( ...new SafeArrayIterator(dictAlgorithm), { key: 'outputLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, validator: (V, opts) => { if (V === 0 || V % 8) throw lazyDOMException('Invalid KangarooTwelveParams outputLength', 'OperationError'); @@ -921,8 +897,7 @@ converters.TurboShakeParams = createDictionaryConverter( ...new SafeArrayIterator(dictAlgorithm), { key: 'outputLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), + converter: enforceRangeUnsignedLong, validator: (V, opts) => { if (V === 0 || V % 8) throw lazyDOMException('Invalid TurboShakeParams outputLength', 'OperationError'); @@ -931,8 +906,7 @@ converters.TurboShakeParams = createDictionaryConverter( }, { key: 'domainSeparation', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), + converter: enforceRangeOctet, validator: (V) => { if (V < 0x01 || V > 0x7F) { throw lazyDOMException(