crypto: add KeyObject.prototype.toCryptoKey

PR-URL: https://github.com/nodejs/node/pull/55262
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
This commit is contained in:
Filip Skokan 2024-10-06 20:09:02 +02:00 committed by GitHub
parent 7af434fc19
commit 90e3e5e173
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 415 additions and 59 deletions

View File

@ -2136,6 +2136,24 @@ added: v11.6.0
For secret keys, this property represents the size of the key in bytes. This
property is `undefined` for asymmetric keys.
### `keyObject.toCryptoKey(algorithm, extractable, keyUsages)`
<!-- YAML
added: REPLACEME
-->
<!--lint disable maximum-line-length remark-lint-->
* `algorithm`: {AlgorithmIdentifier|RsaHashedImportParams|EcKeyImportParams|HmacImportParams}
<!--lint enable maximum-line-length remark-lint-->
* `extractable`: {boolean}
* `keyUsages`: {string\[]} See [Key usages][].
* Returns: {CryptoKey}
Converts a `KeyObject` instance to a `CryptoKey`.
### `keyObject.type`
<!-- YAML
@ -6087,6 +6105,7 @@ See the [list of SSL OP Flags][] for details.
[FIPS provider from OpenSSL 3]: https://www.openssl.org/docs/man3.0/man7/crypto.html#FIPS-provider
[HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed
[JWK]: https://tools.ietf.org/html/rfc7517
[Key usages]: webcrypto.md#cryptokeyusages
[NIST SP 800-131A]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar2.pdf
[NIST SP 800-132]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf

View File

@ -245,7 +245,7 @@ async function aesGenerateKey(algorithm, extractable, keyUsages) {
extractable);
}
async function aesImportKey(
function aesImportKey(
algorithm,
format,
keyData,
@ -266,6 +266,11 @@ async function aesImportKey(
let keyObject;
let length;
switch (format) {
case 'KeyObject': {
validateKeyLength(keyData.symmetricKeySize * 8);
keyObject = keyData;
break;
}
case 'raw': {
validateKeyLength(keyData.byteLength * 8);
keyObject = createSecretKey(keyData);

View File

@ -197,7 +197,7 @@ function cfrgExportKey(key, format) {
key[kKeyObject][kHandle]));
}
async function cfrgImportKey(
function cfrgImportKey(
format,
keyData,
algorithm,
@ -208,6 +208,11 @@ async function cfrgImportKey(
let keyObject;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'KeyObject': {
verifyAcceptableCfrgKeyUse(name, keyData.type === 'public', usagesSet);
keyObject = keyData;
break;
}
case 'spki': {
verifyAcceptableCfrgKeyUse(name, true, usagesSet);
try {

View File

@ -149,7 +149,7 @@ function ecExportKey(key, format) {
key[kKeyObject][kHandle]));
}
async function ecImportKey(
function ecImportKey(
format,
keyData,
algorithm,
@ -167,6 +167,11 @@ async function ecImportKey(
let keyObject;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'KeyObject': {
verifyAcceptableEcKeyUse(name, keyData.type === 'public', usagesSet);
keyObject = keyData;
break;
}
case 'spki': {
verifyAcceptableEcKeyUse(name, true, usagesSet);
try {

View File

@ -6,6 +6,7 @@ const {
ObjectDefineProperties,
ObjectDefineProperty,
ObjectSetPrototypeOf,
SafeSet,
Symbol,
SymbolToStringTag,
Uint8Array,
@ -49,6 +50,8 @@ const {
kKeyObject,
getArrayBufferOrView,
bigIntArrayToUnsignedBigInt,
normalizeAlgorithm,
hasAnyNotIn,
} = require('internal/crypto/util');
const {
@ -65,6 +68,7 @@ const {
const {
customInspectSymbol: kInspect,
kEnumerableProperty,
lazyDOMException,
} = require('internal/util');
const { inspect } = require('internal/util/inspect');
@ -148,6 +152,8 @@ const {
},
});
let webidl;
class SecretKeyObject extends KeyObject {
constructor(handle) {
super('secret', handle);
@ -168,6 +174,51 @@ const {
}
return this[kHandle].export();
}
toCryptoKey(algorithm, extractable, keyUsages) {
webidl ??= require('internal/crypto/webidl');
algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey');
extractable = webidl.converters.boolean(extractable);
keyUsages = webidl.converters['sequence<KeyUsage>'](keyUsages);
let result;
switch (algorithm.name) {
case 'HMAC':
result = require('internal/crypto/mac')
.hmacImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
case 'AES-CTR':
// Fall through
case 'AES-CBC':
// Fall through
case 'AES-GCM':
// Fall through
case 'AES-KW':
result = require('internal/crypto/aes')
.aesImportKey(algorithm, 'KeyObject', this, extractable, keyUsages);
break;
case 'HKDF':
// Fall through
case 'PBKDF2':
result = importGenericSecretKey(
algorithm,
'KeyObject',
this,
extractable,
keyUsages);
break;
default:
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
if (result.usages.length === 0) {
throw lazyDOMException(
`Usages cannot be empty when importing a ${result.type} key.`,
'SyntaxError');
}
return result;
}
}
const kAsymmetricKeyType = Symbol('kAsymmetricKeyType');
@ -209,6 +260,51 @@ const {
return {};
}
}
toCryptoKey(algorithm, extractable, keyUsages) {
webidl ??= require('internal/crypto/webidl');
algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey');
extractable = webidl.converters.boolean(extractable);
keyUsages = webidl.converters['sequence<KeyUsage>'](keyUsages);
let result;
switch (algorithm.name) {
case 'RSASSA-PKCS1-v1_5':
// Fall through
case 'RSA-PSS':
// Fall through
case 'RSA-OAEP':
result = require('internal/crypto/rsa')
.rsaImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
case 'ECDSA':
// Fall through
case 'ECDH':
result = require('internal/crypto/ec')
.ecImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
case 'Ed25519':
// Fall through
case 'Ed448':
// Fall through
case 'X25519':
// Fall through
case 'X448':
result = require('internal/crypto/cfrg')
.cfrgImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
default:
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
if (result.type === 'private' && result.usages.length === 0) {
throw lazyDOMException(
`Usages cannot be empty when importing a ${result.type} key.`,
'SyntaxError');
}
return result;
}
}
class PublicKeyObject extends AsymmetricKeyObject {
@ -801,6 +897,68 @@ function isCryptoKey(obj) {
return obj != null && obj[kKeyObject] !== undefined;
}
function importGenericSecretKey(
{ name, length },
format,
keyData,
extractable,
keyUsages) {
const usagesSet = new SafeSet(keyUsages);
if (extractable)
throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');
if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
switch (format) {
case 'KeyObject': {
if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
const checkLength = keyData.symmetricKeySize * 8;
// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (length !== undefined && length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}
return new InternalCryptoKey(keyData, { name }, keyUsages, false);
}
case 'raw': {
if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
const checkLength = keyData.byteLength * 8;
// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (length !== undefined && length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}
const keyObject = createSecretKey(keyData);
return new InternalCryptoKey(keyObject, { name }, keyUsages, false);
}
}
throw lazyDOMException(
`Unable to import ${name} key with format ${format}`,
'NotSupportedError');
}
module.exports = {
// Public API.
createSecretKey,
@ -822,4 +980,5 @@ module.exports = {
PrivateKeyObject,
isKeyObject,
isCryptoKey,
importGenericSecretKey,
};

View File

@ -82,7 +82,7 @@ function getAlgorithmName(hash) {
}
}
async function hmacImportKey(
function hmacImportKey(
format,
keyData,
algorithm,
@ -96,6 +96,24 @@ async function hmacImportKey(
}
let keyObject;
switch (format) {
case 'KeyObject': {
const checkLength = keyData.symmetricKeySize * 8;
if (checkLength === 0 || algorithm.length === 0)
throw lazyDOMException('Zero-length key is not supported', 'DataError');
// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (algorithm.length !== undefined &&
algorithm.length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}
keyObject = keyData;
break;
}
case 'raw': {
const checkLength = keyData.byteLength * 8;

View File

@ -200,7 +200,7 @@ function rsaExportKey(key, format) {
kRsaVariants[key.algorithm.name]));
}
async function rsaImportKey(
function rsaImportKey(
format,
keyData,
algorithm,
@ -209,6 +209,11 @@ async function rsaImportKey(
const usagesSet = new SafeSet(keyUsages);
let keyObject;
switch (format) {
case 'KeyObject': {
verifyAcceptableRsaKeyUse(algorithm.name, keyData.type === 'public', usagesSet);
keyObject = keyData;
break;
}
case 'spki': {
verifyAcceptableRsaKeyUse(algorithm.name, true, usagesSet);
try {

View File

@ -7,7 +7,6 @@ const {
ObjectDefineProperties,
ReflectApply,
ReflectConstruct,
SafeSet,
StringPrototypeRepeat,
SymbolToStringTag,
} = primordials;
@ -31,8 +30,7 @@ const {
const {
CryptoKey,
InternalCryptoKey,
createSecretKey,
importGenericSecretKey,
} = require('internal/crypto/keys');
const {
@ -41,7 +39,6 @@ const {
const {
getBlockSize,
hasAnyNotIn,
normalizeAlgorithm,
normalizeHashName,
validateMaxBufferLength,
@ -521,50 +518,6 @@ async function exportKey(format, key) {
'Export format is unsupported', 'NotSupportedError');
}
async function importGenericSecretKey(
{ name, length },
format,
keyData,
extractable,
keyUsages) {
const usagesSet = new SafeSet(keyUsages);
if (extractable)
throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');
if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
switch (format) {
case 'raw': {
if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
const checkLength = keyData.byteLength * 8;
// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (length !== undefined && length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}
const keyObject = createSecretKey(keyData);
return new InternalCryptoKey(keyObject, { name }, keyUsages, false);
}
}
throw lazyDOMException(
`Unable to import ${name} key with format ${format}`,
'NotSupportedError');
}
async function importKey(
format,
keyData,
@ -606,13 +559,13 @@ async function importKey(
case 'RSA-PSS':
// Fall through
case 'RSA-OAEP':
result = await require('internal/crypto/rsa')
result = require('internal/crypto/rsa')
.rsaImportKey(format, keyData, algorithm, extractable, keyUsages);
break;
case 'ECDSA':
// Fall through
case 'ECDH':
result = await require('internal/crypto/ec')
result = require('internal/crypto/ec')
.ecImportKey(format, keyData, algorithm, extractable, keyUsages);
break;
case 'Ed25519':
@ -622,11 +575,11 @@ async function importKey(
case 'X25519':
// Fall through
case 'X448':
result = await require('internal/crypto/cfrg')
result = require('internal/crypto/cfrg')
.cfrgImportKey(format, keyData, algorithm, extractable, keyUsages);
break;
case 'HMAC':
result = await require('internal/crypto/mac')
result = require('internal/crypto/mac')
.hmacImportKey(format, keyData, algorithm, extractable, keyUsages);
break;
case 'AES-CTR':
@ -636,13 +589,13 @@ async function importKey(
case 'AES-GCM':
// Fall through
case 'AES-KW':
result = await require('internal/crypto/aes')
result = require('internal/crypto/aes')
.aesImportKey(algorithm, format, keyData, extractable, keyUsages);
break;
case 'HKDF':
// Fall through
case 'PBKDF2':
result = await importGenericSecretKey(
result = importGenericSecretKey(
algorithm,
format,
keyData,

View File

@ -0,0 +1,182 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
createSecretKey,
KeyObject,
randomBytes,
generateKeyPairSync,
} = require('crypto');
function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) {
assert.strictEqual(cryptoKey instanceof CryptoKey, true);
assert.strictEqual(cryptoKey.type, keyObject.type);
assert.strictEqual(cryptoKey.algorithm.name, algorithm);
assert.strictEqual(cryptoKey.extractable, extractable);
assert.deepStrictEqual(cryptoKey.usages, usages);
assert.strictEqual(keyObject.equals(KeyObject.from(cryptoKey)), true);
}
{
for (const length of [128, 192, 256]) {
const aes = createSecretKey(randomBytes(length >> 3));
for (const algorithm of ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']) {
const usages = algorithm === 'AES-KW' ? ['wrapKey', 'unwrapKey'] : ['encrypt', 'decrypt'];
for (const extractable of [true, false]) {
const cryptoKey = aes.toCryptoKey(algorithm, extractable, usages);
assertCryptoKey(cryptoKey, aes, algorithm, extractable, usages);
assert.strictEqual(cryptoKey.algorithm.length, length);
}
}
}
}
{
const pbkdf2 = createSecretKey(randomBytes(16));
const algorithm = 'PBKDF2';
const usages = ['deriveBits'];
assert.throws(() => pbkdf2.toCryptoKey(algorithm, true, usages), {
name: 'SyntaxError',
message: 'PBKDF2 keys are not extractable'
});
assert.throws(() => pbkdf2.toCryptoKey(algorithm, false, ['wrapKey']), {
name: 'SyntaxError',
message: 'Unsupported key usage for a PBKDF2 key'
});
const cryptoKey = pbkdf2.toCryptoKey(algorithm, false, usages);
assertCryptoKey(cryptoKey, pbkdf2, algorithm, false, usages);
assert.strictEqual(cryptoKey.algorithm.length, undefined);
}
{
for (const length of [128, 192, 256]) {
const hmac = createSecretKey(randomBytes(length >> 3));
const algorithm = 'HMAC';
const usages = ['sign', 'verify'];
assert.throws(() => {
createSecretKey(Buffer.alloc(0)).toCryptoKey({ name: algorithm, hash: 'SHA-256' }, true, usages);
}, {
name: 'DataError',
message: 'Zero-length key is not supported',
});
assert.throws(() => {
hmac.toCryptoKey({
name: algorithm,
hash: 'SHA-256',
}, true, []);
}, {
name: 'SyntaxError',
message: 'Usages cannot be empty when importing a secret key.'
});
for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) {
for (const extractable of [true, false]) {
assert.throws(() => {
hmac.toCryptoKey({ name: algorithm, hash: 'SHA-256', length: 0 }, true, usages);
}, {
name: 'DataError',
message: 'Zero-length key is not supported',
});
const cryptoKey = hmac.toCryptoKey({ name: algorithm, hash }, extractable, usages);
assertCryptoKey(cryptoKey, hmac, algorithm, extractable, usages);
assert.strictEqual(cryptoKey.algorithm.length, length);
}
}
}
}
{
for (const algorithm of ['Ed25519', 'Ed448', 'X25519', 'X448']) {
const { publicKey, privateKey } = generateKeyPairSync(algorithm.toLowerCase());
assert.throws(() => {
publicKey.toCryptoKey(algorithm === 'Ed25519' ? 'X25519' : 'Ed25519', true, []);
}, {
name: 'DataError',
message: 'Invalid key type'
});
for (const key of [publicKey, privateKey]) {
let usages;
if (algorithm.startsWith('E')) {
usages = key.type === 'public' ? ['verify'] : ['sign'];
} else {
usages = key.type === 'public' ? [] : ['deriveBits'];
}
for (const extractable of [true, false]) {
const cryptoKey = key.toCryptoKey(algorithm, extractable, usages);
assertCryptoKey(cryptoKey, key, algorithm, extractable, usages);
}
}
}
}
{
const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
for (const key of [publicKey, privateKey]) {
for (const algorithm of ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP']) {
let usages;
if (algorithm === 'RSA-OAEP') {
usages = key.type === 'public' ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey'];
} else {
usages = key.type === 'public' ? ['verify'] : ['sign'];
}
for (const extractable of [true, false]) {
for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) {
const cryptoKey = key.toCryptoKey({
name: algorithm,
hash
}, extractable, usages);
assertCryptoKey(cryptoKey, key, algorithm, extractable, usages);
assert.strictEqual(cryptoKey.algorithm.hash.name, hash);
}
}
}
}
}
{
for (const namedCurve of ['P-256', 'P-384', 'P-521']) {
const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve });
assert.throws(() => {
privateKey.toCryptoKey({
name: 'ECDH',
namedCurve,
}, true, []);
}, {
name: 'SyntaxError',
message: 'Usages cannot be empty when importing a private key.'
});
assert.throws(() => {
publicKey.toCryptoKey({
name: 'ECDH',
namedCurve: namedCurve === 'P-256' ? 'P-384' : 'P-256'
}, true, []);
}, {
name: 'DataError',
message: 'Named curve mismatch'
});
for (const key of [publicKey, privateKey]) {
for (const algorithm of ['ECDH', 'ECDSA']) {
let usages;
if (algorithm === 'ECDH') {
usages = key.type === 'public' ? [] : ['deriveBits'];
} else {
usages = key.type === 'public' ? ['verify'] : ['sign'];
}
for (const extractable of [true, false]) {
const cryptoKey = key.toCryptoKey({
name: algorithm,
namedCurve
}, extractable, usages);
assertCryptoKey(cryptoKey, key, algorithm, extractable, usages);
assert.strictEqual(cryptoKey.algorithm.namedCurve, namedCurve);
}
}
}
}
}

View File

@ -21,6 +21,11 @@ const { subtle } = globalThis.crypto;
subtle.importKey('not valid', keyData, {}, false, ['wrapKey']), {
code: 'ERR_INVALID_ARG_VALUE'
});
await assert.rejects(
subtle.importKey('KeyObject', keyData, {}, false, ['wrapKey']), {
message: /'KeyObject' is not a valid enum value of type KeyFormat/,
code: 'ERR_INVALID_ARG_VALUE'
});
await assert.rejects(
subtle.importKey('raw', 1, {}, false, ['deriveBits']), {
code: 'ERR_INVALID_ARG_TYPE'