src: refactor SubtleCrypto algorithm and length validations

PR-URL: https://github.com/nodejs/node/pull/57273
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jason Zhang <xzha4350@gmail.com>
Reviewed-By: Mattias Buelens <mattias@buelens.com>
This commit is contained in:
Filip Skokan 2025-03-04 18:45:51 +01:00 committed by GitHub
parent 52ac44888d
commit 6fdd4e6dcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 210 additions and 182 deletions

View File

@ -58,7 +58,6 @@ const {
generateKey: _generateKey,
} = require('internal/crypto/keygen');
const kMaxCounterLength = 128;
const kTagLengths = [32, 64, 96, 104, 112, 120, 128];
const generateKey = promisify(_generateKey);
@ -109,15 +108,19 @@ function getVariant(name, length) {
}
}
function asyncAesCtrCipher(mode, key, data, { counter, length }) {
validateByteLength(counter, 'algorithm.counter', 16);
function validateAesCtrAlgorithm(algorithm) {
validateByteLength(algorithm.counter, 'algorithm.counter', 16);
// The length must specify an integer between 1 and 128. While
// there is no default, this should typically be 64.
if (length === 0 || length > kMaxCounterLength) {
if (algorithm.length === 0 || algorithm.length > 128) {
throw lazyDOMException(
'AES-CTR algorithm.length must be between 1 and 128',
'OperationError');
}
}
function asyncAesCtrCipher(mode, key, data, algorithm) {
validateAesCtrAlgorithm(algorithm);
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
@ -125,19 +128,23 @@ function asyncAesCtrCipher(mode, key, data, { counter, length }) {
key[kKeyObject][kHandle],
data,
getVariant('AES-CTR', key.algorithm.length),
counter,
length));
algorithm.counter,
algorithm.length));
}
function asyncAesCbcCipher(mode, key, data, { iv }) {
validateByteLength(iv, 'algorithm.iv', 16);
function validateAesCbcAlgorithm(algorithm) {
validateByteLength(algorithm.iv, 'algorithm.iv', 16);
}
function asyncAesCbcCipher(mode, key, data, algorithm) {
validateAesCbcAlgorithm(algorithm);
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
data,
getVariant('AES-CBC', key.algorithm.length),
iv));
algorithm.iv));
}
function asyncAesKwCipher(mode, key, data) {
@ -149,24 +156,25 @@ function asyncAesKwCipher(mode, key, data) {
getVariant('AES-KW', key.algorithm.length)));
}
function asyncAesGcmCipher(
mode,
key,
data,
{ iv, additionalData, tagLength = 128 }) {
if (!ArrayPrototypeIncludes(kTagLengths, tagLength)) {
return PromiseReject(lazyDOMException(
`${tagLength} is not a valid AES-GCM tag length`,
'OperationError'));
function validateAesGcmAlgorithm(algorithm) {
if (!ArrayPrototypeIncludes(kTagLengths, algorithm.tagLength)) {
throw lazyDOMException(
`${algorithm.tagLength} is not a valid AES-GCM tag length`,
'OperationError');
}
validateMaxBufferLength(iv, 'algorithm.iv');
validateMaxBufferLength(algorithm.iv, 'algorithm.iv');
if (additionalData !== undefined) {
validateMaxBufferLength(additionalData, 'algorithm.additionalData');
if (algorithm.additionalData !== undefined) {
validateMaxBufferLength(algorithm.additionalData, 'algorithm.additionalData');
}
}
const tagByteLength = MathFloor(tagLength / 8);
function asyncAesGcmCipher(mode, key, data, algorithm) {
algorithm.tagLength ??= 128;
validateAesGcmAlgorithm(algorithm);
const tagByteLength = MathFloor(algorithm.tagLength / 8);
let tag;
switch (mode) {
case kWebCryptoCipherDecrypt: {
@ -198,9 +206,9 @@ function asyncAesGcmCipher(
key[kKeyObject][kHandle],
data,
getVariant('AES-GCM', key.algorithm.length),
iv,
algorithm.iv,
tag,
additionalData));
algorithm.additionalData));
}
function aesCipher(mode, key, data, algorithm) {
@ -212,13 +220,17 @@ function aesCipher(mode, key, data, algorithm) {
}
}
async function aesGenerateKey(algorithm, extractable, keyUsages) {
const { name, length } = algorithm;
if (!ArrayPrototypeIncludes(kAesKeyLengths, length)) {
function validateAesGenerateKeyAlgorithm(algorithm) {
if (!ArrayPrototypeIncludes(kAesKeyLengths, algorithm.length)) {
throw lazyDOMException(
'AES key length must be 128, 192, or 256 bits',
'OperationError');
}
}
async function aesGenerateKey(algorithm, extractable, keyUsages) {
validateAesGenerateKeyAlgorithm(algorithm);
const { name, length } = algorithm;
const checkUsages = ['wrapKey', 'unwrapKey'];
if (name !== 'AES-KW')

View File

@ -329,18 +329,21 @@ function cfrgImportKey(
extractable);
}
function eddsaSignVerify(key, data, { name, context }, signature) {
function validateEdDSASignVerifyAlgorithm(algorithm) {
if (algorithm.name === 'Ed448' && algorithm.context?.byteLength) {
throw lazyDOMException(
'Non zero-length context is not yet supported.', 'NotSupportedError');
}
}
function eddsaSignVerify(key, data, algorithm, signature) {
validateEdDSASignVerifyAlgorithm(algorithm);
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';
if (key.type !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
if (name === 'Ed448' && context?.byteLength) {
throw lazyDOMException(
'Non zero-length context is not yet supported.', 'NotSupportedError');
}
return jobPromise(() => new SignJob(
kCryptoJobAsync,
mode,

View File

@ -298,28 +298,28 @@ function diffieHellman(options) {
let masks;
// The ecdhDeriveBits function is part of the Web Crypto API and serves both
// deriveKeys and deriveBits functions.
async function ecdhDeriveBits(algorithm, baseKey, length) {
const { 'public': key } = algorithm;
if (key.type !== 'public') {
function validateEcdhDeriveBitsAlgorithmAndLength(algorithm, length) {
if (algorithm.public.type !== 'public') {
throw lazyDOMException(
'algorithm.public must be a public key', 'InvalidAccessError');
}
if (algorithm.name !== algorithm.public.algorithm.name) {
throw lazyDOMException(`algorithm.public must be an ${algorithm.name} key`, 'InvalidAccessError');
}
}
// The ecdhDeriveBits function is part of the Web Crypto API and serves both
// deriveKeys and deriveBits functions.
async function ecdhDeriveBits(algorithm, baseKey, length) {
validateEcdhDeriveBitsAlgorithmAndLength(algorithm, length);
const { 'public': key } = algorithm;
if (baseKey.type !== 'private') {
throw lazyDOMException(
'baseKey must be a private key', 'InvalidAccessError');
}
if (
key.algorithm.name !== 'ECDH' &&
key.algorithm.name !== 'X25519' &&
key.algorithm.name !== 'X448'
) {
throw lazyDOMException('Keys must be ECDH, X25519, or X448 keys', 'InvalidAccessError');
}
if (key.algorithm.name !== baseKey.algorithm.name) {
throw lazyDOMException(
'The public and private keys must be of the same type',

View File

@ -1,8 +1,7 @@
'use strict';
const {
ArrayPrototypeIncludes,
ObjectKeys,
ObjectPrototypeHasOwnProperty,
SafeSet,
} = primordials;
@ -77,14 +76,17 @@ function createECPublicKeyRaw(namedCurve, keyData) {
return new PublicKeyObject(handle);
}
async function ecGenerateKey(algorithm, extractable, keyUsages) {
const { name, namedCurve } = algorithm;
if (!ArrayPrototypeIncludes(ObjectKeys(kNamedCurveAliases), namedCurve)) {
function validateEcKeyAlgorithm(algorithm) {
if (!ObjectPrototypeHasOwnProperty(kNamedCurveAliases, algorithm.namedCurve)) {
throw lazyDOMException(
'Unrecognized namedCurve',
'NotSupportedError');
}
}
async function ecGenerateKey(algorithm, extractable, keyUsages) {
validateEcKeyAlgorithm(algorithm);
const { name, namedCurve } = algorithm;
const usageSet = new SafeSet(keyUsages);
switch (name) {
@ -154,16 +156,11 @@ function ecImportKey(
keyData,
algorithm,
extractable,
keyUsages) {
keyUsages,
) {
validateEcKeyAlgorithm(algorithm);
const { name, namedCurve } = algorithm;
if (!ArrayPrototypeIncludes(ObjectKeys(kNamedCurveAliases), namedCurve)) {
throw lazyDOMException(
'Unrecognized namedCurve',
'NotSupportedError');
}
let keyObject;
const usagesSet = new SafeSet(keyUsages);
switch (format) {

View File

@ -138,11 +138,7 @@ function hkdfSync(hash, key, salt, info, length) {
}
const hkdfPromise = promisify(hkdf);
async function hkdfDeriveBits(algorithm, baseKey, length) {
const { hash, salt, info } = algorithm;
if (length === 0)
return new ArrayBuffer(0);
function validateHkdfDeriveBitsAlgorithmAndLength(algorithm, length) {
if (length === null)
throw lazyDOMException('length cannot be null', 'OperationError');
if (length % 8) {
@ -150,6 +146,14 @@ async function hkdfDeriveBits(algorithm, baseKey, length) {
'length must be a multiple of 8',
'OperationError');
}
}
async function hkdfDeriveBits(algorithm, baseKey, length) {
validateHkdfDeriveBitsAlgorithmAndLength(algorithm, length);
const { hash, salt, info } = algorithm;
if (length === 0)
return new ArrayBuffer(0);
try {
return await hkdfPromise(

View File

@ -895,12 +895,14 @@ function isCryptoKey(obj) {
}
function importGenericSecretKey(
{ name, length },
algorithm,
format,
keyData,
extractable,
keyUsages) {
keyUsages,
) {
const usagesSet = new SafeSet(keyUsages);
const { name } = algorithm;
if (extractable)
throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');
@ -910,47 +912,22 @@ function importGenericSecretKey(
'SyntaxError');
}
let keyObject;
switch (format) {
case 'KeyObject': {
if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
const checkLength = keyData.symmetricKeySize * 8;
// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (length !== undefined && length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}
return new InternalCryptoKey(keyData, { name }, keyUsages, false);
keyObject = keyData;
break;
}
case 'raw': {
if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
const checkLength = keyData.byteLength * 8;
// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (length !== undefined && length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}
const keyObject = createSecretKey(keyData);
return new InternalCryptoKey(keyObject, { name }, keyUsages, false);
keyObject = createSecretKey(keyData);
break;
}
}
if (keyObject) {
return new InternalCryptoKey(keyObject, { name }, keyUsages, false);
}
throw lazyDOMException(
`Unable to import ${name} key with format ${format}`,
'NotSupportedError');

View File

@ -18,7 +18,6 @@ const {
hasAnyNotIn,
jobPromise,
normalizeHashName,
validateBitLength,
validateKeyOps,
kHandle,
kKeyObject,
@ -41,15 +40,30 @@ const {
const generateKey = promisify(_generateKey);
function validateHmacGenerateKeyAlgorithm(algorithm) {
if (algorithm.length !== undefined) {
if (algorithm.length === 0)
throw lazyDOMException(
'Zero-length key is not supported',
'OperationError');
// The Web Crypto spec allows for key lengths that are not multiples of 8. We don't.
if (algorithm.length % 8) {
throw lazyDOMException(
'Unsupported algorithm.length',
'NotSupportedError');
}
}
}
async function hmacGenerateKey(algorithm, extractable, keyUsages) {
validateHmacGenerateKeyAlgorithm(algorithm);
const { hash, name } = algorithm;
let { length } = algorithm;
if (length === undefined)
length = getBlockSize(hash.name);
validateBitLength(length, 'algorithm.length', true);
const usageSet = new SafeSet(keyUsages);
if (hasAnyNotIn(usageSet, ['sign', 'verify'])) {
throw lazyDOMException(
@ -82,12 +96,27 @@ function getAlgorithmName(hash) {
}
}
function validateHmacImportKeyAlgorithm(algorithm) {
if (algorithm.length !== undefined) {
if (algorithm.length === 0) {
throw lazyDOMException('Zero-length key is not supported', 'DataError');
}
// The Web Crypto spec allows for key lengths that are not multiples of 8. We don't.
if (algorithm.length % 8) {
throw lazyDOMException('Unsupported algorithm.length', 'NotSupportedError');
}
}
}
function hmacImportKey(
format,
keyData,
algorithm,
extractable,
keyUsages) {
keyUsages,
) {
validateHmacImportKeyAlgorithm(algorithm);
const usagesSet = new SafeSet(keyUsages);
if (hasAnyNotIn(usagesSet, ['sign', 'verify'])) {
throw lazyDOMException(
@ -97,38 +126,10 @@ function hmacImportKey(
let keyObject;
switch (format) {
case 'KeyObject': {
const checkLength = keyData.symmetricKeySize * 8;
if (checkLength === 0 || algorithm.length === 0)
throw lazyDOMException('Zero-length key is not supported', 'DataError');
// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (algorithm.length !== undefined &&
algorithm.length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}
keyObject = keyData;
break;
}
case 'raw': {
const checkLength = keyData.byteLength * 8;
if (checkLength === 0 || algorithm.length === 0)
throw lazyDOMException('Zero-length key is not supported', 'DataError');
// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (algorithm.length !== undefined &&
algorithm.length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}
keyObject = createSecretKey(keyData);
break;
}
@ -178,6 +179,14 @@ function hmacImportKey(
const { length } = keyObject[kHandle].keyDetail({});
if (length === 0)
throw lazyDOMException('Zero-length key is not supported', 'DataError');
if (algorithm.length !== undefined &&
algorithm.length !== length) {
throw lazyDOMException('Invalid key length', 'DataError');
}
return new InternalCryptoKey(
keyObject, {
name: 'HMAC',

View File

@ -92,22 +92,28 @@ function check(password, salt, iterations, keylen, digest) {
}
const pbkdf2Promise = promisify(pbkdf2);
async function pbkdf2DeriveBits(algorithm, baseKey, length) {
const { iterations, hash, salt } = algorithm;
if (iterations === 0)
function validatePbkdf2DeriveBitsAlgorithmAndLength(algorithm, length) {
if (algorithm.iterations === 0)
throw lazyDOMException(
'iterations cannot be zero',
'OperationError');
if (length === 0)
return new ArrayBuffer(0);
if (length === null)
throw lazyDOMException('length cannot be null', 'OperationError');
if (length % 8) {
throw lazyDOMException(
'length must be a multiple of 8',
'OperationError');
}
}
async function pbkdf2DeriveBits(algorithm, baseKey, length) {
validatePbkdf2DeriveBitsAlgorithmAndLength(algorithm, length);
const { iterations, hash, salt } = algorithm;
if (length === 0)
return new ArrayBuffer(0);
let result;
try {

View File

@ -85,16 +85,21 @@ function verifyAcceptableRsaKeyUse(name, isPublic, usages) {
}
}
function rsaOaepCipher(mode, key, data, { label }) {
function validateRsaOaepAlgorithm(algorithm) {
if (algorithm.label !== undefined) {
validateMaxBufferLength(algorithm.label, 'algorithm.label');
}
}
function rsaOaepCipher(mode, key, data, algorithm) {
validateRsaOaepAlgorithm(algorithm);
const type = mode === kWebCryptoCipherEncrypt ? 'public' : 'private';
if (key.type !== type) {
throw lazyDOMException(
'The requested operation is not valid for the provided key',
'InvalidAccessError');
}
if (label !== undefined) {
validateMaxBufferLength(label, 'algorithm.label');
}
return jobPromise(() => new RSACipherJob(
kCryptoJobAsync,
@ -103,14 +108,26 @@ function rsaOaepCipher(mode, key, data, { label }) {
data,
kKeyVariantRSA_OAEP,
normalizeHashName(key.algorithm.hash.name),
label));
algorithm.label));
}
function validateRsaKeyGenerateAlgorithm(algorithm) {
const publicExponentConverted = bigIntArrayToUnsignedInt(algorithm.publicExponent);
if (publicExponentConverted === undefined) {
throw lazyDOMException(
'The publicExponent must be equivalent to an unsigned 32-bit value',
'OperationError');
}
return publicExponentConverted;
}
async function rsaKeyGenerate(
algorithm,
extractable,
keyUsages) {
keyUsages,
) {
const publicExponentConverted = validateRsaKeyGenerateAlgorithm(algorithm);
const {
name,
modulusLength,
@ -120,13 +137,6 @@ async function rsaKeyGenerate(
const usageSet = new SafeSet(keyUsages);
const publicExponentConverted = bigIntArrayToUnsignedInt(publicExponent);
if (publicExponentConverted === undefined) {
throw lazyDOMException(
'The publicExponent must be equivalent to an unsigned 32-bit value',
'OperationError');
}
switch (name) {
case 'RSA-OAEP':
if (hasAnyNotIn(usageSet,
@ -319,7 +329,7 @@ function rsaImportKey(
}, keyUsages, extractable);
}
async function rsaSignVerify(key, data, { saltLength }, signature) {
function rsaSignVerify(key, data, { saltLength }, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';

View File

@ -50,8 +50,6 @@ const {
ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED,
ERR_CRYPTO_ENGINE_UNKNOWN,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_OUT_OF_RANGE,
},
hideStackFrames,
} = require('internal/errors');
@ -418,20 +416,6 @@ function hasAnyNotIn(set, checks) {
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(
@ -617,7 +601,6 @@ module.exports = {
normalizeAlgorithm,
normalizeHashName,
hasAnyNotIn,
validateBitLength,
validateByteLength,
validateByteSource,
validateKeyOps,

View File

@ -182,7 +182,7 @@ async function prepareKeys() {
},
keys.X448.privateKey,
8 * keys.X448.size),
{ message: 'The public and private keys must be of the same type' });
{ message: 'algorithm.public must be an X448 key' });
}
{

View File

@ -202,7 +202,7 @@ async function prepareKeys() {
public: keys['P-384'].publicKey
},
keys['P-521'].privateKey,
8 * keys['P-521'].size),
8 * keys['P-384'].size),
{ message: /Named curve mismatch/ });
}
@ -218,7 +218,7 @@ async function prepareKeys() {
name: 'ECDH',
public: publicKey
}, keys['P-521'].privateKey, null), {
message: /Keys must be ECDH, X25519, or X448 keys/
message: 'algorithm.public must be an ECDH key'
});
}

View File

@ -136,7 +136,7 @@ async function prepareKeys() {
},
keys.X448.privateKey,
...otherArgs),
{ message: 'The public and private keys must be of the same type' });
{ message: 'algorithm.public must be an X448 key' });
}
{

View File

@ -174,7 +174,7 @@ async function prepareKeys() {
},
keys['P-521'].privateKey,
...otherArgs),
{ message: /Keys must be ECDH, X25519, or X448 keys/ });
{ message: 'algorithm.public must be an ECDH key' });
}
{

View File

@ -37,6 +37,20 @@ const { subtle } = globalThis.crypto;
assert.strictEqual(
Buffer.from(plaintext).toString('hex'),
Buffer.from(buf).toString('hex'));
await assert.rejects(() => subtle.encrypt({
name: 'RSA-OAEP',
}, privateKey, buf), {
name: 'InvalidAccessError',
message: 'The requested operation is not valid for the provided key'
});
await assert.rejects(() => subtle.decrypt({
name: 'RSA-OAEP',
}, publicKey, ciphertext), {
name: 'InvalidAccessError',
message: 'The requested operation is not valid for the provided key'
});
}
test().then(common.mustCall());

View File

@ -36,6 +36,15 @@ const { subtle } = globalThis.crypto;
}, false, ['sign', 'verify']), {
code: 'ERR_MISSING_OPTION'
});
await assert.rejects(
subtle.importKey('raw', keyData, {
name: 'HMAC',
hash: 'SHA-256',
length: 384,
}, false, ['sign', 'verify']), {
name: 'DataError',
message: 'Invalid key length'
});
await assert.rejects(
subtle.importKey('raw', keyData, {
name: 'HMAC',
@ -59,8 +68,8 @@ const { subtle } = globalThis.crypto;
hash: 'SHA-256',
length: 1
}, false, ['sign', 'verify']), {
name: 'DataError',
message: 'Invalid key length'
name: 'NotSupportedError',
message: 'Unsupported algorithm.length'
});
await assert.rejects(
subtle.importKey('jwk', null, {

View File

@ -345,6 +345,14 @@ const vectors = {
{ code: 'ERR_INVALID_ARG_TYPE' });
}));
await assert.rejects(
subtle.generateKey(
{ name, modulusLength, publicExponent: new Uint8Array([1, 1, 1, 1, 1]), hash }, true, usages),
{
message: /The publicExponent must be equivalent to an unsigned 32-bit value/,
name: 'OperationError',
});
await Promise.all([true, 1].map((hash) => {
return assert.rejects(subtle.generateKey({
name,
@ -494,8 +502,6 @@ const vectors = {
const tests = kTests.map((args) => test(...args));
// Test bad parameters
Promise.all(tests).then(common.mustCall());
}
@ -675,7 +681,5 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' });
const tests = kTests.map((args) => test(...args));
// Test bad parameters
Promise.all(tests).then(common.mustCall());
}