Skip to content

Commit 3b53df0

Browse files
tniessenMylesBorins
authored andcommitted
crypto: add key object API
This commit makes multiple important changes: 1. A new key object API is introduced. The KeyObject class itself is not exposed to users, instead, several new APIs can be used to construct key objects: createSecretKey, createPrivateKey and createPublicKey. The new API also allows to convert between different key formats, and even though the API itself is not compatible to the WebCrypto standard in any way, it makes interoperability much simpler. 2. Key objects can be used instead of the raw key material in all relevant crypto APIs. 3. The handling of asymmetric keys has been unified and greatly improved. Node.js now fully supports both PEM-encoded and DER-encoded public and private keys. 4. Conversions between buffers and strings have been moved to native code for sensitive data such as symmetric keys due to security considerations such as zeroing temporary buffers. 5. For compatibility with older versions of the crypto API, this change allows to specify Buffers and strings as the "passphrase" option when reading or writing an encoded key. Note that this can result in unexpected behavior if the password contains a null byte. PR-URL: #24234 Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent b78d487 commit 3b53df0

21 files changed

+1998
-653
lines changed

doc/api/crypto.md

+218-60
Large diffs are not rendered by default.

doc/api/errors.md

+5
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,11 @@ The selected public or private key encoding is incompatible with other options.
763763

764764
An invalid [crypto digest algorithm][] was specified.
765765

766+
<a id="ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE"></a>
767+
### ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE
768+
769+
The given crypto key object's type is invalid for the attempted operation.
770+
766771
<a id="ERR_CRYPTO_INVALID_STATE"></a>
767772
### ERR_CRYPTO_INVALID_STATE
768773

lib/crypto.js

+8
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ const {
5959
generateKeyPair,
6060
generateKeyPairSync
6161
} = require('internal/crypto/keygen');
62+
const {
63+
createSecretKey,
64+
createPublicKey,
65+
createPrivateKey
66+
} = require('internal/crypto/keys');
6267
const {
6368
DiffieHellman,
6469
DiffieHellmanGroup,
@@ -149,6 +154,9 @@ module.exports = exports = {
149154
createECDH,
150155
createHash,
151156
createHmac,
157+
createPrivateKey,
158+
createPublicKey,
159+
createSecretKey,
152160
createSign,
153161
createVerify,
154162
getCiphers,

lib/internal/crypto/cipher.js

+20-13
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const {
1212
} = require('internal/errors').codes;
1313
const { validateString } = require('internal/validators');
1414

15+
const {
16+
preparePrivateKey,
17+
preparePublicOrPrivateKey,
18+
prepareSecretKey
19+
} = require('internal/crypto/keys');
1520
const {
1621
getDefaultEncoding,
1722
kHandle,
@@ -38,19 +43,25 @@ const { deprecate, normalizeEncoding } = require('internal/util');
3843
// Lazy loaded for startup performance.
3944
let StringDecoder;
4045

41-
function rsaFunctionFor(method, defaultPadding) {
46+
function rsaFunctionFor(method, defaultPadding, keyType) {
4247
return (options, buffer) => {
43-
const key = options.key || options;
48+
const { format, type, data, passphrase } =
49+
keyType === 'private' ?
50+
preparePrivateKey(options) :
51+
preparePublicOrPrivateKey(options);
4452
const padding = options.padding || defaultPadding;
45-
const passphrase = options.passphrase || null;
46-
return method(toBuf(key), buffer, padding, passphrase);
53+
return method(data, format, type, passphrase, buffer, padding);
4754
};
4855
}
4956

50-
const publicEncrypt = rsaFunctionFor(_publicEncrypt, RSA_PKCS1_OAEP_PADDING);
51-
const publicDecrypt = rsaFunctionFor(_publicDecrypt, RSA_PKCS1_PADDING);
52-
const privateEncrypt = rsaFunctionFor(_privateEncrypt, RSA_PKCS1_PADDING);
53-
const privateDecrypt = rsaFunctionFor(_privateDecrypt, RSA_PKCS1_OAEP_PADDING);
57+
const publicEncrypt = rsaFunctionFor(_publicEncrypt, RSA_PKCS1_OAEP_PADDING,
58+
'public');
59+
const publicDecrypt = rsaFunctionFor(_publicDecrypt, RSA_PKCS1_PADDING,
60+
'private');
61+
const privateEncrypt = rsaFunctionFor(_privateEncrypt, RSA_PKCS1_PADDING,
62+
'private');
63+
const privateDecrypt = rsaFunctionFor(_privateDecrypt, RSA_PKCS1_OAEP_PADDING,
64+
'public');
5465

5566
function getDecoder(decoder, encoding) {
5667
encoding = normalizeEncoding(encoding);
@@ -105,11 +116,7 @@ function createCipher(cipher, password, options, decipher) {
105116

106117
function createCipherWithIV(cipher, key, options, decipher, iv) {
107118
validateString(cipher, 'cipher');
108-
key = toBuf(key);
109-
if (!isArrayBufferView(key)) {
110-
throw invalidArrayBufferView('key', key);
111-
}
112-
119+
key = prepareSecretKey(key);
113120
iv = toBuf(iv);
114121
if (iv !== null && !isArrayBufferView(iv)) {
115122
throw invalidArrayBufferView('iv', iv);

lib/internal/crypto/hash.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const {
1212
toBuf
1313
} = require('internal/crypto/util');
1414

15+
const {
16+
prepareSecretKey
17+
} = require('internal/crypto/keys');
18+
1519
const { Buffer } = require('buffer');
1620

1721
const {
@@ -88,10 +92,7 @@ function Hmac(hmac, key, options) {
8892
if (!(this instanceof Hmac))
8993
return new Hmac(hmac, key, options);
9094
validateString(hmac, 'hmac');
91-
if (typeof key !== 'string' && !isArrayBufferView(key)) {
92-
throw new ERR_INVALID_ARG_TYPE('key',
93-
['string', 'TypedArray', 'DataView'], key);
94-
}
95+
key = prepareSecretKey(key);
9596
this[kHandle] = new _Hmac();
9697
this[kHandle].init(hmac, toBuf(key));
9798
this[kState] = {

lib/internal/crypto/keygen.js

+46-89
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,32 @@ const {
66
generateKeyPairDSA,
77
generateKeyPairEC,
88
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
9+
OPENSSL_EC_EXPLICIT_CURVE
1610
} = internalBinding('crypto');
11+
const {
12+
parsePublicKeyEncoding,
13+
parsePrivateKeyEncoding,
14+
15+
PublicKeyObject,
16+
PrivateKeyObject
17+
} = require('internal/crypto/keys');
1718
const { customPromisifyArgs } = require('internal/util');
1819
const { isUint32, validateString } = require('internal/validators');
1920
const {
20-
ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS,
2121
ERR_INVALID_ARG_TYPE,
2222
ERR_INVALID_ARG_VALUE,
2323
ERR_INVALID_CALLBACK,
2424
ERR_INVALID_OPT_VALUE
2525
} = require('internal/errors').codes;
2626

27+
const { isArrayBufferView } = require('internal/util/types');
28+
29+
function wrapKey(key, ctor) {
30+
if (typeof key === 'string' || isArrayBufferView(key))
31+
return key;
32+
return new ctor(key);
33+
}
34+
2735
function generateKeyPair(type, options, callback) {
2836
if (typeof options === 'function') {
2937
callback = options;
@@ -38,6 +46,9 @@ function generateKeyPair(type, options, callback) {
3846
const wrap = new AsyncWrap(Providers.KEYPAIRGENREQUEST);
3947
wrap.ondone = (ex, pubkey, privkey) => {
4048
if (ex) return callback.call(wrap, ex);
49+
// If no encoding was chosen, return key objects instead.
50+
pubkey = wrapKey(pubkey, PublicKeyObject);
51+
privkey = wrapKey(privkey, PrivateKeyObject);
4152
callback.call(wrap, null, pubkey, privkey);
4253
};
4354

@@ -69,86 +80,32 @@ function handleError(impl, wrap) {
6980
function parseKeyEncoding(keyType, options) {
7081
const { publicKeyEncoding, privateKeyEncoding } = options;
7182

72-
if (publicKeyEncoding == null || typeof publicKeyEncoding !== 'object')
73-
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding', publicKeyEncoding);
74-
75-
const { format: strPublicFormat, type: strPublicType } = publicKeyEncoding;
76-
77-
let publicType;
78-
if (strPublicType === 'pkcs1') {
79-
if (keyType !== 'rsa') {
80-
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
81-
strPublicType, 'can only be used for RSA keys');
82-
}
83-
publicType = PK_ENCODING_PKCS1;
84-
} else if (strPublicType === 'spki') {
85-
publicType = PK_ENCODING_SPKI;
83+
let publicFormat, publicType;
84+
if (publicKeyEncoding == null) {
85+
publicFormat = publicType = undefined;
86+
} else if (typeof publicKeyEncoding === 'object') {
87+
({
88+
format: publicFormat,
89+
type: publicType
90+
} = parsePublicKeyEncoding(publicKeyEncoding, keyType,
91+
'publicKeyEncoding'));
8692
} else {
87-
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.type', strPublicType);
93+
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding', publicKeyEncoding);
8894
}
8995

90-
let publicFormat;
91-
if (strPublicFormat === 'der') {
92-
publicFormat = PK_FORMAT_DER;
93-
} else if (strPublicFormat === 'pem') {
94-
publicFormat = PK_FORMAT_PEM;
96+
let privateFormat, privateType, cipher, passphrase;
97+
if (privateKeyEncoding == null) {
98+
privateFormat = privateType = undefined;
99+
} else if (typeof privateKeyEncoding === 'object') {
100+
({
101+
format: privateFormat,
102+
type: privateType,
103+
cipher,
104+
passphrase
105+
} = parsePrivateKeyEncoding(privateKeyEncoding, keyType,
106+
'privateKeyEncoding'));
95107
} else {
96-
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.format',
97-
strPublicFormat);
98-
}
99-
100-
if (privateKeyEncoding == null || typeof privateKeyEncoding !== 'object')
101108
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding', privateKeyEncoding);
102-
103-
const {
104-
cipher,
105-
passphrase,
106-
format: strPrivateFormat,
107-
type: strPrivateType
108-
} = privateKeyEncoding;
109-
110-
let privateType;
111-
if (strPrivateType === 'pkcs1') {
112-
if (keyType !== 'rsa') {
113-
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
114-
strPrivateType, 'can only be used for RSA keys');
115-
}
116-
privateType = PK_ENCODING_PKCS1;
117-
} else if (strPrivateType === 'pkcs8') {
118-
privateType = PK_ENCODING_PKCS8;
119-
} else if (strPrivateType === 'sec1') {
120-
if (keyType !== 'ec') {
121-
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
122-
strPrivateType, 'can only be used for EC keys');
123-
}
124-
privateType = PK_ENCODING_SEC1;
125-
} else {
126-
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.type', strPrivateType);
127-
}
128-
129-
let privateFormat;
130-
if (strPrivateFormat === 'der') {
131-
privateFormat = PK_FORMAT_DER;
132-
} else if (strPrivateFormat === 'pem') {
133-
privateFormat = PK_FORMAT_PEM;
134-
} else {
135-
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.format',
136-
strPrivateFormat);
137-
}
138-
139-
if (cipher != null) {
140-
if (typeof cipher !== 'string')
141-
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.cipher', cipher);
142-
if (privateFormat === PK_FORMAT_DER &&
143-
(privateType === PK_ENCODING_PKCS1 ||
144-
privateType === PK_ENCODING_SEC1)) {
145-
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
146-
strPrivateType, 'does not support encryption');
147-
}
148-
if (typeof passphrase !== 'string') {
149-
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.passphrase',
150-
passphrase);
151-
}
152109
}
153110

154111
return {
@@ -181,8 +138,8 @@ function check(type, options, callback) {
181138
}
182139

183140
impl = (wrap) => generateKeyPairRSA(modulusLength, publicExponent,
184-
publicType, publicFormat,
185-
privateType, privateFormat,
141+
publicFormat, publicType,
142+
privateFormat, privateType,
186143
cipher, passphrase, wrap);
187144
}
188145
break;
@@ -200,8 +157,8 @@ function check(type, options, callback) {
200157
}
201158

202159
impl = (wrap) => generateKeyPairDSA(modulusLength, divisorLength,
203-
publicType, publicFormat,
204-
privateType, privateFormat,
160+
publicFormat, publicType,
161+
privateFormat, privateType,
205162
cipher, passphrase, wrap);
206163
}
207164
break;
@@ -219,8 +176,8 @@ function check(type, options, callback) {
219176
throw new ERR_INVALID_OPT_VALUE('paramEncoding', paramEncoding);
220177

221178
impl = (wrap) => generateKeyPairEC(namedCurve, paramEncoding,
222-
publicType, publicFormat,
223-
privateType, privateFormat,
179+
publicFormat, publicType,
180+
privateFormat, privateType,
224181
cipher, passphrase, wrap);
225182
}
226183
break;

0 commit comments

Comments
 (0)