Skip to content

Commit b41c5d3

Browse files
committed
crypto: support JWK objects in create*Key
1 parent 574590d commit b41c5d3

File tree

4 files changed

+235
-56
lines changed

4 files changed

+235
-56
lines changed

doc/api/crypto.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2452,6 +2452,9 @@ input.on('readable', () => {
24522452
<!-- YAML
24532453
added: v11.6.0
24542454
changes:
2455+
- version: REPLACEME
2456+
pr-url: https://github.com/nodejs/node/pull/37254
2457+
description: The key can also be a JWK object.
24552458
- version: v15.0.0
24562459
pr-url: https://github.com/nodejs/node/pull/35093
24572460
description: The key can also be an ArrayBuffer. The encoding option was
@@ -2460,11 +2463,12 @@ changes:
24602463

24612464
<!--lint disable maximum-line-length remark-lint-->
24622465
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView}
2463-
* `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView} The key material,
2464-
either in PEM or DER format.
2465-
* `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`.
2466+
* `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView|Object} The key
2467+
material, either in PEM, DER, or JWK format.
2468+
* `format`: {string} Must be `'pem'`, `'der'`, or '`'jwk'`.
2469+
**Default:** `'pem'`.
24662470
* `type`: {string} Must be `'pkcs1'`, `'pkcs8'` or `'sec1'`. This option is
2467-
required only if the `format` is `'der'` and ignored if it is `'pem'`.
2471+
required only if the `format` is `'der'` and ignored otherwise.
24682472
* `passphrase`: {string | Buffer} The passphrase to use for decryption.
24692473
* `encoding`: {string} The string encoding to use when `key` is a string.
24702474
* Returns: {KeyObject}
@@ -2481,6 +2485,9 @@ of the passphrase is limited to 1024 bytes.
24812485
<!-- YAML
24822486
added: v11.6.0
24832487
changes:
2488+
- version: REPLACEME
2489+
pr-url: https://github.com/nodejs/node/pull/37254
2490+
description: The key can also be a JWK object.
24842491
- version: v15.0.0
24852492
pr-url: https://github.com/nodejs/node/pull/35093
24862493
description: The key can also be an ArrayBuffer. The encoding option was
@@ -2496,10 +2503,12 @@ changes:
24962503

24972504
<!--lint disable maximum-line-length remark-lint-->
24982505
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView}
2499-
* `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView}
2500-
* `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`.
2501-
* `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is required
2502-
only if the `format` is `'der'`.
2506+
* `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView|Object} The key
2507+
material, either in PEM, DER, or JWK format.
2508+
* `format`: {string} Must be `'pem'`, `'der'`, or '`'jwk'`.
2509+
**Default:** `'pem'`.
2510+
* `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is
2511+
required only if the `format` is `'der'` and ignored otherwise.
25032512
* `encoding` {string} The string encoding to use when `key` is a string.
25042513
* Returns: {KeyObject}
25052514
<!--lint enable maximum-line-length remark-lint-->

lib/internal/crypto/keys.js

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const {
2626
const {
2727
validateObject,
2828
validateOneOf,
29+
validateString,
2930
} = require('internal/validators');
3031

3132
const {
@@ -38,6 +39,7 @@ const {
3839
ERR_OPERATION_FAILED,
3940
ERR_CRYPTO_JWK_UNSUPPORTED_CURVE,
4041
ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE,
42+
ERR_CRYPTO_INVALID_JWK,
4143
}
4244
} = require('internal/errors');
4345

@@ -65,6 +67,8 @@ const {
6567

6668
const { inspect } = require('internal/util/inspect');
6769

70+
const { Buffer } = require('buffer');
71+
6872
const kAlgorithm = Symbol('kAlgorithm');
6973
const kExtractable = Symbol('kExtractable');
7074
const kKeyType = Symbol('kKeyType');
@@ -413,6 +417,111 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) {
413417
return types;
414418
}
415419

420+
function getKeyObjectHandleFromJwk(key, ctx) {
421+
validateObject(key, 'key');
422+
key = { ...key };
423+
validateOneOf(
424+
key.kty, 'key.kty', ['RSA', 'EC', 'OKP']);
425+
const isPublic = ctx === kConsumePublic || ctx === kCreatePublic;
426+
427+
if (key.kty === 'OKP') {
428+
validateString(key.crv, 'key.crv');
429+
validateOneOf(
430+
key.crv, 'key.crv', ['Ed25519', 'Ed448', 'X25519', 'X448']);
431+
validateString(key.x, 'key.x');
432+
if (!isPublic) {
433+
validateString(key.d, 'key.d');
434+
}
435+
436+
let keyData;
437+
if (isPublic)
438+
keyData = Buffer.from(key.x, 'base64');
439+
else
440+
keyData = Buffer.from(key.d, 'base64');
441+
442+
switch (key.crv) {
443+
case 'Ed25519':
444+
case 'X25519':
445+
if (keyData.byteLength !== 32) {
446+
throw new ERR_CRYPTO_INVALID_JWK();
447+
}
448+
break;
449+
case 'Ed448':
450+
if (keyData.byteLength !== 57) {
451+
throw new ERR_CRYPTO_INVALID_JWK();
452+
}
453+
break;
454+
case 'X448':
455+
if (keyData.byteLength !== 56) {
456+
throw new ERR_CRYPTO_INVALID_JWK();
457+
}
458+
break;
459+
}
460+
461+
const handle = new KeyObjectHandle();
462+
if (isPublic) {
463+
handle.initEDRaw(
464+
`NODE-${key.crv.toUpperCase()}`,
465+
keyData,
466+
kKeyTypePublic);
467+
} else {
468+
handle.initEDRaw(
469+
`NODE-${key.crv.toUpperCase()}`,
470+
keyData,
471+
kKeyTypePrivate);
472+
}
473+
474+
return handle;
475+
}
476+
477+
if (key.kty === 'EC') {
478+
validateString(key.crv, 'key.crv');
479+
validateOneOf(
480+
key.crv, 'key.crv', ['P-256', 'secp256k1', 'P-384', 'P-521']);
481+
validateString(key.x, 'key.x');
482+
validateString(key.y, 'key.y');
483+
484+
if (isPublic) {
485+
delete key.d;
486+
} else {
487+
validateString(key.d, 'key.d');
488+
}
489+
490+
const handle = new KeyObjectHandle();
491+
const type = handle.initJwk(key, key.crv);
492+
if (type === undefined)
493+
throw new ERR_CRYPTO_INVALID_JWK();
494+
495+
return handle;
496+
}
497+
498+
// RSA
499+
validateString(key.n, 'key.n');
500+
validateString(key.e, 'key.e');
501+
if (isPublic) {
502+
delete key.d;
503+
delete key.p;
504+
delete key.q;
505+
delete key.dp;
506+
delete key.dq;
507+
delete key.qi;
508+
} else {
509+
validateString(key.d, 'key.d');
510+
validateString(key.p, 'key.p');
511+
validateString(key.q, 'key.q');
512+
validateString(key.dp, 'key.dp');
513+
validateString(key.dq, 'key.dq');
514+
validateString(key.qi, 'key.qi');
515+
}
516+
517+
const handle = new KeyObjectHandle();
518+
const type = handle.initJwk(key);
519+
if (type === undefined)
520+
throw new ERR_CRYPTO_INVALID_JWK();
521+
522+
return handle;
523+
}
524+
416525
function prepareAsymmetricKey(key, ctx) {
417526
if (isKeyObject(key)) {
418527
// Best case: A key object, as simple as that.
@@ -423,13 +532,15 @@ function prepareAsymmetricKey(key, ctx) {
423532
// Expect PEM by default, mostly for backward compatibility.
424533
return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, 'key') };
425534
} else if (typeof key === 'object') {
426-
const { key: data, encoding } = key;
535+
const { key: data, encoding, format } = key;
427536
// The 'key' property can be a KeyObject as well to allow specifying
428537
// additional options such as padding along with the key.
429538
if (isKeyObject(data))
430539
return { data: getKeyObjectHandle(data, ctx) };
431540
else if (isCryptoKey(data))
432541
return { data: getKeyObjectHandle(data[kKeyObject], ctx) };
542+
else if (isJwk(data) && format === 'jwk')
543+
return { data: getKeyObjectHandleFromJwk(data, ctx), format: 'jwk' };
433544
// Either PEM or DER using PKCS#1 or SPKI.
434545
if (!isStringOrBuffer(data)) {
435546
throw new ERR_INVALID_ARG_TYPE(
@@ -494,16 +605,26 @@ function createSecretKey(key, encoding) {
494605
function createPublicKey(key) {
495606
const { format, type, data, passphrase } =
496607
prepareAsymmetricKey(key, kCreatePublic);
497-
const handle = new KeyObjectHandle();
498-
handle.init(kKeyTypePublic, data, format, type, passphrase);
608+
let handle;
609+
if (format === 'jwk') {
610+
handle = data;
611+
} else {
612+
handle = new KeyObjectHandle();
613+
handle.init(kKeyTypePublic, data, format, type, passphrase);
614+
}
499615
return new PublicKeyObject(handle);
500616
}
501617

502618
function createPrivateKey(key) {
503619
const { format, type, data, passphrase } =
504620
prepareAsymmetricKey(key, kCreatePrivate);
505-
const handle = new KeyObjectHandle();
506-
handle.init(kKeyTypePrivate, data, format, type, passphrase);
621+
let handle;
622+
if (format === 'jwk') {
623+
handle = data;
624+
} else {
625+
handle = new KeyObjectHandle();
626+
handle.init(kKeyTypePrivate, data, format, type, passphrase);
627+
}
507628
return new PrivateKeyObject(handle);
508629
}
509630

@@ -609,6 +730,10 @@ function isCryptoKey(obj) {
609730
return obj != null && obj[kKeyObject] !== undefined;
610731
}
611732

733+
function isJwk(obj) {
734+
return obj != null && obj.kty !== undefined;
735+
}
736+
612737
module.exports = {
613738
// Public API.
614739
createSecretKey,

lib/internal/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,7 @@ E('ERR_CRYPTO_INCOMPATIBLE_KEY', 'Incompatible %s: %s', Error);
836836
E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.',
837837
Error);
838838
E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError);
839+
E('ERR_CRYPTO_INVALID_JWK', 'Invalid JWK data', TypeError);
839840
E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
840841
'Invalid key object type %s, expected %s.', TypeError);
841842
E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error);

0 commit comments

Comments
 (0)