mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
40 files changed
+2155
-763
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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());
|
||||
@@ -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));
|
||||
Reference in new issue
Block a user