Skip to content

Commit

Permalink
crypto: allow deriving public from private keys
Browse files Browse the repository at this point in the history
This change allows passing private key objects to
crypto.createPublicKey, resulting in a key object that represents a
valid public key for the given private key. The returned public key
object can be used and exported safely without revealing information
about the private key.

Backport-PR-URL: #26688
PR-URL: #26278
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
  • Loading branch information
tniessen authored and targos committed Mar 27, 2019
1 parent be3ea2a commit ed7599b
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 25 deletions.
17 changes: 12 additions & 5 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1817,28 +1817,35 @@ must be an object with the properties described above.
<!-- YAML
added: v11.6.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/26278
description: The `key` argument can now be a `KeyObject` with type
`private`.
- version: v11.7.0
pr-url: https://github.com/nodejs/node/pull/25217
description: The `key` argument can now be a private key.
-->
* `key` {Object | string | Buffer}
* `key` {Object | string | Buffer | KeyObject}
- `key`: {string | Buffer}
- `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'`.
* Returns: {KeyObject}

Creates and returns a new key object containing a public key. If `key` is a
string or `Buffer`, `format` is assumed to be `'pem'`; otherwise, `key`
must be an object with the properties described above.
string or `Buffer`, `format` is assumed to be `'pem'`; if `key` is a `KeyObject`
with type `'private'`, the public key is derived from the given private key;
otherwise, `key` must be an object with the properties described above.

If the format is `'pem'`, the `'key'` may also be an X.509 certificate.

Because public keys can be derived from private keys, a private key may be
passed instead of a public key. In that case, this function behaves as if
[`crypto.createPrivateKey()`][] had been called, except that the type of the
returned `KeyObject` will be `public` and that the private key cannot be
extracted from the returned `KeyObject`.
returned `KeyObject` will be `'public'` and that the private key cannot be
extracted from the returned `KeyObject`. Similarly, if a `KeyObject` with type
`'private'` is given, a new `KeyObject` with type `'public'` will be returned
and it will be impossible to extract the private key from the returned object.

### crypto.createSecretKey(key)
<!-- YAML
Expand Down
50 changes: 31 additions & 19 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ const { isArrayBufferView } = require('internal/util/types');

const kKeyType = Symbol('kKeyType');

// Key input contexts.
const kConsumePublic = 0;
const kConsumePrivate = 1;
const kCreatePublic = 2;
const kCreatePrivate = 3;

const encodingNames = [];
for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'],
[kKeyEncodingSPKI, 'spki'], [kKeyEncodingSEC1, 'sec1']])
Expand Down Expand Up @@ -203,7 +209,7 @@ function parseKeyEncoding(enc, keyType, isPublic, objName) {
// when this is used to parse an input encoding and must be a valid key type if
// used to parse an output encoding.
function parsePublicKeyEncoding(enc, keyType, objName) {
return parseKeyFormatAndType(enc, keyType, true, objName);
return parseKeyEncoding(enc, keyType, keyType ? true : undefined, objName);
}

// Parses the private key encoding based on an object. keyType must be undefined
Expand All @@ -213,26 +219,31 @@ function parsePrivateKeyEncoding(enc, keyType, objName) {
return parseKeyEncoding(enc, keyType, false, objName);
}

function getKeyObjectHandle(key, isPublic, allowKeyObject) {
if (!allowKeyObject) {
function getKeyObjectHandle(key, ctx) {
if (ctx === kCreatePrivate) {
throw new ERR_INVALID_ARG_TYPE(
'key',
['string', 'Buffer', 'TypedArray', 'DataView'],
key
);
}
if (isPublic != null) {
const expectedType = isPublic ? 'public' : 'private';
if (key.type !== expectedType)
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, expectedType);

if (key.type !== 'private') {
if (ctx === kConsumePrivate || ctx === kCreatePublic)
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'private');
if (key.type !== 'public') {
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type,
'private or public');
}
}

return key[kHandle];
}

function prepareAsymmetricKey(key, isPublic, allowKeyObject = true) {
function prepareAsymmetricKey(key, ctx) {
if (isKeyObject(key)) {
// Best case: A key object, as simple as that.
return { data: getKeyObjectHandle(key, isPublic, allowKeyObject) };
return { data: getKeyObjectHandle(key, ctx) };
} else if (typeof key === 'string' || isArrayBufferView(key)) {
// Expect PEM by default, mostly for backward compatibility.
return { format: kKeyFormatPEM, data: key };
Expand All @@ -241,32 +252,32 @@ function prepareAsymmetricKey(key, isPublic, allowKeyObject = true) {
// 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, isPublic, allowKeyObject) };
return { data: getKeyObjectHandle(data, ctx) };
// Either PEM or DER using PKCS#1 or SPKI.
if (!isStringOrBuffer(data)) {
throw new ERR_INVALID_ARG_TYPE(
'key',
['string', 'Buffer', 'TypedArray', 'DataView',
...(allowKeyObject ? ['KeyObject'] : [])],
...(ctx !== kCreatePrivate ? ['KeyObject'] : [])],
key);
}
return { data, ...parseKeyEncoding(key, undefined, isPublic) };
return { data, ...parseKeyEncoding(key, undefined) };
} else {
throw new ERR_INVALID_ARG_TYPE(
'key',
['string', 'Buffer', 'TypedArray', 'DataView',
...(allowKeyObject ? ['KeyObject'] : [])],
...(ctx !== kCreatePrivate ? ['KeyObject'] : [])],
key
);
}
}

function preparePrivateKey(key, allowKeyObject) {
return prepareAsymmetricKey(key, false, allowKeyObject);
function preparePrivateKey(key) {
return prepareAsymmetricKey(key, kConsumePrivate);
}

function preparePublicOrPrivateKey(key, allowKeyObject) {
return prepareAsymmetricKey(key, undefined, allowKeyObject);
function preparePublicOrPrivateKey(key) {
return prepareAsymmetricKey(key, kConsumePublic);
}

function prepareSecretKey(key, bufferOnly = false) {
Expand Down Expand Up @@ -296,14 +307,15 @@ function createSecretKey(key) {
}

function createPublicKey(key) {
const { format, type, data } = preparePublicOrPrivateKey(key, false);
const { format, type, data } = prepareAsymmetricKey(key, kCreatePublic);
const handle = new KeyObjectHandle(kKeyTypePublic);
handle.init(data, format, type);
return new PublicKeyObject(handle);
}

function createPrivateKey(key) {
const { format, type, data, passphrase } = preparePrivateKey(key, false);
const { format, type, data, passphrase } =
prepareAsymmetricKey(key, kCreatePrivate);
const handle = new KeyObjectHandle(kKeyTypePrivate);
handle.init(data, format, type, passphrase);
return new PrivateKeyObject(handle);
Expand Down
36 changes: 35 additions & 1 deletion test/parallel/test-crypto-key-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,27 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii');
}

{
// Passing an existing key object should throw.
// Passing an existing public key object to createPublicKey should throw.
const publicKey = createPublicKey(publicPem);
common.expectsError(() => createPublicKey(publicKey), {
type: TypeError,
code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
message: 'Invalid key object type public, expected private.'
});

// Constructing a private key from a public key should be impossible, even
// if the public key was derived from a private key.
common.expectsError(() => createPrivateKey(createPublicKey(privatePem)), {
type: TypeError,
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "key" argument must be one of type string, Buffer, ' +
'TypedArray, or DataView. Received type object'
});

// Similarly, passing an existing private key object to createPrivateKey
// should throw.
const privateKey = createPrivateKey(privatePem);
common.expectsError(() => createPrivateKey(privateKey), {
type: TypeError,
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "key" argument must be one of type string, Buffer, ' +
Expand All @@ -80,6 +98,12 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii');
assert.strictEqual(privateKey.asymmetricKeyType, 'rsa');
assert.strictEqual(privateKey.symmetricKeySize, undefined);

// It should be possible to derive a public key from a private key.
const derivedPublicKey = createPublicKey(privateKey);
assert.strictEqual(derivedPublicKey.type, 'public');
assert.strictEqual(derivedPublicKey.asymmetricKeyType, 'rsa');
assert.strictEqual(derivedPublicKey.symmetricKeySize, undefined);

const publicDER = publicKey.export({
format: 'der',
type: 'pkcs1'
Expand All @@ -95,8 +119,18 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii');

const plaintext = Buffer.from('Hello world', 'utf8');
const ciphertexts = [
// Encrypt using the public key.
publicEncrypt(publicKey, plaintext),
publicEncrypt({ key: publicKey }, plaintext),

// Encrypt using the private key.
publicEncrypt(privateKey, plaintext),
publicEncrypt({ key: privateKey }, plaintext),

// Encrypt using a public key derived from the private key.
publicEncrypt(derivedPublicKey, plaintext),
publicEncrypt({ key: derivedPublicKey }, plaintext),

// Test distinguishing PKCS#1 public and private keys based on the
// DER-encoded data only.
publicEncrypt({ format: 'der', type: 'pkcs1', key: publicDER }, plaintext),
Expand Down

0 comments on commit ed7599b

Please sign in to comment.