src: decouple KeyObject and CryptoKey and move CryptoKey to src

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/62924
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Filip Skokan committed 2026-05-04 00:09:07 +02:00
1 parent bb26491933
commit 25f80fb764
40 files changed
+2155 -763

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
'use strict';
const common = require('../common.js');
const { hasOpenSSL } = require('../../test/common/crypto.js');
const { subtle } = globalThis.crypto;
const kAlgorithms = {
'ec': { name: 'ECDSA', namedCurve: 'P-256' },
'rsassa-pkcs1-v1_5': {
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
'rsa-pss': {
name: 'RSA-PSS',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
'ed25519': { name: 'Ed25519' },
};
if (hasOpenSSL(3, 5)) {
kAlgorithms['ml-dsa-44'] = { name: 'ML-DSA-44' };
}
const kSignParams = {
'ec': { name: 'ECDSA', hash: 'SHA-256' },
'rsassa-pkcs1-v1_5': { name: 'RSASSA-PKCS1-v1_5' },
'rsa-pss': { name: 'RSA-PSS', saltLength: 32 },
'ed25519': { name: 'Ed25519' },
'ml-dsa-44': { name: 'ML-DSA-44' },
};
const data = globalThis.crypto.getRandomValues(new Uint8Array(256));
let keys;
const bench = common.createBenchmark(main, {
keyType: Object.keys(kAlgorithms),
mode: ['serial', 'parallel'],
keyReuse: ['shared', 'unique'],
n: [1e3],
}, {
combinationFilter(p) {
// Unique only differs from shared when operations overlap (parallel);
// sequential calls have no contention so unique+serial adds no value.
if (p.keyReuse === 'unique') return p.mode === 'parallel';
return true;
},
});
async function measureSerial(n, signParams, sharedKey) {
bench.start();
for (let i = 0; i < n; ++i) {
await subtle.sign(signParams, sharedKey || keys[i], data);
}
bench.end(n);
}
async function measureParallel(n, signParams, sharedKey) {
const promises = new Array(n);
bench.start();
for (let i = 0; i < n; ++i) {
promises[i] = subtle.sign(signParams, sharedKey || keys[i], data);
}
await Promise.all(promises);
bench.end(n);
}
async function main({ n, mode, keyReuse, keyType }) {
const algorithm = kAlgorithms[keyType];
const signParams = kSignParams[keyType];
if (!keys || keys.length !== n || keys[0].algorithm.name !== signParams.name) {
keys = new Array(n);
// Generate one key pair, then import its pkcs8 bytes n times to get
// distinct CryptoKey instances.
const kp = await subtle.generateKey(algorithm, true, ['sign', 'verify']);
const pkcs8 = await subtle.exportKey('pkcs8', kp.privateKey);
for (let i = 0; i < n; ++i) {
keys[i] = await subtle.importKey('pkcs8', pkcs8, algorithm, false, ['sign']);
}
}
const sharedKey = keyReuse === 'shared' ? keys[0] : undefined;
switch (mode) {
case 'serial':
await measureSerial(n, signParams, sharedKey);
break;
case 'parallel':
await measureParallel(n, signParams, sharedKey);
break;
}
}
+100
View File
@@ -0,0 +1,100 @@
'use strict';
const common = require('../common.js');
const { hasOpenSSL } = require('../../test/common/crypto.js');
const { subtle } = globalThis.crypto;
const kAlgorithms = {
'ec': { name: 'ECDSA', namedCurve: 'P-256' },
'rsassa-pkcs1-v1_5': {
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
'rsa-pss': {
name: 'RSA-PSS',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
'ed25519': { name: 'Ed25519' },
};
if (hasOpenSSL(3, 5)) {
kAlgorithms['ml-dsa-44'] = { name: 'ML-DSA-44' };
}
const kSignParams = {
'ec': { name: 'ECDSA', hash: 'SHA-256' },
'rsassa-pkcs1-v1_5': { name: 'RSASSA-PKCS1-v1_5' },
'rsa-pss': { name: 'RSA-PSS', saltLength: 32 },
'ed25519': { name: 'Ed25519' },
'ml-dsa-44': { name: 'ML-DSA-44' },
};
const data = globalThis.crypto.getRandomValues(new Uint8Array(256));
let publicKeys;
let signature;
const bench = common.createBenchmark(main, {
keyType: Object.keys(kAlgorithms),
mode: ['serial', 'parallel'],
keyReuse: ['shared', 'unique'],
n: [1e3],
}, {
combinationFilter(p) {
// Unique only differs from shared when operations overlap (parallel);
// sequential calls have no contention so unique+serial adds no value.
if (p.keyReuse === 'unique') return p.mode === 'parallel';
return true;
},
});
async function measureSerial(n, verifyParams, sharedKey) {
bench.start();
for (let i = 0; i < n; ++i) {
await subtle.verify(verifyParams, sharedKey || publicKeys[i], signature, data);
}
bench.end(n);
}
async function measureParallel(n, verifyParams, sharedKey) {
const promises = new Array(n);
bench.start();
for (let i = 0; i < n; ++i) {
promises[i] = subtle.verify(verifyParams, sharedKey || publicKeys[i], signature, data);
}
await Promise.all(promises);
bench.end(n);
}
async function main({ n, mode, keyReuse, keyType }) {
const algorithm = kAlgorithms[keyType];
const verifyParams = kSignParams[keyType];
if (!publicKeys || publicKeys.length !== n ||
publicKeys[0].algorithm.name !== verifyParams.name) {
publicKeys = new Array(n);
// Generate one key pair, then import its spki bytes n times to get
// distinct CryptoKey instances.
const kp = await subtle.generateKey(algorithm, true, ['sign', 'verify']);
const spki = await subtle.exportKey('spki', kp.publicKey);
for (let i = 0; i < n; ++i) {
publicKeys[i] = await subtle.importKey('spki', spki, algorithm, false, ['verify']);
}
signature = await subtle.sign(verifyParams, kp.privateKey, data);
}
const sharedKey = keyReuse === 'shared' ? publicKeys[0] : undefined;
switch (mode) {
case 'serial':
await measureSerial(n, verifyParams, sharedKey);
break;
case 'parallel':
await measureParallel(n, verifyParams, sharedKey);
break;
}
}
+4
View File
@@ -71,6 +71,10 @@ export default [
selector: "CallExpression[callee.type='Identifier'][callee.name='ReflectApply'][arguments.2.type='ArrayExpression']",
message: 'Use `FunctionPrototypeCall` to avoid creating an ad-hoc array',
},
{
selector: "VariableDeclarator[init.type='CallExpression'][init.callee.name='internalBinding'][init.arguments.0.value='crypto'] > ObjectPattern > Property[key.name='getCryptoKeySlots']",
message: "Use `const { getCryptoKeySlots } = require('internal/crypto/keys');` instead of destructuring it from `internalBinding('crypto')`.",
},
],
'no-restricted-globals': [
'error',
+30 -60
View File
@@ -7,10 +7,7 @@ const {
const {
AESCipherJob,
KeyObjectHandle,
kCryptoJobAsync,
kKeyFormatJWK,
kKeyTypeSecret,
kKeyVariantAES_CTR_128,
kKeyVariantAES_CBC_128,
kKeyVariantAES_GCM_128,
@@ -26,37 +23,32 @@ const {
kKeyVariantAES_GCM_256,
kKeyVariantAES_KW_256,
kKeyVariantAES_OCB_256,
SecretKeyGenJob,
} = internalBinding('crypto');
const {
getUsagesMask,
hasAnyNotIn,
jobPromise,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
InternalCryptoKey,
SecretKeyObject,
createSecretKey,
kAlgorithm,
getCryptoKeyAlgorithm,
getCryptoKeyHandle,
} = require('internal/crypto/keys');
const {
importJwkSecretKey,
importSecretKey,
validateJwk,
} = require('internal/crypto/webcrypto_util');
const {
generateKey: _generateKey,
} = require('internal/crypto/keygen');
const generateKey = promisify(_generateKey);
function getAlgorithmName(name, length) {
switch (name) {
case 'AES-CBC': return `A${length}CBC`;
@@ -116,9 +108,9 @@ function asyncAesCtrCipher(mode, key, data, algorithm) {
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
data,
getVariant('AES-CTR', key[kAlgorithm].length),
getVariant('AES-CTR', getCryptoKeyAlgorithm(key).length),
algorithm.counter,
algorithm.length));
}
@@ -127,9 +119,9 @@ function asyncAesCbcCipher(mode, key, data, algorithm) {
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
data,
getVariant('AES-CBC', key[kAlgorithm].length),
getVariant('AES-CBC', getCryptoKeyAlgorithm(key).length),
algorithm.iv));
}
@@ -137,9 +129,9 @@ function asyncAesKwCipher(mode, key, data) {
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
data,
getVariant('AES-KW', key[kAlgorithm].length)));
getVariant('AES-KW', getCryptoKeyAlgorithm(key).length)));
}
function asyncAesGcmCipher(mode, key, data, algorithm) {
@@ -149,9 +141,9 @@ function asyncAesGcmCipher(mode, key, data, algorithm) {
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
data,
getVariant('AES-GCM', key[kAlgorithm].length),
getVariant('AES-GCM', getCryptoKeyAlgorithm(key).length),
algorithm.iv,
tagByteLength,
algorithm.additionalData));
@@ -164,9 +156,9 @@ function asyncAesOcbCipher(mode, key, data, algorithm) {
return jobPromise(() => new AESCipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
data,
getVariant('AES-OCB', key.algorithm.length),
getVariant('AES-OCB', getCryptoKeyAlgorithm(key).length),
algorithm.iv,
tagByteLength,
algorithm.additionalData));
@@ -196,20 +188,12 @@ async function aesGenerateKey(algorithm, extractable, keyUsages) {
'SyntaxError');
}
let key;
try {
key = await generateKey('aes', { length });
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason' +
`[${err.message}]`,
{ name: 'OperationError', cause: err });
}
const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length));
return new InternalCryptoKey(
key,
handle,
{ name, length },
usagesSet,
getUsagesMask(usagesSet),
extractable);
}
@@ -231,12 +215,13 @@ function aesImportKey(
'SyntaxError');
}
let keyObject;
let handle;
let length;
switch (format) {
case 'KeyObject': {
validateKeyLength(keyData.symmetricKeySize * 8);
keyObject = keyData;
length = keyData.symmetricKeySize * 8;
validateKeyLength(length);
handle = keyData[kHandle];
break;
}
case 'raw-secret':
@@ -244,47 +229,32 @@ function aesImportKey(
if (format === 'raw' && name === 'AES-OCB') {
return undefined;
}
validateKeyLength(keyData.byteLength * 8);
keyObject = createSecretKey(keyData);
length = keyData.byteLength * 8;
validateKeyLength(length);
handle = importSecretKey(keyData);
break;
}
case 'jwk': {
validateJwk(keyData, 'oct', extractable, usagesSet, 'enc');
const handle = new KeyObjectHandle();
try {
handle.init(kKeyTypeSecret, keyData, kKeyFormatJWK, null, null);
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
({ length } = handle.keyDetail({ }));
handle = importJwkSecretKey(keyData);
length = handle.getSymmetricKeySize() * 8;
validateKeyLength(length);
if (keyData.alg !== undefined) {
if (keyData.alg !== getAlgorithmName(algorithm.name, length))
throw lazyDOMException(
'JWK "alg" does not match the requested algorithm',
'DataError');
}
keyObject = new SecretKeyObject(handle);
break;
}
default:
return undefined;
}
if (length === undefined) {
({ length } = keyObject[kHandle].keyDetail({ }));
validateKeyLength(length);
}
return new InternalCryptoKey(
keyObject,
handle,
{ name, length },
usagesSet,
getUsagesMask(usagesSet),
extractable);
}
+6 -2
View File
@@ -24,9 +24,12 @@ const {
promisify,
} = require('internal/util');
const {
getCryptoKeyHandle,
} = require('internal/crypto/keys');
const {
getArrayBufferOrView,
kKeyObject,
} = require('internal/crypto/util');
const {
@@ -216,7 +219,8 @@ async function argon2DeriveBits(algorithm, baseKey, length) {
result = await argon2Promise(
StringPrototypeToLowerCase(algorithm.name),
{
message: baseKey[kKeyObject].export(),
// TODO(panva): call the job directly without needing to re-export the handle
message: getCryptoKeyHandle(baseKey).export(),
nonce: algorithm.nonce,
parallelism: algorithm.parallelism,
tagLength: length / 8,
+33 -41
View File
@@ -16,28 +16,29 @@ const {
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatSPKI,
NidKeyPairGenJob,
EVP_PKEY_ED25519,
EVP_PKEY_ED448,
EVP_PKEY_X25519,
EVP_PKEY_X448,
} = internalBinding('crypto');
const {
getUsagesMask,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
generateKeyPair: _generateKeyPair,
} = require('internal/crypto/keygen');
const {
getCryptoKeyHandle,
getCryptoKeyType,
InternalCryptoKey,
kKeyType,
} = require('internal/crypto/keys');
const {
@@ -47,8 +48,6 @@ const {
validateJwk,
} = require('internal/crypto/webcrypto_util');
const generateKeyPair = promisify(_generateKeyPair);
function verifyAcceptableCfrgKeyUse(name, isPublic, usages) {
let checkSet;
switch (name) {
@@ -97,30 +96,23 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) {
}
break;
}
let genKeyType;
let nid;
switch (name) {
case 'Ed25519':
genKeyType = 'ed25519';
nid = EVP_PKEY_ED25519;
break;
case 'Ed448':
genKeyType = 'ed448';
nid = EVP_PKEY_ED448;
break;
case 'X25519':
genKeyType = 'x25519';
nid = EVP_PKEY_X25519;
break;
case 'X448':
genKeyType = 'x448';
nid = EVP_PKEY_X448;
break;
}
let keyPair;
try {
keyPair = await generateKeyPair(genKeyType);
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
}
const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid));
let publicUsages;
let privateUsages;
@@ -143,16 +135,16 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) {
const publicKey =
new InternalCryptoKey(
keyPair.publicKey,
handles[0],
keyAlgorithm,
publicUsages,
getUsagesMask(publicUsages),
true);
const privateKey =
new InternalCryptoKey(
keyPair.privateKey,
handles[1],
keyAlgorithm,
privateUsages,
getUsagesMask(privateUsages),
extractable);
return { __proto__: null, privateKey, publicKey };
@@ -162,17 +154,17 @@ function cfrgExportKey(key, format) {
try {
switch (format) {
case kWebCryptoKeyFormatRaw: {
const handle = key[kKeyObject][kHandle];
const handle = getCryptoKeyHandle(key);
return TypedArrayPrototypeGetBuffer(
key[kKeyType] === 'private' ? handle.rawPrivateKey() : handle.rawPublicKey());
getCryptoKeyType(key) === 'private' ? handle.rawPrivateKey() : handle.rawPublicKey());
}
case kWebCryptoKeyFormatSPKI: {
return TypedArrayPrototypeGetBuffer(
key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI));
getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatSPKI));
}
case kWebCryptoKeyFormatPKCS8: {
return TypedArrayPrototypeGetBuffer(
key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null));
getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null));
}
default:
return undefined;
@@ -192,22 +184,22 @@ function cfrgImportKey(
keyUsages) {
const { name } = algorithm;
let keyObject;
let handle;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'KeyObject': {
verifyAcceptableCfrgKeyUse(name, keyData.type === 'public', usagesSet);
keyObject = keyData;
handle = keyData[kHandle];
break;
}
case 'spki': {
verifyAcceptableCfrgKeyUse(name, true, usagesSet);
keyObject = importDerKey(keyData, true);
handle = importDerKey(keyData, true);
break;
}
case 'pkcs8': {
verifyAcceptableCfrgKeyUse(name, false, usagesSet);
keyObject = importDerKey(keyData, false);
handle = importDerKey(keyData, false);
break;
}
case 'jwk': {
@@ -226,26 +218,26 @@ function cfrgImportKey(
const isPublic = keyData.d === undefined;
verifyAcceptableCfrgKeyUse(name, isPublic, usagesSet);
keyObject = importJwkKey(isPublic, keyData);
handle = importJwkKey(isPublic, keyData);
break;
}
case 'raw': {
verifyAcceptableCfrgKeyUse(name, true, usagesSet);
keyObject = importRawKey(true, keyData, kKeyFormatRawPublic, name);
handle = importRawKey(true, keyData, kKeyFormatRawPublic, name);
break;
}
default:
return undefined;
}
if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) {
if (handle.getAsymmetricKeyType() !== StringPrototypeToLowerCase(name)) {
throw lazyDOMException('Invalid key type', 'DataError');
}
return new InternalCryptoKey(
keyObject,
handle,
{ name },
usagesSet,
getUsagesMask(usagesSet),
extractable);
}
@@ -253,13 +245,13 @@ async function eddsaSignVerify(key, data, algorithm, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';
if (key[kKeyType] !== type)
if (getCryptoKeyType(key) !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
return await jobPromise(() => new SignJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
undefined,
undefined,
undefined,
+16 -39
View File
@@ -6,40 +6,32 @@ const {
const {
ChaCha20Poly1305CipherJob,
KeyObjectHandle,
SecretKeyGenJob,
kCryptoJobAsync,
kKeyFormatJWK,
kKeyTypeSecret,
} = internalBinding('crypto');
const {
getUsagesMask,
hasAnyNotIn,
jobPromise,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
InternalCryptoKey,
SecretKeyObject,
createSecretKey,
getCryptoKeyHandle,
} = require('internal/crypto/keys');
const {
importJwkSecretKey,
importSecretKey,
validateJwk,
} = require('internal/crypto/webcrypto_util');
const {
randomBytes: _randomBytes,
} = require('internal/crypto/random');
const randomBytes = promisify(_randomBytes);
function validateKeyLength(length) {
if (length !== 256)
throw lazyDOMException('Invalid key length', 'DataError');
@@ -49,7 +41,7 @@ function c20pCipher(mode, key, data, algorithm) {
return jobPromise(() => new ChaCha20Poly1305CipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
data,
algorithm.iv,
algorithm.additionalData));
@@ -67,20 +59,12 @@ async function c20pGenerateKey(algorithm, extractable, keyUsages) {
'SyntaxError');
}
let keyData;
try {
keyData = await randomBytes(32);
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason' +
`[${err.message}]`,
{ name: 'OperationError', cause: err });
}
const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, 256));
return new InternalCryptoKey(
createSecretKey(keyData),
handle,
{ name },
usagesSet,
getUsagesMask(usagesSet),
extractable);
}
@@ -100,26 +84,20 @@ function c20pImportKey(
'SyntaxError');
}
let keyObject;
let handle;
switch (format) {
case 'KeyObject': {
keyObject = keyData;
handle = keyData[kHandle];
break;
}
case 'raw-secret': {
keyObject = createSecretKey(keyData);
handle = importSecretKey(keyData);
break;
}
case 'jwk': {
validateJwk(keyData, 'oct', extractable, usagesSet, 'enc');
const handle = new KeyObjectHandle();
try {
handle.init(kKeyTypeSecret, keyData, kKeyFormatJWK, null, null);
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
handle = importJwkSecretKey(keyData);
if (keyData.alg !== undefined && keyData.alg !== 'C20P') {
throw lazyDOMException(
@@ -127,19 +105,18 @@ function c20pImportKey(
'DataError');
}
keyObject = new SecretKeyObject(handle);
break;
}
default:
return undefined;
}
validateKeyLength(keyObject.symmetricKeySize * 8);
validateKeyLength(handle.getSymmetricKeySize() * 8);
return new InternalCryptoKey(
keyObject,
handle,
{ name },
usagesSet,
getUsagesMask(usagesSet),
extractable);
}
+11 -9
View File
@@ -47,8 +47,9 @@ const {
} = require('internal/util');
const {
kAlgorithm,
kKeyType,
getCryptoKeyAlgorithm,
getCryptoKeyHandle,
getCryptoKeyType,
preparePrivateKey,
preparePublicOrPrivateKey,
} = require('internal/crypto/keys');
@@ -58,7 +59,6 @@ const {
jobPromise,
toBuf,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
@@ -329,32 +329,34 @@ let masks;
async function ecdhDeriveBits(algorithm, baseKey, length) {
const { 'public': key } = algorithm;
if (baseKey[kKeyType] !== 'private') {
if (getCryptoKeyType(baseKey) !== 'private') {
throw lazyDOMException(
'baseKey must be a private key', 'InvalidAccessError');
}
if (key[kAlgorithm].name !== baseKey[kAlgorithm].name) {
const keyAlgorithm = getCryptoKeyAlgorithm(key);
const baseKeyAlgorithm = getCryptoKeyAlgorithm(baseKey);
if (keyAlgorithm.name !== baseKeyAlgorithm.name) {
throw lazyDOMException(
'The public and private keys must be of the same type',
'InvalidAccessError');
}
if (
key[kAlgorithm].name === 'ECDH' &&
key[kAlgorithm].namedCurve !== baseKey[kAlgorithm].namedCurve
keyAlgorithm.name === 'ECDH' &&
keyAlgorithm.namedCurve !== baseKeyAlgorithm.namedCurve
) {
throw lazyDOMException('Named curve mismatch', 'InvalidAccessError');
}
const bits = await jobPromise(() => new DHBitsJob(
kCryptoJobAsync,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
undefined,
undefined,
undefined,
undefined,
baseKey[kKeyObject][kHandle],
getCryptoKeyHandle(baseKey),
undefined,
undefined,
undefined,
+28 -41
View File
@@ -7,6 +7,7 @@ const {
} = primordials;
const {
EcKeyPairGenJob,
KeyObjectHandle,
SignJob,
kCryptoJobAsync,
@@ -28,28 +29,24 @@ const {
} = internalBinding('constants');
const {
getUsagesMask,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
normalizeHashName,
kHandle,
kKeyObject,
kNamedCurveAliases,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
generateKeyPair: _generateKeyPair,
} = require('internal/crypto/keygen');
const {
InternalCryptoKey,
kAlgorithm,
kKeyType,
getCryptoKeyAlgorithm,
getCryptoKeyHandle,
getCryptoKeyType,
} = require('internal/crypto/keys');
const {
@@ -59,8 +56,6 @@ const {
validateJwk,
} = require('internal/crypto/webcrypto_util');
const generateKeyPair = promisify(_generateKeyPair);
function verifyAcceptableEcKeyUse(name, isPublic, usages) {
let checkSet;
switch (name) {
@@ -102,14 +97,8 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) {
// Fall through
}
let keyPair;
try {
keyPair = await generateKeyPair('ec', { namedCurve });
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
}
const handles = await jobPromise(() => new EcKeyPairGenJob(
kCryptoJobAsync, namedCurve));
let publicUsages;
let privateUsages;
@@ -128,16 +117,16 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) {
const publicKey =
new InternalCryptoKey(
keyPair.publicKey,
handles[0],
keyAlgorithm,
publicUsages,
getUsagesMask(publicUsages),
true);
const privateKey =
new InternalCryptoKey(
keyPair.privateKey,
handles[1],
keyAlgorithm,
privateUsages,
getUsagesMask(privateUsages),
extractable);
return { __proto__: null, publicKey, privateKey };
@@ -145,7 +134,7 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) {
function ecExportKey(key, format) {
try {
const handle = key[kKeyObject][kHandle];
const handle = getCryptoKeyHandle(key);
switch (format) {
case kWebCryptoKeyFormatRaw: {
return TypedArrayPrototypeGetBuffer(
@@ -161,13 +150,14 @@ function ecExportKey(key, format) {
// P-384: 120 = 23 bytes of SPKI ASN.1 + 97-byte uncompressed point.
// P-521: 158 = 25 bytes of SPKI ASN.1 + 133-byte uncompressed point.
// Difference in initial SPKI ASN.1 is caused by OIDs and length encoding.
const { namedCurve } = getCryptoKeyAlgorithm(key);
if (TypedArrayPrototypeGetByteLength(spki) !== {
'__proto__': null, 'P-256': 91, 'P-384': 120, 'P-521': 158,
}[key[kAlgorithm].namedCurve]) {
}[namedCurve]) {
const raw = handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED);
const tmp = new KeyObjectHandle();
tmp.init(kKeyTypePublic, raw, kKeyFormatRawPublic,
'ec', null, key[kAlgorithm].namedCurve);
'ec', null, namedCurve);
spki = tmp.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI);
}
return TypedArrayPrototypeGetBuffer(spki);
@@ -195,22 +185,22 @@ function ecImportKey(
) {
const { name, namedCurve } = algorithm;
let keyObject;
let handle;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'KeyObject': {
verifyAcceptableEcKeyUse(name, keyData.type === 'public', usagesSet);
keyObject = keyData;
handle = keyData[kHandle];
break;
}
case 'spki': {
verifyAcceptableEcKeyUse(name, true, usagesSet);
keyObject = importDerKey(keyData, true);
handle = importDerKey(keyData, true);
break;
}
case 'pkcs8': {
verifyAcceptableEcKeyUse(name, false, usagesSet);
keyObject = importDerKey(keyData, false);
handle = importDerKey(keyData, false);
break;
}
case 'jwk': {
@@ -237,12 +227,12 @@ function ecImportKey(
const isPublic = keyData.d === undefined;
verifyAcceptableEcKeyUse(name, isPublic, usagesSet);
keyObject = importJwkKey(isPublic, keyData);
handle = importJwkKey(isPublic, keyData);
break;
}
case 'raw': {
verifyAcceptableEcKeyUse(name, true, usagesSet);
keyObject = importRawKey(true, keyData, kKeyFormatRawPublic, 'ec', namedCurve);
handle = importRawKey(true, keyData, kKeyFormatRawPublic, 'ec', namedCurve);
break;
}
default:
@@ -253,25 +243,22 @@ function ecImportKey(
case 'ECDSA':
// Fall through
case 'ECDH':
if (keyObject.asymmetricKeyType !== 'ec')
if (handle.getAsymmetricKeyType() !== 'ec')
throw lazyDOMException('Invalid key type', 'DataError');
break;
}
if (!keyObject[kHandle].checkEcKeyData()) {
if (!handle.checkEcKeyData()) {
throw lazyDOMException('Invalid keyData', 'DataError');
}
const {
namedCurve: checkNamedCurve,
} = keyObject[kHandle].keyDetail({});
if (kNamedCurveAliases[namedCurve] !== checkNamedCurve)
if (kNamedCurveAliases[namedCurve] !== handle.keyDetail({}).namedCurve)
throw lazyDOMException('Named curve mismatch', 'DataError');
return new InternalCryptoKey(
keyObject,
handle,
{ name, namedCurve },
usagesSet,
getUsagesMask(usagesSet),
extractable);
}
@@ -279,7 +266,7 @@ async function ecdsaSignVerify(key, data, { name, hash }, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';
if (key[kKeyType] !== type)
if (getCryptoKeyType(key) !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
const hashname = normalizeHashName(hash.name);
@@ -287,7 +274,7 @@ async function ecdsaSignVerify(key, data, { name, hash }, signature) {
return await jobPromise(() => new SignJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
undefined,
undefined,
undefined,
+9 -6
View File
@@ -20,20 +20,20 @@ const {
const { kMaxLength } = require('buffer');
const {
jobPromise,
normalizeHashName,
toBuf,
validateByteSource,
kKeyObject,
} = require('internal/crypto/util');
const {
createSecretKey,
getCryptoKeyHandle,
isKeyObject,
} = require('internal/crypto/keys');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
@@ -137,7 +137,6 @@ function hkdfSync(hash, key, salt, info, length) {
return bits;
}
const hkdfPromise = promisify(hkdf);
function validateHkdfDeriveBitsLength(length) {
if (length === null)
throw lazyDOMException('length cannot be null', 'OperationError');
@@ -156,9 +155,13 @@ async function hkdfDeriveBits(algorithm, baseKey, length) {
return new ArrayBuffer(0);
try {
return await hkdfPromise(
normalizeHashName(hash.name), baseKey[kKeyObject], salt, info, length / 8,
);
return await jobPromise(() => new HKDFJob(
kCryptoJobAsync,
normalizeHashName(hash.name),
getCryptoKeyHandle(baseKey),
salt,
info,
length / 8));
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
+277 -193
View File
@@ -1,7 +1,6 @@
'use strict';
const {
ArrayFrom,
ArrayPrototypeSlice,
ObjectDefineProperties,
ObjectDefineProperty,
@@ -15,6 +14,9 @@ const {
const {
KeyObjectHandle,
createNativeKeyObjectClass,
createCryptoKeyClass,
// eslint-disable-next-line no-restricted-syntax -- intended here
getCryptoKeySlots: nativeGetCryptoKeySlots,
kKeyTypeSecret,
kKeyTypePublic,
kKeyTypePrivate,
@@ -56,12 +58,13 @@ const {
const {
kHandle,
kKeyObject,
getArrayBufferOrView,
bigIntArrayToUnsignedBigInt,
normalizeAlgorithm,
hasAnyNotIn,
getSortedUsages,
getUsagesMask,
getUsagesFromMask,
hasUsage,
} = require('internal/crypto/util');
const {
@@ -69,12 +72,6 @@ const {
isArrayBufferView,
} = require('internal/util/types');
const {
markTransferMode,
kClone,
kDeserialize,
} = require('internal/worker/js_transferable');
const {
customInspectSymbol: kInspect,
getDeprecationWarningEmitter,
@@ -84,12 +81,12 @@ const {
const { inspect } = require('internal/util/inspect');
const kAlgorithm = Symbol('kAlgorithm');
const kExtractable = Symbol('kExtractable');
// Module-local symbol used by KeyObject to store its `type` string
// ("secret"/"public"/"private"). It is also used by `isKeyObject` to
// distinguish KeyObject instances from other types. CryptoKey no longer
// uses any module-local symbol slots - its state lives in C++ internal
// fields on `NativeCryptoKey`.
const kKeyType = Symbol('kKeyType');
const kKeyUsages = Symbol('kKeyUsages');
const kCachedAlgorithm = Symbol('kCachedAlgorithm');
const kCachedKeyUsages = Symbol('kCachedKeyUsages');
const emitDEP0203 = getDeprecationWarningEmitter(
'DEP0203',
@@ -101,7 +98,7 @@ const maybeEmitDEP0204 = getDeprecationWarningEmitter(
'Passing a non-extractable CryptoKey to KeyObject.from() is deprecated.',
undefined,
false,
(key) => !key[kExtractable],
(key) => !getCryptoKeyExtractable(key),
);
// Key input contexts.
@@ -155,7 +152,14 @@ const {
if (!isCryptoKey(key))
throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key);
maybeEmitDEP0204(key);
return key[kKeyObject];
const handle = getCryptoKeyHandle(key);
switch (getCryptoKeyType(key)) {
/* eslint-disable no-use-before-define */
case 'secret': return new SecretKeyObject(handle);
case 'public': return new PublicKeyObject(handle);
case 'private': return new PrivateKeyObject(handle);
/* eslint-enable no-use-before-define */
}
}
equals(otherKeyObject) {
@@ -252,9 +256,9 @@ const {
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
if (result[kKeyUsages].length === 0) {
if (getCryptoKeyUsagesMask(result) === 0) {
throw lazyDOMException(
`Usages cannot be empty when importing a ${result.type} key.`,
`Usages cannot be empty when importing a ${getCryptoKeyType(result)} key.`,
'SyntaxError');
}
@@ -347,9 +351,10 @@ const {
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
if (result.type === 'private' && result[kKeyUsages].length === 0) {
const resultType = getCryptoKeyType(result);
if (resultType === 'private' && getCryptoKeyUsagesMask(result) === 0) {
throw lazyDOMException(
`Usages cannot be empty when importing a ${result.type} key.`,
`Usages cannot be empty when importing a ${resultType} key.`,
'SyntaxError');
}
@@ -589,7 +594,7 @@ function parsePrivateKeyEncoding(enc, keyType, objName) {
return parseKeyEncoding(enc, keyType, false, objName);
}
function getKeyObjectHandle(key, ctx) {
function validateAsymmetricKeyType(type, ctx, key) {
if (ctx === kCreatePrivate) {
throw new ERR_INVALID_ARG_TYPE(
'key',
@@ -598,16 +603,14 @@ function getKeyObjectHandle(key, ctx) {
);
}
if (key.type !== 'private') {
if (type !== 'private') {
if (ctx === kConsumePrivate || ctx === kCreatePublic)
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'private');
if (key.type !== 'public') {
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type,
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'private');
if (type !== 'public') {
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type,
'private or public');
}
}
return key[kHandle];
}
function getKeyTypes(allowKeyObject, bufferOnly = false) {
@@ -632,11 +635,13 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) {
function prepareAsymmetricKey(key, ctx, name = 'key') {
if (isKeyObject(key)) {
// Best case: A key object, as simple as that.
return { data: getKeyObjectHandle(key, ctx) };
validateAsymmetricKeyType(key.type, ctx, key);
return { data: key[kHandle] };
}
if (isCryptoKey(key)) {
emitDEP0203();
return { data: getKeyObjectHandle(key[kKeyObject], ctx) };
validateAsymmetricKeyType(getCryptoKeyType(key), ctx, key);
return { data: getCryptoKeyHandle(key) };
}
if (isStringOrBuffer(key)) {
// Expect PEM by default, mostly for backward compatibility.
@@ -648,11 +653,13 @@ function prepareAsymmetricKey(key, ctx, name = 'key') {
// The 'key' property can be a KeyObject as well to allow specifying
// additional options such as padding along with the key.
if (isKeyObject(data)) {
return { data: getKeyObjectHandle(data, ctx) };
validateAsymmetricKeyType(data.type, ctx, data);
return { data: data[kHandle] };
}
if (isCryptoKey(data)) {
emitDEP0203();
return { data: getKeyObjectHandle(data[kKeyObject], ctx) };
validateAsymmetricKeyType(getCryptoKeyType(data), ctx, data);
return { data: getCryptoKeyHandle(data) };
}
if (format === 'jwk') {
validateObject(data, `${name}.key`);
@@ -717,9 +724,10 @@ function prepareSecretKey(key, encoding, bufferOnly = false) {
}
if (isCryptoKey(key)) {
emitDEP0203();
if (key[kKeyType] !== 'secret')
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key[kKeyType], 'secret');
return key[kKeyObject][kHandle];
const type = getCryptoKeyType(key);
if (type !== 'secret')
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'secret');
return getCryptoKeyHandle(key);
}
}
if (typeof key !== 'string' &&
@@ -759,180 +767,248 @@ function createPrivateKey(key) {
}
function isKeyObject(obj) {
return obj != null && obj[kKeyType] !== undefined && obj[kKeyObject] === undefined;
return obj != null && obj[kKeyType] !== undefined;
}
// Our implementation of CryptoKey is a simple wrapper around a KeyObject
// that adapts it to the standard interface.
// TODO(@jasnell): Embedder environments like electron may have issues
// here similar to other things like URL. A chromium provided CryptoKey
// will not be recognized as a Node.js CryptoKey, and vice versa. It
// would be fantastic if we could find a way of making those interop.
class CryptoKey {
constructor() {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
// CryptoKey is a plain JS class whose prototype's [[Prototype]] is
// Object.prototype, as Web Crypto requires. Instance storage (type enum,
// extractable, algorithm, usages mask, and the KeyObject handle) lives
// on a C++ class, NativeCryptoKey, created by createCryptoKeyClass.
// InternalCryptoKey is the only constructor we expose to internal
// code; it extends NativeCryptoKey to get that storage and then has
// its prototype spliced so the chain visible to user code is:
// instance -> InternalCryptoKey.prototype
// -> CryptoKey.prototype
// -> Object.prototype
//
// All five internal slots are read from C++ in a single call via
// `getCryptoKeySlots`. The resulting array is cached in a private
// class field on `InternalCryptoKey` so that it is invisible to
// reflection (`Object.getOwnPropertySymbols` etc.) and leaves each
// CryptoKey's hidden class pristine. The `getCryptoKey{Type,
// Extractable,Algorithm,Usages,Handle}` helpers index into that
// array and convert native enums/masks back to Web Crypto strings.
// The public `algorithm` getter caches a cloned dictionary and the
// public `usages` getter caches a synthesized array (as Web Crypto
// requires repeat reads to return the same object so a consumer's
// mutation is visible next time).
let getSlots; // Populated by the createCryptoKeyClass callback below.
[kInspect](depth, options) {
if (depth < 0)
return this;
const kSlotType = 0;
const kSlotExtractable = 1;
const kSlotAlgorithm = 2;
const kSlotUsagesMask = 3;
const kSlotHandle = 4;
const kSlotClonedAlgorithm = 5;
const kSlotClonedUsages = 6;
const kSlotUsages = 7;
const opts = {
...options,
depth: options.depth == null ? null : options.depth - 1,
};
function cloneAlgorithm(raw) {
const cloned = { ...raw };
if (cloned.hash !== undefined) cloned.hash = { ...cloned.hash };
if (cloned.publicExponent !== undefined)
cloned.publicExponent = new Uint8Array(cloned.publicExponent);
return cloned;
}
return `CryptoKey ${inspect({
type: this[kKeyType],
extractable: this[kExtractable],
algorithm: this[kAlgorithm],
usages: this[kKeyUsages],
}, opts)}`;
}
const {
0: CryptoKey,
1: InternalCryptoKey,
} = createCryptoKeyClass((NativeCryptoKey) => {
class CryptoKey {
constructor() {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
get [kKeyType]() {
return this[kKeyObject].type;
}
[kInspect](depth, options) {
if (depth < 0)
return this;
get type() {
if (!(this instanceof CryptoKey))
throw new ERR_INVALID_THIS('CryptoKey');
return this[kKeyType];
}
const opts = {
...options,
depth: options.depth == null ? null : options.depth - 1,
};
get extractable() {
if (!(this instanceof CryptoKey))
throw new ERR_INVALID_THIS('CryptoKey');
return this[kExtractable];
}
return `CryptoKey ${inspect({
type: getCryptoKeyType(this),
extractable: getCryptoKeyExtractable(this),
algorithm: getCryptoKeyAlgorithm(this),
usages: getCryptoKeyUsages(this),
}, opts)}`;
}
get algorithm() {
if (!(this instanceof CryptoKey))
throw new ERR_INVALID_THIS('CryptoKey');
if (!this[kCachedAlgorithm]) {
this[kCachedAlgorithm] ??= { ...this[kAlgorithm] };
this[kCachedAlgorithm].hash &&= { ...this[kCachedAlgorithm].hash };
this[kCachedAlgorithm].publicExponent &&= new Uint8Array(this[kCachedAlgorithm].publicExponent);
get type() {
return getCryptoKeyType(this);
}
get extractable() {
return getCryptoKeyExtractable(this);
}
get algorithm() {
const slots = getSlots(this);
let cached = slots[kSlotClonedAlgorithm];
if (cached === undefined) {
cached = cloneAlgorithm(slots[kSlotAlgorithm]);
slots[kSlotClonedAlgorithm] = cached;
}
return cached;
}
get usages() {
const slots = getSlots(this);
let cached = slots[kSlotClonedUsages];
if (cached === undefined) {
cached = ArrayPrototypeSlice(getCryptoKeyUsagesFromSlots(slots), 0);
slots[kSlotClonedUsages] = cached;
}
return cached;
}
return this[kCachedAlgorithm];
}
get usages() {
if (!(this instanceof CryptoKey))
throw new ERR_INVALID_THIS('CryptoKey');
this[kCachedKeyUsages] ??= ArrayFrom(this[kKeyUsages]);
return this[kCachedKeyUsages];
class InternalCryptoKey extends NativeCryptoKey {
#slots;
static getSlots(key) {
if (!key || typeof key !== 'object')
throw new ERR_INVALID_THIS('CryptoKey');
if (#slots in key) {
const cached = key.#slots;
if (cached !== undefined) return cached;
}
const slots = nativeGetCryptoKeySlots(key);
key.#slots = slots;
return slots;
}
}
}
getSlots = InternalCryptoKey.getSlots;
// Hide NativeCryptoKey from user code.
InternalCryptoKey.prototype.constructor = CryptoKey;
ObjectSetPrototypeOf(InternalCryptoKey.prototype, CryptoKey.prototype);
ObjectDefineProperties(CryptoKey.prototype, {
type: kEnumerableProperty,
extractable: kEnumerableProperty,
algorithm: kEnumerableProperty,
usages: kEnumerableProperty,
[SymbolToStringTag]: {
__proto__: null,
configurable: true,
value: 'CryptoKey',
},
});
/**
* @param {InternalCryptoKey} key
* @param {KeyObject} keyObject
* @param {object} algorithm
* @param {boolean} extractable
* @param {Set<string>} keyUsages
*/
function defineCryptoKeyProperties(
key,
keyObject,
algorithm,
extractable,
keyUsages,
) {
// Using symbol properties here currently instead of private
// properties because (for now) the performance penalty of
// private fields is still too high.
ObjectDefineProperties(key, {
[kKeyObject]: {
ObjectDefineProperties(CryptoKey.prototype, {
type: kEnumerableProperty,
extractable: kEnumerableProperty,
algorithm: kEnumerableProperty,
usages: kEnumerableProperty,
[SymbolToStringTag]: {
__proto__: null,
value: keyObject,
enumerable: false,
configurable: false,
writable: false,
},
[kAlgorithm]: {
__proto__: null,
value: algorithm,
enumerable: false,
configurable: false,
writable: false,
},
[kExtractable]: {
__proto__: null,
value: extractable,
enumerable: false,
configurable: false,
writable: false,
},
[kKeyUsages]: {
__proto__: null,
value: keyUsages,
enumerable: false,
configurable: false,
writable: false,
configurable: true,
value: 'CryptoKey',
},
});
}
// All internal code must use new InternalCryptoKey to create
// CryptoKey instances. The CryptoKey class is exposed to end
// user code but is not permitted to be constructed directly.
// Using markTransferMode also allows the CryptoKey to be
// cloned to Workers.
class InternalCryptoKey {
constructor(keyObject, algorithm, keyUsages, extractable) {
markTransferMode(this, true, false);
// When constructed during transfer the properties get assigned
// in the kDeserialize call.
if (keyObject) {
defineCryptoKeyProperties(
this,
keyObject,
algorithm,
extractable,
getSortedUsages(new SafeSet(keyUsages)),
);
return [CryptoKey, InternalCryptoKey];
});
// The helpers below return a CryptoKey's internal slot value,
// populating the per-instance cache on first access via a single
// native call. The public `type` getter converts the native enum to
// the Web Crypto string. The `usages` helper converts the native usage
// mask to Web Crypto strings. The public `algorithm` / `usages` getters
// on `CryptoKey.prototype` cache their returned objects.
/**
* Returns the value of a CryptoKey's `[[type]]` internal slot.
* @param {CryptoKey} key
* @returns {'secret' | 'public' | 'private'}
*/
function getCryptoKeyType(key) {
switch (getSlots(key)[kSlotType]) {
case kKeyTypeSecret: return 'secret';
case kKeyTypePublic: return 'public';
case kKeyTypePrivate: return 'private';
default: {
const assert = require('internal/assert');
assert.fail('Unreachable code');
}
}
[kClone]() {
const keyObject = this[kKeyObject];
const algorithm = this[kAlgorithm];
const extractable = this[kExtractable];
const usages = this[kKeyUsages];
return {
data: {
keyObject,
algorithm,
usages,
extractable,
},
deserializeInfo: 'internal/crypto/keys:InternalCryptoKey',
};
}
[kDeserialize]({ keyObject, algorithm, usages, extractable }) {
defineCryptoKeyProperties(this, keyObject, algorithm, extractable, usages);
}
}
InternalCryptoKey.prototype.constructor = CryptoKey;
ObjectSetPrototypeOf(InternalCryptoKey.prototype, CryptoKey.prototype);
/**
* Returns the value of a CryptoKey's `[[extractable]]` internal slot.
* @param {CryptoKey} key
* @returns {boolean}
*/
function getCryptoKeyExtractable(key) {
return getSlots(key)[kSlotExtractable];
}
/**
* Returns the CryptoKey's `[[algorithm]]` internal slot, bypassing the
* public `algorithm` getter (which returns a cloned copy).
* @param {CryptoKey} key
* @returns {object}
*/
function getCryptoKeyAlgorithm(key) {
return getSlots(key)[kSlotAlgorithm];
}
/**
* Returns the CryptoKey's native `[[usages]]` mask.
* @param {CryptoKey} key
* @returns {number}
*/
function getCryptoKeyUsagesMask(key) {
return getSlots(key)[kSlotUsagesMask];
}
/**
* Returns whether a CryptoKey's `[[usages]]` contains `usage`.
* @param {CryptoKey} key
* @param {string} usage
* @returns {boolean}
*/
function hasCryptoKeyUsage(key, usage) {
return hasUsage(getCryptoKeyUsagesMask(key), usage);
}
/**
* Returns the CryptoKey's cached canonical usages array for internal
* consumers, expanding it from the native usage mask on first access.
* @param {Array} slots
* @returns {string[]}
*/
function getCryptoKeyUsagesFromSlots(slots) {
let usages = slots[kSlotUsages];
if (usages === undefined) {
usages = getUsagesFromMask(slots[kSlotUsagesMask]);
slots[kSlotUsages] = usages;
}
return usages;
}
/**
* Returns the CryptoKey's `[[usages]]` internal slot, bypassing the
* public `usages` getter (which returns a cloned array). The internal
* array is expanded lazily from the native usage mask.
* @param {CryptoKey} key
* @returns {string[]}
*/
function getCryptoKeyUsages(key) {
return getCryptoKeyUsagesFromSlots(getSlots(key));
}
/**
* Returns the KeyObjectHandle wrapping the CryptoKey's underlying
* key material.
* @param {CryptoKey} key
* @returns {KeyObjectHandle}
*/
function getCryptoKeyHandle(key) {
return getSlots(key)[kSlotHandle];
}
function isCryptoKey(obj) {
return obj != null && obj[kKeyObject] !== undefined;
if (obj == null || typeof obj !== 'object')
return false;
try {
getSlots(obj);
return true;
} catch {
return false;
}
}
function importGenericSecretKey(
@@ -953,22 +1029,27 @@ function importGenericSecretKey(
'SyntaxError');
}
let keyObject;
let handle;
switch (format) {
case 'KeyObject': {
keyObject = keyData;
handle = keyData[kHandle];
break;
}
case 'raw-secret':
case 'raw': {
keyObject = createSecretKey(keyData);
handle = new KeyObjectHandle();
handle.init(kKeyTypeSecret, keyData);
break;
}
default:
return undefined;
}
return new InternalCryptoKey(keyObject, { name }, usagesSet, false);
return new InternalCryptoKey(
handle,
{ name },
getUsagesMask(usagesSet),
false);
}
module.exports = {
@@ -992,9 +1073,12 @@ module.exports = {
PrivateKeyObject,
isKeyObject,
isCryptoKey,
getCryptoKeyType,
getCryptoKeyExtractable,
getCryptoKeyAlgorithm,
getCryptoKeyUsages,
getCryptoKeyUsagesMask,
hasCryptoKeyUsage,
getCryptoKeyHandle,
importGenericSecretKey,
kAlgorithm,
kExtractable,
kKeyType,
kKeyUsages,
};
+25 -60
View File
@@ -7,53 +7,38 @@ const {
const {
HmacJob,
KeyObjectHandle,
KmacJob,
kCryptoJobAsync,
kKeyFormatJWK,
kKeyTypeSecret,
kSignJobModeSign,
kSignJobModeVerify,
SecretKeyGenJob,
} = internalBinding('crypto');
const {
getBlockSize,
getUsagesMask,
hasAnyNotIn,
jobPromise,
normalizeHashName,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
generateKey: _generateKey,
} = require('internal/crypto/keygen');
const {
randomBytes: _randomBytes,
} = require('internal/crypto/random');
const randomBytes = promisify(_randomBytes);
const {
InternalCryptoKey,
SecretKeyObject,
createSecretKey,
kAlgorithm,
getCryptoKeyAlgorithm,
getCryptoKeyHandle,
} = require('internal/crypto/keys');
const {
importJwkSecretKey,
importSecretKey,
validateJwk,
} = require('internal/crypto/webcrypto_util');
const generateKey = promisify(_generateKey);
async function hmacGenerateKey(algorithm, extractable, keyUsages) {
const {
hash,
@@ -68,19 +53,12 @@ async function hmacGenerateKey(algorithm, extractable, keyUsages) {
'SyntaxError');
}
let key;
try {
key = await generateKey('hmac', { length });
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
}
const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length));
return new InternalCryptoKey(
key,
handle,
{ name, length, hash },
usageSet,
getUsagesMask(usageSet),
extractable);
}
@@ -101,20 +79,12 @@ async function kmacGenerateKey(algorithm, extractable, keyUsages) {
'SyntaxError');
}
let keyData;
try {
keyData = await randomBytes(length / 8);
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason' +
`[${err.message}]`,
{ name: 'OperationError', cause: err });
}
const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length));
return new InternalCryptoKey(
createSecretKey(keyData),
handle,
{ name, length },
usageSet,
getUsagesMask(usageSet),
extractable);
}
@@ -132,10 +102,12 @@ function macImportKey(
`Unsupported key usage for ${algorithm.name} key`,
'SyntaxError');
}
let keyObject;
let handle;
let length;
switch (format) {
case 'KeyObject': {
keyObject = keyData;
length = keyData.symmetricKeySize * 8;
handle = keyData[kHandle];
break;
}
case 'raw-secret':
@@ -143,7 +115,8 @@ function macImportKey(
if (format === 'raw' && !isHmac) {
return undefined;
}
keyObject = createSecretKey(keyData);
length = keyData.byteLength * 8;
handle = importSecretKey(keyData);
break;
}
case 'jwk': {
@@ -159,22 +132,14 @@ function macImportKey(
'DataError');
}
const handle = new KeyObjectHandle();
try {
handle.init(kKeyTypeSecret, keyData, kKeyFormatJWK, null, null);
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
keyObject = new SecretKeyObject(handle);
handle = importJwkSecretKey(keyData);
length = handle.getSymmetricKeySize() * 8;
break;
}
default:
return undefined;
}
const { length } = keyObject[kHandle].keyDetail({});
if (length === 0)
throw lazyDOMException('Zero-length key is not supported', 'DataError');
@@ -193,9 +158,9 @@ function macImportKey(
}
return new InternalCryptoKey(
keyObject,
handle,
algorithmObject,
usagesSet,
getUsagesMask(usagesSet),
extractable);
}
@@ -204,8 +169,8 @@ function hmacSignVerify(key, data, algorithm, signature) {
return jobPromise(() => new HmacJob(
kCryptoJobAsync,
mode,
normalizeHashName(key[kAlgorithm].hash.name),
key[kKeyObject][kHandle],
normalizeHashName(getCryptoKeyAlgorithm(key).hash.name),
getCryptoKeyHandle(key),
data,
signature));
}
@@ -215,7 +180,7 @@ function kmacSignVerify(key, data, algorithm, signature) {
return jobPromise(() => new KmacJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
algorithm.name,
algorithm.customization,
algorithm.outputLength / 8,
+42 -37
View File
@@ -18,28 +18,28 @@ const {
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatSPKI,
NidKeyPairGenJob,
EVP_PKEY_ML_DSA_44,
EVP_PKEY_ML_DSA_65,
EVP_PKEY_ML_DSA_87,
} = internalBinding('crypto');
const {
getUsagesMask,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
generateKeyPair: _generateKeyPair,
} = require('internal/crypto/keygen');
const {
getCryptoKeyHandle,
getCryptoKeyType,
InternalCryptoKey,
kKeyType,
} = require('internal/crypto/keys');
const {
@@ -49,8 +49,6 @@ const {
validateJwk,
} = require('internal/crypto/webcrypto_util');
const generateKeyPair = promisify(_generateKeyPair);
function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) {
const checkSet = isPublic ? ['verify'] : ['sign'];
if (hasAnyNotIn(usages, checkSet)) {
@@ -70,32 +68,37 @@ async function mlDsaGenerateKey(algorithm, extractable, keyUsages) {
'SyntaxError');
}
let keyPair;
try {
keyPair = await generateKeyPair(name.toLowerCase());
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
let nid;
switch (name) {
case 'ML-DSA-44':
nid = EVP_PKEY_ML_DSA_44;
break;
case 'ML-DSA-65':
nid = EVP_PKEY_ML_DSA_65;
break;
case 'ML-DSA-87':
nid = EVP_PKEY_ML_DSA_87;
break;
}
const publicUsages = getUsagesUnion(usageSet, 'verify');
const privateUsages = getUsagesUnion(usageSet, 'sign');
const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid));
const publicUsagesMask = getUsagesMask(getUsagesUnion(usageSet, 'verify'));
const privateUsagesMask = getUsagesMask(getUsagesUnion(usageSet, 'sign'));
const keyAlgorithm = { name };
const publicKey =
new InternalCryptoKey(
keyPair.publicKey,
handles[0],
keyAlgorithm,
publicUsages,
publicUsagesMask,
true);
const privateKey =
new InternalCryptoKey(
keyPair.privateKey,
handles[1],
keyAlgorithm,
privateUsages,
privateUsagesMask,
extractable);
return { __proto__: null, privateKey, publicKey };
@@ -103,17 +106,19 @@ async function mlDsaGenerateKey(algorithm, extractable, keyUsages) {
function mlDsaExportKey(key, format) {
try {
const handle = getCryptoKeyHandle(key);
switch (format) {
case kWebCryptoKeyFormatRaw: {
const handle = key[kKeyObject][kHandle];
return TypedArrayPrototypeGetBuffer(
key[kKeyType] === 'private' ? handle.rawSeed() : handle.rawPublicKey());
getCryptoKeyType(key) === 'private' ? handle.rawSeed() : handle.rawPublicKey());
}
case kWebCryptoKeyFormatSPKI: {
return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI));
return TypedArrayPrototypeGetBuffer(
handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI));
}
case kWebCryptoKeyFormatPKCS8: {
const pkcs8 = key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null);
const pkcs8 = handle.export(
kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null);
// Edge case only possible when user creates a seedless KeyObject
// first and converts it with KeyObject.prototype.toCryptoKey.
// 54 = 22 bytes of PKCS#8 ASN.1 + 32-byte seed.
@@ -142,17 +147,17 @@ function mlDsaImportKey(
keyUsages) {
const { name } = algorithm;
let keyObject;
let handle;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'KeyObject': {
verifyAcceptableMlDsaKeyUse(name, keyData.type === 'public', usagesSet);
keyObject = keyData;
handle = keyData[kHandle];
break;
}
case 'spki': {
verifyAcceptableMlDsaKeyUse(name, true, usagesSet);
keyObject = importDerKey(keyData, true);
handle = importDerKey(keyData, true);
break;
}
case 'pkcs8': {
@@ -170,7 +175,7 @@ function mlDsaImportKey(
'NotSupportedError');
}
keyObject = importDerKey(keyData, false);
handle = importDerKey(keyData, false);
break;
}
case 'jwk': {
@@ -182,28 +187,28 @@ function mlDsaImportKey(
const isPublic = keyData.priv === undefined;
verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet);
keyObject = importJwkKey(isPublic, keyData);
handle = importJwkKey(isPublic, keyData);
break;
}
case 'raw-public':
case 'raw-seed': {
const isPublic = format === 'raw-public';
verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet);
keyObject = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name);
handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name);
break;
}
default:
return undefined;
}
if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) {
if (handle.getAsymmetricKeyType() !== StringPrototypeToLowerCase(name)) {
throw lazyDOMException('Invalid key type', 'DataError');
}
return new InternalCryptoKey(
keyObject,
handle,
{ name },
usagesSet,
getUsagesMask(usagesSet),
extractable);
}
@@ -211,13 +216,13 @@ async function mlDsaSignVerify(key, data, algorithm, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';
if (key[kKeyType] !== type)
if (getCryptoKeyType(key) !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
return await jobPromise(() => new SignJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
undefined,
undefined,
undefined,
+42 -40
View File
@@ -18,27 +18,28 @@ const {
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatSPKI,
NidKeyPairGenJob,
EVP_PKEY_ML_KEM_512,
EVP_PKEY_ML_KEM_768,
EVP_PKEY_ML_KEM_1024,
} = internalBinding('crypto');
const {
getUsagesMask,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
generateKeyPair: _generateKeyPair,
} = require('internal/crypto/keygen');
const {
getCryptoKeyHandle,
getCryptoKeyType,
InternalCryptoKey,
kKeyType,
} = require('internal/crypto/keys');
const {
@@ -48,8 +49,6 @@ const {
validateJwk,
} = require('internal/crypto/webcrypto_util');
const generateKeyPair = promisify(_generateKeyPair);
async function mlKemGenerateKey(algorithm, extractable, keyUsages) {
const { name } = algorithm;
@@ -60,32 +59,33 @@ async function mlKemGenerateKey(algorithm, extractable, keyUsages) {
'SyntaxError');
}
let keyPair;
try {
keyPair = await generateKeyPair(name.toLowerCase());
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
}
const nid = {
'__proto__': null,
'ML-KEM-512': EVP_PKEY_ML_KEM_512,
'ML-KEM-768': EVP_PKEY_ML_KEM_768,
'ML-KEM-1024': EVP_PKEY_ML_KEM_1024,
}[name];
const publicUsages = getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits');
const privateUsages = getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits');
const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid));
const publicUsagesMask = getUsagesMask(
getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits'));
const privateUsagesMask = getUsagesMask(
getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits'));
const keyAlgorithm = { name };
const publicKey =
new InternalCryptoKey(
keyPair.publicKey,
handles[0],
keyAlgorithm,
publicUsages,
publicUsagesMask,
true);
const privateKey =
new InternalCryptoKey(
keyPair.privateKey,
handles[1],
keyAlgorithm,
privateUsages,
privateUsagesMask,
extractable);
return { __proto__: null, privateKey, publicKey };
@@ -93,17 +93,19 @@ async function mlKemGenerateKey(algorithm, extractable, keyUsages) {
function mlKemExportKey(key, format) {
try {
const handle = getCryptoKeyHandle(key);
switch (format) {
case kWebCryptoKeyFormatRaw: {
const handle = key[kKeyObject][kHandle];
return TypedArrayPrototypeGetBuffer(
key[kKeyType] === 'private' ? handle.rawSeed() : handle.rawPublicKey());
getCryptoKeyType(key) === 'private' ? handle.rawSeed() : handle.rawPublicKey());
}
case kWebCryptoKeyFormatSPKI: {
return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI));
return TypedArrayPrototypeGetBuffer(
handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI));
}
case kWebCryptoKeyFormatPKCS8: {
const pkcs8 = key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null);
const pkcs8 = handle.export(
kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null);
// Edge case only possible when user creates a seedless KeyObject
// first and converts it with KeyObject.prototype.toCryptoKey.
// 86 = 22 bytes of PKCS#8 ASN.1 + 64-byte seed.
@@ -141,17 +143,17 @@ function mlKemImportKey(
keyUsages) {
const { name } = algorithm;
let keyObject;
let handle;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'KeyObject': {
verifyAcceptableMlKemKeyUse(name, keyData.type === 'public', usagesSet);
keyObject = keyData;
handle = keyData[kHandle];
break;
}
case 'spki': {
verifyAcceptableMlKemKeyUse(name, true, usagesSet);
keyObject = importDerKey(keyData, true);
handle = importDerKey(keyData, true);
break;
}
case 'pkcs8': {
@@ -169,14 +171,14 @@ function mlKemImportKey(
'NotSupportedError');
}
keyObject = importDerKey(keyData, false);
handle = importDerKey(keyData, false);
break;
}
case 'raw-public':
case 'raw-seed': {
const isPublic = format === 'raw-public';
verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet);
keyObject = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawPrivate, name);
handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawPrivate, name);
break;
}
case 'jwk': {
@@ -188,26 +190,26 @@ function mlKemImportKey(
const isPublic = keyData.priv === undefined;
verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet);
keyObject = importJwkKey(isPublic, keyData);
handle = importJwkKey(isPublic, keyData);
break;
}
default:
return undefined;
}
if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) {
if (handle.getAsymmetricKeyType() !== StringPrototypeToLowerCase(name)) {
throw lazyDOMException('Invalid key type', 'DataError');
}
return new InternalCryptoKey(
keyObject,
handle,
{ name },
usagesSet,
getUsagesMask(usagesSet),
extractable);
}
function mlKemEncapsulate(encapsulationKey) {
if (encapsulationKey[kKeyType] !== 'public') {
if (getCryptoKeyType(encapsulationKey) !== 'public') {
throw lazyDOMException(`Key must be a public key`, 'InvalidAccessError');
}
@@ -215,7 +217,7 @@ function mlKemEncapsulate(encapsulationKey) {
const job = new KEMEncapsulateJob(
kCryptoJobAsync,
encapsulationKey[kKeyObject][kHandle],
getCryptoKeyHandle(encapsulationKey),
undefined,
undefined,
undefined,
@@ -241,7 +243,7 @@ function mlKemEncapsulate(encapsulationKey) {
}
function mlKemDecapsulate(decapsulationKey, ciphertext) {
if (decapsulationKey[kKeyType] !== 'private') {
if (getCryptoKeyType(decapsulationKey) !== 'private') {
throw lazyDOMException(`Key must be a private key`, 'InvalidAccessError');
}
@@ -249,7 +251,7 @@ function mlKemDecapsulate(decapsulationKey, ciphertext) {
const job = new KEMDecapsulateJob(
kCryptoJobAsync,
decapsulationKey[kKeyObject][kHandle],
getCryptoKeyHandle(decapsulationKey),
undefined,
undefined,
undefined,
+6 -2
View File
@@ -23,7 +23,6 @@ const {
const {
getArrayBufferOrView,
normalizeHashName,
kKeyObject,
} = require('internal/crypto/util');
const {
@@ -31,6 +30,10 @@ const {
promisify,
} = require('internal/util');
const {
getCryptoKeyHandle,
} = require('internal/crypto/keys');
function pbkdf2(password, salt, iterations, keylen, digest, callback) {
if (typeof digest === 'function') {
callback = digest;
@@ -113,8 +116,9 @@ async function pbkdf2DeriveBits(algorithm, baseKey, length) {
let result;
try {
// TODO(panva): call the job directly without needing to re-export the handle
result = await pbkdf2Promise(
baseKey[kKeyObject].export(), salt, iterations, length / 8, normalizeHashName(hash.name),
getCryptoKeyHandle(baseKey).export(), salt, iterations, length / 8, normalizeHashName(hash.name),
);
} catch (err) {
throw lazyDOMException(
+36 -45
View File
@@ -15,9 +15,11 @@ const {
kSignJobModeSign,
kSignJobModeVerify,
kKeyVariantRSA_OAEP,
kKeyVariantRSA_SSA_PKCS1_v1_5,
kWebCryptoCipherEncrypt,
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatSPKI,
RsaKeyPairGenJob,
RSA_PKCS1_PSS_PADDING,
} = internalBinding('crypto');
@@ -28,24 +30,24 @@ const {
const {
bigIntArrayToUnsignedInt,
getDigestSizeInBytes,
getUsagesMask,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
normalizeHashName,
validateMaxBufferLength,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
InternalCryptoKey,
kAlgorithm,
kKeyType,
getCryptoKeyAlgorithm,
getCryptoKeyHandle,
getCryptoKeyType,
} = require('internal/crypto/keys');
const {
@@ -54,12 +56,6 @@ const {
validateJwk,
} = require('internal/crypto/webcrypto_util');
const {
generateKeyPair: _generateKeyPair,
} = require('internal/crypto/keygen');
const generateKeyPair = promisify(_generateKeyPair);
function verifyAcceptableRsaKeyUse(name, isPublic, usages) {
let checkSet;
switch (name) {
@@ -92,7 +88,7 @@ async function rsaOaepCipher(mode, key, data, algorithm) {
validateRsaOaepAlgorithm(algorithm);
const type = mode === kWebCryptoCipherEncrypt ? 'public' : 'private';
if (key[kKeyType] !== type) {
if (getCryptoKeyType(key) !== type) {
throw lazyDOMException(
'The requested operation is not valid for the provided key',
'InvalidAccessError');
@@ -101,10 +97,10 @@ async function rsaOaepCipher(mode, key, data, algorithm) {
return await jobPromise(() => new RSACipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
data,
kKeyVariantRSA_OAEP,
normalizeHashName(key[kAlgorithm].hash.name),
normalizeHashName(getCryptoKeyAlgorithm(key).hash.name),
algorithm.label));
}
@@ -145,17 +141,11 @@ async function rsaKeyGenerate(
}
}
let keyPair;
try {
keyPair = await generateKeyPair('rsa', {
modulusLength,
publicExponent: publicExponentConverted,
});
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
}
const handles = await jobPromise(() => new RsaKeyPairGenJob(
kCryptoJobAsync,
kKeyVariantRSA_SSA_PKCS1_v1_5,
modulusLength,
publicExponentConverted));
const keyAlgorithm = {
name,
@@ -181,16 +171,16 @@ async function rsaKeyGenerate(
const publicKey =
new InternalCryptoKey(
keyPair.publicKey,
handles[0],
keyAlgorithm,
publicUsages,
getUsagesMask(publicUsages),
true);
const privateKey =
new InternalCryptoKey(
keyPair.privateKey,
handles[1],
keyAlgorithm,
privateUsages,
getUsagesMask(privateUsages),
extractable);
return { __proto__: null, publicKey, privateKey };
@@ -201,11 +191,11 @@ function rsaExportKey(key, format) {
switch (format) {
case kWebCryptoKeyFormatSPKI: {
return TypedArrayPrototypeGetBuffer(
key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI));
getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatSPKI));
}
case kWebCryptoKeyFormatPKCS8: {
return TypedArrayPrototypeGetBuffer(
key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null));
getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null));
}
default:
return undefined;
@@ -224,21 +214,21 @@ function rsaImportKey(
extractable,
keyUsages) {
const usagesSet = new SafeSet(keyUsages);
let keyObject;
let handle;
switch (format) {
case 'KeyObject': {
verifyAcceptableRsaKeyUse(algorithm.name, keyData.type === 'public', usagesSet);
keyObject = keyData;
handle = keyData[kHandle];
break;
}
case 'spki': {
verifyAcceptableRsaKeyUse(algorithm.name, true, usagesSet);
keyObject = importDerKey(keyData, true);
handle = importDerKey(keyData, true);
break;
}
case 'pkcs8': {
verifyAcceptableRsaKeyUse(algorithm.name, false, usagesSet);
keyObject = importDerKey(keyData, false);
handle = importDerKey(keyData, false);
break;
}
case 'jwk': {
@@ -260,58 +250,59 @@ function rsaImportKey(
const isPublic = keyData.d === undefined;
verifyAcceptableRsaKeyUse(algorithm.name, isPublic, usagesSet);
keyObject = importJwkKey(isPublic, keyData);
handle = importJwkKey(isPublic, keyData);
break;
}
default:
return undefined;
}
if (keyObject.asymmetricKeyType !== 'rsa') {
if (handle.getAsymmetricKeyType() !== 'rsa') {
throw lazyDOMException('Invalid key type', 'DataError');
}
const {
modulusLength,
publicExponent,
} = keyObject[kHandle].keyDetail({});
} = handle.keyDetail({});
return new InternalCryptoKey(keyObject, {
return new InternalCryptoKey(handle, {
name: algorithm.name,
modulusLength,
publicExponent: new Uint8Array(publicExponent),
hash: algorithm.hash,
}, usagesSet, extractable);
}, getUsagesMask(usagesSet), extractable);
}
async function rsaSignVerify(key, data, { saltLength }, signature) {
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';
if (key[kKeyType] !== type)
if (getCryptoKeyType(key) !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
return await jobPromise(() => {
if (key[kAlgorithm].name === 'RSA-PSS') {
const algorithm = getCryptoKeyAlgorithm(key);
if (algorithm.name === 'RSA-PSS') {
validateInt32(
saltLength,
'algorithm.saltLength',
0,
MathCeil((key[kAlgorithm].modulusLength - 1) / 8) - getDigestSizeInBytes(key[kAlgorithm].hash.name) - 2);
MathCeil((algorithm.modulusLength - 1) / 8) - getDigestSizeInBytes(algorithm.hash.name) - 2);
}
return new SignJob(
kCryptoJobAsync,
signature === undefined ? kSignJobModeSign : kSignJobModeVerify,
key[kKeyObject][kHandle],
getCryptoKeyHandle(key),
undefined,
undefined,
undefined,
undefined,
data,
normalizeHashName(key[kAlgorithm].hash.name),
normalizeHashName(algorithm.hash.name),
saltLength,
key[kAlgorithm].name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined,
algorithm.name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined,
undefined,
undefined,
signature);
+79 -43
View File
@@ -3,7 +3,6 @@
const {
ArrayBufferIsView,
ArrayBufferPrototypeGetByteLength,
ArrayFrom,
ArrayPrototypeIncludes,
ArrayPrototypePush,
BigInt,
@@ -94,7 +93,6 @@ const {
} = require('internal/util/types');
const kHandle = Symbol('kHandle');
const kKeyObject = Symbol('kKeyObject');
// This is here because many functions accepted binary strings without
// any explicit encoding in older versions of node, and we don't want
@@ -529,10 +527,10 @@ const simpleAlgorithmDictionaries = {
TurboShakeParams: {},
};
function validateMaxBufferLength(data, name) {
if (data.byteLength > kMaxBufferLength) {
function validateMaxBufferLength(data, name, max = kMaxBufferLength) {
if (data.byteLength > max) {
throw lazyDOMException(
`${name} must be less than ${kMaxBufferLength + 1} bits`,
`${name} must be at most ${max} bytes`,
'OperationError');
}
}
@@ -719,6 +717,12 @@ function getStringOption(options, key) {
return value;
}
/**
* Returns the requested usages that are present in `usageSet`.
* @param {SafeSet<string>} usageSet
* @param {...string} usages
* @returns {SafeSet<string>}
*/
function getUsagesUnion(usageSet, ...usages) {
const newset = new SafeSet();
for (let n = 0; n < usages.length; n++) {
@@ -728,28 +732,76 @@ function getUsagesUnion(usageSet, ...usages) {
return newset;
}
const kCanonicalUsageOrder = new SafeSet([
// Must be at most 31 entries.
const kCanonicalUsageOrder = [
'encrypt', 'decrypt',
'sign', 'verify',
'deriveKey', 'deriveBits',
'wrapKey', 'unwrapKey',
'encapsulateKey', 'encapsulateBits',
'decapsulateKey', 'decapsulateBits',
]);
];
const kUsageMasks = {
__proto__: null,
};
const kUsageByMask = {
__proto__: null,
};
// Derive both lookup tables from kCanonicalUsageOrder so adding a new
// usage only requires updating the canonical list above. The numeric
// mask uses the usage's canonical index as its bit position.
for (let n = 0; n < kCanonicalUsageOrder.length; n++) {
const usage = kCanonicalUsageOrder[n];
const mask = 1 << n;
kUsageMasks[usage] = mask;
kUsageByMask[mask] = usage;
}
/**
* Returns the usages from `usageSet` as an array in the canonical order
* defined by {@link kCanonicalUsageOrder}.
* Returns a bit mask representing the usages from `usageSet`.
* @param {SafeSet<string>} usageSet
* @returns {number}
*/
function getUsagesMask(usageSet) {
// No usages is a valid state for some public keys, represented by a
// zero mask.
if (usageSet.size === 0) return 0;
let mask = 0;
for (const usage of usageSet) {
mask |= kUsageMasks[usage];
}
return mask;
}
/**
* Returns whether `mask` contains `usage`.
* @param {number} mask
* @param {string} usage
* @returns {boolean}
*/
function hasUsage(mask, usage) {
return (mask & kUsageMasks[usage]) !== 0;
}
/**
* Returns the usages represented by `mask` in canonical order.
* @param {number} mask
* @returns {string[]}
*/
function getSortedUsages(usageSet) {
if (usageSet.size <= 1) {
return ArrayFrom(usageSet);
function getUsagesFromMask(mask) {
// Short circuit most common cases, empty and single usage
// No usages is a valid state for some public keys
if (mask === 0) return [];
// A mask with exactly one bit set maps directly to one usage
if ((mask & (mask - 1)) === 0) {
return [kUsageByMask[mask]];
}
// Multiple usages need to be expanded in canonical order.
const result = [];
for (const usage of kCanonicalUsageOrder) {
if (usageSet.has(usage)) ArrayPrototypePush(result, usage);
for (let n = 0; n < kCanonicalUsageOrder.length; n++) {
if (mask & (1 << n))
ArrayPrototypePush(result, kCanonicalUsageOrder[n]);
}
return result;
}
@@ -791,36 +843,20 @@ function getDigestSizeInBytes(name) {
}
}
const kKeyOps = {
__proto__: null,
sign: 1,
verify: 2,
encrypt: 3,
decrypt: 4,
wrapKey: 5,
unwrapKey: 6,
deriveKey: 7,
deriveBits: 8,
encapsulateKey: 9,
encapsulateBits: 10,
decapsulateKey: 11,
decapsulateBits: 12,
};
function validateKeyOps(keyOps, usagesSet) {
if (keyOps === undefined) return;
validateArray(keyOps, 'keyData.key_ops');
let flags = 0;
let keyOpsMask = 0;
for (let n = 0; n < keyOps.length; n++) {
const op = keyOps[n];
const op_flag = kKeyOps[op];
const opMask = kUsageMasks[op];
// Skipping unknown key ops
if (op_flag === undefined)
if (opMask === undefined)
continue;
// Have we seen it already? if so, error
if (flags & (1 << op_flag))
if (keyOpsMask & opMask)
throw lazyDOMException('Duplicate key operation', 'DataError');
flags |= (1 << op_flag);
keyOpsMask |= opMask;
// TODO(@jasnell): RFC7517 section 4.3 strong recommends validating
// key usage combinations. Specifically, it says that unrelated key
@@ -828,12 +864,11 @@ function validateKeyOps(keyOps, usagesSet) {
}
if (usagesSet !== undefined) {
for (const use of usagesSet) {
if (!ArrayPrototypeIncludes(keyOps, use)) {
throw lazyDOMException(
'Key operations and usage mismatch',
'DataError');
}
const usagesMask = getUsagesMask(usagesSet);
if ((keyOpsMask & usagesMask) !== usagesMask) {
throw lazyDOMException(
'Key operations and usage mismatch',
'DataError');
}
}
}
@@ -856,7 +891,6 @@ module.exports = {
getDataViewOrTypedArrayBuffer,
getHashes,
kHandle,
kKeyObject,
setEngine,
toBuf,
@@ -874,7 +908,9 @@ module.exports = {
getDigestSizeInBytes,
getStringOption,
getUsagesUnion,
getSortedUsages,
getUsagesMask,
getUsagesFromMask,
hasUsage,
secureHeapUsed,
getCachedHashId,
getHashCache,
+92 -79
View File
@@ -1,7 +1,7 @@
'use strict';
const {
ArrayPrototypeIncludes,
ArrayPrototypeSlice,
FunctionPrototypeCall,
JSONParse,
JSONStringify,
@@ -35,11 +35,15 @@ const {
const {
createPublicKey,
CryptoKey,
getCryptoKeyAlgorithm,
getCryptoKeyExtractable,
getCryptoKeyHandle,
getCryptoKeyType,
getCryptoKeyUsages,
getCryptoKeyUsagesMask,
hasCryptoKeyUsage,
importGenericSecretKey,
kAlgorithm,
kKeyUsages,
kExtractable,
kKeyType,
PrivateKeyObject,
} = require('internal/crypto/keys');
const {
@@ -51,8 +55,6 @@ const {
normalizeAlgorithm,
normalizeHashName,
validateMaxBufferLength,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
@@ -198,12 +200,15 @@ async function generateKey(
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
if (
(resultType === 'CryptoKey' &&
(result[kKeyType] === 'secret' || result[kKeyType] === 'private') &&
result[kKeyUsages].length === 0) ||
(resultType === 'CryptoKeyPair' && result.privateKey[kKeyUsages].length === 0)
) {
if (resultType === 'CryptoKey') {
const type = getCryptoKeyType(result);
if ((type === 'secret' || type === 'private') &&
getCryptoKeyUsagesMask(result) === 0) {
throw lazyDOMException(
'Usages cannot be empty when creating a key.',
'SyntaxError');
}
} else if (getCryptoKeyUsagesMask(result.privateKey) === 0) {
throw lazyDOMException(
'Usages cannot be empty when creating a key.',
'SyntaxError');
@@ -234,12 +239,12 @@ async function deriveBits(algorithm, baseKey, length = null) {
}
algorithm = normalizeAlgorithm(algorithm, 'deriveBits');
if (!ArrayPrototypeIncludes(baseKey[kKeyUsages], 'deriveBits')) {
if (!hasCryptoKeyUsage(baseKey, 'deriveBits')) {
throw lazyDOMException(
'baseKey does not have deriveBits usage',
'InvalidAccessError');
}
if (baseKey[kAlgorithm].name !== algorithm.name)
if (getCryptoKeyAlgorithm(baseKey).name !== algorithm.name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
switch (algorithm.name) {
case 'X25519':
@@ -339,12 +344,12 @@ async function deriveKey(
algorithm = normalizeAlgorithm(algorithm, 'deriveBits');
derivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm, 'importKey');
if (!ArrayPrototypeIncludes(baseKey[kKeyUsages], 'deriveKey')) {
if (!hasCryptoKeyUsage(baseKey, 'deriveKey')) {
throw lazyDOMException(
'baseKey does not have deriveKey usage',
'InvalidAccessError');
}
if (baseKey[kAlgorithm].name !== algorithm.name)
if (getCryptoKeyAlgorithm(baseKey).name !== algorithm.name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
const length = getKeyLength(normalizeAlgorithm(arguments[2], 'get key length'));
@@ -386,7 +391,7 @@ async function deriveKey(
}
async function exportKeySpki(key) {
switch (key[kAlgorithm].name) {
switch (getCryptoKeyAlgorithm(key).name) {
case 'RSASSA-PKCS1-v1_5':
// Fall through
case 'RSA-PSS':
@@ -428,7 +433,7 @@ async function exportKeySpki(key) {
}
async function exportKeyPkcs8(key) {
switch (key[kAlgorithm].name) {
switch (getCryptoKeyAlgorithm(key).name) {
case 'RSASSA-PKCS1-v1_5':
// Fall through
case 'RSA-PSS':
@@ -470,7 +475,7 @@ async function exportKeyPkcs8(key) {
}
async function exportKeyRawPublic(key, format) {
switch (key[kAlgorithm].name) {
switch (getCryptoKeyAlgorithm(key).name) {
case 'ECDSA':
// Fall through
case 'ECDH':
@@ -515,7 +520,7 @@ async function exportKeyRawPublic(key, format) {
}
async function exportKeyRawSeed(key) {
switch (key[kAlgorithm].name) {
switch (getCryptoKeyAlgorithm(key).name) {
case 'ML-DSA-44':
// Fall through
case 'ML-DSA-65':
@@ -536,7 +541,7 @@ async function exportKeyRawSeed(key) {
}
async function exportKeyRawSecret(key, format) {
switch (key[kAlgorithm].name) {
switch (getCryptoKeyAlgorithm(key).name) {
case 'AES-CTR':
// Fall through
case 'AES-CBC':
@@ -546,7 +551,7 @@ async function exportKeyRawSecret(key, format) {
case 'AES-KW':
// Fall through
case 'HMAC':
return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export());
return TypedArrayPrototypeGetBuffer(getCryptoKeyHandle(key).export());
case 'AES-OCB':
// Fall through
case 'KMAC128':
@@ -555,7 +560,7 @@ async function exportKeyRawSecret(key, format) {
// Fall through
case 'ChaCha20-Poly1305':
if (format === 'raw-secret') {
return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export());
return TypedArrayPrototypeGetBuffer(getCryptoKeyHandle(key).export());
}
return undefined;
default:
@@ -564,28 +569,29 @@ async function exportKeyRawSecret(key, format) {
}
async function exportKeyJWK(key) {
const algorithm = getCryptoKeyAlgorithm(key);
const parameters = {
key_ops: key[kKeyUsages],
ext: key[kExtractable],
key_ops: ArrayPrototypeSlice(getCryptoKeyUsages(key), 0),
ext: getCryptoKeyExtractable(key),
};
switch (key[kAlgorithm].name) {
switch (algorithm.name) {
case 'RSASSA-PKCS1-v1_5': {
const alg = normalizeHashName(
key[kAlgorithm].hash.name,
algorithm.hash.name,
normalizeHashName.kContextJwkRsa);
if (alg) parameters.alg = alg;
break;
}
case 'RSA-PSS': {
const alg = normalizeHashName(
key[kAlgorithm].hash.name,
algorithm.hash.name,
normalizeHashName.kContextJwkRsaPss);
if (alg) parameters.alg = alg;
break;
}
case 'RSA-OAEP': {
const alg = normalizeHashName(
key[kAlgorithm].hash.name,
algorithm.hash.name,
normalizeHashName.kContextJwkRsaOaep);
if (alg) parameters.alg = alg;
break;
@@ -613,7 +619,7 @@ async function exportKeyJWK(key) {
case 'Ed25519':
// Fall through
case 'Ed448':
parameters.alg = key[kAlgorithm].name;
parameters.alg = algorithm.name;
break;
case 'AES-CTR':
// Fall through
@@ -625,14 +631,14 @@ async function exportKeyJWK(key) {
// Fall through
case 'AES-KW':
parameters.alg = require('internal/crypto/aes')
.getAlgorithmName(key[kAlgorithm].name, key[kAlgorithm].length);
.getAlgorithmName(algorithm.name, algorithm.length);
break;
case 'ChaCha20-Poly1305':
parameters.alg = 'C20P';
break;
case 'HMAC': {
const alg = normalizeHashName(
key[kAlgorithm].hash.name,
algorithm.hash.name,
normalizeHashName.kContextJwkHmac);
if (alg) parameters.alg = alg;
break;
@@ -648,7 +654,7 @@ async function exportKeyJWK(key) {
return undefined;
}
return key[kKeyObject][kHandle].exportJwk(parameters, true);
return getCryptoKeyHandle(key).exportJwk(parameters, true);
}
async function exportKey(format, key) {
@@ -666,26 +672,28 @@ async function exportKey(format, key) {
context: '2nd argument',
});
const algorithm = getCryptoKeyAlgorithm(key);
try {
normalizeAlgorithm(key[kAlgorithm], 'exportKey');
normalizeAlgorithm(algorithm, 'exportKey');
} catch {
throw lazyDOMException(
`${key[kAlgorithm].name} key export is not supported`, 'NotSupportedError');
`${algorithm.name} key export is not supported`, 'NotSupportedError');
}
if (!key[kExtractable])
if (!getCryptoKeyExtractable(key))
throw lazyDOMException('key is not extractable', 'InvalidAccessError');
const type = getCryptoKeyType(key);
let result;
switch (format) {
case 'spki': {
if (key[kKeyType] === 'public') {
if (type === 'public') {
result = await exportKeySpki(key);
}
break;
}
case 'pkcs8': {
if (key[kKeyType] === 'private') {
if (type === 'private') {
result = await exportKeyPkcs8(key);
}
break;
@@ -695,27 +703,27 @@ async function exportKey(format, key) {
break;
}
case 'raw-secret': {
if (key[kKeyType] === 'secret') {
if (type === 'secret') {
result = await exportKeyRawSecret(key, format);
}
break;
}
case 'raw-public': {
if (key[kKeyType] === 'public') {
if (type === 'public') {
result = await exportKeyRawPublic(key, format);
}
break;
}
case 'raw-seed': {
if (key[kKeyType] === 'private') {
if (type === 'private') {
result = await exportKeyRawSeed(key);
}
break;
}
case 'raw': {
if (key[kKeyType] === 'secret') {
if (type === 'secret') {
result = await exportKeyRawSecret(key, format);
} else if (key[kKeyType] === 'public') {
} else if (type === 'public') {
result = await exportKeyRawPublic(key, format);
}
break;
@@ -724,7 +732,7 @@ async function exportKey(format, key) {
if (!result) {
throw lazyDOMException(
`Unable to export ${key[kAlgorithm].name} ${key[kKeyType]} key using ${format} format`,
`Unable to export ${algorithm.name} ${type} key using ${format} format`,
'NotSupportedError');
}
@@ -844,9 +852,10 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) {
'NotSupportedError');
}
if ((result.type === 'secret' || result.type === 'private') && result[kKeyUsages].length === 0) {
const type = getCryptoKeyType(result);
if ((type === 'secret' || type === 'private') && getCryptoKeyUsagesMask(result) === 0) {
throw lazyDOMException(
`Usages cannot be empty when importing a ${result.type} key.`,
`Usages cannot be empty when importing a ${type} key.`,
'SyntaxError');
}
@@ -926,10 +935,10 @@ async function wrapKey(format, key, wrappingKey, algorithm) {
algorithm = normalizeAlgorithm(algorithm, 'encrypt');
}
if (algorithm.name !== wrappingKey[kAlgorithm].name)
if (algorithm.name !== getCryptoKeyAlgorithm(wrappingKey).name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
if (!ArrayPrototypeIncludes(wrappingKey[kKeyUsages], 'wrapKey'))
if (!hasCryptoKeyUsage(wrappingKey, 'wrapKey'))
throw lazyDOMException(
'Unable to use this key to wrapKey', 'InvalidAccessError');
@@ -1011,10 +1020,10 @@ async function unwrapKey(
unwrappedKeyAlgo = normalizeAlgorithm(unwrappedKeyAlgo, 'importKey');
if (unwrapAlgo.name !== unwrappingKey[kAlgorithm].name)
if (unwrapAlgo.name !== getCryptoKeyAlgorithm(unwrappingKey).name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
if (!ArrayPrototypeIncludes(unwrappingKey[kKeyUsages], 'unwrapKey'))
if (!hasCryptoKeyUsage(unwrappingKey, 'unwrapKey'))
throw lazyDOMException(
'Unable to use this key to unwrapKey', 'InvalidAccessError');
@@ -1045,18 +1054,15 @@ async function unwrapKey(
}
async function signVerify(algorithm, key, data, signature) {
let usage = 'sign';
if (signature !== undefined) {
usage = 'verify';
}
algorithm = normalizeAlgorithm(algorithm, usage);
const op = signature !== undefined ? 'verify' : 'sign'; // This is also usage
algorithm = normalizeAlgorithm(algorithm, op);
if (algorithm.name !== key[kAlgorithm].name)
if (algorithm.name !== getCryptoKeyAlgorithm(key).name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
if (!ArrayPrototypeIncludes(key[kKeyUsages], usage))
if (!hasCryptoKeyUsage(key, op))
throw lazyDOMException(
`Unable to use this key to ${usage}`, 'InvalidAccessError');
`Unable to use this key to ${op}`, 'InvalidAccessError');
switch (algorithm.name) {
case 'RSA-PSS':
@@ -1192,10 +1198,10 @@ async function encrypt(algorithm, key, data) {
algorithm = normalizeAlgorithm(algorithm, 'encrypt');
if (algorithm.name !== key[kAlgorithm].name)
if (algorithm.name !== getCryptoKeyAlgorithm(key).name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
if (!ArrayPrototypeIncludes(key[kKeyUsages], 'encrypt'))
if (!hasCryptoKeyUsage(key, 'encrypt'))
throw lazyDOMException(
'Unable to use this key to encrypt', 'InvalidAccessError');
@@ -1229,10 +1235,10 @@ async function decrypt(algorithm, key, data) {
algorithm = normalizeAlgorithm(algorithm, 'decrypt');
if (algorithm.name !== key[kAlgorithm].name)
if (algorithm.name !== getCryptoKeyAlgorithm(key).name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
if (!ArrayPrototypeIncludes(key[kKeyUsages], 'decrypt'))
if (!hasCryptoKeyUsage(key, 'decrypt'))
throw lazyDOMException(
'Unable to use this key to decrypt', 'InvalidAccessError');
@@ -1262,13 +1268,16 @@ async function getPublicKey(key, keyUsages) {
context: '2nd argument',
});
if (key[kKeyType] !== 'private')
const type = getCryptoKeyType(key);
if (type !== 'private')
throw lazyDOMException('key must be a private key',
key[kKeyType] === 'secret' ? 'NotSupportedError' : 'InvalidAccessError');
type === 'secret' ? 'NotSupportedError' : 'InvalidAccessError');
const keyObject = createPublicKey(key[kKeyObject]);
return keyObject.toCryptoKey(key[kAlgorithm], true, keyUsages);
// TODO(panva): this is by no means a hot path, but let's still follow up to get
// rid of this awkwardness
const keyObject = createPublicKey(new PrivateKeyObject(getCryptoKeyHandle(key)));
return keyObject.toCryptoKey(getCryptoKeyAlgorithm(key), true, keyUsages);
}
async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) {
@@ -1288,20 +1297,21 @@ async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) {
});
const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate');
const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey);
if (normalizedEncapsulationAlgorithm.name !== encapsulationKey[kAlgorithm].name) {
if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) {
throw lazyDOMException(
'key algorithm mismatch',
'InvalidAccessError');
}
if (!ArrayPrototypeIncludes(encapsulationKey[kKeyUsages], 'encapsulateBits')) {
if (!hasCryptoKeyUsage(encapsulationKey, 'encapsulateBits')) {
throw lazyDOMException(
'encapsulationKey does not have encapsulateBits usage',
'InvalidAccessError');
}
switch (encapsulationKey[kAlgorithm].name) {
switch (keyAlgorithm.name) {
case 'ML-KEM-512':
case 'ML-KEM-768':
case 'ML-KEM-1024':
@@ -1342,21 +1352,22 @@ async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKe
const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate');
const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey');
const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey);
if (normalizedEncapsulationAlgorithm.name !== encapsulationKey[kAlgorithm].name) {
if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) {
throw lazyDOMException(
'key algorithm mismatch',
'InvalidAccessError');
}
if (!ArrayPrototypeIncludes(encapsulationKey[kKeyUsages], 'encapsulateKey')) {
if (!hasCryptoKeyUsage(encapsulationKey, 'encapsulateKey')) {
throw lazyDOMException(
'encapsulationKey does not have encapsulateKey usage',
'InvalidAccessError');
}
let encapsulateBits;
switch (encapsulationKey[kAlgorithm].name) {
switch (keyAlgorithm.name) {
case 'ML-KEM-512':
case 'ML-KEM-768':
case 'ML-KEM-1024':
@@ -1402,20 +1413,21 @@ async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphert
});
const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate');
const keyAlgorithm = getCryptoKeyAlgorithm(decapsulationKey);
if (normalizedDecapsulationAlgorithm.name !== decapsulationKey[kAlgorithm].name) {
if (normalizedDecapsulationAlgorithm.name !== keyAlgorithm.name) {
throw lazyDOMException(
'key algorithm mismatch',
'InvalidAccessError');
}
if (!ArrayPrototypeIncludes(decapsulationKey[kKeyUsages], 'decapsulateBits')) {
if (!hasCryptoKeyUsage(decapsulationKey, 'decapsulateBits')) {
throw lazyDOMException(
'decapsulationKey does not have decapsulateBits usage',
'InvalidAccessError');
}
switch (decapsulationKey[kAlgorithm].name) {
switch (keyAlgorithm.name) {
case 'ML-KEM-512':
case 'ML-KEM-768':
case 'ML-KEM-1024':
@@ -1462,21 +1474,22 @@ async function decapsulateKey(
const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate');
const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey');
const keyAlgorithm = getCryptoKeyAlgorithm(decapsulationKey);
if (normalizedDecapsulationAlgorithm.name !== decapsulationKey[kAlgorithm].name) {
if (normalizedDecapsulationAlgorithm.name !== keyAlgorithm.name) {
throw lazyDOMException(
'key algorithm mismatch',
'InvalidAccessError');
}
if (!ArrayPrototypeIncludes(decapsulationKey[kKeyUsages], 'decapsulateKey')) {
if (!hasCryptoKeyUsage(decapsulationKey, 'decapsulateKey')) {
throw lazyDOMException(
'decapsulationKey does not have decapsulateKey usage',
'InvalidAccessError');
}
let decapsulatedBits;
switch (decapsulationKey[kAlgorithm].name) {
switch (keyAlgorithm.name) {
case 'ML-KEM-512':
case 'ML-KEM-768':
case 'ML-KEM-1024':
+67 -15
View File
@@ -8,6 +8,7 @@ const {
kKeyEncodingSPKI,
kKeyTypePublic,
kKeyTypePrivate,
kKeyTypeSecret,
} = internalBinding('crypto');
const {
@@ -18,11 +19,6 @@ const {
lazyDOMException,
} = require('internal/util');
const {
PrivateKeyObject,
PublicKeyObject,
} = require('internal/crypto/keys');
function importDerKey(keyData, isPublic) {
const handle = new KeyObjectHandle();
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
@@ -33,16 +29,57 @@ function importDerKey(keyData, isPublic) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
return isPublic ?
new PublicKeyObject(handle) :
new PrivateKeyObject(handle);
return handle;
}
function validateJwk(keyData, kty, extractable, usagesSet, expectedUse) {
if (!keyData.kty)
if (typeof keyData.kty !== 'string')
throw lazyDOMException('Invalid keyData', 'DataError');
if (keyData.kty !== kty)
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
switch (kty) {
case 'RSA':
if (typeof keyData.n !== 'string' ||
typeof keyData.e !== 'string' ||
(keyData.d !== undefined && typeof keyData.d !== 'string'))
throw lazyDOMException('Invalid keyData', 'DataError');
if (typeof keyData.d === 'string' &&
(typeof keyData.p !== 'string' ||
typeof keyData.q !== 'string' ||
typeof keyData.dp !== 'string' ||
typeof keyData.dq !== 'string' ||
typeof keyData.qi !== 'string'))
throw lazyDOMException('Invalid keyData', 'DataError');
break;
case 'EC':
if (typeof keyData.crv !== 'string' ||
typeof keyData.x !== 'string' ||
typeof keyData.y !== 'string' ||
(keyData.d !== undefined && typeof keyData.d !== 'string'))
throw lazyDOMException('Invalid keyData', 'DataError');
break;
case 'OKP':
if (typeof keyData.crv !== 'string' ||
typeof keyData.x !== 'string' ||
(keyData.d !== undefined && typeof keyData.d !== 'string'))
throw lazyDOMException('Invalid keyData', 'DataError');
break;
case 'oct':
if (typeof keyData.k !== 'string')
throw lazyDOMException('Invalid keyData', 'DataError');
break;
case 'AKP':
if (typeof keyData.alg !== 'string' ||
typeof keyData.pub !== 'string' ||
(keyData.priv !== undefined && typeof keyData.priv !== 'string'))
throw lazyDOMException('Invalid keyData', 'DataError');
break;
default: {
// It is not possible to get here because all possible cases are handled above.
const assert = require('internal/assert');
assert.fail('Unreachable code');
}
}
if (usagesSet.size > 0 && keyData.use !== undefined) {
if (keyData.use !== expectedUse)
throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
@@ -66,9 +103,7 @@ function importJwkKey(isPublic, keyData) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
return isPublic ?
new PublicKeyObject(handle) :
new PrivateKeyObject(handle);
return handle;
}
function importRawKey(isPublic, keyData, format, name, namedCurve) {
@@ -80,14 +115,31 @@ function importRawKey(isPublic, keyData, format, name, namedCurve) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
return isPublic ?
new PublicKeyObject(handle) :
new PrivateKeyObject(handle);
return handle;
}
function importSecretKey(keyData) {
const handle = new KeyObjectHandle();
handle.init(kKeyTypeSecret, keyData);
return handle;
}
function importJwkSecretKey(keyData) {
const handle = new KeyObjectHandle();
try {
handle.init(kKeyTypeSecret, keyData, kKeyFormatJWK, null, null);
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
return handle;
}
module.exports = {
importDerKey,
importJwkKey,
importJwkSecretKey,
importRawKey,
importSecretKey,
validateJwk,
};
+7 -2
View File
@@ -43,6 +43,10 @@ const {
setOwnProperty,
} = require('internal/util');
const { CryptoKey } = require('internal/crypto/webcrypto');
const {
getCryptoKeyAlgorithm,
getCryptoKeyType,
} = require('internal/crypto/keys');
const {
validateMaxBufferLength,
kNamedCurveAliases,
@@ -563,6 +567,7 @@ converters.HkdfParams = createDictionaryConverter(
{
key: 'info',
converter: converters.BufferSource,
validator: (V, dict) => validateMaxBufferLength(V, 'algorithm.info', 1024),
required: true,
},
]);
@@ -738,11 +743,11 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter(
key: 'public',
converter: converters.CryptoKey,
validator: (V, dict) => {
if (V.type !== 'public')
if (getCryptoKeyType(V) !== 'public')
throw lazyDOMException(
'algorithm.public must be a public key', 'InvalidAccessError');
if (StringPrototypeToLowerCase(V.algorithm.name) !== StringPrototypeToLowerCase(dict.name))
if (StringPrototypeToLowerCase(getCryptoKeyAlgorithm(V).name) !== StringPrototypeToLowerCase(dict.name))
throw lazyDOMException(
'key algorithm mismatch',
'InvalidAccessError');
+18 -11
View File
@@ -127,10 +127,11 @@ const {
getOwnNonIndexProperties,
} = internalBinding('util');
let kKeyObject;
let kExtractable;
let kAlgorithm;
let kKeyUsages;
let getCryptoKeyHandle;
let getCryptoKeyType;
let getCryptoKeyExtractable;
let getCryptoKeyAlgorithm;
let getCryptoKeyUsagesMask;
const kStrict = 2;
const kStrictWithoutPrototypes = 3;
@@ -413,15 +414,21 @@ function objectComparisonStart(val1, val2, mode, memos) {
return false;
}
} else if (isCryptoKey(val1)) {
if (kKeyObject === undefined) {
kKeyObject = require('internal/crypto/util').kKeyObject;
({ kExtractable, kAlgorithm, kKeyUsages } = require('internal/crypto/keys'));
if (getCryptoKeyHandle === undefined) {
({
getCryptoKeyHandle,
getCryptoKeyType,
getCryptoKeyExtractable,
getCryptoKeyAlgorithm,
getCryptoKeyUsagesMask,
} = require('internal/crypto/keys'));
}
if (!isCryptoKey(val2) ||
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)
getCryptoKeyType(val1) !== getCryptoKeyType(val2) ||
getCryptoKeyExtractable(val1) !== getCryptoKeyExtractable(val2) ||
!innerDeepEqual(getCryptoKeyAlgorithm(val1), getCryptoKeyAlgorithm(val2), mode, memos) ||
getCryptoKeyUsagesMask(val1) !== getCryptoKeyUsagesMask(val2) ||
!getCryptoKeyHandle(val1).equals(getCryptoKeyHandle(val2))
) {
return false;
}
+17 -9
View File
@@ -149,24 +149,32 @@ core key objects.
#### `KeyObjectData`
`KeyObjectData` is an internal thread-safe structure used to wrap either
a `EVPKeyPointer` (for Public or Private keys) or a `ByteSource` containing
a Secret key.
an `EVPKeyPointer` (for Public or Private keys) or a `ByteSource` containing
a Secret key. It is the shared backing representation used by `KeyObject`,
`CryptoKey`, and native crypto jobs that operate on key material.
#### `KeyObjectHandle`
The `KeyObjectHandle` provides the interface between the native C++ code
handling keys and the public JavaScript `KeyObject` API.
`KeyObjectHandle` is the JavaScript-visible C++ handle for a
`KeyObjectData`. It exposes operations that internal JavaScript uses to
initialize, inspect, compare, and export key material. Native code passes
`KeyObjectData` across threads and jobs; a `KeyObjectHandle` is created when
JavaScript needs access to those operations.
#### `KeyObject`
A `KeyObject` is the public Node.js-specific API for keys. A single
`KeyObject` wraps exactly one `KeyObjectHandle`.
A `KeyObject` is the public Node.js-specific API for keys. It extends a
native `NativeKeyObject`, which stores `KeyObjectData` for structured
cloning, and it owns one `KeyObjectHandle` used by the JavaScript API
surface.
#### `CryptoKey`
A `CryptoKey` is the Web Crypto API's alternative to `KeyObject`. In the
Node.js implementation, `CryptoKey` is a thin wrapper around the
`KeyObject` and it is largely possible to use them interchangeably.
A `CryptoKey` is the Web Crypto API key type. In the Node.js implementation,
public `CryptoKey` instances are backed by a native `NativeCryptoKey`, not by
a `KeyObject`. `NativeCryptoKey` stores the same `KeyObjectData`
representation as `KeyObject`, plus the Web Crypto internal slots
(`[[extractable]]`, `[[algorithm]]`, and `[[usages]]`).
### `CryptoJob`
+11 -6
View File
@@ -436,7 +436,6 @@ Maybe<void> EcKeyGenTraits::AdditionalConfig(
EcKeyPairGenConfig* params) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[*offset]->IsString()); // curve name
CHECK(args[*offset + 1]->IsInt32()); // param encoding
Utf8Value curve_name(env->isolate(), args[*offset]);
params->params.curve_nid = Ec::GetCurveIdFromName(*curve_name);
@@ -445,11 +444,17 @@ Maybe<void> EcKeyGenTraits::AdditionalConfig(
return Nothing<void>();
}
params->params.param_encoding = args[*offset + 1].As<Int32>()->Value();
if (params->params.param_encoding != OPENSSL_EC_NAMED_CURVE &&
params->params.param_encoding != OPENSSL_EC_EXPLICIT_CURVE) {
THROW_ERR_OUT_OF_RANGE(env, "Invalid param_encoding specified");
return Nothing<void>();
// param encoding
if (args[*offset + 1]->IsNullOrUndefined()) {
params->params.param_encoding = OPENSSL_EC_NAMED_CURVE;
} else {
CHECK(args[*offset + 1]->IsInt32());
params->params.param_encoding = args[*offset + 1].As<Int32>()->Value();
if (params->params.param_encoding != OPENSSL_EC_NAMED_CURVE &&
params->params.param_encoding != OPENSSL_EC_EXPLICIT_CURVE) {
THROW_ERR_OUT_OF_RANGE(env, "Invalid param_encoding specified");
return Nothing<void>();
}
}
*offset += 2;
+264
View File
@@ -1788,6 +1788,270 @@ std::unique_ptr<worker::TransferData> NativeKeyObject::CloneForMessaging()
return std::make_unique<KeyObjectTransferData>(handle_data_);
}
void NativeCryptoKey::Initialize(Environment* env, Local<Object> target) {
SetMethod(env->context(),
target,
"createCryptoKeyClass",
NativeCryptoKey::CreateCryptoKeyClass);
SetMethod(
env->context(), target, "getCryptoKeySlots", NativeCryptoKey::GetSlots);
}
void NativeCryptoKey::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(NativeCryptoKey::CreateCryptoKeyClass);
registry->Register(NativeCryptoKey::GetSlots);
registry->Register(NativeCryptoKey::New);
}
namespace {
// Verifies that `value` is a `NativeCryptoKey` by checking whether it
// was constructed from the Environment's `NativeCryptoKey` template.
bool IsNativeCryptoKey(Environment* env, Local<Value> value) {
auto t = env->crypto_cryptokey_constructor_template();
return !t.IsEmpty() && t->HasInstance(value);
}
} // namespace
bool NativeCryptoKey::HasInstance(Environment* env, Local<Value> value) {
return IsNativeCryptoKey(env, value);
}
void NativeCryptoKey::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_EQ(args.Length(), 4);
// args[0] is a KeyObjectHandle; we keep its KeyObjectData directly.
// args[1] is the algorithm dictionary object.
// args[2] is the usages mask.
// args[3] is the extractable boolean.
//
// args[1] is undefined only when called from
// CryptoKeyTransferData::Deserialize for a partially-initialized
// CryptoKey: algorithm/usages mask/extractable get filled in afterwards
// by FinalizeTransferRead before any JS can see the object.
//
// This constructor is not exposed to user JS - the public CryptoKey
// class throws from its constructor and InternalCryptoKey is kept
// in a module-closure.
CHECK(KeyObjectHandle::HasInstance(env, args[0]));
KeyObjectHandle* handle = Unwrap<KeyObjectHandle>(args[0].As<Object>());
CHECK_NOT_NULL(handle);
auto* native = new NativeCryptoKey(env, args.This(), handle->Data());
if (!args[1]->IsUndefined()) {
CHECK(args[1]->IsObject());
CHECK(args[2]->IsUint32());
CHECK(args[3]->IsBoolean());
args.This()->SetInternalField(kAlgorithmField, args[1]);
native->usages_mask_ = args[2].As<Uint32>()->Value();
native->extractable_ = args[3]->IsTrue();
}
}
void NativeCryptoKey::CreateCryptoKeyClass(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
CHECK_EQ(args.Length(), 1);
Local<Value> callback = args[0];
CHECK(callback->IsFunction());
Local<FunctionTemplate> t =
NewFunctionTemplate(isolate, NativeCryptoKey::New);
t->InstanceTemplate()->SetInternalFieldCount(
NativeCryptoKey::kInternalFieldCount);
CHECK(env->crypto_cryptokey_constructor_template().IsEmpty());
env->set_crypto_cryptokey_constructor_template(t);
Local<Value> ctor;
if (!t->GetFunction(env->context()).ToLocal(&ctor)) return;
Local<Value> recv = Undefined(env->isolate());
Local<Value> ret_v;
if (!callback.As<Function>()
->Call(env->context(), recv, 1, &ctor)
.ToLocal(&ret_v)) {
return;
}
Local<Array> ret = ret_v.As<Array>();
Local<Value> internal_ctor_v;
if (!ret->Get(env->context(), 1).ToLocal(&internal_ctor_v)) return;
CHECK(env->crypto_internal_cryptokey_constructor().IsEmpty());
env->set_crypto_internal_cryptokey_constructor(
internal_ctor_v.As<Function>());
args.GetReturnValue().Set(ret);
}
// Returns all of the key's internal slot values as a single Array:
// [type enum, extractable, algorithm, usages mask, handle]. JS-side helpers
// call this once per key to prime a per-instance cache, so subsequent
// reads don't need to cross into C++ at all.
void NativeCryptoKey::GetSlots(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_EQ(args.Length(), 1);
if (!HasInstance(env, args[0])) {
THROW_ERR_INVALID_THIS(env, "Value of \"this\" must be of type CryptoKey");
return;
}
Local<Object> obj = args[0].As<Object>();
NativeCryptoKey* native = Unwrap<NativeCryptoKey>(obj);
Local<Object> handle;
if (!KeyObjectHandle::Create(env, native->handle_data_).ToLocal(&handle)) {
return;
}
Local<Value> algorithm = obj->GetInternalField(kAlgorithmField).As<Value>();
CHECK(algorithm->IsObject());
Isolate* isolate = env->isolate();
Local<Value> slots[] = {
Uint32::NewFromUnsigned(isolate, native->handle_data_.GetKeyType()),
v8::Boolean::New(isolate, native->extractable_),
algorithm,
Uint32::NewFromUnsigned(isolate, native->usages_mask_),
handle,
};
args.GetReturnValue().Set(Array::New(isolate, slots, arraysize(slots)));
}
BaseObject::TransferMode NativeCryptoKey::GetTransferMode() const {
return BaseObject::TransferMode::kCloneable;
}
std::unique_ptr<worker::TransferData> NativeCryptoKey::CloneForMessaging()
const {
Isolate* isolate = env()->isolate();
Local<Object> obj = object();
Local<Value> algorithm_v = obj->GetInternalField(kAlgorithmField).As<Value>();
CHECK(algorithm_v->IsObject());
v8::Global<Object> algorithm_copy(isolate, algorithm_v.As<Object>());
return std::make_unique<CryptoKeyTransferData>(
handle_data_, std::move(algorithm_copy), usages_mask_, extractable_);
}
Maybe<void> NativeCryptoKey::FinalizeTransferRead(
Local<Context> context, v8::ValueDeserializer* deserializer) {
Local<Value> bundle_v;
if (!deserializer->ReadValue(context).ToLocal(&bundle_v)) {
return Nothing<void>();
}
CHECK(bundle_v->IsObject());
Local<Object> bundle = bundle_v.As<Object>();
Isolate* isolate = env()->isolate();
Local<Object> obj = object();
// The partially-initialized object produced by
// CryptoKeyTransferData::Deserialize should not have algorithm set yet.
CHECK(obj->GetInternalField(kAlgorithmField).As<Value>()->IsUndefined());
Local<Value> algorithm_v;
if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "algorithm"))
.ToLocal(&algorithm_v)) {
return Nothing<void>();
}
CHECK(algorithm_v->IsObject());
obj->SetInternalField(kAlgorithmField, algorithm_v);
Local<Value> usages_v;
if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "usages"))
.ToLocal(&usages_v)) {
return Nothing<void>();
}
CHECK(usages_v->IsUint32());
usages_mask_ = usages_v.As<Uint32>()->Value();
Local<Value> extractable_v;
if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "extractable"))
.ToLocal(&extractable_v)) {
return Nothing<void>();
}
CHECK(extractable_v->IsBoolean());
extractable_ = extractable_v->IsTrue();
return v8::JustVoid();
}
Maybe<bool> NativeCryptoKey::CryptoKeyTransferData::FinalizeTransferWrite(
Local<Context> context, v8::ValueSerializer* serializer) {
Isolate* isolate = Isolate::GetCurrent();
CHECK(!algorithm_.IsEmpty());
Local<Object> bundle = Object::New(isolate);
Local<Value> algorithm_v = PersistentToLocal::Strong(algorithm_);
if (bundle
->Set(
context, FIXED_ONE_BYTE_STRING(isolate, "algorithm"), algorithm_v)
.IsNothing() ||
bundle
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "usages"),
Uint32::NewFromUnsigned(isolate, usages_mask_))
.IsNothing() ||
bundle
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "extractable"),
v8::Boolean::New(isolate, extractable_))
.IsNothing()) {
return Nothing<bool>();
}
auto ret = serializer->WriteValue(context, bundle);
algorithm_.Reset();
return ret;
}
BaseObjectPtr<BaseObject> NativeCryptoKey::CryptoKeyTransferData::Deserialize(
Environment* env,
Local<Context> context,
std::unique_ptr<worker::TransferData> self) {
if (context != env->context()) {
THROW_ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE(env);
return {};
}
// Reconstruct the KeyObjectHandle for the transferred KeyObjectData.
Local<Object> handle;
if (!KeyObjectHandle::Create(env, data_).ToLocal(&handle)) return {};
// Make sure internal/crypto/keys has been loaded so that the
// CryptoKey constructor is registered with the Environment.
Isolate* isolate = env->isolate();
Local<Value> arg = FIXED_ONE_BYTE_STRING(isolate, "internal/crypto/keys");
if (env->builtin_module_require()
->Call(context, Null(isolate), 1, &arg)
.IsEmpty()) {
return {};
}
// Construct a partially-initialized InternalCryptoKey; algorithm,
// usages mask and extractable are filled in via FinalizeTransferRead.
Local<Function> cryptokey_ctor = env->crypto_internal_cryptokey_constructor();
CHECK(!cryptokey_ctor.IsEmpty());
Local<Value> ctor_args[] = {
handle,
Undefined(isolate),
Undefined(isolate),
Undefined(isolate),
};
Local<Value> cryptokey;
if (!cryptokey_ctor->NewInstance(context, 4, ctor_args).ToLocal(&cryptokey)) {
return {};
}
return BaseObjectPtr<BaseObject>(
Unwrap<NativeCryptoKey>(cryptokey.As<Object>()));
}
void NativeCryptoKey::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("handle_data", handle_data_);
}
void NativeCryptoKey::CryptoKeyTransferData::MemoryInfo(
MemoryTracker* tracker) const {
tracker->TrackField("data", data_);
tracker->TrackField("algorithm", algorithm_);
}
namespace Keys {
void Initialize(Environment* env, Local<Object> target) {
target->Set(env->context(),
+89
View File
@@ -13,6 +13,7 @@
#include <openssl/evp.h>
#include <cstdint>
#include <memory>
#include <string>
@@ -233,6 +234,94 @@ class NativeKeyObject : public BaseObject {
KeyObjectData handle_data_;
};
// NativeCryptoKey is the native base class for the Web Crypto
// `CryptoKey`. It holds the internal slots - `[[type]]` as an enum,
// `[[extractable]]`, `[[algorithm]]`, `[[usages]]` as a mask, and the
// underlying KeyObjectData. The public `type`, `extractable`,
// `algorithm`, and `usages` accessors on `CryptoKey.prototype` are
// user-configurable per Web IDL, so internal consumers read these
// values directly from the C++ side via a single `GetSlots` call
// which returns all slots at once; JS primes a per-instance cache
// from that result.
class NativeCryptoKey : public BaseObject {
public:
enum InternalFields {
kAlgorithmField = BaseObject::kInternalFieldCount,
kInternalFieldCount,
};
static void Initialize(Environment* env, v8::Local<v8::Object> target);
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void CreateCryptoKeyClass(
const v8::FunctionCallbackInfo<v8::Value>& args);
// True if `value` is a real NativeCryptoKey instance. Uses the
// FunctionTemplate stored on the Environment as a brand check.
// Used by `GetSlots` to validate its receiver.
static bool HasInstance(Environment* env, v8::Local<v8::Value> value);
// Returns [type, extractable, algorithm, usages mask, handle] in one call
// so JS can prime a per-instance cache on first access.
static void GetSlots(const v8::FunctionCallbackInfo<v8::Value>& args);
void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(NativeCryptoKey)
SET_SELF_SIZE(NativeCryptoKey)
class CryptoKeyTransferData : public worker::TransferData {
public:
CryptoKeyTransferData(const KeyObjectData& data,
v8::Global<v8::Object>&& algorithm,
uint32_t usages_mask,
bool extractable)
: data_(data.addRef()),
algorithm_(std::move(algorithm)),
usages_mask_(usages_mask),
extractable_(extractable) {}
BaseObjectPtr<BaseObject> Deserialize(
Environment* env,
v8::Local<v8::Context> context,
std::unique_ptr<worker::TransferData> self) override;
v8::Maybe<bool> FinalizeTransferWrite(
v8::Local<v8::Context> context,
v8::ValueSerializer* serializer) override;
void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(CryptoKeyTransferData)
SET_SELF_SIZE(CryptoKeyTransferData)
private:
KeyObjectData data_;
v8::Global<v8::Object> algorithm_;
uint32_t usages_mask_;
bool extractable_;
};
BaseObject::TransferMode GetTransferMode() const override;
std::unique_ptr<worker::TransferData> CloneForMessaging() const override;
v8::Maybe<void> FinalizeTransferRead(
v8::Local<v8::Context> context,
v8::ValueDeserializer* deserializer) override;
const KeyObjectData& handle_data() const { return handle_data_; }
private:
NativeCryptoKey(Environment* env,
v8::Local<v8::Object> wrap,
const KeyObjectData& handle_data)
: BaseObject(env, wrap), handle_data_(handle_data.addRef()) {
MakeWeak();
}
KeyObjectData handle_data_;
uint32_t usages_mask_ = 0;
bool extractable_ = false;
};
enum WebCryptoKeyFormat {
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatPKCS8,
+2 -2
View File
@@ -128,9 +128,9 @@ Maybe<void> RsaKeyGenTraits::AdditionalConfig(
static_cast<RSAKeyVariant>(args[*offset].As<Uint32>()->Value());
CHECK_IMPLIES(params->params.variant != kKeyVariantRSA_PSS,
args.Length() == 10);
static_cast<unsigned int>(args.Length()) >= *offset + 3);
CHECK_IMPLIES(params->params.variant == kKeyVariantRSA_PSS,
args.Length() == 13);
static_cast<unsigned int>(args.Length()) >= *offset + 6);
params->params.modulus_bits = args[*offset + 1].As<Uint32>()->Value();
params->params.exponent = args[*offset + 2].As<Uint32>()->Value();
+2
View File
@@ -407,6 +407,7 @@
V(contextify_global_template, v8::ObjectTemplate) \
V(contextify_wrapper_template, v8::ObjectTemplate) \
V(cpu_usage_template, v8::DictionaryTemplate) \
V(crypto_cryptokey_constructor_template, v8::FunctionTemplate) \
V(crypto_key_object_handle_constructor, v8::FunctionTemplate) \
V(env_proxy_template, v8::ObjectTemplate) \
V(env_proxy_ctor_template, v8::FunctionTemplate) \
@@ -481,6 +482,7 @@
V(async_hooks_init_function, v8::Function) \
V(async_hooks_promise_resolve_function, v8::Function) \
V(buffer_prototype_object, v8::Object) \
V(crypto_internal_cryptokey_constructor, v8::Function) \
V(crypto_key_object_private_constructor, v8::Function) \
V(crypto_key_object_public_constructor, v8::Function) \
V(crypto_key_object_secret_constructor, v8::Function) \
+1
View File
@@ -48,6 +48,7 @@ namespace crypto {
V(Hmac) \
V(Keygen) \
V(Keys) \
V(NativeCryptoKey) \
V(NativeKeyObject) \
V(PBKDF2Job) \
V(Random) \
@@ -0,0 +1,126 @@
'use strict';
// The four CryptoKey prototype getters (`type`, `extractable`,
// `algorithm`, `usages`) are user-configurable per Web IDL, so they
// can be invoked with an arbitrary `this`. The native callbacks that
// implement them must brand-check their receiver and throw cleanly
// (ERR_INVALID_THIS) rather than crashing the process or returning
// garbage. This test exercises four progressively more hostile
// receiver shapes, including subverting `instanceof` via
// `Symbol.hasInstance`, to make sure the C++ brand check holds.
//
// It also verifies that `util.types.isCryptoKey()` cannot be fooled
// by prototype spoofing.
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('node:assert');
const { types: { isCryptoKey } } = require('node:util');
const { subtle } = globalThis.crypto;
(async () => {
const key = await subtle.generateKey(
{ name: 'HMAC', hash: 'SHA-256' },
true,
['sign'],
);
const CryptoKey = key.constructor;
// Capture the underlying prototype getters once, so that subsequent
// tampering with `CryptoKey.prototype` cannot affect what we call.
const getters = {
type: Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'type').get,
extractable:
Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'extractable').get,
algorithm:
Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'algorithm').get,
usages:
Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'usages').get,
};
// Sanity: each getter works on a real CryptoKey.
Object.entries(getters).forEach(([name, getter]) => {
assert.notStrictEqual(getter.call(key), undefined, `baseline ${name}`);
});
assert.strictEqual(isCryptoKey(key), true);
const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' };
// Plain object receiver.
Object.entries(getters).forEach(([, getter]) => {
assert.throws(() => getter.call({}), invalidThis);
});
// Null-prototype object receiver.
Object.entries(getters).forEach(([, getter]) => {
assert.throws(() => getter.call({ __proto__: null }), invalidThis);
});
// Primitive receiver.
Object.entries(getters).forEach(([, getter]) => {
assert.throws(() => getter.call(1), invalidThis);
});
// Null.
Object.entries(getters).forEach(([, getter]) => {
// eslint-disable-next-line no-useless-call
assert.throws(() => getter.call(null), invalidThis);
});
// Undefined.
Object.entries(getters).forEach(([, getter]) => {
assert.throws(() => getter.call(), invalidThis);
});
// Function
Object.entries(getters).forEach(([, getter]) => {
assert.throws(() => getter.call(function() {}), invalidThis);
});
// Prototype spoofing with InternalCryptoKey.prototype must not pass
// util.types.isCryptoKey().
const spoofed = {};
Object.setPrototypeOf(spoofed, Object.getPrototypeOf(key));
assert.strictEqual(spoofed instanceof CryptoKey, true);
assert.strictEqual(isCryptoKey(spoofed), false);
await assert.rejects(
subtle.sign('HMAC', spoofed, Buffer.from('payload')),
invalidThis);
await assert.rejects(
subtle.exportKey('jwk', spoofed),
invalidThis);
// Subvert `instanceof CryptoKey` via Symbol.hasInstance, then
// invoke the native getters on a forged object. The C++ tag
// check must reject the receiver even though `instanceof`
// reports true.
Object.defineProperty(CryptoKey, Symbol.hasInstance, {
configurable: true,
value: () => true,
});
const fake = { foo: 'bar' };
assert.strictEqual(fake instanceof CryptoKey, true);
assert.strictEqual(isCryptoKey(fake), false);
Object.entries(getters).forEach(([, getter]) => {
assert.throws(() => getter.call(fake), invalidThis);
});
// Subverted `instanceof` plus a real BaseObject of a different
// kind (a Buffer) as the receiver. Without the C++ tag check
// this would type-confuse `Unwrap<NativeCryptoKey>`.
const buf = Buffer.alloc(16);
assert.strictEqual(buf instanceof CryptoKey, true);
assert.strictEqual(isCryptoKey(buf), false);
Object.entries(getters).forEach(([, getter]) => {
assert.throws(() => getter.call(buf), invalidThis);
});
// The real CryptoKey continues to work after all of the above.
assert.strictEqual(getters.type.call(key), 'secret');
assert.strictEqual(getters.extractable.call(key), true);
assert.strictEqual(getters.algorithm.call(key).name, 'HMAC');
assert.deepStrictEqual(getters.usages.call(key), ['sign']);
})().then(common.mustCall());
@@ -0,0 +1,351 @@
'use strict';
// Tests that CryptoKey instances can be structured-cloned (same-realm
// via `structuredClone`, cross-realm via `MessagePort.postMessage` and
// `Worker.postMessage`) and that the clones:
// 1. preserve all of [[type]], [[extractable]], [[algorithm]],
// [[usages]] internal slots (as observed through both the public
// accessors and the custom util.inspect output),
// 2. are usable in cryptographic operations (sign/verify/encrypt/
// decrypt/exportKey) and produce the same output as the original,
// 3. can themselves be cloned again (round-trip), and
// 4. work for secret, public, and private keys and for both
// extractable and non-extractable keys.
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('node:assert');
const { inspect } = require('node:util');
const { once } = require('node:events');
const { Worker, MessageChannel } = require('node:worker_threads');
const { subtle } = globalThis.crypto;
function assertNoOwnReflection(key) {
assert.deepStrictEqual(Object.getOwnPropertySymbols(key), []);
assert.deepStrictEqual(Object.getOwnPropertyNames(key), []);
assert.deepStrictEqual(Reflect.ownKeys(key), []);
}
function describeKey(key) {
const algorithm = { ...key.algorithm };
if (algorithm.hash !== undefined)
algorithm.hash = { ...algorithm.hash };
if (algorithm.publicExponent !== undefined)
algorithm.publicExponent = Array.from(algorithm.publicExponent);
return {
type: key.type,
extractable: key.extractable,
algorithm,
usages: [...key.usages].sort(),
};
}
function assertSameCryptoKey(a, b) {
assert.notStrictEqual(a, b);
assert.strictEqual(a.type, b.type);
assert.strictEqual(a.extractable, b.extractable);
assert.deepStrictEqual(a.algorithm, b.algorithm);
assert.deepStrictEqual([...a.usages].sort(), [...b.usages].sort());
assertNoOwnReflection(a);
assertNoOwnReflection(b);
// util.inspect reads native internal slots directly, so a clone's
// rendered form must match the original's.
assert.strictEqual(inspect(a, { depth: 4 }), inspect(b, { depth: 4 }));
// assert.deepStrictEqual on CryptoKey objects goes through the
// dedicated isCryptoKey branch in comparisons.js; a clone must be
// deep-equal to its source.
assert.deepStrictEqual(a, b);
}
async function roundTripViaMessageChannel(key) {
const { port1, port2 } = new MessageChannel();
port1.postMessage(key);
const [received] = await once(port2, 'message');
port1.close();
port2.close();
return received;
}
async function checkHmacKey(original) {
const data = Buffer.from('some data to sign');
const cloned = structuredClone(original);
assertSameCryptoKey(original, cloned);
const viaPort = await roundTripViaMessageChannel(original);
assertSameCryptoKey(original, viaPort);
// Round-trip: clone a clone.
const clonedAgain = structuredClone(viaPort);
assertSameCryptoKey(original, clonedAgain);
const viaPortAgain = await roundTripViaMessageChannel(cloned);
assertSameCryptoKey(original, viaPortAgain);
// Signatures produced by every copy must match.
const sigs = await Promise.all(
[original, cloned, viaPort, clonedAgain, viaPortAgain].map(
(k) => subtle.sign('HMAC', k, data),
),
);
for (let i = 1; i < sigs.length; i++) {
assert.deepStrictEqual(Buffer.from(sigs[0]), Buffer.from(sigs[i]));
}
// Each copy must verify a signature produced by any other copy.
for (const verifier of [original, cloned, viaPort, clonedAgain]) {
for (const sig of sigs) {
assert.strictEqual(
await subtle.verify('HMAC', verifier, sig, data), true);
}
}
// Exported JWK must match byte-for-byte when extractable.
if (original.extractable) {
const jwks = await Promise.all(
[original, cloned, viaPort, clonedAgain].map(
(k) => subtle.exportKey('jwk', k),
),
);
for (let i = 1; i < jwks.length; i++) {
assert.deepStrictEqual(jwks[0], jwks[i]);
}
} else {
// Non-extractable keys must refuse export on every copy.
for (const k of [cloned, viaPort, clonedAgain]) {
await assert.rejects(subtle.exportKey('jwk', k),
{ name: 'InvalidAccessError' });
}
}
}
async function checkAsymmetricKeyPair({ publicKey, privateKey }) {
const data = Buffer.from('payload');
for (const original of [publicKey, privateKey]) {
const cloned = structuredClone(original);
assertSameCryptoKey(original, cloned);
const viaPort = await roundTripViaMessageChannel(original);
assertSameCryptoKey(original, viaPort);
const clonedAgain = structuredClone(viaPort);
assertSameCryptoKey(original, clonedAgain);
}
// Sign with the original private key, verify with every cloned public key.
const signature = await subtle.sign(
{ name: 'ECDSA', hash: 'SHA-256' }, privateKey, data);
const publicClones = [
publicKey,
structuredClone(publicKey),
await roundTripViaMessageChannel(publicKey),
structuredClone(await roundTripViaMessageChannel(publicKey)),
];
for (const pub of publicClones) {
assert.strictEqual(
await subtle.verify({ name: 'ECDSA', hash: 'SHA-256' },
pub, signature, data),
true);
}
// Sign with every cloned private key, verify with the original public key.
const privateClones = [
structuredClone(privateKey),
await roundTripViaMessageChannel(privateKey),
structuredClone(await roundTripViaMessageChannel(privateKey)),
];
for (const priv of privateClones) {
const sig = await subtle.sign(
{ name: 'ECDSA', hash: 'SHA-256' }, priv, data);
assert.strictEqual(
await subtle.verify({ name: 'ECDSA', hash: 'SHA-256' },
publicKey, sig, data),
true);
}
}
async function checkTransferToWorker(key) {
// A one-shot worker that receives a key, asserts its properties,
// signs with it, and echoes the key back together with the signature.
const worker = new Worker(`
'use strict';
const { parentPort } = require('node:worker_threads');
const { subtle } = globalThis.crypto;
parentPort.once('message', async ({ key, expected }) => {
try {
if (key.type !== expected.type ||
key.extractable !== expected.extractable ||
key.algorithm.name !== expected.algorithm.name ||
key.algorithm.hash?.name !== expected.algorithm.hash?.name) {
throw new Error('slot mismatch in worker');
}
const sig = await subtle.sign('HMAC', key, Buffer.from('wdata'));
// Echo the key back so the parent can verify round-trip.
parentPort.postMessage({ key, sig: Buffer.from(sig) });
} catch (err) {
parentPort.postMessage({ error: err.message });
}
});
`, { eval: true });
worker.postMessage({
key,
expected: {
type: key.type,
extractable: key.extractable,
algorithm: {
name: key.algorithm.name,
hash: key.algorithm.hash ? { name: key.algorithm.hash.name } : undefined,
},
},
});
const [msg] = await once(worker, 'message');
await worker.terminate();
assert.strictEqual(msg.error, undefined, msg.error);
// The key echoed back from the worker must itself be a fully-formed
// CryptoKey with all slots preserved.
assertSameCryptoKey(key, msg.key);
// The signature produced inside the worker must verify against the
// parent-side key.
assert.strictEqual(
await subtle.verify('HMAC', key, msg.sig, Buffer.from('wdata')),
true);
}
async function checkRsaPssTransferToWorker({ publicKey, privateKey }) {
const data = Buffer.from('rsa-pss worker payload');
const algorithm = { name: 'RSA-PSS', saltLength: 32 };
const parentSignature = Buffer.from(
await subtle.sign(algorithm, privateKey, data));
const worker = new Worker(`
'use strict';
const assert = require('node:assert');
const { parentPort } = require('node:worker_threads');
const { subtle } = globalThis.crypto;
${describeKey}
parentPort.once('message', async (message) => {
try {
const {
publicKey,
privateKey,
expectedPublic,
expectedPrivate,
signature,
data,
} = message;
assert.deepStrictEqual(describeKey(publicKey), expectedPublic);
assert.deepStrictEqual(describeKey(privateKey), expectedPrivate);
const algorithm = { name: 'RSA-PSS', saltLength: 32 };
const verified = await subtle.verify(
algorithm, publicKey, signature, data);
const workerSignature = Buffer.from(
await subtle.sign(algorithm, privateKey, data));
parentPort.postMessage({
publicKey,
privateKey,
verified,
signature: workerSignature,
});
} catch (err) {
parentPort.postMessage({ error: err.stack || err.message });
}
});
`, { eval: true });
worker.postMessage({
publicKey,
privateKey,
expectedPublic: describeKey(publicKey),
expectedPrivate: describeKey(privateKey),
signature: parentSignature,
data,
});
const [msg] = await once(worker, 'message');
await worker.terminate();
assert.strictEqual(msg.error, undefined, msg.error);
assert.strictEqual(msg.verified, true);
assertSameCryptoKey(publicKey, msg.publicKey);
assertSameCryptoKey(privateKey, msg.privateKey);
assert.strictEqual(
await subtle.verify(algorithm, publicKey, msg.signature, data),
true);
}
(async () => {
// Extractable HMAC (secret)
const hmacExtractable = await subtle.importKey(
'raw',
Buffer.from(
'000102030405060708090a0b0c0d0e0f' +
'101112131415161718191a1b1c1d1e1f', 'hex'),
{ name: 'HMAC', hash: 'SHA-256' },
true,
['sign', 'verify']);
await checkHmacKey(hmacExtractable);
await checkTransferToWorker(hmacExtractable);
// Non-extractable HMAC (secret)
const hmacNonExtractable = await subtle.generateKey(
{ name: 'HMAC', hash: 'SHA-384' },
false,
['sign', 'verify']);
await checkHmacKey(hmacNonExtractable);
await checkTransferToWorker(hmacNonExtractable);
// AES-GCM secret key
{
const key = await subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
const cloned = structuredClone(key);
assertSameCryptoKey(key, cloned);
const viaPort = await roundTripViaMessageChannel(key);
assertSameCryptoKey(key, viaPort);
const clonedAgain = structuredClone(viaPort);
assertSameCryptoKey(key, clonedAgain);
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
const plaintext = Buffer.from('secret payload');
const ciphertext = await subtle.encrypt(
{ name: 'AES-GCM', iv }, key, plaintext);
// Decrypt with every clone.
for (const k of [cloned, viaPort, clonedAgain]) {
const decrypted = await subtle.decrypt(
{ name: 'AES-GCM', iv }, k, ciphertext);
assert.deepStrictEqual(Buffer.from(decrypted), plaintext);
}
}
// ECDSA keypair (public extractable, private non-extractable)
const ecKeypair = await subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
false,
['sign', 'verify']);
await checkAsymmetricKeyPair(ecKeypair);
// ECDSA with extractable private key (covers the extractable-private path)
const ecKeypairExtractable = await subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-384' },
true,
['sign', 'verify']);
await checkAsymmetricKeyPair(ecKeypairExtractable);
// RSA-PSS keypair through a Worker (covers public/private native key
// handles and cloning of the publicExponent algorithm member).
const rsaPssKeypair = await subtle.generateKey(
{
name: 'RSA-PSS',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
false,
['sign', 'verify']);
await checkRsaPssTransferToWorker(rsaPssKeypair);
})().then(common.mustCall());
@@ -0,0 +1,161 @@
'use strict';
// CryptoKey prototype getters (`type`, `extractable`,
// `algorithm`, `usages`) are configurable in the Web Crypto IDL and
// can therefore be replaced by user code. Internal consumers of those
// attributes, and the custom inspect output, must NOT go through the
// public prototype getters, they must read the underlying native
// internal slots directly. This test mutates the prototype getters
// (and mutates the per-instance `algorithm`/`usages` caches returned
// by those getters) and asserts that:
//
// 1. util.inspect() shows the real internal values, unaffected by
// the replacement getters.
// 2. Internal Web Crypto and Node.js crypto bridge operations that
// receive the mutated CryptoKey still succeed and observe the real
// internal state, not the replaced one.
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('node:assert');
const {
createHmac,
KeyObject,
sign: cryptoSign,
verify: cryptoVerify,
} = require('node:crypto');
const { inspect } = require('node:util');
const { subtle } = globalThis.crypto;
common.expectWarning({
DeprecationWarning: {
DEP0203: 'Passing a CryptoKey to node:crypto functions is deprecated.',
},
});
(async () => {
const key = await subtle.generateKey(
{ name: 'HMAC', hash: 'SHA-256' },
true,
['sign', 'verify'],
);
const { publicKey: ecPublicKey, privateKey: ecPrivateKey } =
await subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
false,
['sign', 'verify'],
);
// Snapshot the real values BEFORE tampering.
const realType = key.type;
const realExtractable = key.extractable;
const realAlgorithm = { ...key.algorithm, hash: { ...key.algorithm.hash } };
const realUsages = [...key.usages];
// 1) Replace all four prototype getters.
let proto = Object.getPrototypeOf(key);
while (proto && !Object.getOwnPropertyDescriptor(proto, 'type')) {
proto = Object.getPrototypeOf(proto);
}
assert.ok(proto, 'could not find CryptoKey.prototype');
const forgedDescriptors = {
type: {
configurable: true,
enumerable: true,
get() { return 'FORGED-TYPE'; },
},
extractable: {
configurable: true,
enumerable: true,
get() { return 'FORGED-EXTRACTABLE'; },
},
algorithm: {
configurable: true,
enumerable: true,
get() { return { name: 'FORGED-ALGORITHM', hash: { name: 'FORGED-HASH' } }; },
},
usages: {
configurable: true,
enumerable: true,
get() { return ['forged-usage']; },
},
};
const originalDescriptors = {};
for (const [name, descriptor] of Object.entries(forgedDescriptors)) {
originalDescriptors[name] = Object.getOwnPropertyDescriptor(proto, name);
Object.defineProperty(proto, name, descriptor);
}
try {
// Confirm the forgeries are in effect from user-code perspective.
assert.strictEqual(key.type, 'FORGED-TYPE');
assert.strictEqual(key.extractable, 'FORGED-EXTRACTABLE');
assert.strictEqual(key.algorithm.name, 'FORGED-ALGORITHM');
assert.deepStrictEqual(key.usages, ['forged-usage']);
// 2) util.inspect() must not be influenced by the forged getters,
// it must read the real internal slots directly.
const rendered = inspect(key, { depth: 4 });
assert.match(rendered, /type: 'secret'/);
assert.match(rendered, /extractable: true/);
assert.match(rendered, /name: 'HMAC'/);
assert.match(rendered, /name: 'SHA-256'/);
assert.match(rendered, /'sign'/);
assert.match(rendered, /'verify'/);
assert.doesNotMatch(rendered, /FORGED/);
// 3) Internal consumers that receive this CryptoKey must see the
// real internal slots. exportKey('jwk') reads [[type]],
// [[extractable]], [[algorithm]], and [[usages]]; if any
// went through the user-visible getter the call would either
// throw or produce forged output.
const jwk = await subtle.exportKey('jwk', key);
assert.strictEqual(jwk.kty, 'oct');
assert.strictEqual(jwk.alg, 'HS256');
assert.strictEqual(jwk.ext, true);
assert.deepStrictEqual(jwk.key_ops.sort(), ['sign', 'verify']);
// 4) The Node.js crypto bridge must also read the real native
// slots directly, both for KeyObject.from() and for deprecated
// direct CryptoKey consumption.
const keyObject = KeyObject.from(key);
assert.strictEqual(keyObject.type, 'secret');
assert.deepStrictEqual(keyObject.export(), Buffer.from(jwk.k, 'base64url'));
const payload = Buffer.from('payload');
const digest = createHmac('sha256', key).update(payload).digest('hex');
const expectedDigest =
createHmac('sha256', keyObject).update(payload).digest('hex');
assert.strictEqual(digest, expectedDigest);
const signature = cryptoSign('sha256', payload, ecPrivateKey);
assert.strictEqual(
cryptoVerify('sha256', payload, ecPublicKey, signature),
true);
// 5) Importing back from the exported JWK must yield an equivalent
// key, i.e. the real algorithm and usages round-trip.
const reimported = await subtle.importKey('jwk', jwk,
{ name: 'HMAC', hash: 'SHA-256' },
true, ['sign', 'verify']);
// Reimported's prototype is the same mutated prototype, so we must
// also inspect() the reimported key to check real slots.
const rerendered = inspect(reimported, { depth: 4 });
assert.match(rerendered, /type: 'secret'/);
assert.match(rerendered, /name: 'HMAC'/);
assert.doesNotMatch(rerendered, /FORGED/);
} finally {
// Restore the original getters so subsequent tests are unaffected.
for (const [name, descriptor] of Object.entries(originalDescriptors)) {
Object.defineProperty(proto, name, descriptor);
}
}
// After restoration, the real values come back through the public API.
assert.strictEqual(key.type, realType);
assert.strictEqual(key.extractable, realExtractable);
assert.deepStrictEqual(key.algorithm, realAlgorithm);
assert.deepStrictEqual(key.usages, realUsages);
})().then(common.mustCall());
@@ -0,0 +1,52 @@
'use strict';
// CryptoKey instances must not expose any own Symbol-keyed properties
// to user code. The internal slots backing the public getters are
// kept in native internal fields, with JS-side caches in private fields,
// so that reflection APIs such as Object.getOwnPropertySymbols() and
// Reflect.ownKeys() cannot enumerate them, even after the public getters
// have been invoked and their per-instance caches populated.
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('node:assert');
const { subtle } = globalThis.crypto;
(async () => {
const keys = [
await subtle.generateKey(
{ name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify']),
(await subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])).privateKey,
(await subtle.generateKey(
{ name: 'RSA-PSS', modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
true, ['sign', 'verify'])).publicKey,
await subtle.generateKey(
{ name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt']),
];
for (const key of keys) {
// Touch every public getter so any lazy per-instance caching would
// materialise now.
/* eslint-disable no-unused-expressions */
key.type;
key.extractable;
key.algorithm;
key.usages;
// Read the getters a second time to exercise the cache-hit path.
key.algorithm;
key.usages;
/* eslint-enable no-unused-expressions */
assert.deepStrictEqual(
Object.getOwnPropertySymbols(key), [],
`CryptoKey has own Symbol properties: ${
Object.getOwnPropertySymbols(key).map(String).join(', ')}`,
);
assert.deepStrictEqual(Object.getOwnPropertyNames(key), []);
assert.deepStrictEqual(Reflect.ownKeys(key), []);
}
})().then(common.mustCall());
+24 -13
View File
@@ -628,16 +628,27 @@ async function testWrongKeyType(
})().then(common.mustCall());
// https://github.com/w3c/webcrypto/pull/380
{
crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']).then((key) => {
return crypto.subtle.deriveBits({
name: 'HKDF',
hash: { name: 'SHA-256' },
info: new Uint8Array(0),
salt: new Uint8Array(0),
}, key, 0);
}).then((bits) => {
assert.deepStrictEqual(bits, new ArrayBuffer(0));
})
.then(common.mustCall());
}
(async function() {
const key = await crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']);
const bits = await crypto.subtle.deriveBits({
name: 'HKDF',
hash: { name: 'SHA-256' },
info: new Uint8Array(0),
salt: new Uint8Array(0),
}, key, 0);
assert.deepStrictEqual(bits, new ArrayBuffer(0));
})().then(common.mustCall());
// OpenSSL limits info to 1024 bytes
(async function() {
const key = await crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']);
await assert.rejects(crypto.subtle.deriveBits({
name: 'HKDF',
hash: { name: 'SHA-256' },
info: new Uint8Array(1025),
salt: new Uint8Array(0),
}, key, 0), {
name: 'OperationError',
message: 'algorithm.info must be at most 1024 bytes',
});
})().then(common.mustCall());
@@ -220,6 +220,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) {
// JWK error conditions
const jwkTests = [
[{ kty: 'oct' }, /Invalid keyData/],
[{ k: baseJwk.k }, /Invalid keyData/],
[{ ...baseJwk, kty: 'RSA' }, /Invalid JWK "kty" Parameter/],
[{ ...baseJwk, use: 'sig' }, /Invalid JWK "use" Parameter/],
@@ -355,7 +355,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable)
{ name },
extractable,
publicUsages),
{ message: 'JWK "crv" Parameter and algorithm name mismatch' });
{ message: crv ? 'JWK "crv" Parameter and algorithm name mismatch' : 'Invalid keyData' });
await assert.rejects(
subtle.importKey(
@@ -364,7 +364,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable)
{ name },
extractable,
privateUsages),
{ message: 'JWK "crv" Parameter and algorithm name mismatch' });
{ message: crv ? 'JWK "crv" Parameter and algorithm name mismatch' : 'Invalid keyData' });
}
await assert.rejects(
@@ -322,7 +322,7 @@ async function testImportJwk(
{ name, namedCurve },
extractable,
publicUsages),
{ message: 'JWK "crv" does not match the requested algorithm' });
{ message: crv ? 'JWK "crv" does not match the requested algorithm' : 'Invalid keyData' });
await assert.rejects(
subtle.importKey(
@@ -331,7 +331,7 @@ async function testImportJwk(
{ name, namedCurve },
extractable,
privateUsages),
{ message: 'JWK "crv" does not match the requested algorithm' });
{ message: crv ? 'JWK "crv" does not match the requested algorithm' : 'Invalid keyData' });
}
await assert.rejects(
@@ -347,7 +347,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable)
{ name },
extractable,
publicUsages),
{ message: 'JWK "alg" Parameter and algorithm name mismatch' });
{ message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' });
await assert.rejects(
subtle.importKey(
@@ -356,7 +356,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable)
{ name },
extractable,
privateUsages),
{ message: 'JWK "alg" Parameter and algorithm name mismatch' });
{ message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' });
}
await assert.rejects(
@@ -423,7 +423,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable)
{ name },
extractable,
publicUsages),
{ message: 'JWK "alg" Parameter and algorithm name mismatch' });
{ message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' });
await assert.rejects(
subtle.importKey(
@@ -432,7 +432,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable)
{ name },
extractable,
privateUsages),
{ message: 'JWK "alg" Parameter and algorithm name mismatch' });
{ message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' });
}
await assert.rejects(
@@ -600,6 +600,20 @@ async function testImportJwk(
extractable,
publicUsages),
{ name: 'DataError', message: 'Invalid keyData' });
for (const field of ['p', 'q', 'dp', 'dq', 'qi']) {
const jwkMissingCrtField = { ...jwk };
delete jwkMissingCrtField[field];
await assert.rejects(
subtle.importKey(
'jwk',
jwkMissingCrtField,
{ name, hash },
extractable,
privateUsages),
{ name: 'DataError', message: 'Invalid keyData' },
`missing private JWK CRT field ${field}`);
}
}
// combinations to test
@@ -18,4 +18,11 @@ assert.ok(kp.publicKey.usages.includes('foo'));
assert.ok(util.inspect(kp.publicKey).includes("algorithm: { name: 'Ed25519' }"));
assert.ok(util.inspect(kp.publicKey).includes("usages: [ 'verify' ]"));
const jwk = await subtle.exportKey('jwk', kp.publicKey);
assert.deepStrictEqual(jwk.key_ops, ['verify']);
jwk.key_ops.push('foo');
assert.ok(!util.inspect(kp.publicKey).includes('foo'));
assert.deepStrictEqual((await subtle.exportKey('jwk', kp.publicKey)).key_ops,
['verify']);
await subtle.sign('Ed25519', kp.privateKey, Buffer.alloc(32));