Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

Commit 8c502f5

Browse files
committed
crypto: add API for key pair generation
This adds support for RSA, DSA and EC key pair generation with a variety of possible output formats etc. PR-URL: nodejs/node#22660 Fixes: nodejs/node#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>
1 parent df9abb6 commit 8c502f5

File tree

10 files changed

+1457
-0
lines changed

10 files changed

+1457
-0
lines changed

doc/api/crypto.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,6 +1673,116 @@ Use [`crypto.getHashes()`][] to obtain an array of names of the available
16731673
signing algorithms. Optional `options` argument controls the
16741674
`stream.Writable` behavior.
16751675

1676+
### crypto.generateKeyPair(type, options, callback)
1677+
<!-- YAML
1678+
added: REPLACEME
1679+
-->
1680+
* `type`: {string} Must be `'rsa'`, `'dsa'` or `'ec'`.
1681+
* `options`: {Object}
1682+
- `modulusLength`: {number} Key size in bits (RSA, DSA).
1683+
- `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`.
1684+
- `divisorLength`: {number} Size of `q` in bits (DSA).
1685+
- `namedCurve`: {string} Name of the curve to use (EC).
1686+
- `publicKeyEncoding`: {Object}
1687+
- `type`: {string} Must be one of `'pkcs1'` (RSA only) or `'spki'`.
1688+
- `format`: {string} Must be `'pem'` or `'der'`.
1689+
- `privateKeyEncoding`: {Object}
1690+
- `type`: {string} Must be one of `'pkcs1'` (RSA only), `'pkcs8'` or
1691+
`'sec1'` (EC only).
1692+
- `format`: {string} Must be `'pem'` or `'der'`.
1693+
- `cipher`: {string} If specified, the private key will be encrypted with
1694+
the given `cipher` and `passphrase` using PKCS#5 v2.0 password based
1695+
encryption.
1696+
- `passphrase`: {string} The passphrase to use for encryption, see `cipher`.
1697+
* `callback`: {Function}
1698+
- `err`: {Error}
1699+
- `publicKey`: {string|Buffer}
1700+
- `privateKey`: {string|Buffer}
1701+
1702+
Generates a new asymmetric key pair of the given `type`. Only RSA, DSA and EC
1703+
are currently supported.
1704+
1705+
It is recommended to encode public keys as `'spki'` and private keys as
1706+
`'pkcs8'` with encryption:
1707+
1708+
```js
1709+
const { generateKeyPair } = require('crypto');
1710+
generateKeyPair('rsa', {
1711+
modulusLength: 4096,
1712+
publicKeyEncoding: {
1713+
type: 'spki',
1714+
format: 'pem'
1715+
},
1716+
privateKeyEncoding: {
1717+
type: 'pkcs8',
1718+
format: 'pem',
1719+
cipher: 'aes-256-cbc',
1720+
passphrase: 'top secret'
1721+
}
1722+
}, (err, publicKey, privateKey) => {
1723+
// Handle errors and use the generated key pair.
1724+
});
1725+
```
1726+
1727+
On completion, `callback` will be called with `err` set to `undefined` and
1728+
`publicKey` / `privateKey` representing the generated key pair. When PEM
1729+
encoding was selected, the result will be a string, otherwise it will be a
1730+
buffer containing the data encoded as DER. Note that Node.js itself does not
1731+
accept DER, it is supported for interoperability with other libraries such as
1732+
WebCrypto only.
1733+
1734+
### crypto.generateKeyPairSync(type, options)
1735+
<!-- YAML
1736+
added: REPLACEME
1737+
-->
1738+
* `type`: {string} Must be `'rsa'`, `'dsa'` or `'ec'`.
1739+
* `options`: {Object}
1740+
- `modulusLength`: {number} Key size in bits (RSA, DSA).
1741+
- `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`.
1742+
- `divisorLength`: {number} Size of `q` in bits (DSA).
1743+
- `namedCurve`: {string} Name of the curve to use (EC).
1744+
- `publicKeyEncoding`: {Object}
1745+
- `type`: {string} Must be one of `'pkcs1'` (RSA only) or `'spki'`.
1746+
- `format`: {string} Must be `'pem'` or `'der'`.
1747+
- `privateKeyEncoding`: {Object}
1748+
- `type`: {string} Must be one of `'pkcs1'` (RSA only), `'pkcs8'` or
1749+
`'sec1'` (EC only).
1750+
- `format`: {string} Must be `'pem'` or `'der'`.
1751+
- `cipher`: {string} If specified, the private key will be encrypted with
1752+
the given `cipher` and `passphrase` using PKCS#5 v2.0 password based
1753+
encryption.
1754+
- `passphrase`: {string} The passphrase to use for encryption, see `cipher`.
1755+
* Returns: {Object}
1756+
- `publicKey`: {string|Buffer}
1757+
- `privateKey`: {string|Buffer}
1758+
1759+
Generates a new asymmetric key pair of the given `type`. Only RSA, DSA and EC
1760+
are currently supported.
1761+
1762+
It is recommended to encode public keys as `'spki'` and private keys as
1763+
`'pkcs8'` with encryption:
1764+
1765+
```js
1766+
const { generateKeyPairSync } = require('crypto');
1767+
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
1768+
modulusLength: 4096,
1769+
publicKeyEncoding: {
1770+
type: 'spki',
1771+
format: 'pem'
1772+
},
1773+
privateKeyEncoding: {
1774+
type: 'pkcs8',
1775+
format: 'pem',
1776+
cipher: 'aes-256-cbc',
1777+
passphrase: 'top secret'
1778+
}
1779+
});
1780+
```
1781+
1782+
The return value `{ publicKey, privateKey }` represents the generated key pair.
1783+
When PEM encoding was selected, the respective key will be a string, otherwise
1784+
it will be a buffer containing the data encoded as DER.
1785+
16761786
### crypto.getCiphers()
16771787
<!-- YAML
16781788
added: v0.9.3

doc/api/errors.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,11 @@ be called no more than one time per instance of a `Hash` object.
729729

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

732+
<a id="ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS"></a>
733+
### ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS
734+
735+
The selected public or private key encoding is incompatible with other options.
736+
732737
<a id="ERR_CRYPTO_INVALID_DIGEST"></a>
733738
### ERR_CRYPTO_INVALID_DIGEST
734739

lib/crypto.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ const {
5454
scrypt,
5555
scryptSync
5656
} = require('internal/crypto/scrypt');
57+
const {
58+
generateKeyPair,
59+
generateKeyPairSync
60+
} = require('internal/crypto/keygen');
5761
const {
5862
DiffieHellman,
5963
DiffieHellmanGroup,
@@ -152,6 +156,8 @@ module.exports = exports = {
152156
getHashes,
153157
pbkdf2,
154158
pbkdf2Sync,
159+
generateKeyPair,
160+
generateKeyPairSync,
155161
privateDecrypt,
156162
privateEncrypt,
157163
publicDecrypt,

lib/internal/crypto/keygen.js

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
'use strict';
2+
3+
const { internalBinding } = require('internal/bootstrap/loaders');
4+
const { AsyncWrap, Providers } = internalBinding('async_wrap');
5+
const {
6+
generateKeyPairRSA,
7+
generateKeyPairDSA,
8+
generateKeyPairEC,
9+
OPENSSL_EC_NAMED_CURVE,
10+
OPENSSL_EC_EXPLICIT_CURVE,
11+
PK_ENCODING_PKCS1,
12+
PK_ENCODING_PKCS8,
13+
PK_ENCODING_SPKI,
14+
PK_ENCODING_SEC1,
15+
PK_FORMAT_DER,
16+
PK_FORMAT_PEM
17+
} = internalBinding('crypto');
18+
const { isUint32 } = require('internal/validators');
19+
const {
20+
ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS,
21+
ERR_INVALID_ARG_TYPE,
22+
ERR_INVALID_ARG_VALUE,
23+
ERR_INVALID_CALLBACK,
24+
ERR_INVALID_OPT_VALUE
25+
} = require('internal/errors').codes;
26+
27+
function generateKeyPair(type, options, callback) {
28+
if (typeof options === 'function') {
29+
callback = options;
30+
options = undefined;
31+
}
32+
33+
const impl = check(type, options);
34+
35+
if (typeof callback !== 'function')
36+
throw new ERR_INVALID_CALLBACK();
37+
38+
const wrap = new AsyncWrap(Providers.KEYPAIRGENREQUEST);
39+
wrap.ondone = (ex, pubkey, privkey) => {
40+
if (ex) return callback.call(wrap, ex);
41+
callback.call(wrap, null, pubkey, privkey);
42+
};
43+
44+
handleError(impl, wrap);
45+
}
46+
47+
function generateKeyPairSync(type, options) {
48+
const impl = check(type, options);
49+
return handleError(impl);
50+
}
51+
52+
function handleError(impl, wrap) {
53+
const ret = impl(wrap);
54+
if (ret === undefined)
55+
return; // async
56+
57+
const [err, publicKey, privateKey] = ret;
58+
if (err !== undefined)
59+
throw err;
60+
61+
return { publicKey, privateKey };
62+
}
63+
64+
function parseKeyEncoding(keyType, options) {
65+
const { publicKeyEncoding, privateKeyEncoding } = options;
66+
67+
if (publicKeyEncoding == null || typeof publicKeyEncoding !== 'object')
68+
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding', publicKeyEncoding);
69+
70+
const { format: strPublicFormat, type: strPublicType } = publicKeyEncoding;
71+
72+
let publicType;
73+
if (strPublicType === 'pkcs1') {
74+
if (keyType !== 'rsa') {
75+
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
76+
strPublicType, 'can only be used for RSA keys');
77+
}
78+
publicType = PK_ENCODING_PKCS1;
79+
} else if (strPublicType === 'spki') {
80+
publicType = PK_ENCODING_SPKI;
81+
} else {
82+
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.type', strPublicType);
83+
}
84+
85+
let publicFormat;
86+
if (strPublicFormat === 'der') {
87+
publicFormat = PK_FORMAT_DER;
88+
} else if (strPublicFormat === 'pem') {
89+
publicFormat = PK_FORMAT_PEM;
90+
} else {
91+
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.format',
92+
strPublicFormat);
93+
}
94+
95+
if (privateKeyEncoding == null || typeof privateKeyEncoding !== 'object')
96+
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding', privateKeyEncoding);
97+
98+
const {
99+
cipher,
100+
passphrase,
101+
format: strPrivateFormat,
102+
type: strPrivateType
103+
} = privateKeyEncoding;
104+
105+
let privateType;
106+
if (strPrivateType === 'pkcs1') {
107+
if (keyType !== 'rsa') {
108+
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
109+
strPrivateType, 'can only be used for RSA keys');
110+
}
111+
privateType = PK_ENCODING_PKCS1;
112+
} else if (strPrivateType === 'pkcs8') {
113+
privateType = PK_ENCODING_PKCS8;
114+
} else if (strPrivateType === 'sec1') {
115+
if (keyType !== 'ec') {
116+
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
117+
strPrivateType, 'can only be used for EC keys');
118+
}
119+
privateType = PK_ENCODING_SEC1;
120+
} else {
121+
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.type', strPrivateType);
122+
}
123+
124+
let privateFormat;
125+
if (strPrivateFormat === 'der') {
126+
privateFormat = PK_FORMAT_DER;
127+
} else if (strPrivateFormat === 'pem') {
128+
privateFormat = PK_FORMAT_PEM;
129+
} else {
130+
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.format',
131+
strPrivateFormat);
132+
}
133+
134+
if (cipher != null) {
135+
if (typeof cipher !== 'string')
136+
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.cipher', cipher);
137+
if (privateType !== PK_ENCODING_PKCS8) {
138+
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
139+
strPrivateType, 'does not support encryption');
140+
}
141+
if (typeof passphrase !== 'string') {
142+
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.passphrase',
143+
passphrase);
144+
}
145+
}
146+
147+
return {
148+
cipher, passphrase, publicType, publicFormat, privateType, privateFormat
149+
};
150+
}
151+
152+
function check(type, options, callback) {
153+
if (typeof type !== 'string')
154+
throw new ERR_INVALID_ARG_TYPE('type', 'string', type);
155+
if (options == null || typeof options !== 'object')
156+
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
157+
158+
// These will be set after parsing the type and type-specific options to make
159+
// the order a bit more intuitive.
160+
let cipher, passphrase, publicType, publicFormat, privateType, privateFormat;
161+
162+
let impl;
163+
switch (type) {
164+
case 'rsa':
165+
{
166+
const { modulusLength } = options;
167+
if (!isUint32(modulusLength))
168+
throw new ERR_INVALID_OPT_VALUE('modulusLength', modulusLength);
169+
170+
let { publicExponent } = options;
171+
if (publicExponent == null) {
172+
publicExponent = 0x10001;
173+
} else if (!isUint32(publicExponent)) {
174+
throw new ERR_INVALID_OPT_VALUE('publicExponent', publicExponent);
175+
}
176+
177+
impl = (wrap) => generateKeyPairRSA(modulusLength, publicExponent,
178+
publicType, publicFormat,
179+
privateType, privateFormat,
180+
cipher, passphrase, wrap);
181+
}
182+
break;
183+
case 'dsa':
184+
{
185+
const { modulusLength } = options;
186+
if (!isUint32(modulusLength))
187+
throw new ERR_INVALID_OPT_VALUE('modulusLength', modulusLength);
188+
189+
let { divisorLength } = options;
190+
if (divisorLength == null) {
191+
divisorLength = -1;
192+
} else if (!isUint32(divisorLength)) {
193+
throw new ERR_INVALID_OPT_VALUE('divisorLength', divisorLength);
194+
}
195+
196+
impl = (wrap) => generateKeyPairDSA(modulusLength, divisorLength,
197+
publicType, publicFormat,
198+
privateType, privateFormat,
199+
cipher, passphrase, wrap);
200+
}
201+
break;
202+
case 'ec':
203+
{
204+
const { namedCurve } = options;
205+
if (typeof namedCurve !== 'string')
206+
throw new ERR_INVALID_OPT_VALUE('namedCurve', namedCurve);
207+
let { paramEncoding } = options;
208+
if (paramEncoding == null || paramEncoding === 'named')
209+
paramEncoding = OPENSSL_EC_NAMED_CURVE;
210+
else if (paramEncoding === 'explicit')
211+
paramEncoding = OPENSSL_EC_EXPLICIT_CURVE;
212+
else
213+
throw new ERR_INVALID_OPT_VALUE('paramEncoding', paramEncoding);
214+
215+
impl = (wrap) => generateKeyPairEC(namedCurve, paramEncoding,
216+
publicType, publicFormat,
217+
privateType, privateFormat,
218+
cipher, passphrase, wrap);
219+
}
220+
break;
221+
default:
222+
throw new ERR_INVALID_ARG_VALUE('type', type,
223+
"must be one of 'rsa', 'dsa', 'ec'");
224+
}
225+
226+
({
227+
cipher,
228+
passphrase,
229+
publicType,
230+
publicFormat,
231+
privateType,
232+
privateFormat
233+
} = parseKeyEncoding(type, options));
234+
235+
return impl;
236+
}
237+
238+
module.exports = { generateKeyPair, generateKeyPairSync };

lib/internal/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,8 @@ E('ERR_CRYPTO_HASH_DIGEST_NO_UTF16', 'hash.digest() does not support UTF-16',
509509
Error);
510510
E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error);
511511
E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error);
512+
E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.',
513+
Error);
512514
E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError);
513515
E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error);
514516
E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error);

0 commit comments

Comments
 (0)