Skip to content

Commit

Permalink
crypto: add key object API
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
tniessen authored and MylesBorins committed Dec 26, 2018
1 parent b78d487 commit 3b53df0
Show file tree
Hide file tree
Showing 21 changed files with 1,998 additions and 653 deletions.
278 changes: 218 additions & 60 deletions doc/api/crypto.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,11 @@ The selected public or private key encoding is incompatible with other options.

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

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

The given crypto key object's type is invalid for the attempted operation.

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

Expand Down
8 changes: 8 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ const {
generateKeyPair,
generateKeyPairSync
} = require('internal/crypto/keygen');
const {
createSecretKey,
createPublicKey,
createPrivateKey
} = require('internal/crypto/keys');
const {
DiffieHellman,
DiffieHellmanGroup,
Expand Down Expand Up @@ -149,6 +154,9 @@ module.exports = exports = {
createECDH,
createHash,
createHmac,
createPrivateKey,
createPublicKey,
createSecretKey,
createSign,
createVerify,
getCiphers,
Expand Down
33 changes: 20 additions & 13 deletions lib/internal/crypto/cipher.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const {
} = require('internal/errors').codes;
const { validateString } = require('internal/validators');

const {
preparePrivateKey,
preparePublicOrPrivateKey,
prepareSecretKey
} = require('internal/crypto/keys');
const {
getDefaultEncoding,
kHandle,
Expand All @@ -38,19 +43,25 @@ const { deprecate, normalizeEncoding } = require('internal/util');
// Lazy loaded for startup performance.
let StringDecoder;

function rsaFunctionFor(method, defaultPadding) {
function rsaFunctionFor(method, defaultPadding, keyType) {
return (options, buffer) => {
const key = options.key || options;
const { format, type, data, passphrase } =
keyType === 'private' ?
preparePrivateKey(options) :
preparePublicOrPrivateKey(options);
const padding = options.padding || defaultPadding;
const passphrase = options.passphrase || null;
return method(toBuf(key), buffer, padding, passphrase);
return method(data, format, type, passphrase, buffer, padding);
};
}

const publicEncrypt = rsaFunctionFor(_publicEncrypt, RSA_PKCS1_OAEP_PADDING);
const publicDecrypt = rsaFunctionFor(_publicDecrypt, RSA_PKCS1_PADDING);
const privateEncrypt = rsaFunctionFor(_privateEncrypt, RSA_PKCS1_PADDING);
const privateDecrypt = rsaFunctionFor(_privateDecrypt, RSA_PKCS1_OAEP_PADDING);
const publicEncrypt = rsaFunctionFor(_publicEncrypt, RSA_PKCS1_OAEP_PADDING,
'public');
const publicDecrypt = rsaFunctionFor(_publicDecrypt, RSA_PKCS1_PADDING,
'private');
const privateEncrypt = rsaFunctionFor(_privateEncrypt, RSA_PKCS1_PADDING,
'private');
const privateDecrypt = rsaFunctionFor(_privateDecrypt, RSA_PKCS1_OAEP_PADDING,
'public');

function getDecoder(decoder, encoding) {
encoding = normalizeEncoding(encoding);
Expand Down Expand Up @@ -105,11 +116,7 @@ function createCipher(cipher, password, options, decipher) {

function createCipherWithIV(cipher, key, options, decipher, iv) {
validateString(cipher, 'cipher');
key = toBuf(key);
if (!isArrayBufferView(key)) {
throw invalidArrayBufferView('key', key);
}

key = prepareSecretKey(key);
iv = toBuf(iv);
if (iv !== null && !isArrayBufferView(iv)) {
throw invalidArrayBufferView('iv', iv);
Expand Down
9 changes: 5 additions & 4 deletions lib/internal/crypto/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const {
toBuf
} = require('internal/crypto/util');

const {
prepareSecretKey
} = require('internal/crypto/keys');

const { Buffer } = require('buffer');

const {
Expand Down Expand Up @@ -88,10 +92,7 @@ function Hmac(hmac, key, options) {
if (!(this instanceof Hmac))
return new Hmac(hmac, key, options);
validateString(hmac, 'hmac');
if (typeof key !== 'string' && !isArrayBufferView(key)) {
throw new ERR_INVALID_ARG_TYPE('key',
['string', 'TypedArray', 'DataView'], key);
}
key = prepareSecretKey(key);
this[kHandle] = new _Hmac();
this[kHandle].init(hmac, toBuf(key));
this[kState] = {
Expand Down
135 changes: 46 additions & 89 deletions lib/internal/crypto/keygen.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,32 @@ const {
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
OPENSSL_EC_EXPLICIT_CURVE
} = internalBinding('crypto');
const {
parsePublicKeyEncoding,
parsePrivateKeyEncoding,

PublicKeyObject,
PrivateKeyObject
} = require('internal/crypto/keys');
const { customPromisifyArgs } = require('internal/util');
const { isUint32, validateString } = 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;

const { isArrayBufferView } = require('internal/util/types');

function wrapKey(key, ctor) {
if (typeof key === 'string' || isArrayBufferView(key))
return key;
return new ctor(key);
}

function generateKeyPair(type, options, callback) {
if (typeof options === 'function') {
callback = options;
Expand All @@ -38,6 +46,9 @@ function generateKeyPair(type, options, callback) {
const wrap = new AsyncWrap(Providers.KEYPAIRGENREQUEST);
wrap.ondone = (ex, pubkey, privkey) => {
if (ex) return callback.call(wrap, ex);
// If no encoding was chosen, return key objects instead.
pubkey = wrapKey(pubkey, PublicKeyObject);
privkey = wrapKey(privkey, PrivateKeyObject);
callback.call(wrap, null, pubkey, privkey);
};

Expand Down Expand Up @@ -69,86 +80,32 @@ function handleError(impl, wrap) {
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;
let publicFormat, publicType;
if (publicKeyEncoding == null) {
publicFormat = publicType = undefined;
} else if (typeof publicKeyEncoding === 'object') {
({
format: publicFormat,
type: publicType
} = parsePublicKeyEncoding(publicKeyEncoding, keyType,
'publicKeyEncoding'));
} else {
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding.type', strPublicType);
throw new ERR_INVALID_OPT_VALUE('publicKeyEncoding', publicKeyEncoding);
}

let publicFormat;
if (strPublicFormat === 'der') {
publicFormat = PK_FORMAT_DER;
} else if (strPublicFormat === 'pem') {
publicFormat = PK_FORMAT_PEM;
let privateFormat, privateType, cipher, passphrase;
if (privateKeyEncoding == null) {
privateFormat = privateType = undefined;
} else if (typeof privateKeyEncoding === 'object') {
({
format: privateFormat,
type: privateType,
cipher,
passphrase
} = parsePrivateKeyEncoding(privateKeyEncoding, keyType,
'privateKeyEncoding'));
} 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 (privateFormat === PK_FORMAT_DER &&
(privateType === PK_ENCODING_PKCS1 ||
privateType === PK_ENCODING_SEC1)) {
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 {
Expand Down Expand Up @@ -181,8 +138,8 @@ function check(type, options, callback) {
}

impl = (wrap) => generateKeyPairRSA(modulusLength, publicExponent,
publicType, publicFormat,
privateType, privateFormat,
publicFormat, publicType,
privateFormat, privateType,
cipher, passphrase, wrap);
}
break;
Expand All @@ -200,8 +157,8 @@ function check(type, options, callback) {
}

impl = (wrap) => generateKeyPairDSA(modulusLength, divisorLength,
publicType, publicFormat,
privateType, privateFormat,
publicFormat, publicType,
privateFormat, privateType,
cipher, passphrase, wrap);
}
break;
Expand All @@ -219,8 +176,8 @@ function check(type, options, callback) {
throw new ERR_INVALID_OPT_VALUE('paramEncoding', paramEncoding);

impl = (wrap) => generateKeyPairEC(namedCurve, paramEncoding,
publicType, publicFormat,
privateType, privateFormat,
publicFormat, publicType,
privateFormat, privateType,
cipher, passphrase, wrap);
}
break;
Expand Down
Loading

0 comments on commit 3b53df0

Please sign in to comment.