node/lib/internal/crypto/mac.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

199 lines
4.4 KiB
JavaScript

'use strict';
const {
StringPrototypeSubstring,
} = primordials;
const {
HmacJob,
KmacJob,
kCryptoJobWebCrypto,
kSignJobModeSign,
kSignJobModeVerify,
SecretKeyGenJob,
} = internalBinding('crypto');
const {
getBlockSize,
getUsagesMask,
jobPromise,
normalizeHashName,
numBitsToBytes,
truncateToBitLength,
} = require('internal/crypto/util');
const {
lazyDOMException,
} = require('internal/util');
const {
InternalCryptoKey,
getCryptoKeyAlgorithm,
getCryptoKeyHandle,
} = require('internal/crypto/keys');
const {
importJwkSecretKey,
importSecretKey,
validateJwk,
validateKeyUsages,
validateUsagesNotEmpty,
} = require('internal/crypto/webcrypto_util');
const kUsages = ['sign', 'verify'];
function normalizeKeyLength(handle, algorithm) {
let length = handle.getSymmetricKeySize() * 8;
if (length === 0 && algorithm.name === 'HMAC')
throw lazyDOMException('Zero-length key is not supported', 'DataError');
if (algorithm.length !== undefined) {
const byteLength = numBitsToBytes(algorithm.length);
if (byteLength !== handle.getSymmetricKeySize())
throw lazyDOMException('Invalid key length', 'DataError');
if (algorithm.length % 8 !== 0) {
handle = importSecretKey(
truncateToBitLength(algorithm.length, handle.export()));
}
length = algorithm.length;
}
return { handle, length };
}
function hmacGenerateKey(algorithm, extractable, usages) {
const {
hash,
name,
length = getBlockSize(hash.name),
} = algorithm;
const usageSet = validateUsagesNotEmpty(
validateKeyUsages(usages, kUsages, name));
return jobPromise(() => new SecretKeyGenJob(
kCryptoJobWebCrypto,
length,
{ name, length, hash },
getUsagesMask(usageSet),
extractable));
}
function kmacGenerateKey(algorithm, extractable, usages) {
const {
name,
length = {
__proto__: null,
KMAC128: 128,
KMAC256: 256,
}[name],
} = algorithm;
const usageSet = validateUsagesNotEmpty(
validateKeyUsages(usages, kUsages, name));
return jobPromise(() => new SecretKeyGenJob(
kCryptoJobWebCrypto,
length,
{ name, length },
getUsagesMask(usageSet),
extractable));
}
function macImportKey(
format,
keyData,
algorithm,
extractable,
usages,
) {
const isHmac = algorithm.name === 'HMAC';
const usagesSet = validateKeyUsages(
usages, kUsages, algorithm.name);
let handle;
let length;
switch (format) {
case 'KeyObjectHandle': {
handle = keyData;
break;
}
case 'raw-secret':
case 'raw': {
if (format === 'raw' && !isHmac) {
return undefined;
}
handle = importSecretKey(keyData);
break;
}
case 'jwk': {
validateJwk(keyData, 'oct', extractable, usagesSet, 'sig');
if (keyData.alg !== undefined) {
const expected = isHmac ?
normalizeHashName(algorithm.hash.name, normalizeHashName.kContextJwkHmac) :
`K${StringPrototypeSubstring(algorithm.name, 4)}`;
if (expected && keyData.alg !== expected)
throw lazyDOMException(
'JWK "alg" does not match the requested algorithm',
'DataError');
}
handle = importJwkSecretKey(keyData);
break;
}
default:
return undefined;
}
({ handle, length } = normalizeKeyLength(handle, algorithm)); // eslint-disable-line prefer-const
const algorithmObject = {
name: algorithm.name,
length,
};
if (isHmac) {
algorithmObject.hash = algorithm.hash;
}
return new InternalCryptoKey(
handle,
algorithmObject,
getUsagesMask(usagesSet),
extractable);
}
function hmacSignVerify(key, data, algorithm, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
return jobPromise(() => new HmacJob(
kCryptoJobWebCrypto,
mode,
normalizeHashName(getCryptoKeyAlgorithm(key).hash.name),
getCryptoKeyHandle(key),
data,
signature));
}
function kmacSignVerify(key, data, algorithm, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
return jobPromise(() => new KmacJob(
kCryptoJobWebCrypto,
mode,
getCryptoKeyHandle(key),
algorithm.name,
algorithm.customization,
getCryptoKeyAlgorithm(key).length,
algorithm.outputLength,
data,
signature));
}
module.exports = {
macImportKey,
hmacGenerateKey,
hmacSignVerify,
kmacGenerateKey,
kmacSignVerify,
};