node/lib/internal/crypto/webidl.js
Filip Skokan 8ec37ebbd6
crypto: support non-byte WebCrypto lengths and cSHAKE
Add shared bit-length helpers for WebCrypto operations that accept bit
sequences whose length is not byte-aligned.

Use the helpers for cSHAKE output, ECDH-derived bits, HMAC/KMAC key
generation/import/derivation, and KMAC sign/verify output. Preserve the
requested bit length in CryptoKey algorithm metadata while storing and
exporting rounded-up byte material with unused low bits cleared.

Keep byte-multiple validation for algorithms whose specs require it.

Extend the lower-end of KMAC's key length support.

Enable cSHAKE customization and functionName parameters.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/63988
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
2026-06-20 18:42:04 +02:00

789 lines
20 KiB
JavaScript

'use strict';
const {
ArrayBufferIsView,
ArrayPrototypeIncludes,
MathPow,
NumberParseInt,
ObjectPrototypeHasOwnProperty,
StringPrototypeCharCodeAt,
StringPrototypeStartsWith,
StringPrototypeToLowerCase,
Uint8Array,
} = primordials;
const {
lazyDOMException,
} = require('internal/util');
const {
isUint32,
} = require('internal/validators');
const { CryptoKey } = require('internal/crypto/webcrypto');
const {
getCryptoKeyAlgorithm,
getCryptoKeyType,
} = require('internal/crypto/keys');
const {
validateMaxBufferLength,
kNamedCurveAliases,
numBitsToBytes,
} = require('internal/crypto/util');
const {
converters: webidl,
createDictionaryConverter,
createEnumConverter,
createInterfaceConverter,
createSequenceConverter,
requiredArguments,
type,
} = require('internal/webidl');
function validateByteLength(buf, name, target) {
if (buf.byteLength !== target) {
throw lazyDOMException(
`${name} must contain exactly ${target} bytes`,
'OperationError');
}
}
function AESLengthValidator(V, dict) {
if (V !== 128 && V !== 192 && V !== 256)
throw lazyDOMException(
'AES key length must be 128, 192, or 256 bits',
'OperationError');
}
function namedCurveValidator(V, dict) {
if (!ObjectPrototypeHasOwnProperty(kNamedCurveAliases, V))
throw lazyDOMException(
'Unrecognized namedCurve',
'NotSupportedError');
}
const converters = { __proto__: null, ...webidl };
/**
* @param {string | object} V - The hash algorithm identifier (string or object).
* @param {string} label - The dictionary name for the error message.
*/
function ensureSHA(V, label) {
const name = typeof V === 'string' ? V : V.name;
if (typeof name !== 'string' ||
!StringPrototypeStartsWith(StringPrototypeToLowerCase(name), 'sha'))
throw lazyDOMException(
`Only SHA hashes are supported in ${label}`, 'NotSupportedError');
}
converters.AlgorithmIdentifier = (V, opts) => {
// Union for (object or DOMString)
if (type(V) === 'Object') {
return converters.object(V, opts);
}
return converters.DOMString(V, opts);
};
converters.KeyFormat = createEnumConverter('KeyFormat', [
'raw',
'raw-public',
'raw-seed',
'raw-secret',
'raw-private',
'pkcs8',
'spki',
'jwk',
]);
converters.KeyUsage = createEnumConverter('KeyUsage', [
'encrypt',
'decrypt',
'sign',
'verify',
'deriveKey',
'deriveBits',
'wrapKey',
'unwrapKey',
'encapsulateBits',
'decapsulateBits',
'encapsulateKey',
'decapsulateKey',
]);
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',
converter: converters.DOMString,
required: true,
},
];
converters.Algorithm = createDictionaryConverter(
'Algorithm', dictAlgorithm);
converters.BigInteger = webidl.Uint8Array;
const dictRsaKeyGenParams = [
{
key: 'modulusLength',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
required: true,
},
{
key: 'publicExponent',
converter: converters.BigInteger,
required: true,
},
];
converters.RsaKeyGenParams = createDictionaryConverter(
'RsaKeyGenParams', [
dictAlgorithm,
dictRsaKeyGenParams,
]);
converters.RsaHashedKeyGenParams = createDictionaryConverter(
'RsaHashedKeyGenParams', [
dictAlgorithm,
dictRsaKeyGenParams,
[
{
key: 'hash',
converter: converters.HashAlgorithmIdentifier,
validator: (V, dict) => ensureSHA(V, 'RsaHashedKeyGenParams'),
required: true,
},
],
]);
converters.RsaHashedImportParams = createDictionaryConverter(
'RsaHashedImportParams', [
dictAlgorithm,
[
{
key: 'hash',
converter: converters.HashAlgorithmIdentifier,
validator: (V, dict) => ensureSHA(V, 'RsaHashedImportParams'),
required: true,
},
],
]);
converters.NamedCurve = converters.DOMString;
converters.EcKeyImportParams = createDictionaryConverter(
'EcKeyImportParams', [
dictAlgorithm,
[
{
key: 'namedCurve',
converter: converters.NamedCurve,
validator: namedCurveValidator,
required: true,
},
],
]);
converters.EcKeyGenParams = createDictionaryConverter(
'EcKeyGenParams', [
dictAlgorithm,
[
{
key: 'namedCurve',
converter: converters.NamedCurve,
validator: namedCurveValidator,
required: true,
},
],
]);
converters.AesKeyGenParams = createDictionaryConverter(
'AesKeyGenParams', [
dictAlgorithm,
[
{
key: 'length',
converter: (V, opts) =>
converters['unsigned short'](V, enforceRangeOptions(opts)),
validator: AESLengthValidator,
required: true,
},
],
]);
function validateZeroLength(parameterName) {
return (V, dict) => {
if (V.byteLength) {
throw lazyDOMException(
`Non zero-length ${parameterName} is not supported.`, 'NotSupportedError');
}
};
}
function validateCShakeOutputLength(V) {
if (!isUint32(numBitsToBytes(V) * 8)) {
throw lazyDOMException(
'Invalid CShakeParams outputLength',
'OperationError');
}
}
function bufferSourceEqualsAscii(V, string) {
if (V.byteLength !== string.length) return false;
const bytes = ArrayBufferIsView(V) ?
new Uint8Array(V.buffer, V.byteOffset, V.byteLength) :
new Uint8Array(V);
for (let i = 0; i < bytes.length; i++) {
if (bytes[i] !== StringPrototypeCharCodeAt(string, i)) return false;
}
return true;
}
function validateCShakeFunctionName(V) {
if (V.byteLength === 0 ||
bufferSourceEqualsAscii(V, 'KMAC') ||
bufferSourceEqualsAscii(V, 'TupleHash') ||
bufferSourceEqualsAscii(V, 'ParallelHash')) {
return;
}
throw lazyDOMException(
'Unsupported CShakeParams functionName',
'NotSupportedError');
}
function validateCShakeCustomization(V) {
validateMaxBufferLength(V, 'CShakeParams.customization', 512);
}
converters.RsaPssParams = createDictionaryConverter(
'RsaPssParams', [
dictAlgorithm,
[
{
key: 'saltLength',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
required: true,
},
],
]);
converters.RsaOaepParams = createDictionaryConverter(
'RsaOaepParams', [
dictAlgorithm,
[
{
key: 'label',
converter: converters.BufferSource,
},
],
]);
converters.EcdsaParams = createDictionaryConverter(
'EcdsaParams', [
dictAlgorithm,
[
{
key: 'hash',
converter: converters.HashAlgorithmIdentifier,
validator: (V, dict) => ensureSHA(V, 'EcdsaParams'),
required: true,
},
],
]);
function validateHmacKeyLength(parameterName, zeroError) {
return (V) => {
if (V === 0)
throw lazyDOMException(`${parameterName} cannot be 0`, zeroError);
};
}
for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], ['HmacImportParams', 'DataError']]) {
converters[name] = createDictionaryConverter(
name, [
dictAlgorithm,
[
{
key: 'hash',
converter: converters.HashAlgorithmIdentifier,
validator: (V, dict) => ensureSHA(V, name),
required: true,
},
{
key: 'length',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: validateHmacKeyLength(`${name}.length`, zeroError),
},
],
]);
}
const simpleDomStringKey = (key) => ({ key, converter: converters.DOMString });
converters.RsaOtherPrimesInfo = createDictionaryConverter(
'RsaOtherPrimesInfo', [
simpleDomStringKey('r'),
simpleDomStringKey('d'),
simpleDomStringKey('t'),
]);
converters['sequence<RsaOtherPrimesInfo>'] = createSequenceConverter(
converters.RsaOtherPrimesInfo);
converters.JsonWebKey = createDictionaryConverter(
'JsonWebKey', [
simpleDomStringKey('kty'),
simpleDomStringKey('use'),
{
key: 'key_ops',
converter: converters['sequence<DOMString>'],
},
simpleDomStringKey('alg'),
{
key: 'ext',
converter: converters.boolean,
},
simpleDomStringKey('crv'),
simpleDomStringKey('x'),
simpleDomStringKey('y'),
simpleDomStringKey('d'),
simpleDomStringKey('n'),
simpleDomStringKey('e'),
simpleDomStringKey('p'),
simpleDomStringKey('q'),
simpleDomStringKey('dp'),
simpleDomStringKey('dq'),
simpleDomStringKey('qi'),
{
key: 'oth',
converter: converters['sequence<RsaOtherPrimesInfo>'],
},
simpleDomStringKey('k'),
simpleDomStringKey('pub'),
simpleDomStringKey('priv'),
]);
converters.HkdfParams = createDictionaryConverter(
'HkdfParams', [
dictAlgorithm,
[
{
key: 'hash',
converter: converters.HashAlgorithmIdentifier,
validator: (V, dict) => ensureSHA(V, 'HkdfParams'),
required: true,
},
{
key: 'salt',
converter: converters.BufferSource,
required: true,
},
{
key: 'info',
converter: converters.BufferSource,
validator: (V, dict) => validateMaxBufferLength(V, 'algorithm.info', 1024),
required: true,
},
],
]);
converters.CShakeParams = createDictionaryConverter(
'CShakeParams', [
dictAlgorithm,
[
{
key: 'outputLength',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: validateCShakeOutputLength,
required: true,
},
{
key: 'functionName',
converter: converters.BufferSource,
validator: validateCShakeFunctionName,
},
{
key: 'customization',
converter: converters.BufferSource,
validator: validateCShakeCustomization,
},
],
]);
converters.Pbkdf2Params = createDictionaryConverter(
'Pbkdf2Params', [
dictAlgorithm,
[
{
key: 'salt',
converter: converters.BufferSource,
required: true,
},
{
key: 'iterations',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: (V, dict) => {
if (V === 0)
throw lazyDOMException('iterations cannot be zero', 'OperationError');
},
required: true,
},
{
key: 'hash',
converter: converters.HashAlgorithmIdentifier,
validator: (V, dict) => ensureSHA(V, 'Pbkdf2Params'),
required: true,
},
],
]);
converters.AesDerivedKeyParams = createDictionaryConverter(
'AesDerivedKeyParams', [
dictAlgorithm,
[
{
key: 'length',
converter: (V, opts) =>
converters['unsigned short'](V, enforceRangeOptions(opts)),
validator: AESLengthValidator,
required: true,
},
],
]);
converters.AesCbcParams = createDictionaryConverter(
'AesCbcParams', [
dictAlgorithm,
[
{
key: 'iv',
converter: converters.BufferSource,
validator: (V, dict) => validateByteLength(V, 'algorithm.iv', 16),
required: true,
},
],
]);
converters.AeadParams = createDictionaryConverter(
'AeadParams', [
dictAlgorithm,
[
{
key: 'iv',
converter: converters.BufferSource,
validator: (V, dict) => {
switch (StringPrototypeToLowerCase(dict.name)) {
case 'chacha20-poly1305':
validateByteLength(V, 'algorithm.iv', 12);
break;
case 'aes-gcm':
validateMaxBufferLength(V, 'algorithm.iv');
break;
case 'aes-ocb':
if (V.byteLength > 15) {
throw lazyDOMException(
'AES-OCB algorithm.iv must be no more than 15 bytes',
'OperationError');
}
break;
}
},
required: true,
},
{
key: 'additionalData',
converter: converters.BufferSource,
validator: (V, dict) => validateMaxBufferLength(V, 'algorithm.additionalData'),
},
{
key: 'tagLength',
converter: (V, opts) =>
converters.octet(V, enforceRangeOptions(opts)),
validator: (V, dict) => {
switch (StringPrototypeToLowerCase(dict.name)) {
case 'chacha20-poly1305':
if (V !== 128) {
throw lazyDOMException(
`${V} is not a valid ChaCha20-Poly1305 tag length`,
'OperationError');
}
break;
case 'aes-gcm':
if (!ArrayPrototypeIncludes([32, 64, 96, 104, 112, 120, 128], V)) {
throw lazyDOMException(
`${V} is not a valid AES-GCM tag length`,
'OperationError');
}
break;
case 'aes-ocb':
if (!ArrayPrototypeIncludes([64, 96, 128], V)) {
throw lazyDOMException(
`${V} is not a valid AES-OCB tag length`,
'OperationError');
}
break;
}
},
},
],
]);
converters.AesCtrParams = createDictionaryConverter(
'AesCtrParams', [
dictAlgorithm,
[
{
key: 'counter',
converter: converters.BufferSource,
validator: (V, dict) => validateByteLength(V, 'algorithm.counter', 16),
required: true,
},
{
key: 'length',
converter: (V, opts) =>
converters.octet(V, enforceRangeOptions(opts)),
validator: (V, dict) => {
if (V === 0 || V > 128)
throw lazyDOMException(
'AES-CTR algorithm.length must be between 1 and 128',
'OperationError');
},
required: true,
},
],
]);
converters.CryptoKey = createInterfaceConverter(
'CryptoKey', CryptoKey.prototype);
converters.EcdhKeyDeriveParams = createDictionaryConverter(
'EcdhKeyDeriveParams', [
dictAlgorithm,
[
{
key: 'public',
converter: converters.CryptoKey,
validator: (V, dict) => {
if (getCryptoKeyType(V) !== 'public')
throw lazyDOMException(
'algorithm.public must be a public key', 'InvalidAccessError');
if (StringPrototypeToLowerCase(getCryptoKeyAlgorithm(V).name) !== StringPrototypeToLowerCase(dict.name))
throw lazyDOMException(
'key algorithm mismatch',
'InvalidAccessError');
},
required: true,
},
],
]);
converters.ContextParams = createDictionaryConverter(
'ContextParams', [
dictAlgorithm,
[
{
key: 'context',
converter: converters.BufferSource,
validator(V, dict) {
if (process.features.openssl_is_boringssl) {
this.validator = undefined;
} else {
let { 0: major, 1: minor } = process.versions.openssl.split('.');
major = NumberParseInt(major, 10);
minor = NumberParseInt(minor, 10);
if (major > 3 || (major === 3 && minor >= 2)) {
this.validator = undefined;
} else {
this.validator = validateZeroLength('ContextParams.context');
this.validator(V, dict);
}
}
},
},
],
]);
converters.Argon2Params = createDictionaryConverter(
'Argon2Params', [
dictAlgorithm,
[
{
key: 'nonce',
converter: converters.BufferSource,
validator: (V) => {
if (V.byteLength < 8) {
throw lazyDOMException('nonce must be at least 8 bytes', 'OperationError');
}
},
required: true,
},
{
key: 'parallelism',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: (V, dict) => {
if (V === 0 || V > MathPow(2, 24) - 1) {
throw lazyDOMException(
'parallelism must be > 0 and <= 16777215',
'OperationError');
}
},
required: true,
},
{
key: 'memory',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: (V, dict) => {
if (V < 8 * dict.parallelism) {
throw lazyDOMException(
'memory must be at least 8 times the degree of parallelism',
'OperationError');
}
},
required: true,
},
{
key: 'passes',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: (V) => {
if (V === 0) {
throw lazyDOMException('passes must be > 0', 'OperationError');
}
},
required: true,
},
{
key: 'version',
converter: (V, opts) =>
converters.octet(V, enforceRangeOptions(opts)),
validator: (V, dict) => {
if (V !== 0x13) {
throw lazyDOMException(
`${V} is not a valid Argon2 version`,
'OperationError');
}
},
},
{
key: 'secretValue',
converter: converters.BufferSource,
},
{
key: 'associatedData',
converter: converters.BufferSource,
},
],
]);
for (const name of ['KmacKeyGenParams', 'KmacImportParams']) {
converters[name] = createDictionaryConverter(
name, [
dictAlgorithm,
[
{
key: 'length',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
},
],
]);
}
converters.KmacParams = createDictionaryConverter(
'KmacParams', [
dictAlgorithm,
[
{
key: 'outputLength',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
required: true,
},
{
key: 'customization',
converter: converters.BufferSource,
},
],
]);
converters.KangarooTwelveParams = createDictionaryConverter(
'KangarooTwelveParams', [
dictAlgorithm,
[
{
key: 'outputLength',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: (V, opts) => {
if (V === 0 || V % 8)
throw lazyDOMException('Invalid KangarooTwelveParams outputLength', 'OperationError');
},
required: true,
},
{
key: 'customization',
converter: converters.BufferSource,
},
],
]);
converters.TurboShakeParams = createDictionaryConverter(
'TurboShakeParams', [
dictAlgorithm,
[
{
key: 'outputLength',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: (V, opts) => {
if (V === 0 || V % 8)
throw lazyDOMException('Invalid TurboShakeParams outputLength', 'OperationError');
},
required: true,
},
{
key: 'domainSeparation',
converter: (V, opts) =>
converters.octet(V, enforceRangeOptions(opts)),
validator: (V) => {
if (V < 0x01 || V > 0x7F) {
throw lazyDOMException(
'TurboShakeParams.domainSeparation must be in range 0x01-0x7f',
'OperationError');
}
},
},
],
]);
module.exports = {
converters,
requiredArguments,
};