Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 57 additions & 49 deletions lib/internal/crypto/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
ObjectKeys,
ObjectPrototypeHasOwnProperty,
PromiseWithResolvers,
SafeMap,
SafeSet,
StringPrototypeToUpperCase,
Symbol,
Expand Down Expand Up @@ -453,8 +454,11 @@ const experimentalAlgorithms = [
];

// Transform the algorithm definitions into the operation-keyed structure
// Also builds a parallel Map<UPPERCASED_NAME, canonicalName> 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
Expand All @@ -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)) {
Expand All @@ -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' },
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -549,69 +570,56 @@ 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 };

// 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');
}
}
}

Expand Down
63 changes: 41 additions & 22 deletions lib/internal/crypto/webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ converters['sequence<KeyUsage>'] = 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',
Expand All @@ -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,
});
Expand All @@ -132,18 +149,20 @@ 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,
});
};

const dictRsaKeyGenParams = [
{
key: 'modulusLength',
converter: (V, opts) =>
converters['unsigned long'](V, { ...opts, enforceRange: true }),
converters['unsigned long'](V, enforceRangeOptions(opts)),
required: true,
},
{
Expand Down Expand Up @@ -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,
},
Expand All @@ -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,
},
],
Expand Down Expand Up @@ -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),
},
],
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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');
Expand All @@ -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,
},
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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');
Expand All @@ -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(
Expand Down Expand Up @@ -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),
},
],
Expand All @@ -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)
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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(
Expand Down
Loading
Loading