Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: support JWK objects in create(Public|Private)Key #37254

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
146 changes: 141 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,122 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) {
return types;
}

function getKeyObjectHandleFromJwk(key, ctx) {
validateObject(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');

const jwk = {
kty: key.kty,
crv: key.crv,
x: key.x,
y: key.y
};

if (!isPublic) {
validateString(key.d, 'key.d');
jwk.d = key.d;
}

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

return handle;
}

// RSA
validateString(key.n, 'key.n');
validateString(key.e, 'key.e');

const jwk = {
kty: key.kty,
n: key.n,
e: key.e
};

if (!isPublic) {
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');
jwk.d = key.d;
jwk.p = key.p;
jwk.q = key.q;
jwk.dp = key.dp;
jwk.dq = key.dq;
jwk.qi = key.qi;
}

const handle = new KeyObjectHandle();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how I feel about creating KeyObjectHandles in more places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a concrete suggestion? I'm only using what's available.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not right now, sorry. We can probably improve that later. I don't think this internal inconsistency has any visible effect to the user, but I'm not entirely sure.

const type = handle.initJwk(jwk);
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 +543,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 +616,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 +741,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