Skip to content

Commit 4219093

Browse files
tniessentargos
authored andcommitted
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: #22660 Fixes: #15116 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ujjwal Sharma <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]>
1 parent a7f4d5e commit 4219093

File tree

10 files changed

+1456
-0
lines changed

10 files changed

+1456
-0
lines changed

doc/api/crypto.md

+110
Original file line numberDiff line numberDiff line change
@@ -1695,6 +1695,116 @@ Use [`crypto.getHashes()`][] to obtain an array of names of the available
16951695
signing algorithms. Optional `options` argument controls the
16961696
`stream.Writable` behavior.
16971697

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

doc/api/errors.md

+5
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

+6
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ const {
5656
scrypt,
5757
scryptSync
5858
} = require('internal/crypto/scrypt');
59+
const {
60+
generateKeyPair,
61+
generateKeyPairSync
62+
} = require('internal/crypto/keygen');
5963
const {
6064
DiffieHellman,
6165
DiffieHellmanGroup,
@@ -157,6 +161,8 @@ module.exports = exports = {
157161
getHashes,
158162
pbkdf2,
159163
pbkdf2Sync,
164+
generateKeyPair,
165+
generateKeyPairSync,
160166
privateDecrypt,
161167
privateEncrypt,
162168
prng: randomBytes,

lib/internal/crypto/keygen.js

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

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,8 @@ E('ERR_CRYPTO_HASH_DIGEST_NO_UTF16', 'hash.digest() does not support UTF-16',
511511
Error);
512512
E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error);
513513
E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error);
514+
E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.',
515+
Error);
514516
E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError);
515517
E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error);
516518
E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error);

0 commit comments

Comments
 (0)