mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
crypto: optimize and benchmark key preparation
Optimize asymmetric key preparation with benchmark and regression coverage for the supported input paths. Assisted-by: GitHub Copilot, Codex Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: https://github.com/nodejs/node/pull/65892 Backport-PR-URL: https://github.com/nodejs/node/pull/66092 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
1 parent
da341e2571
commit
bfb544eb00
4 files changed
+449
-93
No files matched your search
@@ -0,0 +1,178 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common.js');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
const { hasOpenSSL, isBoringSSL } = require('../../test/common/crypto.js');
|
||||
|
||||
const inputs = [
|
||||
'public-keyobject', 'public-keyobject-wrapped', 'public-pem-buffer-wrapped',
|
||||
'private-keyobject', 'private-keyobject-wrapped',
|
||||
'private-pem-string', 'private-pem-buffer', 'private-pem-arraybuffer',
|
||||
'private-pem-string-wrapped', 'private-pem-buffer-wrapped', 'private-pem-arraybuffer-wrapped',
|
||||
'private-jwk',
|
||||
'public-der-pkcs1-bytes', 'private-der-pkcs1-bytes', 'private-pem-pkcs8-bytes',
|
||||
'private-der-pkcs8-bytes', 'private-der-pkcs8-base64', 'private-der-pkcs8-hex',
|
||||
'public-der-spki-bytes', 'private-der-sec1-bytes',
|
||||
'raw-public-ec-buffer', 'raw-public-ed25519-buffer',
|
||||
'raw-private-ec-buffer', 'raw-private-ec-arraybuffer', 'raw-private-ed25519-buffer',
|
||||
'pem-passphrase-absent', 'pem-passphrase-empty', 'pem-passphrase-string',
|
||||
'pem-passphrase-base64', 'pem-passphrase-buffer', 'pem-passphrase-arraybuffer',
|
||||
'file-passphrase-absent', 'file-passphrase-string', 'file-direct',
|
||||
'provider-passphrase-absent', 'provider-passphrase-absent-properties',
|
||||
'provider-passphrase-empty', 'provider-passphrase-string', 'provider-passphrase-base64',
|
||||
'provider-passphrase-buffer', 'provider-passphrase-arraybuffer', 'provider-direct',
|
||||
];
|
||||
|
||||
if (hasOpenSSL(3, 5) || isBoringSSL)
|
||||
inputs.push('raw-seed-ml-dsa-44-buffer');
|
||||
|
||||
const bench = common.createBenchmark(main, {
|
||||
input: inputs,
|
||||
context: ['consume-public', 'consume-private', 'create-public'],
|
||||
n: [1e6],
|
||||
}, {
|
||||
flags: ['--expose-internals'],
|
||||
combinationFilter({ input, context }) {
|
||||
if (input === 'private-keyobject' || input === 'private-keyobject-wrapped')
|
||||
return true;
|
||||
if (input.startsWith('public-') || input.startsWith('raw-public-'))
|
||||
return context === 'consume-public';
|
||||
return context === 'consume-private';
|
||||
},
|
||||
});
|
||||
|
||||
function main({ input, context, n }) {
|
||||
const {
|
||||
prepareAsymmetricKey,
|
||||
kConsumePublic,
|
||||
kConsumePrivate,
|
||||
kCreatePublic,
|
||||
} = require('internal/crypto/keys');
|
||||
const ctx = {
|
||||
'consume-public': kConsumePublic,
|
||||
'consume-private': kConsumePrivate,
|
||||
'create-public': kCreatePublic,
|
||||
}[context];
|
||||
const fixtureDir = path.resolve(__dirname, '../../test/fixtures/keys');
|
||||
const privateKeyPath = path.join(fixtureDir, 'rsa_private_2048.pem');
|
||||
const privateKey = crypto.createPrivateKey(fs.readFileSync(privateKeyPath));
|
||||
const publicKey = crypto.createPublicKey(privateKey);
|
||||
const pem = privateKey.export({ format: 'pem', type: 'pkcs8' });
|
||||
const buffer = Buffer.from(pem);
|
||||
const arraybuffer = Uint8Array.from(buffer).buffer;
|
||||
const fileURL = pathToFileURL(privateKeyPath);
|
||||
const providerURL = new URL('pkcs11:object=signing-key;type=private');
|
||||
const passphrase = Buffer.from('password');
|
||||
|
||||
let key;
|
||||
switch (input) {
|
||||
case 'public-keyobject': key = publicKey; break;
|
||||
case 'public-keyobject-wrapped': key = { key: publicKey }; break;
|
||||
case 'public-pem-buffer-wrapped':
|
||||
key = { key: Buffer.from(publicKey.export({ format: 'pem', type: 'spki' })) };
|
||||
break;
|
||||
case 'private-keyobject': key = privateKey; break;
|
||||
case 'private-keyobject-wrapped': key = { key: privateKey }; break;
|
||||
case 'private-pem-string': key = pem; break;
|
||||
case 'private-pem-buffer': key = buffer; break;
|
||||
case 'private-pem-arraybuffer': key = arraybuffer; break;
|
||||
case 'private-pem-string-wrapped': key = { key: pem }; break;
|
||||
case 'private-pem-buffer-wrapped': key = { key: buffer }; break;
|
||||
case 'private-pem-arraybuffer-wrapped': key = { key: arraybuffer }; break;
|
||||
case 'private-jwk': key = { key: privateKey.export({ format: 'jwk' }), format: 'jwk' }; break;
|
||||
case 'public-der-pkcs1-bytes': key = { key: publicKey, format: 'der', type: 'pkcs1' }; break;
|
||||
case 'private-der-pkcs1-bytes': key = { key: privateKey, format: 'der', type: 'pkcs1' }; break;
|
||||
case 'private-pem-pkcs8-bytes': key = { key: privateKey, format: 'pem', type: 'pkcs8' }; break;
|
||||
case 'private-der-pkcs8-bytes': key = { key: privateKey, format: 'der', type: 'pkcs8' }; break;
|
||||
case 'private-der-pkcs8-base64':
|
||||
key = { key: privateKey, format: 'der', type: 'pkcs8', encoding: 'base64' };
|
||||
break;
|
||||
case 'private-der-pkcs8-hex':
|
||||
key = { key: privateKey, format: 'der', type: 'pkcs8', encoding: 'hex' };
|
||||
break;
|
||||
case 'public-der-spki-bytes': key = { key: publicKey, format: 'der', type: 'spki' }; break;
|
||||
case 'private-der-sec1-bytes':
|
||||
key = {
|
||||
key: crypto.createPrivateKey(fs.readFileSync(path.join(fixtureDir, 'ec_p256_private.pem'))),
|
||||
format: 'der', type: 'sec1',
|
||||
};
|
||||
break;
|
||||
case 'raw-public-ec-buffer':
|
||||
case 'raw-public-ed25519-buffer':
|
||||
case 'raw-private-ec-buffer':
|
||||
case 'raw-private-ec-arraybuffer':
|
||||
case 'raw-private-ed25519-buffer': {
|
||||
const asymmetricKeyType = input.includes('-ec-') ? 'ec' : 'ed25519';
|
||||
const fixture = asymmetricKeyType === 'ec' ? 'ec_p256_private.pem' : 'ed25519_private.pem';
|
||||
let rawKey = crypto.createPrivateKey(fs.readFileSync(path.join(fixtureDir, fixture)));
|
||||
const format = input.startsWith('raw-public-') ? 'raw-public' : 'raw-private';
|
||||
if (format === 'raw-public') rawKey = crypto.createPublicKey(rawKey);
|
||||
const bytes = rawKey.export({ format });
|
||||
key = {
|
||||
key: input.endsWith('-arraybuffer') ? Uint8Array.from(bytes).buffer : bytes,
|
||||
format,
|
||||
asymmetricKeyType,
|
||||
namedCurve: asymmetricKeyType === 'ec' ? 'prime256v1' : undefined,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'raw-seed-ml-dsa-44-buffer': {
|
||||
const seedKey = crypto.createPrivateKey(
|
||||
fs.readFileSync(path.join(fixtureDir, 'ml_dsa_44_private_seed_only.pem')));
|
||||
key = { key: seedKey.export({ format: 'raw-seed' }), format: 'raw-seed', asymmetricKeyType: 'ml-dsa-44' };
|
||||
break;
|
||||
}
|
||||
case 'pem-passphrase-absent': key = { key: privateKey }; break;
|
||||
case 'pem-passphrase-empty': key = { key: privateKey, passphrase: '' }; break;
|
||||
case 'pem-passphrase-string': key = { key: privateKey, passphrase: 'password' }; break;
|
||||
case 'pem-passphrase-base64':
|
||||
key = { key: privateKey, passphrase: passphrase.toString('base64'), encoding: 'base64' };
|
||||
break;
|
||||
case 'pem-passphrase-buffer': key = { key: privateKey, passphrase }; break;
|
||||
case 'pem-passphrase-arraybuffer':
|
||||
key = { key: privateKey, passphrase: Uint8Array.from(passphrase).buffer };
|
||||
break;
|
||||
case 'file-passphrase-absent': key = { key: fileURL }; break;
|
||||
case 'file-passphrase-string': key = { key: fileURL, passphrase: 'password' }; break;
|
||||
case 'file-direct': key = fileURL; break;
|
||||
case 'provider-passphrase-absent': key = { key: providerURL }; break;
|
||||
case 'provider-passphrase-absent-properties': key = { key: providerURL, properties: 'provider=default' }; break;
|
||||
case 'provider-passphrase-empty': key = { key: providerURL, passphrase: '' }; break;
|
||||
case 'provider-passphrase-string': key = { key: providerURL, passphrase: 'password' }; break;
|
||||
case 'provider-passphrase-base64':
|
||||
key = { key: providerURL, passphrase: passphrase.toString('base64'), encoding: 'base64' };
|
||||
break;
|
||||
case 'provider-passphrase-buffer': key = { key: providerURL, passphrase }; break;
|
||||
case 'provider-passphrase-arraybuffer':
|
||||
key = { key: providerURL, passphrase: Uint8Array.from(passphrase).buffer };
|
||||
break;
|
||||
case 'provider-direct': key = providerURL; break;
|
||||
default: throw new Error(`Unsupported input: ${input}`);
|
||||
}
|
||||
|
||||
if (input.startsWith('pem-passphrase-')) {
|
||||
const bytes = key.passphrase === undefined ? undefined :
|
||||
typeof key.passphrase === 'string' ? Buffer.from(key.passphrase, key.encoding) :
|
||||
Buffer.from(key.passphrase);
|
||||
key = {
|
||||
key: Buffer.from(privateKey.export({
|
||||
format: 'pem', type: 'pkcs8',
|
||||
...(bytes === undefined ? {} : { cipher: 'aes-256-cbc', passphrase: bytes }),
|
||||
})),
|
||||
format: 'pem', type: 'pkcs8', passphrase: key.passphrase, encoding: key.encoding,
|
||||
};
|
||||
} else if (key.key instanceof crypto.KeyObject && key.format !== undefined) {
|
||||
const bytes = Buffer.from(key.key.export({ format: key.format, type: key.type }));
|
||||
key.key = key.encoding === undefined ? bytes : bytes.toString(key.encoding);
|
||||
}
|
||||
|
||||
prepareAsymmetricKey(key, ctx);
|
||||
bench.start();
|
||||
for (let index = 0; index < n; index++) {
|
||||
prepareAsymmetricKey(key, ctx);
|
||||
}
|
||||
bench.end(n);
|
||||
}
|
||||
+120
-93
@@ -392,7 +392,7 @@ const {
|
||||
return [KeyObject, SecretKeyObject, PublicKeyObject, PrivateKeyObject];
|
||||
});
|
||||
|
||||
function parseKeyFormat(formatStr, defaultFormat, optionName) {
|
||||
function parseKeyFormat(formatStr, defaultFormat, objName) {
|
||||
if (formatStr === undefined && defaultFormat !== undefined)
|
||||
return defaultFormat;
|
||||
else if (formatStr === 'pem')
|
||||
@@ -407,10 +407,10 @@ function parseKeyFormat(formatStr, defaultFormat, optionName) {
|
||||
return kKeyFormatRawPrivate;
|
||||
else if (formatStr === 'raw-seed')
|
||||
return kKeyFormatRawSeed;
|
||||
throw new ERR_INVALID_ARG_VALUE(optionName, formatStr);
|
||||
throw new ERR_INVALID_ARG_VALUE(option('format', objName), formatStr);
|
||||
}
|
||||
|
||||
function parseKeyType(typeStr, required, keyType, isPublic, optionName) {
|
||||
function parseKeyType(typeStr, required, keyType, isPublic, objName) {
|
||||
if (typeStr === undefined && !required) {
|
||||
return undefined;
|
||||
} else if (typeStr === 'pkcs1') {
|
||||
@@ -431,7 +431,7 @@ function parseKeyType(typeStr, required, keyType, isPublic, optionName) {
|
||||
return kKeyEncodingSEC1;
|
||||
}
|
||||
|
||||
throw new ERR_INVALID_ARG_VALUE(optionName, typeStr);
|
||||
throw new ERR_INVALID_ARG_VALUE(option('type', objName), typeStr);
|
||||
}
|
||||
|
||||
function option(name, prefix) {
|
||||
@@ -445,7 +445,7 @@ function parseKeyFormatAndType(enc, keyType, isPublic, objName) {
|
||||
const isInput = keyType === undefined;
|
||||
const format = parseKeyFormat(formatStr,
|
||||
isInput ? kKeyFormatPEM : undefined,
|
||||
option('format', objName));
|
||||
objName);
|
||||
|
||||
if (format === kKeyFormatRawPublic) {
|
||||
if (isPublic === false) {
|
||||
@@ -481,7 +481,7 @@ function parseKeyFormatAndType(enc, keyType, isPublic, objName) {
|
||||
isRequired,
|
||||
keyType,
|
||||
isPublic,
|
||||
option('type', objName));
|
||||
objName);
|
||||
return { format, type };
|
||||
}
|
||||
|
||||
@@ -491,6 +491,10 @@ function isStringOrBuffer(val) {
|
||||
isAnyArrayBuffer(val);
|
||||
}
|
||||
|
||||
function encodeKeyString(value, encoding) {
|
||||
return Buffer.from(value, encoding === 'buffer' ? 'utf8' : encoding);
|
||||
}
|
||||
|
||||
function parseKeyEncoding(enc, keyType, isPublic, objName) {
|
||||
validateObject(enc, 'options');
|
||||
|
||||
@@ -538,8 +542,8 @@ function parseKeyEncoding(enc, keyType, isPublic, objName) {
|
||||
}
|
||||
}
|
||||
|
||||
if (passphrase !== undefined)
|
||||
passphrase = getArrayBufferOrView(passphrase, 'key.passphrase', encoding);
|
||||
if (typeof passphrase === 'string')
|
||||
passphrase = encodeKeyString(passphrase, encoding);
|
||||
|
||||
return { format, type, cipher, passphrase };
|
||||
}
|
||||
@@ -567,11 +571,11 @@ function validateAsymmetricKeyType(type, ctx, key) {
|
||||
);
|
||||
}
|
||||
|
||||
if (type !== 'private') {
|
||||
if (type !== kKeyTypePrivate) {
|
||||
if (ctx === kConsumePrivate || ctx === kCreatePublic)
|
||||
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'private');
|
||||
if (type !== 'public') {
|
||||
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type,
|
||||
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(keyObjectTypeToString(type), 'private');
|
||||
if (type !== kKeyTypePublic) {
|
||||
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(keyObjectTypeToString(type),
|
||||
'private or public');
|
||||
}
|
||||
}
|
||||
@@ -613,10 +617,9 @@ function prepareStorePrivateKey(url, name, passphrase, encoding, properties) {
|
||||
if (!isStringOrBuffer(passphrase)) {
|
||||
throw new ERR_INVALID_ARG_VALUE(option('passphrase', name), passphrase);
|
||||
}
|
||||
passphrase = getArrayBufferOrView(
|
||||
passphrase,
|
||||
option('passphrase', name),
|
||||
encoding);
|
||||
if (typeof passphrase === 'string') {
|
||||
passphrase = encodeKeyString(passphrase, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
if (properties !== undefined) {
|
||||
@@ -641,97 +644,35 @@ function prepareStorePrivateKey(url, name, passphrase, encoding, properties) {
|
||||
}
|
||||
|
||||
function prepareAsymmetricKey(key, ctx, name = 'key') {
|
||||
if (typeof key === 'string') {
|
||||
return { format: kKeyFormatPEM, data: encodeKeyString(key) };
|
||||
}
|
||||
if (isArrayBufferView(key)) {
|
||||
return { format: kKeyFormatPEM, data: key };
|
||||
}
|
||||
const isPrivateKeyInput = ctx === kConsumePrivate || ctx === kCreatePrivate;
|
||||
|
||||
if (isKeyObject(key)) {
|
||||
// Best case: A key object, as simple as that.
|
||||
const type = getKeyObjectType(key);
|
||||
validateAsymmetricKeyType(type, ctx, key);
|
||||
return { data: getKeyObjectHandle(key) };
|
||||
const slots = getKeyObjectSlots(key);
|
||||
validateAsymmetricKeyType(slots[kKeyObjectSlotType], ctx, key);
|
||||
return { data: slots[kKeyObjectSlotHandle] };
|
||||
}
|
||||
if (isCryptoKey(key)) {
|
||||
emitDEP0203();
|
||||
validateAsymmetricKeyType(getCryptoKeyType(key), ctx, key);
|
||||
return { data: getCryptoKeyHandle(key) };
|
||||
const slots = getSlots(key);
|
||||
validateAsymmetricKeyType(slots[kSlotType], ctx, key);
|
||||
return { data: slots[kSlotHandle] };
|
||||
}
|
||||
if (isPrivateKeyInput && isURLInstance(key)) {
|
||||
// A private key referenced by a URI for an OpenSSL STORE loader.
|
||||
return prepareStorePrivateKey(key, name);
|
||||
}
|
||||
if (isStringOrBuffer(key)) {
|
||||
// Expect PEM by default, mostly for backward compatibility.
|
||||
return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, name) };
|
||||
if (isAnyArrayBuffer(key)) {
|
||||
return { format: kKeyFormatPEM, data: key };
|
||||
}
|
||||
if (typeof key === 'object') {
|
||||
const { key: data, encoding, format, properties } = 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)) {
|
||||
const type = getKeyObjectType(data);
|
||||
validateAsymmetricKeyType(type, ctx, data);
|
||||
return { data: getKeyObjectHandle(data) };
|
||||
}
|
||||
if (isCryptoKey(data)) {
|
||||
emitDEP0203();
|
||||
validateAsymmetricKeyType(getCryptoKeyType(data), ctx, data);
|
||||
return { data: getCryptoKeyHandle(data) };
|
||||
}
|
||||
if (isPrivateKeyInput && isURLInstance(data)) {
|
||||
// A private key referenced by a URI for an OpenSSL STORE loader, with
|
||||
// an optional passphrase/PIN.
|
||||
return prepareStorePrivateKey(
|
||||
data,
|
||||
name,
|
||||
key.passphrase,
|
||||
encoding,
|
||||
properties);
|
||||
}
|
||||
if (format === 'jwk') {
|
||||
validateObject(data, `${name}.key`);
|
||||
return { data, format: kKeyFormatJWK };
|
||||
} else if (format === 'raw-public' || format === 'raw-private' ||
|
||||
format === 'raw-seed') {
|
||||
if ((ctx === kConsumePrivate || ctx === kCreatePrivate) &&
|
||||
format === 'raw-public') {
|
||||
throw new ERR_INVALID_ARG_VALUE(`${name}.format`, format);
|
||||
}
|
||||
if (!isArrayBufferView(data) && !isAnyArrayBuffer(data)) {
|
||||
throw new ERR_INVALID_ARG_TYPE(
|
||||
`${name}.key`,
|
||||
['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'],
|
||||
data);
|
||||
}
|
||||
validateString(key.asymmetricKeyType, `${name}.asymmetricKeyType`);
|
||||
if (key.asymmetricKeyType === 'ec') {
|
||||
validateString(key.namedCurve, `${name}.namedCurve`);
|
||||
}
|
||||
const rawFormat = parseKeyFormat(format, undefined, `${name}.format`);
|
||||
return {
|
||||
data: getArrayBufferOrView(data, `${name}.key`),
|
||||
format: rawFormat,
|
||||
type: key.asymmetricKeyType,
|
||||
namedCurve: key.namedCurve ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// Either PEM or DER using PKCS#1 or SPKI.
|
||||
if (!isStringOrBuffer(data)) {
|
||||
throw new ERR_INVALID_ARG_TYPE(
|
||||
`${name}.key`,
|
||||
getKeyTypes({
|
||||
allowKeyObject: ctx !== kCreatePrivate,
|
||||
allowURL: isPrivateKeyInput,
|
||||
}),
|
||||
data);
|
||||
}
|
||||
|
||||
const isPublic =
|
||||
(ctx === kConsumePrivate || ctx === kCreatePrivate) ? false : undefined;
|
||||
return {
|
||||
data: getArrayBufferOrView(data, `${name}.key`, encoding),
|
||||
...parseKeyEncoding(key, undefined, isPublic, name),
|
||||
};
|
||||
return prepareAsymmetricKeyOptions(key, ctx, name, isPrivateKeyInput);
|
||||
}
|
||||
|
||||
throw new ERR_INVALID_ARG_TYPE(
|
||||
@@ -743,6 +684,87 @@ function prepareAsymmetricKey(key, ctx, name = 'key') {
|
||||
key);
|
||||
}
|
||||
|
||||
function prepareAsymmetricKeyOptions(key, ctx, name, isPrivateKeyInput) {
|
||||
const { key: data, encoding, format, properties } = key;
|
||||
let isData = typeof data === 'string' || isArrayBufferView(data);
|
||||
|
||||
// The 'key' property can be a KeyObject as well to allow specifying
|
||||
// additional options such as padding along with the key.
|
||||
if (!isData && isKeyObject(data)) {
|
||||
const slots = getKeyObjectSlots(data);
|
||||
validateAsymmetricKeyType(slots[kKeyObjectSlotType], ctx, data);
|
||||
return { data: slots[kKeyObjectSlotHandle] };
|
||||
}
|
||||
if (!isData && isCryptoKey(data)) {
|
||||
emitDEP0203();
|
||||
const slots = getSlots(data);
|
||||
validateAsymmetricKeyType(slots[kSlotType], ctx, data);
|
||||
return { data: slots[kSlotHandle] };
|
||||
}
|
||||
isData ||= isAnyArrayBuffer(data);
|
||||
if (!isData && isPrivateKeyInput && isURLInstance(data)) {
|
||||
// A private key referenced by a URI for an OpenSSL STORE loader, with
|
||||
// an optional passphrase/PIN.
|
||||
return prepareStorePrivateKey(
|
||||
data,
|
||||
name,
|
||||
key.passphrase,
|
||||
encoding,
|
||||
properties);
|
||||
}
|
||||
if (format === 'jwk') {
|
||||
validateObject(data, `${name}.key`);
|
||||
return { data, format: kKeyFormatJWK };
|
||||
}
|
||||
if (format === 'raw-public' || format === 'raw-private' ||
|
||||
format === 'raw-seed') {
|
||||
if (isPrivateKeyInput && format === 'raw-public') {
|
||||
throw new ERR_INVALID_ARG_VALUE(`${name}.format`, format);
|
||||
}
|
||||
if (!isData || typeof data === 'string') {
|
||||
throw new ERR_INVALID_ARG_TYPE(
|
||||
`${name}.key`,
|
||||
['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'],
|
||||
data);
|
||||
}
|
||||
validateString(key.asymmetricKeyType, `${name}.asymmetricKeyType`);
|
||||
if (key.asymmetricKeyType === 'ec') {
|
||||
validateString(key.namedCurve, `${name}.namedCurve`);
|
||||
}
|
||||
const rawFormat = format === 'raw-public' ? kKeyFormatRawPublic :
|
||||
format === 'raw-private' ? kKeyFormatRawPrivate : kKeyFormatRawSeed;
|
||||
return {
|
||||
data,
|
||||
format: rawFormat,
|
||||
type: key.asymmetricKeyType,
|
||||
namedCurve: key.namedCurve ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// Either PEM or DER using PKCS#1 or SPKI.
|
||||
if (!isData) {
|
||||
throw new ERR_INVALID_ARG_TYPE(
|
||||
`${name}.key`,
|
||||
getKeyTypes({
|
||||
allowKeyObject: ctx !== kCreatePrivate,
|
||||
allowURL: isPrivateKeyInput,
|
||||
}),
|
||||
data);
|
||||
}
|
||||
|
||||
const isPublic = isPrivateKeyInput ? false : undefined;
|
||||
const keyData = typeof data === 'string' ?
|
||||
encodeKeyString(data, encoding) : data;
|
||||
const parsed = parseKeyEncoding(key, undefined, isPublic, name);
|
||||
return {
|
||||
data: keyData,
|
||||
format: parsed.format,
|
||||
type: parsed.type,
|
||||
cipher: parsed.cipher,
|
||||
passphrase: parsed.passphrase,
|
||||
};
|
||||
}
|
||||
|
||||
function preparePrivateKey(key, name) {
|
||||
return prepareAsymmetricKey(key, kConsumePrivate, name);
|
||||
}
|
||||
@@ -1512,6 +1534,11 @@ module.exports = {
|
||||
parsePrivateKeyEncoding,
|
||||
parseKeyEncoding,
|
||||
toPublicCryptoKey,
|
||||
prepareAsymmetricKey,
|
||||
kConsumePublic,
|
||||
kConsumePrivate,
|
||||
kCreatePublic,
|
||||
kCreatePrivate,
|
||||
preparePrivateKey,
|
||||
preparePublicOrPrivateKey,
|
||||
prepareSecretKey,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
// Flags: --expose-internals
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto) common.skip('missing crypto');
|
||||
const assert = require('node:assert');
|
||||
const crypto = require('node:crypto');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const {
|
||||
prepareAsymmetricKey,
|
||||
getCryptoKeyHandle,
|
||||
kConsumePublic,
|
||||
kConsumePrivate,
|
||||
kCreatePublic,
|
||||
kCreatePrivate,
|
||||
} = require('internal/crypto/keys');
|
||||
|
||||
common.expectWarning({
|
||||
DeprecationWarning: {
|
||||
DEP0203: 'Passing a CryptoKey to node:crypto functions is deprecated.',
|
||||
},
|
||||
});
|
||||
|
||||
const privateKeyObject = crypto.createPrivateKey(fixtures.readKey('ec_p256_private.pem'));
|
||||
const algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
|
||||
const privateKey = privateKeyObject.toCryptoKey(algorithm, true, ['sign']);
|
||||
const publicKey = crypto.createPublicKey(privateKeyObject).toCryptoKey(algorithm, true, ['verify']);
|
||||
const secretKey = crypto.createSecretKey(Buffer.alloc(16)).toCryptoKey('AES-CBC', true, ['encrypt']);
|
||||
|
||||
for (const key of [privateKey, publicKey, secretKey]) {
|
||||
for (const input of [key, { key, format: 'raw-public', asymmetricKeyType: 'invalid' }]) {
|
||||
for (const context of [kConsumePublic, kConsumePrivate, kCreatePublic]) {
|
||||
if (key === privateKey || (key === publicKey && context === kConsumePublic)) {
|
||||
assert.strictEqual(prepareAsymmetricKey(input, context).data, getCryptoKeyHandle(key));
|
||||
} else {
|
||||
const expected = context === kConsumePublic ? 'private or public' : 'private';
|
||||
assert.throws(() => prepareAsymmetricKey(input, context), {
|
||||
code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
|
||||
name: 'TypeError',
|
||||
message: `Invalid key object type ${key.type}, expected ${expected}.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
assert.throws(() => prepareAsymmetricKey(input, kCreatePrivate), {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
name: 'TypeError',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
// Flags: --expose-internals
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto) common.skip('missing crypto');
|
||||
const assert = require('node:assert');
|
||||
const crypto = require('node:crypto');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const {
|
||||
prepareAsymmetricKey,
|
||||
getKeyObjectHandle,
|
||||
kConsumePublic,
|
||||
kConsumePrivate,
|
||||
kCreatePublic,
|
||||
kCreatePrivate,
|
||||
} = require('internal/crypto/keys');
|
||||
|
||||
const pem = fixtures.readKey('ec_p256_private.pem');
|
||||
const keyObject = crypto.createPrivateKey(pem);
|
||||
const contexts = [kConsumePublic, kConsumePrivate, kCreatePublic, kCreatePrivate];
|
||||
|
||||
for (const input of [
|
||||
pem,
|
||||
new Uint8Array(pem),
|
||||
new DataView(Uint8Array.from(pem).buffer),
|
||||
Uint8Array.from(pem).buffer,
|
||||
new SharedArrayBuffer(pem.length),
|
||||
]) {
|
||||
Object.defineProperty(input, 'key', { get: common.mustNotCall() });
|
||||
Object.defineProperty(input, 'format', { get: common.mustNotCall() });
|
||||
Object.defineProperty(input, Symbol.toStringTag, { value: 'KeyObject' });
|
||||
for (const context of contexts) {
|
||||
assert.strictEqual(prepareAsymmetricKey(input, context).data, input);
|
||||
assert.strictEqual(prepareAsymmetricKey({ key: input }, context).data, input);
|
||||
}
|
||||
}
|
||||
|
||||
for (const context of [kConsumePublic, kConsumePrivate, kCreatePublic]) {
|
||||
const options = { key: keyObject, format: 'raw-public', asymmetricKeyType: 'invalid' };
|
||||
assert.strictEqual(prepareAsymmetricKey(options, context).data, getKeyObjectHandle(keyObject));
|
||||
}
|
||||
|
||||
{
|
||||
const reads = [];
|
||||
const values = { key: pem.toString(), encoding: 'utf8', format: 'pem' };
|
||||
const options = {};
|
||||
for (const name of ['key', 'encoding', 'format', 'properties', 'type', 'cipher', 'passphrase']) {
|
||||
Object.defineProperty(options, name, {
|
||||
get() {
|
||||
reads.push(name);
|
||||
return values[name];
|
||||
},
|
||||
});
|
||||
}
|
||||
assert.deepStrictEqual(prepareAsymmetricKey(options, kConsumePrivate).data, pem);
|
||||
assert.deepStrictEqual(reads, [
|
||||
'key', 'encoding', 'format', 'properties', 'format', 'type', 'cipher', 'passphrase', 'encoding',
|
||||
]);
|
||||
}
|
||||
|
||||
{
|
||||
const input = {
|
||||
key: pem.toString(),
|
||||
encoding: 'invalid',
|
||||
get format() { return 'pem'; },
|
||||
get type() { return assert.fail('String conversion must precede encoding option parsing'); },
|
||||
};
|
||||
assert.throws(() => prepareAsymmetricKey(input, kConsumePrivate), { code: 'ERR_UNKNOWN_ENCODING' });
|
||||
}
|
||||
|
||||
{
|
||||
const raw = keyObject.export({ format: 'raw-private' });
|
||||
const input = { key: raw, format: 'raw-private', asymmetricKeyType: 'ec', namedCurve: 'P-256' };
|
||||
assert.strictEqual(prepareAsymmetricKey(input, kConsumePrivate).data, raw);
|
||||
assert.throws(() => prepareAsymmetricKey({ ...input, key: raw.toString('hex') }, kConsumePrivate), {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
});
|
||||
}
|
||||
|
||||
for (const encoding of [undefined, 'buffer', 'utf8', 'hex', 'base64']) {
|
||||
const inputEncoding = encoding === 'buffer' ? 'utf8' : encoding;
|
||||
const passphrase = Buffer.from('password');
|
||||
const result = prepareAsymmetricKey({
|
||||
key: pem.toString(inputEncoding),
|
||||
passphrase: passphrase.toString(inputEncoding),
|
||||
encoding,
|
||||
}, kConsumePrivate);
|
||||
assert.deepStrictEqual(result.data, pem);
|
||||
assert.deepStrictEqual(result.passphrase, passphrase);
|
||||
}
|
||||
|
||||
for (const [options, optionName] of [
|
||||
[{ format: 'invalid' }, 'format'],
|
||||
[{ format: 'der', type: 'invalid' }, 'type'],
|
||||
]) {
|
||||
assert.throws(() => prepareAsymmetricKey({ key: pem, ...options }, kConsumePrivate, 'options.key'), {
|
||||
code: 'ERR_INVALID_ARG_VALUE',
|
||||
message: new RegExp(`options\\.key\\.${optionName}`),
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user