Skip to content

Commit bb9a212

Browse files
committed
crypto: support JWK objects in create*Key
1 parent 97ebcdd commit bb9a212

File tree

4 files changed

+246
-56
lines changed

4 files changed

+246
-56
lines changed

doc/api/crypto.md

+17-8
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

+141-5
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,122 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) {
413417
return types;
414418
}
415419

420+
function getKeyObjectHandleFromJwk(key, ctx) {
421+
validateObject(key, 'key');
422+
validateOneOf(
423+
key.kty, 'key.kty', ['RSA', 'EC', 'OKP']);
424+
const isPublic = ctx === kConsumePublic || ctx === kCreatePublic;
425+
426+
if (key.kty === 'OKP') {
427+
validateString(key.crv, 'key.crv');
428+
validateOneOf(
429+
key.crv, 'key.crv', ['Ed25519', 'Ed448', 'X25519', 'X448']);
430+
validateString(key.x, 'key.x');
431+
432+
if (!isPublic)
433+
validateString(key.d, 'key.d');
434+
435+
let keyData;
436+
if (isPublic)
437+
keyData = Buffer.from(key.x, 'base64');
438+
else
439+
keyData = Buffer.from(key.d, 'base64');
440+
441+
switch (key.crv) {
442+
case 'Ed25519':
443+
case 'X25519':
444+
if (keyData.byteLength !== 32) {
445+
throw new ERR_CRYPTO_INVALID_JWK();
446+
}
447+
break;
448+
case 'Ed448':
449+
if (keyData.byteLength !== 57) {
450+
throw new ERR_CRYPTO_INVALID_JWK();
451+
}
452+
break;
453+
case 'X448':
454+
if (keyData.byteLength !== 56) {
455+
throw new ERR_CRYPTO_INVALID_JWK();
456+
}
457+
break;
458+
}
459+
460+
const handle = new KeyObjectHandle();
461+
if (isPublic) {
462+
handle.initEDRaw(
463+
`NODE-${key.crv.toUpperCase()}`,
464+
keyData,
465+
kKeyTypePublic);
466+
} else {
467+
handle.initEDRaw(
468+
`NODE-${key.crv.toUpperCase()}`,
469+
keyData,
470+
kKeyTypePrivate);
471+
}
472+
473+
return handle;
474+
}
475+
476+
if (key.kty === 'EC') {
477+
validateString(key.crv, 'key.crv');
478+
validateOneOf(
479+
key.crv, 'key.crv', ['P-256', 'secp256k1', 'P-384', 'P-521']);
480+
validateString(key.x, 'key.x');
481+
validateString(key.y, 'key.y');
482+
483+
const jwk = {
484+
kty: key.kty,
485+
crv: key.crv,
486+
x: key.x,
487+
y: key.y
488+
};
489+
490+
if (!isPublic) {
491+
validateString(key.d, 'key.d');
492+
jwk.d = key.d;
493+
}
494+
495+
const handle = new KeyObjectHandle();
496+
const type = handle.initJwk(key, key.crv);
497+
if (type === undefined)
498+
throw new ERR_CRYPTO_INVALID_JWK();
499+
500+
return handle;
501+
}
502+
503+
// RSA
504+
validateString(key.n, 'key.n');
505+
validateString(key.e, 'key.e');
506+
507+
const jwk = {
508+
kty: key.kty,
509+
n: key.n,
510+
e: key.e
511+
};
512+
513+
if (!isPublic) {
514+
validateString(key.d, 'key.d');
515+
validateString(key.p, 'key.p');
516+
validateString(key.q, 'key.q');
517+
validateString(key.dp, 'key.dp');
518+
validateString(key.dq, 'key.dq');
519+
validateString(key.qi, 'key.qi');
520+
jwk.d = key.d;
521+
jwk.p = key.p;
522+
jwk.q = key.q;
523+
jwk.dp = key.dp;
524+
jwk.dq = key.dq;
525+
jwk.qi = key.qi;
526+
}
527+
528+
const handle = new KeyObjectHandle();
529+
const type = handle.initJwk(key);
530+
if (type === undefined)
531+
throw new ERR_CRYPTO_INVALID_JWK();
532+
533+
return handle;
534+
}
535+
416536
function prepareAsymmetricKey(key, ctx) {
417537
if (isKeyObject(key)) {
418538
// Best case: A key object, as simple as that.
@@ -423,13 +543,15 @@ function prepareAsymmetricKey(key, ctx) {
423543
// Expect PEM by default, mostly for backward compatibility.
424544
return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, 'key') };
425545
} else if (typeof key === 'object') {
426-
const { key: data, encoding } = key;
546+
const { key: data, encoding, format } = key;
427547
// The 'key' property can be a KeyObject as well to allow specifying
428548
// additional options such as padding along with the key.
429549
if (isKeyObject(data))
430550
return { data: getKeyObjectHandle(data, ctx) };
431551
else if (isCryptoKey(data))
432552
return { data: getKeyObjectHandle(data[kKeyObject], ctx) };
553+
else if (isJwk(data) && format === 'jwk')
554+
return { data: getKeyObjectHandleFromJwk(data, ctx), format: 'jwk' };
433555
// Either PEM or DER using PKCS#1 or SPKI.
434556
if (!isStringOrBuffer(data)) {
435557
throw new ERR_INVALID_ARG_TYPE(
@@ -494,16 +616,26 @@ function createSecretKey(key, encoding) {
494616
function createPublicKey(key) {
495617
const { format, type, data, passphrase } =
496618
prepareAsymmetricKey(key, kCreatePublic);
497-
const handle = new KeyObjectHandle();
498-
handle.init(kKeyTypePublic, data, format, type, passphrase);
619+
let handle;
620+
if (format === 'jwk') {
621+
handle = data;
622+
} else {
623+
handle = new KeyObjectHandle();
624+
handle.init(kKeyTypePublic, data, format, type, passphrase);
625+
}
499626
return new PublicKeyObject(handle);
500627
}
501628

502629
function createPrivateKey(key) {
503630
const { format, type, data, passphrase } =
504631
prepareAsymmetricKey(key, kCreatePrivate);
505-
const handle = new KeyObjectHandle();
506-
handle.init(kKeyTypePrivate, data, format, type, passphrase);
632+
let handle;
633+
if (format === 'jwk') {
634+
handle = data;
635+
} else {
636+
handle = new KeyObjectHandle();
637+
handle.init(kKeyTypePrivate, data, format, type, passphrase);
638+
}
507639
return new PrivateKeyObject(handle);
508640
}
509641

@@ -609,6 +741,10 @@ function isCryptoKey(obj) {
609741
return obj != null && obj[kKeyObject] !== undefined;
610742
}
611743

744+
function isJwk(obj) {
745+
return obj != null && obj.kty !== undefined;
746+
}
747+
612748
module.exports = {
613749
// Public API.
614750
createSecretKey,

lib/internal/errors.js

+1
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)