Skip to content

Commit

Permalink
crypto: add API for key pair generation
Browse files Browse the repository at this point in the history
This adds support for RSA, DSA and EC key pair generation with a
variety of possible output formats etc.

PR-URL: #22660
Fixes: #15116
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ujjwal Sharma <usharma1998@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
  • Loading branch information
tniessen committed Sep 20, 2018
1 parent df9abb6 commit 8c502f5
Show file tree
Hide file tree
Showing 10 changed files with 1,457 additions and 0 deletions.
110 changes: 110 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1673,6 +1673,116 @@ Use [`crypto.getHashes()`][] to obtain an array of names of the available
signing algorithms. Optional `options` argument controls the
`stream.Writable` behavior.

### crypto.generateKeyPair(type, options, callback)
<!-- YAML
added: REPLACEME
-->
* `type`: {string} Must be `'rsa'`, `'dsa'` or `'ec'`.
* `options`: {Object}
- `modulusLength`: {number} Key size in bits (RSA, DSA).
- `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`.
- `divisorLength`: {number} Size of `q` in bits (DSA).
- `namedCurve`: {string} Name of the curve to use (EC).
- `publicKeyEncoding`: {Object}
- `type`: {string} Must be one of `'pkcs1'` (RSA only) or `'spki'`.
- `format`: {string} Must be `'pem'` or `'der'`.
- `privateKeyEncoding`: {Object}
- `type`: {string} Must be one of `'pkcs1'` (RSA only), `'pkcs8'` or
`'sec1'` (EC only).
- `format`: {string} Must be `'pem'` or `'der'`.
- `cipher`: {string} If specified, the private key will be encrypted with
the given `cipher` and `passphrase` using PKCS#5 v2.0 password based
encryption.
- `passphrase`: {string} The passphrase to use for encryption, see `cipher`.
* `callback`: {Function}
- `err`: {Error}
- `publicKey`: {string|Buffer}
- `privateKey`: {string|Buffer}

Generates a new asymmetric key pair of the given `type`. Only RSA, DSA and EC
are currently supported.

It is recommended to encode public keys as `'spki'` and private keys as
`'pkcs8'` with encryption:

```js
const { generateKeyPair } = require('crypto');
generateKeyPair('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: 'top secret'
}
}, (err, publicKey, privateKey) => {
// Handle errors and use the generated key pair.
});
```

On completion, `callback` will be called with `err` set to `undefined` and
`publicKey` / `privateKey` representing the generated key pair. When PEM
encoding was selected, the result will be a string, otherwise it will be a
buffer containing the data encoded as DER. Note that Node.js itself does not
accept DER, it is supported for interoperability with other libraries such as
WebCrypto only.

### crypto.generateKeyPairSync(type, options)
<!-- YAML
added: REPLACEME
-->
* `type`: {string} Must be `'rsa'`, `'dsa'` or `'ec'`.
* `options`: {Object}
- `modulusLength`: {number} Key size in bits (RSA, DSA).
- `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`.
- `divisorLength`: {number} Size of `q` in bits (DSA).
- `namedCurve`: {string} Name of the curve to use (EC).
- `publicKeyEncoding`: {Object}
- `type`: {string} Must be one of `'pkcs1'` (RSA only) or `'spki'`.
- `format`: {string} Must be `'pem'` or `'der'`.
- `privateKeyEncoding`: {Object}
- `type`: {string} Must be one of `'pkcs1'` (RSA only), `'pkcs8'` or
`'sec1'` (EC only).
- `format`: {string} Must be `'pem'` or `'der'`.
- `cipher`: {string} If specified, the private key will be encrypted with
the given `cipher` and `passphrase` using PKCS#5 v2.0 password based
encryption.
- `passphrase`: {string} The passphrase to use for encryption, see `cipher`.
* Returns: {Object}
- `publicKey`: {string|Buffer}
- `privateKey`: {string|Buffer}

Generates a new asymmetric key pair of the given `type`. Only RSA, DSA and EC
are currently supported.

It is recommended to encode public keys as `'spki'` and private keys as
`'pkcs8'` with encryption:

```js
const { generateKeyPairSync } = require('crypto');
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: 'top secret'
}
});
```

The return value `{ publicKey, privateKey }` represents the generated key pair.
When PEM encoding was selected, the respective key will be a string, otherwise
it will be a buffer containing the data encoded as DER.

### crypto.getCiphers()
<!-- YAML
added: v0.9.3
Expand Down
5 changes: 5 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,11 @@ be called no more than one time per instance of a `Hash` object.

[`hash.update()`][] failed for any reason. This should rarely, if ever, happen.

<a id="ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS"></a>
### ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS

The selected public or private key encoding is incompatible with other options.

<a id="ERR_CRYPTO_INVALID_DIGEST"></a>
### ERR_CRYPTO_INVALID_DIGEST

Expand Down
6 changes: 6 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ const {
scrypt,
scryptSync
} = require('internal/crypto/scrypt');
const {
generateKeyPair,
generateKeyPairSync
} = require('internal/crypto/keygen');
const {
DiffieHellman,
DiffieHellmanGroup,
Expand Down Expand Up @@ -152,6 +156,8 @@ module.exports = exports = {
getHashes,
pbkdf2,
pbkdf2Sync,
generateKeyPair,
generateKeyPairSync,
privateDecrypt,
privateEncrypt,
publicDecrypt,
Expand Down
238 changes: 238 additions & 0 deletions lib/internal/crypto/keygen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
'use strict';

const { internalBinding } = require('internal/bootstrap/loaders');
const { AsyncWrap, Providers } = internalBinding('async_wrap');
const {
generateKeyPairRSA,
generateKeyPairDSA,
generateKeyPairEC,
OPENSSL_EC_NAMED_CURVE,
OPENSSL_EC_EXPLICIT_CURVE,
PK_ENCODING_PKCS1,
PK_ENCODING_PKCS8,
PK_ENCODING_SPKI,
PK_ENCODING_SEC1,
PK_FORMAT_DER,
PK_FORMAT_PEM
} = internalBinding('crypto');
const { isUint32 } = require('internal/validators');
const {
ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_CALLBACK,
ERR_INVALID_OPT_VALUE
} = require('internal/errors').codes;

function generateKeyPair(type, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}

const impl = check(type, options);

if (typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK();

const wrap = new AsyncWrap(Providers.KEYPAIRGENREQUEST);
wrap.ondone = (ex, pubkey, privkey) => {
if (ex) return callback.call(wrap, ex);
callback.call(wrap, null, pubkey, privkey);
};

handleError(impl, wrap);
}

function generateKeyPairSync(type, options) {
const impl = check(type, options);
return handleError(impl);
}

function handleError(impl, wrap) {
const ret = impl(wrap);
if (ret === undefined)
return; // async

const [err, publicKey, privateKey] = ret;
if (err !== undefined)
throw err;

return { publicKey, privateKey };
}

function parseKeyEncoding(keyType, options) {
const { publicKeyEncoding, privateKeyEncoding } = options;

if (publicKeyEncoding == null || typeof publicKeyEncoding !== 'object')
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding', publicKeyEncoding);

const { format: strPublicFormat, type: strPublicType } = publicKeyEncoding;

let publicType;
if (strPublicType === 'pkcs1') {
if (keyType !== 'rsa') {
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
strPublicType, 'can only be used for RSA keys');
}
publicType = PK_ENCODING_PKCS1;
} else if (strPublicType === 'spki') {
publicType = PK_ENCODING_SPKI;
} else {
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.type', strPublicType);
}

let publicFormat;
if (strPublicFormat === 'der') {
publicFormat = PK_FORMAT_DER;
} else if (strPublicFormat === 'pem') {
publicFormat = PK_FORMAT_PEM;
} else {
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.format',
strPublicFormat);
}

if (privateKeyEncoding == null || typeof privateKeyEncoding !== 'object')
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding', privateKeyEncoding);

const {
cipher,
passphrase,
format: strPrivateFormat,
type: strPrivateType
} = privateKeyEncoding;

let privateType;
if (strPrivateType === 'pkcs1') {
if (keyType !== 'rsa') {
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
strPrivateType, 'can only be used for RSA keys');
}
privateType = PK_ENCODING_PKCS1;
} else if (strPrivateType === 'pkcs8') {
privateType = PK_ENCODING_PKCS8;
} else if (strPrivateType === 'sec1') {
if (keyType !== 'ec') {
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
strPrivateType, 'can only be used for EC keys');
}
privateType = PK_ENCODING_SEC1;
} else {
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.type', strPrivateType);
}

let privateFormat;
if (strPrivateFormat === 'der') {
privateFormat = PK_FORMAT_DER;
} else if (strPrivateFormat === 'pem') {
privateFormat = PK_FORMAT_PEM;
} else {
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.format',
strPrivateFormat);
}

if (cipher != null) {
if (typeof cipher !== 'string')
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.cipher', cipher);
if (privateType !== PK_ENCODING_PKCS8) {
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
strPrivateType, 'does not support encryption');
}
if (typeof passphrase !== 'string') {
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.passphrase',
passphrase);
}
}

return {
cipher, passphrase, publicType, publicFormat, privateType, privateFormat
};
}

function check(type, options, callback) {
if (typeof type !== 'string')
throw new ERR_INVALID_ARG_TYPE('type', 'string', type);
if (options == null || typeof options !== 'object')
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);

// These will be set after parsing the type and type-specific options to make
// the order a bit more intuitive.
let cipher, passphrase, publicType, publicFormat, privateType, privateFormat;

let impl;
switch (type) {
case 'rsa':
{
const { modulusLength } = options;
if (!isUint32(modulusLength))
throw new ERR_INVALID_OPT_VALUE('modulusLength', modulusLength);

let { publicExponent } = options;
if (publicExponent == null) {
publicExponent = 0x10001;
} else if (!isUint32(publicExponent)) {
throw new ERR_INVALID_OPT_VALUE('publicExponent', publicExponent);
}

impl = (wrap) => generateKeyPairRSA(modulusLength, publicExponent,
publicType, publicFormat,
privateType, privateFormat,
cipher, passphrase, wrap);
}
break;
case 'dsa':
{
const { modulusLength } = options;
if (!isUint32(modulusLength))
throw new ERR_INVALID_OPT_VALUE('modulusLength', modulusLength);

let { divisorLength } = options;
if (divisorLength == null) {
divisorLength = -1;
} else if (!isUint32(divisorLength)) {
throw new ERR_INVALID_OPT_VALUE('divisorLength', divisorLength);
}

impl = (wrap) => generateKeyPairDSA(modulusLength, divisorLength,
publicType, publicFormat,
privateType, privateFormat,
cipher, passphrase, wrap);
}
break;
case 'ec':
{
const { namedCurve } = options;
if (typeof namedCurve !== 'string')
throw new ERR_INVALID_OPT_VALUE('namedCurve', namedCurve);
let { paramEncoding } = options;
if (paramEncoding == null || paramEncoding === 'named')
paramEncoding = OPENSSL_EC_NAMED_CURVE;
else if (paramEncoding === 'explicit')
paramEncoding = OPENSSL_EC_EXPLICIT_CURVE;
else
throw new ERR_INVALID_OPT_VALUE('paramEncoding', paramEncoding);

impl = (wrap) => generateKeyPairEC(namedCurve, paramEncoding,
publicType, publicFormat,
privateType, privateFormat,
cipher, passphrase, wrap);
}
break;
default:
throw new ERR_INVALID_ARG_VALUE('type', type,
"must be one of 'rsa', 'dsa', 'ec'");
}

({
cipher,
passphrase,
publicType,
publicFormat,
privateType,
privateFormat
} = parseKeyEncoding(type, options));

return impl;
}

module.exports = { generateKeyPair, generateKeyPairSync };
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,8 @@ E('ERR_CRYPTO_HASH_DIGEST_NO_UTF16', 'hash.digest() does not support UTF-16',
Error);
E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error);
E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', 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_STATE', 'Invalid state for operation %s', Error);
E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error);
Expand Down
Loading

0 comments on commit 8c502f5

Please sign in to comment.