node/lib/internal/crypto/mac.js
Filip Skokan 6fdd4e6dcf
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>
2025-03-04 17:45:51 +00:00

216 lines
5.2 KiB
JavaScript

'use strict';
const {
ArrayFrom,
SafeSet,
} = primordials;
const {
HmacJob,
KeyObjectHandle,
kCryptoJobAsync,
kSignJobModeSign,
kSignJobModeVerify,
} = internalBinding('crypto');
const {
getBlockSize,
hasAnyNotIn,
jobPromise,
normalizeHashName,
validateKeyOps,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
generateKey: _generateKey,
} = require('internal/crypto/keygen');
const {
InternalCryptoKey,
SecretKeyObject,
createSecretKey,
} = require('internal/crypto/keys');
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);
const usageSet = new SafeSet(keyUsages);
if (hasAnyNotIn(usageSet, ['sign', 'verify'])) {
throw lazyDOMException(
'Unsupported key usage for an HMAC key',
'SyntaxError');
}
const key = await generateKey('hmac', { length }).catch((err) => {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
});
return new InternalCryptoKey(
key,
{ name, length, hash: { name: hash.name } },
ArrayFrom(usageSet),
extractable);
}
function getAlgorithmName(hash) {
switch (hash) {
case 'SHA-1': // Fall through
case 'SHA-256': // Fall through
case 'SHA-384': // Fall through
case 'SHA-512': // Fall through
return `HS${hash.slice(4)}`;
default:
throw lazyDOMException('Unsupported digest algorithm', 'DataError');
}
}
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,
) {
validateHmacImportKeyAlgorithm(algorithm);
const usagesSet = new SafeSet(keyUsages);
if (hasAnyNotIn(usagesSet, ['sign', 'verify'])) {
throw lazyDOMException(
'Unsupported key usage for an HMAC key',
'SyntaxError');
}
let keyObject;
switch (format) {
case 'KeyObject': {
keyObject = keyData;
break;
}
case 'raw': {
keyObject = createSecretKey(keyData);
break;
}
case 'jwk': {
if (!keyData.kty)
throw lazyDOMException('Invalid keyData', 'DataError');
if (keyData.kty !== 'oct')
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
if (usagesSet.size > 0 &&
keyData.use !== undefined &&
keyData.use !== 'sig') {
throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
}
validateKeyOps(keyData.key_ops, usagesSet);
if (keyData.ext !== undefined &&
keyData.ext === false &&
extractable === true) {
throw lazyDOMException(
'JWK "ext" Parameter and extractable mismatch',
'DataError');
}
if (keyData.alg !== undefined) {
if (keyData.alg !== getAlgorithmName(algorithm.hash.name))
throw lazyDOMException(
'JWK "alg" does not match the requested algorithm',
'DataError');
}
const handle = new KeyObjectHandle();
try {
handle.initJwk(keyData);
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
keyObject = new SecretKeyObject(handle);
break;
}
default:
throw lazyDOMException(`Unable to import HMAC key with format ${format}`);
}
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',
hash: algorithm.hash,
length,
},
keyUsages,
extractable);
}
function hmacSignVerify(key, data, algorithm, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
return jobPromise(() => new HmacJob(
kCryptoJobAsync,
mode,
normalizeHashName(key.algorithm.hash.name),
key[kKeyObject][kHandle],
data,
signature));
}
module.exports = {
hmacImportKey,
hmacGenerateKey,
hmacSignVerify,
};