crypto: use CryptoKey internal slots in Web Cryptography

PR-URL: https://github.com/nodejs/node/pull/59538
Fixes: https://github.com/nodejs/node/issues/59535
Fixes: https://github.com/nodejs/node/issues/59534
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
Filip Skokan 2025-08-19 14:02:15 +02:00 committed by Node.js GitHub Bot
parent 7a47cbf4c5
commit 9d744b5b63
10 changed files with 98 additions and 77 deletions

View File

@ -47,6 +47,7 @@ const {
InternalCryptoKey,
SecretKeyObject,
createSecretKey,
kAlgorithm,
} = require('internal/crypto/keys');
const {
@ -108,7 +109,7 @@ function asyncAesCtrCipher(mode, key, data, algorithm) {
mode,
key[kKeyObject][kHandle],
data,
getVariant('AES-CTR', key.algorithm.length),
getVariant('AES-CTR', key[kAlgorithm].length),
algorithm.counter,
algorithm.length));
}
@ -119,7 +120,7 @@ function asyncAesCbcCipher(mode, key, data, algorithm) {
mode,
key[kKeyObject][kHandle],
data,
getVariant('AES-CBC', key.algorithm.length),
getVariant('AES-CBC', key[kAlgorithm].length),
algorithm.iv));
}
@ -129,7 +130,7 @@ function asyncAesKwCipher(mode, key, data) {
mode,
key[kKeyObject][kHandle],
data,
getVariant('AES-KW', key.algorithm.length)));
getVariant('AES-KW', key[kAlgorithm].length)));
}
function asyncAesGcmCipher(mode, key, data, algorithm) {
@ -166,7 +167,7 @@ function asyncAesGcmCipher(mode, key, data, algorithm) {
mode,
key[kKeyObject][kHandle],
data,
getVariant('AES-GCM', key.algorithm.length),
getVariant('AES-GCM', key[kAlgorithm].length),
algorithm.iv,
tag,
algorithm.additionalData));

View File

@ -47,6 +47,7 @@ const {
PublicKeyObject,
createPrivateKey,
createPublicKey,
kKeyType,
} = require('internal/crypto/keys');
const generateKeyPair = promisify(_generateKeyPair);
@ -343,7 +344,7 @@ function eddsaSignVerify(key, data, algorithm, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';
if (key.type !== type)
if (key[kKeyType] !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
return jobPromise(() => new SignJob(

View File

@ -52,6 +52,8 @@ const {
const {
KeyObject,
kAlgorithm,
kKeyType,
} = require('internal/crypto/keys');
const {
@ -328,20 +330,20 @@ let masks;
async function ecdhDeriveBits(algorithm, baseKey, length) {
const { 'public': key } = algorithm;
if (baseKey.type !== 'private') {
if (baseKey[kKeyType] !== 'private') {
throw lazyDOMException(
'baseKey must be a private key', 'InvalidAccessError');
}
if (key.algorithm.name !== baseKey.algorithm.name) {
if (key[kAlgorithm].name !== baseKey[kAlgorithm].name) {
throw lazyDOMException(
'The public and private keys must be of the same type',
'InvalidAccessError');
}
if (
key.algorithm.name === 'ECDH' &&
key.algorithm.namedCurve !== baseKey.algorithm.namedCurve
key[kAlgorithm].name === 'ECDH' &&
key[kAlgorithm].namedCurve !== baseKey[kAlgorithm].namedCurve
) {
throw lazyDOMException('Named curve mismatch', 'InvalidAccessError');
}

View File

@ -41,6 +41,7 @@ const {
PublicKeyObject,
createPrivateKey,
createPublicKey,
kKeyType,
} = require('internal/crypto/keys');
const generateKeyPair = promisify(_generateKeyPair);
@ -284,7 +285,7 @@ function ecdsaSignVerify(key, data, { name, hash }, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';
if (key.type !== type)
if (key[kKeyType] !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
const hashname = normalizeHashName(hash.name);

View File

@ -217,7 +217,7 @@ const {
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
if (result.usages.length === 0) {
if (result[kKeyUsages].length === 0) {
throw lazyDOMException(
`Usages cannot be empty when importing a ${result.type} key.`,
'SyntaxError');
@ -309,7 +309,7 @@ const {
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
if (result.type === 'private' && result.usages.length === 0) {
if (result.type === 'private' && result[kKeyUsages].length === 0) {
throw lazyDOMException(
`Usages cannot be empty when importing a ${result.type} key.`,
'SyntaxError');
@ -735,8 +735,8 @@ function prepareSecretKey(key, encoding, bufferOnly = false) {
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret');
return key[kHandle];
} else if (isCryptoKey(key)) {
if (key.type !== 'secret')
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret');
if (key[kKeyType] !== 'secret')
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key[kKeyType], 'secret');
return key[kKeyObject][kHandle];
}
}
@ -785,7 +785,7 @@ function createPrivateKey(key) {
}
function isKeyObject(obj) {
return obj != null && obj[kKeyType] !== undefined;
return obj != null && obj[kKeyType] !== undefined && obj[kKeyObject] === undefined;
}
// Our implementation of CryptoKey is a simple wrapper around a KeyObject
@ -809,17 +809,21 @@ class CryptoKey {
};
return `CryptoKey ${inspect({
type: this.type,
extractable: this.extractable,
algorithm: this.algorithm,
usages: this.usages,
type: this[kKeyType],
extractable: this[kExtractable],
algorithm: this[kAlgorithm],
usages: this[kKeyUsages],
}, opts)}`;
}
get [kKeyType]() {
return this[kKeyObject].type;
}
get type() {
if (!(this instanceof CryptoKey))
throw new ERR_INVALID_THIS('CryptoKey');
return this[kKeyObject].type;
return this[kKeyType];
}
get extractable() {
@ -1008,4 +1012,8 @@ module.exports = {
isKeyObject,
isCryptoKey,
importGenericSecretKey,
kAlgorithm,
kExtractable,
kKeyType,
kKeyUsages,
};

View File

@ -36,6 +36,7 @@ const {
InternalCryptoKey,
SecretKeyObject,
createSecretKey,
kAlgorithm,
} = require('internal/crypto/keys');
const generateKey = promisify(_generateKey);
@ -161,7 +162,7 @@ function hmacSignVerify(key, data, algorithm, signature) {
return jobPromise(() => new HmacJob(
kCryptoJobAsync,
mode,
normalizeHashName(key.algorithm.hash.name),
normalizeHashName(key[kAlgorithm].hash.name),
key[kKeyObject][kHandle],
data,
signature));

View File

@ -51,6 +51,8 @@ const {
PublicKeyObject,
createPrivateKey,
createPublicKey,
kAlgorithm,
kKeyType,
} = require('internal/crypto/keys');
const generateKeyPair = promisify(_generateKeyPair);
@ -116,7 +118,7 @@ function mlDsaExportKey(key, format) {
try {
switch (format) {
case kWebCryptoKeyFormatRaw: {
if (key.type === 'private') {
if (key[kKeyType] === 'private') {
const { priv } = key[kKeyObject][kHandle].exportJwk({}, false);
return Buffer.alloc(32, priv, 'base64url').buffer;
}
@ -136,7 +138,7 @@ function mlDsaExportKey(key, format) {
0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
0x03, 0x00, 0x04, 0x22, 0x80, 0x20,
], 0);
switch (key.algorithm.name) {
switch (key[kAlgorithm].name) {
case 'ML-DSA-44':
buffer.set([0x11], 17);
break;
@ -292,7 +294,7 @@ function mlDsaSignVerify(key, data, algorithm, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';
if (key.type !== type)
if (key[kKeyType] !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
return jobPromise(() => new SignJob(

View File

@ -50,6 +50,8 @@ const {
PublicKeyObject,
createPublicKey,
createPrivateKey,
kAlgorithm,
kKeyType,
} = require('internal/crypto/keys');
const {
@ -95,7 +97,7 @@ function rsaOaepCipher(mode, key, data, algorithm) {
validateRsaOaepAlgorithm(algorithm);
const type = mode === kWebCryptoCipherEncrypt ? 'public' : 'private';
if (key.type !== type) {
if (key[kKeyType] !== type) {
throw lazyDOMException(
'The requested operation is not valid for the provided key',
'InvalidAccessError');
@ -107,7 +109,7 @@ function rsaOaepCipher(mode, key, data, algorithm) {
key[kKeyObject][kHandle],
data,
kKeyVariantRSA_OAEP,
normalizeHashName(key.algorithm.hash.name),
normalizeHashName(key[kAlgorithm].hash.name),
algorithm.label));
}
@ -201,7 +203,7 @@ function rsaExportKey(key, format) {
kCryptoJobAsync,
format,
key[kKeyObject][kHandle],
kRsaVariants[key.algorithm.name]));
kRsaVariants[key[kAlgorithm].name]));
}
function rsaImportKey(
@ -329,16 +331,16 @@ function rsaSignVerify(key, data, { saltLength }, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';
if (key.type !== type)
if (key[kKeyType] !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
return jobPromise(() => {
if (key.algorithm.name === 'RSA-PSS') {
if (key[kAlgorithm].name === 'RSA-PSS') {
validateInt32(
saltLength,
'algorithm.saltLength',
0,
MathCeil((key.algorithm.modulusLength - 1) / 8) - getDigestSizeInBytes(key.algorithm.hash.name) - 2);
MathCeil((key[kAlgorithm].modulusLength - 1) / 8) - getDigestSizeInBytes(key[kAlgorithm].hash.name) - 2);
}
return new SignJob(
@ -349,9 +351,9 @@ function rsaSignVerify(key, data, { saltLength }, signature) {
undefined,
undefined,
data,
normalizeHashName(key.algorithm.hash.name),
normalizeHashName(key[kAlgorithm].hash.name),
saltLength,
key.algorithm.name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined,
key[kAlgorithm].name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined,
undefined,
signature);
});

View File

@ -33,6 +33,10 @@ const {
createPublicKey,
CryptoKey,
importGenericSecretKey,
kAlgorithm,
kKeyUsages,
kExtractable,
kKeyType,
} = require('internal/crypto/keys');
const {
@ -175,9 +179,9 @@ async function generateKey(
if (
(resultType === 'CryptoKey' &&
(result.type === 'secret' || result.type === 'private') &&
result.usages.length === 0) ||
(resultType === 'CryptoKeyPair' && result.privateKey.usages.length === 0)
(result[kKeyType] === 'secret' || result[kKeyType] === 'private') &&
result[kKeyUsages].length === 0) ||
(resultType === 'CryptoKeyPair' && result.privateKey[kKeyUsages].length === 0)
) {
throw lazyDOMException(
'Usages cannot be empty when creating a key.',
@ -209,12 +213,12 @@ async function deriveBits(algorithm, baseKey, length = null) {
}
algorithm = normalizeAlgorithm(algorithm, 'deriveBits');
if (!ArrayPrototypeIncludes(baseKey.usages, 'deriveBits')) {
if (!ArrayPrototypeIncludes(baseKey[kKeyUsages], 'deriveBits')) {
throw lazyDOMException(
'baseKey does not have deriveBits usage',
'InvalidAccessError');
}
if (baseKey.algorithm.name !== algorithm.name)
if (baseKey[kAlgorithm].name !== algorithm.name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
switch (algorithm.name) {
case 'X25519':
@ -296,12 +300,12 @@ async function deriveKey(
algorithm = normalizeAlgorithm(algorithm, 'deriveBits');
derivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm, 'importKey');
if (!ArrayPrototypeIncludes(baseKey.usages, 'deriveKey')) {
if (!ArrayPrototypeIncludes(baseKey[kKeyUsages], 'deriveKey')) {
throw lazyDOMException(
'baseKey does not have deriveKey usage',
'InvalidAccessError');
}
if (baseKey.algorithm.name !== algorithm.name)
if (baseKey[kAlgorithm].name !== algorithm.name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
const length = getKeyLength(normalizeAlgorithm(arguments[2], 'get key length'));
@ -335,7 +339,7 @@ async function deriveKey(
}
async function exportKeySpki(key) {
switch (key.algorithm.name) {
switch (key[kAlgorithm].name) {
case 'RSASSA-PKCS1-v1_5':
// Fall through
case 'RSA-PSS':
@ -371,7 +375,7 @@ async function exportKeySpki(key) {
}
async function exportKeyPkcs8(key) {
switch (key.algorithm.name) {
switch (key[kAlgorithm].name) {
case 'RSASSA-PKCS1-v1_5':
// Fall through
case 'RSA-PSS':
@ -407,7 +411,7 @@ async function exportKeyPkcs8(key) {
}
async function exportKeyRawPublic(key, format) {
switch (key.algorithm.name) {
switch (key[kAlgorithm].name) {
case 'ECDSA':
// Fall through
case 'ECDH':
@ -440,7 +444,7 @@ async function exportKeyRawPublic(key, format) {
}
async function exportKeyRawSeed(key) {
switch (key.algorithm.name) {
switch (key[kAlgorithm].name) {
case 'ML-DSA-44':
// Fall through
case 'ML-DSA-65':
@ -455,7 +459,7 @@ async function exportKeyRawSeed(key) {
}
async function exportKeyRawSecret(key, format) {
switch (key.algorithm.name) {
switch (key[kAlgorithm].name) {
case 'AES-CTR':
// Fall through
case 'AES-CBC':
@ -478,27 +482,27 @@ async function exportKeyRawSecret(key, format) {
async function exportKeyJWK(key) {
const parameters = {
key_ops: key.usages,
ext: key.extractable,
key_ops: key[kKeyUsages],
ext: key[kExtractable],
};
switch (key.algorithm.name) {
switch (key[kAlgorithm].name) {
case 'RSASSA-PKCS1-v1_5': {
const alg = normalizeHashName(
key.algorithm.hash.name,
key[kAlgorithm].hash.name,
normalizeHashName.kContextJwkRsa);
if (alg) parameters.alg = alg;
break;
}
case 'RSA-PSS': {
const alg = normalizeHashName(
key.algorithm.hash.name,
key[kAlgorithm].hash.name,
normalizeHashName.kContextJwkRsaPss);
if (alg) parameters.alg = alg;
break;
}
case 'RSA-OAEP': {
const alg = normalizeHashName(
key.algorithm.hash.name,
key[kAlgorithm].hash.name,
normalizeHashName.kContextJwkRsaOaep);
if (alg) parameters.alg = alg;
break;
@ -520,7 +524,7 @@ async function exportKeyJWK(key) {
case 'Ed25519':
// Fall through
case 'Ed448':
parameters.alg = key.algorithm.name;
parameters.alg = key[kAlgorithm].name;
break;
case 'AES-CTR':
// Fall through
@ -530,14 +534,14 @@ async function exportKeyJWK(key) {
// Fall through
case 'AES-KW':
parameters.alg = require('internal/crypto/aes')
.getAlgorithmName(key.algorithm.name, key.algorithm.length);
.getAlgorithmName(key[kAlgorithm].name, key[kAlgorithm].length);
break;
case 'ChaCha20-Poly1305':
parameters.alg = 'C20P';
break;
case 'HMAC': {
const alg = normalizeHashName(
key.algorithm.hash.name,
key[kAlgorithm].hash.name,
normalizeHashName.kContextJwkHmac);
if (alg) parameters.alg = alg;
break;
@ -565,25 +569,25 @@ async function exportKey(format, key) {
});
try {
normalizeAlgorithm(key.algorithm, 'exportKey');
normalizeAlgorithm(key[kAlgorithm], 'exportKey');
} catch {
throw lazyDOMException(
`${key.algorithm.name} key export is not supported`, 'NotSupportedError');
`${key[kAlgorithm].name} key export is not supported`, 'NotSupportedError');
}
if (!key.extractable)
if (!key[kExtractable])
throw lazyDOMException('key is not extractable', 'InvalidAccessException');
let result;
switch (format) {
case 'spki': {
if (key.type === 'public') {
if (key[kKeyType] === 'public') {
result = await exportKeySpki(key);
}
break;
}
case 'pkcs8': {
if (key.type === 'private') {
if (key[kKeyType] === 'private') {
result = await exportKeyPkcs8(key);
}
break;
@ -593,27 +597,27 @@ async function exportKey(format, key) {
break;
}
case 'raw-secret': {
if (key.type === 'secret') {
if (key[kKeyType] === 'secret') {
result = await exportKeyRawSecret(key, format);
}
break;
}
case 'raw-public': {
if (key.type === 'public') {
if (key[kKeyType] === 'public') {
result = await exportKeyRawPublic(key, format);
}
break;
}
case 'raw-seed': {
if (key.type === 'private') {
if (key[kKeyType] === 'private') {
result = await exportKeyRawSeed(key);
}
break;
}
case 'raw': {
if (key.type === 'secret') {
if (key[kKeyType] === 'secret') {
result = await exportKeyRawSecret(key, format);
} else if (key.type === 'public') {
} else if (key[kKeyType] === 'public') {
result = await exportKeyRawPublic(key, format);
}
break;
@ -622,7 +626,7 @@ async function exportKey(format, key) {
if (!result) {
throw lazyDOMException(
`Unable to export ${key.algorithm.name} ${key.type} key using ${format} format`,
`Unable to export ${key[kAlgorithm].name} ${key[kKeyType]} key using ${format} format`,
'NotSupportedError');
}
@ -749,7 +753,7 @@ async function importKey(
'NotSupportedError');
}
if ((result.type === 'secret' || result.type === 'private') && result.usages.length === 0) {
if ((result.type === 'secret' || result.type === 'private') && result[kKeyUsages].length === 0) {
throw lazyDOMException(
`Usages cannot be empty when importing a ${result.type} key.`,
'SyntaxError');
@ -897,8 +901,8 @@ function signVerify(algorithm, key, data, signature) {
}
algorithm = normalizeAlgorithm(algorithm, usage);
if (!ArrayPrototypeIncludes(key.usages, usage) ||
algorithm.name !== key.algorithm.name) {
if (!ArrayPrototypeIncludes(key[kKeyUsages], usage) ||
algorithm.name !== key[kAlgorithm].name) {
throw lazyDOMException(
`Unable to use this key to ${usage}`,
'InvalidAccessError');
@ -987,8 +991,8 @@ async function cipherOrWrap(mode, algorithm, key, data, op) {
// in this case. Both Firefox and Chrome throw simple TypeErrors here.
// The key algorithm and cipher algorithm must match, and the
// key must have the proper usage.
if (key.algorithm.name !== algorithm.name ||
!ArrayPrototypeIncludes(key.usages, op)) {
if (key[kAlgorithm].name !== algorithm.name ||
!ArrayPrototypeIncludes(key[kKeyUsages], op)) {
throw lazyDOMException(
'The requested operation is not valid for the provided key',
'InvalidAccessError');
@ -1085,12 +1089,12 @@ async function getPublicKey(key, keyUsages) {
context: '2nd argument',
});
if (key.type !== 'private')
if (key[kKeyType] !== 'private')
throw lazyDOMException('key must be a private key', 'InvalidAccessError');
const keyObject = createPublicKey(key[kKeyObject]);
return keyObject.toCryptoKey(key.algorithm, true, keyUsages);
return keyObject.toCryptoKey(key[kAlgorithm], true, keyUsages);
}
// The SubtleCrypto and Crypto classes are defined as part of the

View File

@ -136,8 +136,6 @@ const kIsArray = 1;
const kIsSet = 2;
const kIsMap = 3;
let kKeyObject;
// Check if they have the same source and flags
function areSimilarRegExps(a, b) {
return a.source === b.source &&
@ -401,11 +399,12 @@ function objectComparisonStart(val1, val2, mode, memos) {
return false;
}
} else if (isCryptoKey(val1)) {
kKeyObject ??= require('internal/crypto/util').kKeyObject;
const { kKeyObject } = require('internal/crypto/util');
const { kExtractable, kAlgorithm, kKeyUsages } = require('internal/crypto/keys');
if (!isCryptoKey(val2) ||
val1.extractable !== val2.extractable ||
!innerDeepEqual(val1.algorithm, val2.algorithm, mode, memos) ||
!innerDeepEqual(val1.usages, val2.usages, mode, memos) ||
val1[kExtractable] !== val2[kExtractable] ||
!innerDeepEqual(val1[kAlgorithm], val2[kAlgorithm], mode, memos) ||
!innerDeepEqual(val1[kKeyUsages], val2[kKeyUsages], mode, memos) ||
!innerDeepEqual(val1[kKeyObject], val2[kKeyObject], mode, memos)
) {
return false;