/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/internal/crypto/util.js
1 125 строк
30 KB
Filip Skokan
crypto: read WebCrypto inputs through primordials
09 авг 2026, 22:10
09 авг 2026, 22:10
e04663a
Код
Авторство
О чём код?
'use strict'; const { ArrayBufferIsView, ArrayBufferPrototypeGetByteLength, ArrayPrototypeIncludes, ArrayPrototypePush, BigInt, DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, DataViewPrototypeGetByteOffset, MathFloor, Number, ObjectDefineProperty, ObjectEntries, ObjectKeys, ObjectPrototypeHasOwnProperty, ObjectSetPrototypeOf, PromisePrototypeThen, PromiseReject, PromiseWithResolvers, SafeMap, SafeSet, StringPrototypeToUpperCase, Symbol, TypedArrayPrototypeGetBuffer, TypedArrayPrototypeGetByteLength, TypedArrayPrototypeGetByteOffset, TypedArrayPrototypeGetLength, TypedArrayPrototypeSlice, Uint8Array, } = primordials; const { getCiphers: _getCiphers, getCurves: _getCurves, getHashes: _getHashes, setEngine: _setEngine, secureHeapUsed: _secureHeapUsed, getCachedAliases, getOpenSSLSecLevelCrypto: getOpenSSLSecLevel, EVP_PKEY_ML_DSA_44, EVP_PKEY_ML_DSA_65, EVP_PKEY_ML_DSA_87, EVP_PKEY_ML_KEM_512, EVP_PKEY_ML_KEM_768, EVP_PKEY_ML_KEM_1024, kKeyVariantAES_OCB_128: hasAesOcbMode, Argon2Job, KmacJob, } = internalBinding('crypto'); const { getOptionValue } = require('internal/options'); const { crypto: { ENGINE_METHOD_ALL, }, } = internalBinding('constants'); const normalizeHashName = require('internal/crypto/hashnames'); const { codes: { ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED, ERR_CRYPTO_ENGINE_UNKNOWN, ERR_INVALID_ARG_TYPE, }, hideStackFrames, } = require('internal/errors'); const { validateArray, validateNumber, validateString, } = require('internal/validators'); const { Buffer } = require('buffer'); const { cachedResult, emitExperimentalWarning, filterDuplicateStrings, getDeprecationWarningEmitter, lazyDOMException, setOwnProperty, } = require('internal/util'); const { namespace: { isBuildingSnapshot, addSerializeCallback, }, } = require('internal/v8/startup_snapshot'); const { isDataView, isArrayBufferView, isAnyArrayBuffer, isPromise, } = require('internal/util/types'); const kHandle = Symbol('kHandle'); // This is here because many functions accepted binary strings without // any explicit encoding in older versions of node, and we don't want // to break them unnecessarily. function toBuf(val, encoding) { if (typeof val === 'string') { if (encoding === 'buffer') encoding = 'utf8'; return Buffer.from(val, encoding); } return val; } let _hashCache; function getHashCache() { if (_hashCache === undefined) { _hashCache = getCachedAliases(); if (isBuildingSnapshot()) { // For dynamic linking, clear the map. addSerializeCallback(() => { _hashCache = undefined; }); } } return _hashCache; } function getCachedHashId(algorithm) { const result = getHashCache()[algorithm]; return result === undefined ? -1 : result; } const getCiphers = cachedResult(() => filterDuplicateStrings(_getCiphers())); const getHashes = cachedResult(() => filterDuplicateStrings(_getHashes())); const getCurves = cachedResult(() => filterDuplicateStrings(_getCurves())); const emitOpenSSLEngineDeprecation = getDeprecationWarningEmitter( 'DEP0183', 'OpenSSL engine-based APIs are deprecated.', ); function setEngine(id, flags) { validateString(id, 'id'); if (flags) validateNumber(flags, 'flags'); flags = flags >>> 0; // Use provided engine for everything by default if (flags === 0) flags = ENGINE_METHOD_ALL; emitOpenSSLEngineDeprecation(); if (typeof _setEngine !== 'function') throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED(); if (!_setEngine(id, flags)) throw new ERR_CRYPTO_ENGINE_UNKNOWN(id); } const getArrayBufferOrView = hideStackFrames((buffer, name, encoding) => { if (isAnyArrayBuffer(buffer)) return buffer; if (typeof buffer === 'string') { if (encoding === 'buffer') encoding = 'utf8'; return Buffer.from(buffer, encoding); } if (!isArrayBufferView(buffer)) { throw new ERR_INVALID_ARG_TYPE.HideStackFramesError( name, [ 'string', 'ArrayBuffer', 'Buffer', 'TypedArray', 'DataView', ], buffer, ); } return buffer; }); // The maximum buffer size that we'll support in the WebCrypto impl const kMaxBufferLength = (2 ** 31) - 1; // The EC named curves that we currently support via the Web Crypto API. const kNamedCurveAliases = { 'P-256': 'prime256v1', 'P-384': 'secp384r1', 'P-521': 'secp521r1', }; // Algorithm definitions organized by algorithm name const kAlgorithmDefinitions = { 'AES-CBC': { 'generateKey': 'AesKeyGenParams', 'exportKey': null, 'importKey': null, 'encrypt': 'AesCbcParams', 'decrypt': 'AesCbcParams', 'get key length': 'AesDerivedKeyParams', }, 'AES-CTR': { 'generateKey': 'AesKeyGenParams', 'exportKey': null, 'importKey': null, 'encrypt': 'AesCtrParams', 'decrypt': 'AesCtrParams', 'get key length': 'AesDerivedKeyParams', }, 'AES-GCM': { 'generateKey': 'AesKeyGenParams', 'exportKey': null, 'importKey': null, 'encrypt': 'AeadParams', 'decrypt': 'AeadParams', 'get key length': 'AesDerivedKeyParams', }, 'AES-KW': { 'generateKey': 'AesKeyGenParams', 'exportKey': null, 'importKey': null, 'get key length': 'AesDerivedKeyParams', 'wrapKey': null, 'unwrapKey': null, }, 'AES-OCB': { 'generateKey': 'AesKeyGenParams', 'exportKey': null, 'importKey': null, 'encrypt': 'AeadParams', 'decrypt': 'AeadParams', 'get key length': 'AesDerivedKeyParams', }, 'Argon2d': { 'deriveBits': 'Argon2Params', 'get key length': null, 'importKey': null, }, 'Argon2i': { 'deriveBits': 'Argon2Params', 'get key length': null, 'importKey': null, }, 'Argon2id': { 'deriveBits': 'Argon2Params', 'get key length': null, 'importKey': null, }, 'ChaCha20-Poly1305': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'encrypt': 'AeadParams', 'decrypt': 'AeadParams', 'get key length': null, }, 'cSHAKE128': { 'digest': 'CShakeParams' }, 'cSHAKE256': { 'digest': 'CShakeParams' }, 'KT128': { 'digest': 'KangarooTwelveParams' }, 'KT256': { 'digest': 'KangarooTwelveParams' }, 'TurboSHAKE128': { 'digest': 'TurboShakeParams' }, 'TurboSHAKE256': { 'digest': 'TurboShakeParams' }, 'ECDH': { 'generateKey': 'EcKeyGenParams', 'exportKey': null, 'importKey': 'EcKeyImportParams', 'deriveBits': 'EcdhKeyDeriveParams', }, 'ECDSA': { 'generateKey': 'EcKeyGenParams', 'exportKey': null, 'importKey': 'EcKeyImportParams', 'sign': 'EcdsaParams', 'verify': 'EcdsaParams', }, 'Ed25519': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'sign': null, 'verify': null, }, 'Ed448': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'sign': 'ContextParams', 'verify': 'ContextParams', }, 'HKDF': { 'importKey': null, 'deriveBits': 'HkdfParams', 'get key length': null, }, 'HMAC': { 'generateKey': 'HmacKeyGenParams', 'exportKey': null, 'importKey': 'HmacImportParams', 'sign': null, 'verify': null, 'get key length': 'HmacImportParams', }, 'KMAC128': { 'generateKey': 'KmacKeyGenParams', 'exportKey': null, 'importKey': 'KmacImportParams', 'sign': 'KmacParams', 'verify': 'KmacParams', 'get key length': 'KmacImportParams', }, 'KMAC256': { 'generateKey': 'KmacKeyGenParams', 'exportKey': null, 'importKey': 'KmacImportParams', 'sign': 'KmacParams', 'verify': 'KmacParams', 'get key length': 'KmacImportParams', }, 'ML-DSA-44': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'sign': 'ContextParams', 'verify': 'ContextParams', }, 'ML-DSA-65': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'sign': 'ContextParams', 'verify': 'ContextParams', }, 'ML-DSA-87': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'sign': 'ContextParams', 'verify': 'ContextParams', }, 'ML-KEM-512': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'encapsulate': null, 'decapsulate': null, }, 'ML-KEM-768': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'encapsulate': null, 'decapsulate': null, }, 'ML-KEM-1024': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'encapsulate': null, 'decapsulate': null, }, 'PBKDF2': { 'importKey': null, 'deriveBits': 'Pbkdf2Params', 'get key length': null, }, 'RSA-OAEP': { 'generateKey': 'RsaHashedKeyGenParams', 'exportKey': null, 'importKey': 'RsaHashedImportParams', 'encrypt': 'RsaOaepParams', 'decrypt': 'RsaOaepParams', }, 'RSA-PSS': { 'generateKey': 'RsaHashedKeyGenParams', 'exportKey': null, 'importKey': 'RsaHashedImportParams', 'sign': 'RsaPssParams', 'verify': 'RsaPssParams', }, 'RSASSA-PKCS1-v1_5': { 'generateKey': 'RsaHashedKeyGenParams', 'exportKey': null, 'importKey': 'RsaHashedImportParams', 'sign': null, 'verify': null, }, 'SHA-1': { 'digest': null }, 'SHA-256': { 'digest': null }, 'SHA-384': { 'digest': null }, 'SHA-512': { 'digest': null }, 'SHA3-256': { 'digest': null }, 'SHA3-384': { 'digest': null }, 'SHA3-512': { 'digest': null }, 'X25519': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'deriveBits': 'EcdhKeyDeriveParams', }, 'X448': { 'generateKey': null, 'exportKey': null, 'importKey': null, 'deriveBits': 'EcdhKeyDeriveParams', }, }; // Conditionally supported algorithms const conditionalAlgorithms = { 'AES-OCB': !!hasAesOcbMode, 'Argon2d': !!Argon2Job, 'Argon2i': !!Argon2Job, 'Argon2id': !!Argon2Job, 'ChaCha20-Poly1305': process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getCiphers(), 'chacha20-poly1305'), 'cSHAKE128': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getHashes(), 'shake128'), 'cSHAKE256': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getHashes(), 'shake256'), 'Ed448': !process.features.openssl_is_boringssl, 'KMAC128': !!KmacJob, 'KMAC256': !!KmacJob, 'ML-DSA-44': !!EVP_PKEY_ML_DSA_44, 'ML-DSA-65': !!EVP_PKEY_ML_DSA_65, 'ML-DSA-87': !!EVP_PKEY_ML_DSA_87, 'ML-KEM-512': !!EVP_PKEY_ML_KEM_512, 'ML-KEM-768': !!EVP_PKEY_ML_KEM_768, 'ML-KEM-1024': !!EVP_PKEY_ML_KEM_1024, 'SHA3-256': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getHashes(), 'sha3-256'), 'SHA3-384': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getHashes(), 'sha3-384'), 'SHA3-512': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getHashes(), 'sha3-512'), 'X448': !process.features.openssl_is_boringssl, }; // Experimental algorithms const experimentalAlgorithms = [ 'AES-OCB', 'Argon2d', 'Argon2i', 'Argon2id', 'ChaCha20-Poly1305', 'cSHAKE128', 'cSHAKE256', 'Ed448', 'KMAC128', 'KMAC256', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', 'SHA3-256', 'SHA3-384', 'SHA3-512', 'TurboSHAKE128', 'TurboSHAKE256', 'KT128', 'KT256', 'X448', ]; // 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) { // Detached below rather than declared `__proto__: null`: V8 puts that // literal form in dictionary mode, slowing every registry lookup. const result = {}; const nameMap = {}; const algorithmEntries = ObjectEntries(algorithmDefs); for (let i = 0; i < algorithmEntries.length; i++) { const { 0: algorithmName, 1: operations } = algorithmEntries[i]; // Skip algorithms that are conditionally not supported if (ObjectPrototypeHasOwnProperty(conditionalAlgorithms, algorithmName) && !conditionalAlgorithms[algorithmName]) { continue; } const operationEntries = ObjectEntries(operations); for (let j = 0; j < operationEntries.length; j++) { const { 0: operation, 1: dict } = operationEntries[j]; result[operation] ||= {}; nameMap[operation] ||= new SafeMap(); nameMap[operation].set(StringPrototypeToUpperCase(algorithmName), algorithmName); // Add experimental warnings for experimental algorithms if (ArrayPrototypeIncludes(experimentalAlgorithms, algorithmName)) { ObjectDefineProperty(result[operation], algorithmName, { get() { emitExperimentalWarning(`The ${algorithmName} Web Crypto API algorithm`); return dict; }, __proto__: null, enumerable: true, }); } else { result[operation][algorithmName] = dict; } } } const operations = ObjectKeys(result); for (let i = 0; i < operations.length; i++) { ObjectSetPrototypeOf(result[operations[i]], null); } ObjectSetPrototypeOf(result, null); ObjectSetPrototypeOf(nameMap, null); return { algorithms: result, nameMap }; } const { algorithms: kSupportedAlgorithms, nameMap: kAlgorithmNameMap } = createSupportedAlgorithms(kAlgorithmDefinitions); const simpleAlgorithmDictionaries = { AesCbcParams: { iv: 'BufferSource' }, AesCtrParams: { counter: 'BufferSource' }, AeadParams: { iv: 'BufferSource', additionalData: 'BufferSource' }, // publicExponent is not strictly a BufferSource but it is a Uint8Array that we normalize // this way RsaHashedKeyGenParams: { hash: 'HashAlgorithmIdentifier', publicExponent: 'BufferSource' }, EcKeyGenParams: {}, HmacKeyGenParams: { hash: 'HashAlgorithmIdentifier' }, RsaPssParams: {}, EcdsaParams: { hash: 'HashAlgorithmIdentifier' }, HmacImportParams: { hash: 'HashAlgorithmIdentifier' }, HkdfParams: { hash: 'HashAlgorithmIdentifier', salt: 'BufferSource', info: 'BufferSource', }, ContextParams: { context: 'BufferSource' }, Pbkdf2Params: { hash: 'HashAlgorithmIdentifier', salt: 'BufferSource' }, RsaOaepParams: { label: 'BufferSource' }, RsaHashedImportParams: { hash: 'HashAlgorithmIdentifier' }, EcKeyImportParams: {}, CShakeParams: { functionName: 'BufferSource', customization: 'BufferSource', }, Argon2Params: { associatedData: 'BufferSource', nonce: 'BufferSource', secretValue: 'BufferSource', }, KmacParams: { customization: 'BufferSource', }, KangarooTwelveParams: { customization: 'BufferSource', }, TurboShakeParams: {}, }; // Pre-compute ObjectKeys() for each dictionary entry at module init // to avoid allocating a new keys array on every normalizeAlgorithm call. const simpleAlgorithmDictionaryEntries = ObjectEntries(simpleAlgorithmDictionaries); for (let i = 0; i < simpleAlgorithmDictionaryEntries.length; i++) { const { 0: name, 1: types } = simpleAlgorithmDictionaryEntries[i]; simpleAlgorithmDictionaries[name] = { keys: ObjectKeys(types), types }; } // See createSupportedAlgorithms() for why this is detached here. ObjectSetPrototypeOf(simpleAlgorithmDictionaries, null); function validateMaxBufferLength(data, name, max = kMaxBufferLength) { if (getBufferSourceByteLength(data) > max) { throw lazyDOMException( `${name} must be at most ${max} bytes`, 'OperationError'); } } /** * Converts a bit length to the number of bytes needed to contain it. * Non-byte lengths are rounded up to the next byte. * @param {number} length * @returns {number} */ function numBitsToBytes(length) { return MathFloor(length / 8) + MathFloor((7 + (length % 8)) / 8); } /** * Copies `bytes` up to the byte length needed for `length` bits, then clears * unused least-significant bits in the final byte. * @param {number} length * @param {ArrayBuffer|ArrayBufferView} bytes * @returns {Uint8Array} */ function truncateToBitLength(length, bytes) { const lengthBytes = numBitsToBytes(length); const isView = ArrayBufferIsView(bytes); const byteView = isView ? new Uint8Array( getDataViewOrTypedArrayBuffer(bytes), getDataViewOrTypedArrayByteOffset(bytes), getDataViewOrTypedArrayByteLength(bytes), ) : new Uint8Array(bytes, 0, ArrayBufferPrototypeGetByteLength(bytes)); const result = TypedArrayPrototypeSlice( byteView, 0, lengthBytes, ); const remainder = length % 8; if (remainder !== 0) result[lengthBytes - 1] &= (0xff << (8 - remainder)) & 0xff; return result; } 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 function normalizeAlgorithm(algorithm, op) { if (typeof algorithm === 'string') return normalizeAlgorithm({ name: algorithm }, op); webidl ??= require('internal/crypto/webidl'); // 1. const registeredAlgorithms = kSupportedAlgorithms[op]; // 2. 3. const initialAlg = webidl.converters.Algorithm(algorithm, kNormalizeAlgorithmOpts); // 4. let algName = initialAlg.name; // 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 }, kNormalizeAlgorithmOpts, ); // 7. normalizedAlgorithm.name = algName; // 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) { normalizedAlgorithm[member] = TypedArrayPrototypeSlice( getBufferSourceBytes(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'); } } } return normalizedAlgorithm; } function getDataViewOrTypedArrayBuffer(V) { return isDataView(V) ? DataViewPrototypeGetBuffer(V) : TypedArrayPrototypeGetBuffer(V); } function getDataViewOrTypedArrayByteOffset(V) { return isDataView(V) ? DataViewPrototypeGetByteOffset(V) : TypedArrayPrototypeGetByteOffset(V); } function getDataViewOrTypedArrayByteLength(V) { return isDataView(V) ? DataViewPrototypeGetByteLength(V) : TypedArrayPrototypeGetByteLength(V); } function getBufferSourceByteLength(V) { return ArrayBufferIsView(V) ? getDataViewOrTypedArrayByteLength(V) : ArrayBufferPrototypeGetByteLength(V); } function getBufferSourceBytes(V) { return ArrayBufferIsView(V) ? new Uint8Array( getDataViewOrTypedArrayBuffer(V), getDataViewOrTypedArrayByteOffset(V), getDataViewOrTypedArrayByteLength(V), ) : new Uint8Array(V, 0, ArrayBufferPrototypeGetByteLength(V)); } function getOptionalByteLength(V) { return V === undefined ? 0 : TypedArrayPrototypeGetByteLength(V); } function hasAnyNotIn(set, checks) { for (const s of set) if (!ArrayPrototypeIncludes(checks, s)) return true; return false; } const validateByteSource = hideStackFrames((val, name) => { val = toBuf(val); if (isAnyArrayBuffer(val) || isArrayBufferView(val)) return val; throw new ERR_INVALID_ARG_TYPE.HideStackFramesError( name, [ 'string', 'ArrayBuffer', 'TypedArray', 'DataView', 'Buffer', ], val); }); /** * @template T * @typedef {{ run(): Promise<T> }} WebCryptoJob */ /** * CryptoJob constructors can synchronously throw while running their native * AdditionalConfig hook. WebCrypto needs those operation-specific setup * failures to reject with an OperationError. * @template T * @param {() => WebCryptoJob<T>} getJob * @returns {Promise<T>} */ function jobPromise(getJob) { try { return getJob().run(); } catch (err) { return PromiseReject(lazyDOMException( 'The operation failed for an operation-specific reason', { name: 'OperationError', cause: err })); } } /** * Temporarily shadow inherited then accessors on WebCrypto result objects. * Promise resolution reads "then" synchronously for thenable assimilation. * Returning an own undefined data property keeps that lookup from reaching * user-mutated prototypes. * @param {unknown} value * @returns {boolean} */ function prepareWebCryptoResult(value) { if ((value === null || typeof value !== 'object') && typeof value !== 'function') { return false; } if (isPromise(value) || ObjectPrototypeHasOwnProperty(value, 'then')) return false; setOwnProperty(value, 'then', undefined); return true; } /** * Remove the temporary then property installed by prepareWebCryptoResult(). * @param {{ then?: unknown }} value * @returns {void} */ function cleanupWebCryptoResult(value) { delete value.then; } /** * @template T * @typedef {(value: T | PromiseLike<T>) => void} WebCryptoResolve */ /** * Resolve a WebCrypto promise while inherited then accessors are shadowed. * @template T * @param {WebCryptoResolve<T>} resolve * @param {T | PromiseLike<T>} value * @returns {void} */ function resolveWebCryptoResult(resolve, value) { const shouldCleanupResult = prepareWebCryptoResult(value); try { resolve(value); } finally { if (shouldCleanupResult) cleanupWebCryptoResult(value); } } /** * Run a WebCrypto promise reaction and settle the outer promise. * @template T * @template TResult * @param {((value: T) => TResult | PromiseLike<TResult>) | undefined} handler * @param {WebCryptoResolve<T | TResult>} resolve * @param {(reason?: unknown) => void} reject * @param {T} value * @param {boolean} isRejected * @returns {void} */ function settleJobPromise(handler, resolve, reject, value, isRejected) { try { if (typeof handler === 'function') { resolveWebCryptoResult(resolve, handler(value)); } else if (isRejected) { reject(value); } else { resolveWebCryptoResult(resolve, value); } } catch (err) { reject(err); } } /** * Promise.prototype.then gets promise.constructor to determine the result * promise's species. These promises are internal WebCrypto intermediates, so * make that lookup stay on the promise itself instead of user-mutated state. * @template T * @template [TResult1=T] * @template [TResult2=never] * @param {Promise<T>} promise * @param {((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined} [onFulfilled] * @param {((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined} [onRejected] * @returns {Promise<TResult1 | TResult2>} */ function jobPromiseThen(promise, onFulfilled, onRejected) { const { promise: resultPromise, resolve, reject, } = PromiseWithResolvers(); setOwnProperty(promise, 'constructor', undefined); PromisePrototypeThen( promise, (value) => settleJobPromise(onFulfilled, resolve, reject, value, false), (value) => settleJobPromise(onRejected, resolve, reject, value, true)); return resultPromise; } // In WebCrypto, the publicExponent option in RSA is represented as a // WebIDL "BigInteger"... that is, a Uint8Array that allows an arbitrary // number of leading zero bits. Our conventional APIs for reading // an unsigned int from a Buffer are not adequate. The implementation // here is adapted from the chromium implementation here: // https://github.com/chromium/chromium/blob/HEAD/third_party/blink/public/platform/web_crypto_algorithm_params.h, but ported to JavaScript // Returns undefined if the conversion was unsuccessful. function bigIntArrayToUnsignedInt(input) { let result = 0; const length = TypedArrayPrototypeGetLength(input); for (let n = 0; n < length; ++n) { const n_reversed = length - n - 1; if (n_reversed >= 4 && input[n]) return; // Too large result |= input[n] << 8 * n_reversed; } return result >>> 0; } function bigIntArrayToUnsignedBigInt(input) { let result = 0n; const length = TypedArrayPrototypeGetLength(input); for (let n = 0; n < length; ++n) { const n_reversed = length - n - 1; result |= BigInt(input[n]) << 8n * BigInt(n_reversed); } return result; } function getStringOption(options, key) { let value; if (options && (value = options[key]) != null) validateString(value, `options.${key}`); return value; } // Must be at most 31 entries. const kCanonicalUsageOrder = [ 'encrypt', 'decrypt', 'sign', 'verify', 'deriveKey', 'deriveBits', 'wrapKey', 'unwrapKey', 'encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits', ]; const kUsageMasks = { __proto__: null, }; const kUsageByMask = { __proto__: null, }; // Derive both lookup tables from kCanonicalUsageOrder so adding a new // usage only requires updating the canonical list above. The numeric // mask uses the usage's canonical index as its bit position. for (let n = 0; n < kCanonicalUsageOrder.length; n++) { const usage = kCanonicalUsageOrder[n]; const mask = 1 << n; kUsageMasks[usage] = mask; kUsageByMask[mask] = usage; } /** * Collects a key usage list into a set. * @param {string[]} usages * @returns {SafeSet<string>} */ function toUsagesSet(usages) { const usagesSet = new SafeSet(); for (let n = 0; n < usages.length; n++) { usagesSet.add(usages[n]); } return usagesSet; } /** * Returns a bit mask representing the usages from `usageSet`. * @param {SafeSet<string>} usageSet * @returns {number} */ function getUsagesMask(usageSet) { // No usages is a valid state for some public keys, represented by a // zero mask. if (usageSet.size === 0) return 0; let mask = 0; for (const usage of usageSet) { mask |= kUsageMasks[usage]; } return mask; } /** * Returns whether `mask` contains `usage`. * @param {number} mask * @param {string} usage * @returns {boolean} */ function hasUsage(mask, usage) { return (mask & kUsageMasks[usage]) !== 0; } /** * Returns the usages represented by `mask` in canonical order. * @param {number} mask * @returns {string[]} */ function getUsagesFromMask(mask) { // Short circuit most common cases, empty and single usage // No usages is a valid state for some public keys if (mask === 0) return []; // A mask with exactly one bit set maps directly to one usage if ((mask & (mask - 1)) === 0) { return [kUsageByMask[mask]]; } // Multiple usages need to be expanded in canonical order. const result = []; for (let n = 0; n < kCanonicalUsageOrder.length; n++) { if (mask & (1 << n)) ArrayPrototypePush(result, kCanonicalUsageOrder[n]); } return result; } function getBlockSize(name) { switch (name) { case 'SHA-1': // Fall through case 'SHA-256': return 512; case 'SHA-384': // Fall through case 'SHA-512': return 1024; case 'SHA3-256': // Fall through case 'SHA3-384': // Fall through case 'SHA3-512': // This interaction is not defined for now. // https://github.com/WICG/webcrypto-modern-algos/issues/23 throw lazyDOMException('Explicit algorithm length member is required', 'NotSupportedError'); } } function getDigestSizeInBytes(name) { switch (name) { case 'SHA-1': return 20; case 'SHA-256': // Fall through case 'SHA3-256': return 32; case 'SHA-384': // Fall through case 'SHA3-384': return 48; case 'SHA-512': // Fall through case 'SHA3-512': return 64; } } function validateKeyOps(keyOps, usagesSet) { if (keyOps === undefined) return; validateArray(keyOps, 'keyData.key_ops'); let keyOpsMask = 0; for (let n = 0; n < keyOps.length; n++) { const op = keyOps[n]; 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 // key usage combinations. Specifically, it says that unrelated key // ops SHOULD NOT be used together. We're not yet validating that here. } if (usagesSet !== undefined) { const usagesMask = getUsagesMask(usagesSet); if ((keyOpsMask & usagesMask) !== usagesMask) { throw lazyDOMException( 'Key operations and usage mismatch', 'DataError'); } } } function secureHeapUsed() { const val = _secureHeapUsed(); if (val === undefined) return { total: 0, used: 0, utilization: 0, min: 0 }; const used = Number(_secureHeapUsed()); const total = Number(getOptionValue('--secure-heap')); const min = Number(getOptionValue('--secure-heap-min')); const utilization = used / total; return { total, used, utilization, min }; } module.exports = { getArrayBufferOrView, getBufferSourceByteLength, getBufferSourceBytes, getCiphers, getCurves, getDataViewOrTypedArrayBuffer, getHashes, getOptionalByteLength, emitOpenSSLEngineDeprecation, kHandle, setEngine, toBuf, kNamedCurveAliases, kSupportedAlgorithms, normalizeAlgorithm, normalizeHashName, hasAnyNotIn, validateByteSource, validateKeyOps, jobPromise, jobPromiseThen, cleanupWebCryptoResult, prepareWebCryptoResult, validateMaxBufferLength, numBitsToBytes, truncateToBitLength, bigIntArrayToUnsignedBigInt, bigIntArrayToUnsignedInt, getBlockSize, getDigestSizeInBytes, getStringOption, getUsagesMask, getUsagesFromMask, toUsagesSet, hasUsage, secureHeapUsed, getCachedHashId, getHashCache, getOpenSSLSecLevel, };