Skip to content

Commit

Permalink
crypto: support JWK objects in create*Key
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Feb 24, 2021
1 parent 80098e6 commit d54e775
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 56 deletions.
25 changes: 17 additions & 8 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -2452,6 +2452,9 @@ input.on('readable', () => {
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37254
description: The key can also be a JWK object.
- version: v15.0.0
pr-url: https://github.com/nodejs/node/pull/35093
description: The key can also be an ArrayBuffer. The encoding option was
Expand All @@ -2460,11 +2463,12 @@ changes:

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

<!--lint disable maximum-line-length remark-lint-->
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView}
* `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView}
* `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`.
* `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is required
only if the `format` is `'der'`.
* `key`: {string|ArrayBuffer|Buffer|TypedArray|DataView|Object} The key
material, either in PEM, DER, or JWK format.
* `format`: {string} Must be `'pem'`, `'der'`, or '`'jwk'`.
**Default:** `'pem'`.
* `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is
required only if the `format` is `'der'` and ignored otherwise.
* `encoding` {string} The string encoding to use when `key` is a string.
* Returns: {KeyObject}
<!--lint enable maximum-line-length remark-lint-->
Expand Down
135 changes: 130 additions & 5 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
const {
validateObject,
validateOneOf,
validateString,
} = require('internal/validators');

const {
Expand All @@ -38,6 +39,7 @@ const {
ERR_OPERATION_FAILED,
ERR_CRYPTO_JWK_UNSUPPORTED_CURVE,
ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE,
ERR_CRYPTO_INVALID_JWK,
}
} = require('internal/errors');

Expand Down Expand Up @@ -65,6 +67,8 @@ const {

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

const { Buffer } = require('buffer');

const kAlgorithm = Symbol('kAlgorithm');
const kExtractable = Symbol('kExtractable');
const kKeyType = Symbol('kKeyType');
Expand Down Expand Up @@ -413,6 +417,111 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) {
return types;
}

function getKeyObjectHandleFromJwk(key, ctx) {
validateObject(key, 'key');
key = { ...key };
validateOneOf(
key.kty, 'key.kty', ['RSA', 'EC', 'OKP']);
const isPublic = ctx === kConsumePublic || ctx === kCreatePublic;

if (key.kty === 'OKP') {
validateString(key.crv, 'key.crv');
validateOneOf(
key.crv, 'key.crv', ['Ed25519', 'Ed448', 'X25519', 'X448']);
validateString(key.x, 'key.x');
if (!isPublic) {
validateString(key.d, 'key.d');
}

let keyData;
if (isPublic)
keyData = Buffer.from(key.x, 'base64');
else
keyData = Buffer.from(key.d, 'base64');

switch (key.crv) {
case 'Ed25519':
case 'X25519':
if (keyData.byteLength !== 32) {
throw new ERR_CRYPTO_INVALID_JWK();
}
break;
case 'Ed448':
if (keyData.byteLength !== 57) {
throw new ERR_CRYPTO_INVALID_JWK();
}
break;
case 'X448':
if (keyData.byteLength !== 56) {
throw new ERR_CRYPTO_INVALID_JWK();
}
break;
}

const handle = new KeyObjectHandle();
if (isPublic) {
handle.initEDRaw(
`NODE-${key.crv.toUpperCase()}`,
keyData,
kKeyTypePublic);
} else {
handle.initEDRaw(
`NODE-${key.crv.toUpperCase()}`,
keyData,
kKeyTypePrivate);
}

return handle;
}

if (key.kty === 'EC') {
validateString(key.crv, 'key.crv');
validateOneOf(
key.crv, 'key.crv', ['P-256', 'secp256k1', 'P-384', 'P-521']);
validateString(key.x, 'key.x');
validateString(key.y, 'key.y');

if (isPublic) {
delete key.d;
} else {
validateString(key.d, 'key.d');
}

const handle = new KeyObjectHandle();
const type = handle.initJwk(key, key.crv);
if (type === undefined)
throw new ERR_CRYPTO_INVALID_JWK();

return handle;
}

// RSA
validateString(key.n, 'key.n');
validateString(key.e, 'key.e');
if (isPublic) {
delete key.d;
delete key.p;
delete key.q;
delete key.dp;
delete key.dq;
delete key.qi;
} else {
validateString(key.d, 'key.d');
validateString(key.p, 'key.p');
validateString(key.q, 'key.q');
validateString(key.dp, 'key.dp');
validateString(key.dq, 'key.dq');
validateString(key.qi, 'key.qi');
}

const handle = new KeyObjectHandle();
const type = handle.initJwk(key);
if (type === undefined)
throw new ERR_CRYPTO_INVALID_JWK();

return handle;
}

function prepareAsymmetricKey(key, ctx) {
if (isKeyObject(key)) {
// Best case: A key object, as simple as that.
Expand All @@ -423,13 +532,15 @@ function prepareAsymmetricKey(key, ctx) {
// Expect PEM by default, mostly for backward compatibility.
return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, 'key') };
} else if (typeof key === 'object') {
const { key: data, encoding } = key;
const { key: data, encoding, format } = 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) };
else if (isCryptoKey(data))
return { data: getKeyObjectHandle(data[kKeyObject], ctx) };
else if (isJwk(data) && format === 'jwk')
return { data: getKeyObjectHandleFromJwk(data, ctx), format: 'jwk' };
// Either PEM or DER using PKCS#1 or SPKI.
if (!isStringOrBuffer(data)) {
throw new ERR_INVALID_ARG_TYPE(
Expand Down Expand Up @@ -494,16 +605,26 @@ function createSecretKey(key, encoding) {
function createPublicKey(key) {
const { format, type, data, passphrase } =
prepareAsymmetricKey(key, kCreatePublic);
const handle = new KeyObjectHandle();
handle.init(kKeyTypePublic, data, format, type, passphrase);
let handle;
if (format === 'jwk') {
handle = data;
} else {
handle = new KeyObjectHandle();
handle.init(kKeyTypePublic, data, format, type, passphrase);
}
return new PublicKeyObject(handle);
}

function createPrivateKey(key) {
const { format, type, data, passphrase } =
prepareAsymmetricKey(key, kCreatePrivate);
const handle = new KeyObjectHandle();
handle.init(kKeyTypePrivate, data, format, type, passphrase);
let handle;
if (format === 'jwk') {
handle = data;
} else {
handle = new KeyObjectHandle();
handle.init(kKeyTypePrivate, data, format, type, passphrase);
}
return new PrivateKeyObject(handle);
}

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

function isJwk(obj) {
return obj != null && obj.kty !== undefined;
}

module.exports = {
// Public API.
createSecretKey,
Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,7 @@ E('ERR_CRYPTO_INCOMPATIBLE_KEY', 'Incompatible %s: %s', Error);
E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.',
Error);
E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError);
E('ERR_CRYPTO_INVALID_JWK', 'Invalid JWK data', TypeError);
E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
'Invalid key object type %s, expected %s.', TypeError);
E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error);
Expand Down
Loading

0 comments on commit d54e775

Please sign in to comment.