mirror of
https://github.com/nodejs/node.git
synced 2025-04-30 23:56:58 +00:00

WebCryptoAPI functions' arguments are now coersed and validated as per their WebIDL definitions like in other Web Crypto API implementations. This further improves interoperability with other implementations of Web Crypto API. PR-URL: https://github.com/nodejs/node/pull/46067 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
587 lines
15 KiB
JavaScript
587 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayBufferIsView,
|
|
ArrayBufferPrototypeGetByteLength,
|
|
ArrayPrototypeIncludes,
|
|
ArrayPrototypePush,
|
|
BigInt,
|
|
DataViewPrototypeGetBuffer,
|
|
DataViewPrototypeGetByteLength,
|
|
DataViewPrototypeGetByteOffset,
|
|
FunctionPrototypeBind,
|
|
Number,
|
|
ObjectKeys,
|
|
ObjectPrototypeHasOwnProperty,
|
|
Promise,
|
|
StringPrototypeToUpperCase,
|
|
Symbol,
|
|
TypedArrayPrototypeGetBuffer,
|
|
TypedArrayPrototypeGetByteLength,
|
|
TypedArrayPrototypeGetByteOffset,
|
|
TypedArrayPrototypeSlice,
|
|
Uint8Array,
|
|
} = primordials;
|
|
|
|
const {
|
|
getCiphers: _getCiphers,
|
|
getCurves: _getCurves,
|
|
getHashes: _getHashes,
|
|
setEngine: _setEngine,
|
|
secureHeapUsed: _secureHeapUsed,
|
|
} = internalBinding('crypto');
|
|
|
|
const { getOptionValue } = require('internal/options');
|
|
|
|
const {
|
|
crypto: {
|
|
ENGINE_METHOD_ALL
|
|
}
|
|
} = internalBinding('constants');
|
|
|
|
const normalizeHashName = require('internal/crypto/hashnames');
|
|
|
|
const {
|
|
hideStackFrames,
|
|
codes: {
|
|
ERR_CRYPTO_ENGINE_UNKNOWN,
|
|
ERR_INVALID_ARG_TYPE,
|
|
ERR_INVALID_ARG_VALUE,
|
|
ERR_OUT_OF_RANGE,
|
|
}
|
|
} = require('internal/errors');
|
|
|
|
const {
|
|
validateArray,
|
|
validateNumber,
|
|
validateString
|
|
} = require('internal/validators');
|
|
|
|
const { Buffer } = require('buffer');
|
|
|
|
const {
|
|
cachedResult,
|
|
filterDuplicateStrings,
|
|
lazyDOMException,
|
|
} = require('internal/util');
|
|
|
|
const {
|
|
isDataView,
|
|
isArrayBufferView,
|
|
isAnyArrayBuffer,
|
|
} = require('internal/util/types');
|
|
|
|
const kHandle = Symbol('kHandle');
|
|
const kKeyObject = Symbol('kKeyObject');
|
|
|
|
let defaultEncoding = 'buffer';
|
|
|
|
function setDefaultEncoding(val) {
|
|
defaultEncoding = val;
|
|
}
|
|
|
|
function getDefaultEncoding() {
|
|
return defaultEncoding;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
const getCiphers = cachedResult(() => filterDuplicateStrings(_getCiphers()));
|
|
const getHashes = cachedResult(() => filterDuplicateStrings(_getHashes()));
|
|
const getCurves = cachedResult(() => filterDuplicateStrings(_getCurves()));
|
|
|
|
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;
|
|
|
|
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(
|
|
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',
|
|
};
|
|
|
|
const kAesKeyLengths = [128, 192, 256];
|
|
|
|
// These are the only hash algorithms we currently support via
|
|
// the Web Crypto API.
|
|
const kHashTypes = [
|
|
'SHA-1',
|
|
'SHA-256',
|
|
'SHA-384',
|
|
'SHA-512',
|
|
];
|
|
|
|
const kSupportedAlgorithms = {
|
|
'digest': {
|
|
'SHA-1': null,
|
|
'SHA-256': null,
|
|
'SHA-384': null,
|
|
'SHA-512': null,
|
|
},
|
|
'generateKey': {
|
|
'RSASSA-PKCS1-v1_5': 'RsaHashedKeyGenParams',
|
|
'RSA-PSS': 'RsaHashedKeyGenParams',
|
|
'RSA-OAEP': 'RsaHashedKeyGenParams',
|
|
'ECDSA': 'EcKeyGenParams',
|
|
'ECDH': 'EcKeyGenParams',
|
|
'AES-CTR': 'AesKeyGenParams',
|
|
'AES-CBC': 'AesKeyGenParams',
|
|
'AES-GCM': 'AesKeyGenParams',
|
|
'AES-KW': 'AesKeyGenParams',
|
|
'HMAC': 'HmacKeyGenParams',
|
|
'X25519': null,
|
|
'Ed25519': null,
|
|
'X448': null,
|
|
'Ed448': null,
|
|
},
|
|
'sign': {
|
|
'RSASSA-PKCS1-v1_5': null,
|
|
'RSA-PSS': 'RsaPssParams',
|
|
'ECDSA': 'EcdsaParams',
|
|
'HMAC': null,
|
|
'Ed25519': null,
|
|
'Ed448': 'Ed448Params',
|
|
},
|
|
'verify': {
|
|
'RSASSA-PKCS1-v1_5': null,
|
|
'RSA-PSS': 'RsaPssParams',
|
|
'ECDSA': 'EcdsaParams',
|
|
'HMAC': null,
|
|
'Ed25519': null,
|
|
'Ed448': 'Ed448Params',
|
|
},
|
|
'importKey': {
|
|
'RSASSA-PKCS1-v1_5': 'RsaHashedImportParams',
|
|
'RSA-PSS': 'RsaHashedImportParams',
|
|
'RSA-OAEP': 'RsaHashedImportParams',
|
|
'ECDSA': 'EcKeyImportParams',
|
|
'ECDH': 'EcKeyImportParams',
|
|
'HMAC': 'HmacImportParams',
|
|
'HKDF': null,
|
|
'PBKDF2': null,
|
|
'AES-CTR': null,
|
|
'AES-CBC': null,
|
|
'AES-GCM': null,
|
|
'AES-KW': null,
|
|
'Ed25519': null,
|
|
'X25519': null,
|
|
'Ed448': null,
|
|
'X448': null,
|
|
},
|
|
'deriveBits': {
|
|
'HKDF': 'HkdfParams',
|
|
'PBKDF2': 'Pbkdf2Params',
|
|
'ECDH': 'EcdhKeyDeriveParams',
|
|
'X25519': 'EcdhKeyDeriveParams',
|
|
'X448': 'EcdhKeyDeriveParams',
|
|
},
|
|
'encrypt': {
|
|
'RSA-OAEP': 'RsaOaepParams',
|
|
'AES-CBC': 'AesCbcParams',
|
|
'AES-GCM': 'AesGcmParams',
|
|
'AES-CTR': 'AesCtrParams',
|
|
},
|
|
'decrypt': {
|
|
'RSA-OAEP': 'RsaOaepParams',
|
|
'AES-CBC': 'AesCbcParams',
|
|
'AES-GCM': 'AesGcmParams',
|
|
'AES-CTR': 'AesCtrParams',
|
|
},
|
|
'get key length': {
|
|
'AES-CBC': 'AesDerivedKeyParams',
|
|
'AES-CTR': 'AesDerivedKeyParams',
|
|
'AES-GCM': 'AesDerivedKeyParams',
|
|
'AES-KW': 'AesDerivedKeyParams',
|
|
'HMAC': 'HmacImportParams',
|
|
'HKDF': null,
|
|
'PBKDF2': null,
|
|
},
|
|
'wrapKey': {
|
|
'AES-KW': null,
|
|
},
|
|
'unwrapKey': {
|
|
'AES-KW': null,
|
|
},
|
|
};
|
|
|
|
const simpleAlgorithmDictionaries = {
|
|
AesGcmParams: { iv: 'BufferSource', additionalData: 'BufferSource' },
|
|
RsaHashedKeyGenParams: { hash: 'HashAlgorithmIdentifier' },
|
|
EcKeyGenParams: {},
|
|
HmacKeyGenParams: { hash: 'HashAlgorithmIdentifier' },
|
|
RsaPssParams: {},
|
|
EcdsaParams: { hash: 'HashAlgorithmIdentifier' },
|
|
HmacImportParams: { hash: 'HashAlgorithmIdentifier' },
|
|
HkdfParams: {
|
|
hash: 'HashAlgorithmIdentifier',
|
|
salt: 'BufferSource',
|
|
info: 'BufferSource',
|
|
},
|
|
Ed448Params: { context: 'BufferSource' },
|
|
Pbkdf2Params: { hash: 'HashAlgorithmIdentifier', salt: 'BufferSource' },
|
|
RsaOaepParams: { label: 'BufferSource' },
|
|
RsaHashedImportParams: { hash: 'HashAlgorithmIdentifier' },
|
|
EcKeyImportParams: {},
|
|
};
|
|
|
|
function validateMaxBufferLength(data, name) {
|
|
if (data.byteLength > kMaxBufferLength) {
|
|
throw lazyDOMException(
|
|
`${name} must be less than ${kMaxBufferLength + 1} bits`,
|
|
'OperationError');
|
|
}
|
|
}
|
|
|
|
let webidl;
|
|
|
|
// 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, {
|
|
prefix: 'Failed to normalize algorithm',
|
|
context: 'passed algorithm',
|
|
});
|
|
// 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)
|
|
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
|
|
|
|
// Fast path everything below if the registered dictionary is null
|
|
if (desiredType === null)
|
|
return { name: algName };
|
|
|
|
// 6.
|
|
const normalizedAlgorithm = webidl.converters[desiredType](algorithm, {
|
|
prefix: 'Failed to normalize algorithm',
|
|
context: 'passed algorithm',
|
|
});
|
|
// 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');
|
|
}
|
|
}
|
|
|
|
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 hasAnyNotIn(set, checks) {
|
|
for (const s of set)
|
|
if (!ArrayPrototypeIncludes(checks, s))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
function validateBitLength(length, name, required = false) {
|
|
if (length !== undefined || required) {
|
|
validateNumber(length, name);
|
|
if (length < 0)
|
|
throw new ERR_OUT_OF_RANGE(name, '> 0');
|
|
if (length % 8) {
|
|
throw new ERR_INVALID_ARG_VALUE(
|
|
name,
|
|
length,
|
|
'must be a multiple of 8');
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateByteLength(buf, name, target) {
|
|
if (buf.byteLength !== target) {
|
|
throw lazyDOMException(
|
|
`${name} must contain exactly ${target} bytes`,
|
|
'OperationError');
|
|
}
|
|
}
|
|
|
|
const validateByteSource = hideStackFrames((val, name) => {
|
|
val = toBuf(val);
|
|
|
|
if (isAnyArrayBuffer(val) || isArrayBufferView(val))
|
|
return val;
|
|
|
|
throw new ERR_INVALID_ARG_TYPE(
|
|
name,
|
|
[
|
|
'string',
|
|
'ArrayBuffer',
|
|
'TypedArray',
|
|
'DataView',
|
|
'Buffer',
|
|
],
|
|
val);
|
|
});
|
|
|
|
function onDone(resolve, reject, err, result) {
|
|
if (err) {
|
|
return reject(lazyDOMException(
|
|
'The operation failed for an operation-specific reason',
|
|
{ name: 'OperationError', cause: err }));
|
|
}
|
|
resolve(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);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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;
|
|
|
|
for (let n = 0; n < input.length; ++n) {
|
|
const n_reversed = input.length - n - 1;
|
|
if (n_reversed >= 4 && input[n])
|
|
return; // Too large
|
|
result |= input[n] << 8 * n_reversed;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function bigIntArrayToUnsignedBigInt(input) {
|
|
let result = 0n;
|
|
|
|
for (let n = 0; n < input.length; ++n) {
|
|
const n_reversed = input.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;
|
|
}
|
|
|
|
function getUsagesUnion(usageSet, ...usages) {
|
|
const newset = [];
|
|
for (let n = 0; n < usages.length; n++) {
|
|
if (usageSet.has(usages[n]))
|
|
ArrayPrototypePush(newset, usages[n]);
|
|
}
|
|
return newset;
|
|
}
|
|
|
|
function getBlockSize(name) {
|
|
switch (name) {
|
|
case 'SHA-1': return 512;
|
|
case 'SHA-256': return 512;
|
|
case 'SHA-384': return 1024;
|
|
case 'SHA-512': return 1024;
|
|
}
|
|
}
|
|
|
|
const kKeyOps = {
|
|
sign: 1,
|
|
verify: 2,
|
|
encrypt: 3,
|
|
decrypt: 4,
|
|
wrapKey: 5,
|
|
unwrapKey: 6,
|
|
deriveKey: 7,
|
|
deriveBits: 8,
|
|
};
|
|
|
|
function validateKeyOps(keyOps, usagesSet) {
|
|
if (keyOps === undefined) return;
|
|
validateArray(keyOps, 'keyData.key_ops');
|
|
let flags = 0;
|
|
for (let n = 0; n < keyOps.length; n++) {
|
|
const op = keyOps[n];
|
|
const op_flag = kKeyOps[op];
|
|
// Skipping unknown key ops
|
|
if (op_flag === undefined)
|
|
continue;
|
|
// Have we seen it already? if so, error
|
|
if (flags & (1 << op_flag))
|
|
throw lazyDOMException('Duplicate key operation', 'DataError');
|
|
flags |= (1 << op_flag);
|
|
|
|
// 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) {
|
|
for (const use of usagesSet) {
|
|
if (!ArrayPrototypeIncludes(keyOps, use)) {
|
|
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,
|
|
getCiphers,
|
|
getCurves,
|
|
getDataViewOrTypedArrayBuffer,
|
|
getDefaultEncoding,
|
|
getHashes,
|
|
kHandle,
|
|
kKeyObject,
|
|
setDefaultEncoding,
|
|
setEngine,
|
|
toBuf,
|
|
|
|
kHashTypes,
|
|
kNamedCurveAliases,
|
|
kAesKeyLengths,
|
|
normalizeAlgorithm,
|
|
normalizeHashName,
|
|
hasAnyNotIn,
|
|
validateBitLength,
|
|
validateByteLength,
|
|
validateByteSource,
|
|
validateKeyOps,
|
|
jobPromise,
|
|
validateMaxBufferLength,
|
|
bigIntArrayToUnsignedBigInt,
|
|
bigIntArrayToUnsignedInt,
|
|
getBlockSize,
|
|
getStringOption,
|
|
getUsagesUnion,
|
|
secureHeapUsed,
|
|
};
|