crypto: refactor argument validation for pbkdf2

Move input argument validation to js, using internal/errors.

Also update docs

* `password` and `salt` may be Buffers or any TypedArrays
* `crypto.DEFAULT_ENCODING` changes the returned derivedKey type

PR-URL: https://github.com/nodejs/node/pull/15746
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Sakthipriyan Vairamani <thechargingvolcano@gmail.com>
Reviewed-By: Fedor Indutny <fedor.indutny@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
James M Snell 2017-10-02 20:56:49 -07:00
parent 4eb9365d66
commit 7124b466d9
7 changed files with 198 additions and 81 deletions

View File

@ -1587,8 +1587,8 @@ changes:
description: The default encoding for `password` if it is a string changed description: The default encoding for `password` if it is a string changed
from `binary` to `utf8`. from `binary` to `utf8`.
--> -->
- `password` {string} - `password` {string|Buffer|TypedArray}
- `salt` {string} - `salt` {string|Buffer|TypedArray}
- `iterations` {number} - `iterations` {number}
- `keylen` {number} - `keylen` {number}
- `digest` {string} - `digest` {string}
@ -1602,8 +1602,10 @@ applied to derive a key of the requested byte length (`keylen`) from the
`password`, `salt` and `iterations`. `password`, `salt` and `iterations`.
The supplied `callback` function is called with two arguments: `err` and The supplied `callback` function is called with two arguments: `err` and
`derivedKey`. If an error occurs, `err` will be set; otherwise `err` will be `derivedKey`. If an error occurs while deriving the key, `err` will be set;
null. The successfully generated `derivedKey` will be passed as a [`Buffer`][]. otherwise `err` will be null. By default, the successfully generated
`derivedKey` will be passed to the callback as a [`Buffer`][]. An error will be
thrown if any of the input arguments specify invalid values or types.
The `iterations` argument must be a number set as high as possible. The The `iterations` argument must be a number set as high as possible. The
higher the number of iterations, the more secure the derived key will be, higher the number of iterations, the more secure the derived key will be,
@ -1623,6 +1625,18 @@ crypto.pbkdf2('secret', 'salt', 100000, 64, 'sha512', (err, derivedKey) => {
}); });
``` ```
The `crypto.DEFAULT_ENCODING` may be used to change the way the `derivedKey`
is passed to the callback:
```js
const crypto = require('crypto');
crypto.DEFAULT_ENCODING = 'hex';
crypto.pbkdf2('secret', 'salt', 100000, 512, 'sha512', (err, derivedKey) => {
if (err) throw err;
console.log(derivedKey); // '3745e48...aa39b34'
});
```
An array of supported digest functions can be retrieved using An array of supported digest functions can be retrieved using
[`crypto.getHashes()`][]. [`crypto.getHashes()`][].
@ -1643,8 +1657,8 @@ changes:
description: The default encoding for `password` if it is a string changed description: The default encoding for `password` if it is a string changed
from `binary` to `utf8`. from `binary` to `utf8`.
--> -->
- `password` {string} - `password` {string|Buffer|TypedArray}
- `salt` {string} - `salt` {string|Buffer|TypedArray}
- `iterations` {number} - `iterations` {number}
- `keylen` {number} - `keylen` {number}
- `digest` {string} - `digest` {string}
@ -1673,6 +1687,16 @@ const key = crypto.pbkdf2Sync('secret', 'salt', 100000, 64, 'sha512');
console.log(key.toString('hex')); // '3745e48...08d59ae' console.log(key.toString('hex')); // '3745e48...08d59ae'
``` ```
The `crypto.DEFAULT_ENCODING` may be used to change the way the `derivedKey`
is returned:
```js
const crypto = require('crypto');
crypto.DEFAULT_ENCODING = 'hex';
const key = crypto.pbkdf2Sync('secret', 'salt', 100000, 512, 'sha512');
console.log(key); // '3745e48...aa39b34'
```
An array of supported digest functions can be retrieved using An array of supported digest functions can be retrieved using
[`crypto.getHashes()`][]. [`crypto.getHashes()`][].

View File

@ -637,6 +637,11 @@ Used when the native call from `process.cpuUsage` cannot be processed properly.
Used when an invalid value for the `format` argument has been passed to the Used when an invalid value for the `format` argument has been passed to the
`crypto.ECDH()` class `getPublicKey()` method. `crypto.ECDH()` class `getPublicKey()` method.
<a id="ERR_CRYPTO_INVALID_DIGEST"></a>
### ERR_CRYPTO_INVALID_DIGEST
Used when an invalid [crypto digest algorithm][] is specified.
<a id="ERR_DNS_SET_SERVERS_FAILED"></a> <a id="ERR_DNS_SET_SERVERS_FAILED"></a>
### ERR_DNS_SET_SERVERS_FAILED ### ERR_DNS_SET_SERVERS_FAILED
@ -1355,6 +1360,7 @@ closed.
[Node.js Error Codes]: #nodejs-error-codes [Node.js Error Codes]: #nodejs-error-codes
[V8's stack trace API]: https://github.com/v8/v8/wiki/Stack-Trace-API [V8's stack trace API]: https://github.com/v8/v8/wiki/Stack-Trace-API
[WHATWG URL API]: url.html#url_the_whatwg_url_api [WHATWG URL API]: url.html#url_the_whatwg_url_api
[crypto digest algorithm]: crypto.html#crypto_crypto_gethashes
[domains]: domain.html [domains]: domain.html
[event emitter-based]: events.html#events_class_eventemitter [event emitter-based]: events.html#events_class_eventemitter
[file descriptors]: https://en.wikipedia.org/wiki/File_descriptor [file descriptors]: https://en.wikipedia.org/wiki/File_descriptor

View File

@ -5,9 +5,13 @@ const {
getDefaultEncoding, getDefaultEncoding,
toBuf toBuf
} = require('internal/crypto/util'); } = require('internal/crypto/util');
const { isArrayBufferView } = require('internal/util/types');
const { const {
PBKDF2 PBKDF2
} = process.binding('crypto'); } = process.binding('crypto');
const {
INT_MAX
} = process.binding('constants').crypto;
function pbkdf2(password, salt, iterations, keylen, digest, callback) { function pbkdf2(password, salt, iterations, keylen, digest, callback) {
if (typeof digest === 'function') { if (typeof digest === 'function') {
@ -34,10 +38,39 @@ function _pbkdf2(password, salt, iterations, keylen, digest, callback) {
password = toBuf(password); password = toBuf(password);
salt = toBuf(salt); salt = toBuf(salt);
if (!isArrayBufferView(password)) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'password',
['string', 'Buffer', 'TypedArray']);
}
if (!isArrayBufferView(salt)) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'salt',
['string', 'Buffer', 'TypedArray']);
}
if (typeof iterations !== 'number')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'iterations', 'number');
if (iterations < 0)
throw new errors.RangeError('ERR_OUT_OF_RANGE', 'iterations');
if (typeof keylen !== 'number')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'keylen', 'number');
if (keylen < 0 ||
!Number.isFinite(keylen) ||
keylen > INT_MAX) {
throw new errors.RangeError('ERR_OUT_OF_RANGE', 'keylen');
}
const encoding = getDefaultEncoding(); const encoding = getDefaultEncoding();
if (encoding === 'buffer') if (encoding === 'buffer') {
return PBKDF2(password, salt, iterations, keylen, digest, callback); const ret = PBKDF2(password, salt, iterations, keylen, digest, callback);
if (ret === -1)
throw new errors.TypeError('ERR_CRYPTO_INVALID_DIGEST', digest);
return ret;
}
// at this point, we need to handle encodings. // at this point, we need to handle encodings.
if (callback) { if (callback) {
@ -46,9 +79,12 @@ function _pbkdf2(password, salt, iterations, keylen, digest, callback) {
ret = ret.toString(encoding); ret = ret.toString(encoding);
callback(er, ret); callback(er, ret);
} }
PBKDF2(password, salt, iterations, keylen, digest, next); if (PBKDF2(password, salt, iterations, keylen, digest, next) === -1)
throw new errors.TypeError('ERR_CRYPTO_INVALID_DIGEST', digest);
} else { } else {
var ret = PBKDF2(password, salt, iterations, keylen, digest); const ret = PBKDF2(password, salt, iterations, keylen, digest);
if (ret === -1)
throw new errors.TypeError('ERR_CRYPTO_INVALID_DIGEST', digest);
return ret.toString(encoding); return ret.toString(encoding);
} }
} }

View File

@ -157,6 +157,7 @@ E('ERR_CRYPTO_ECDH_INVALID_FORMAT', 'Invalid ECDH format: %s');
E('ERR_CRYPTO_HASH_DIGEST_NO_UTF16', 'hash.digest() does not support UTF-16'); E('ERR_CRYPTO_HASH_DIGEST_NO_UTF16', 'hash.digest() does not support UTF-16');
E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called'); E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called');
E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed'); E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed');
E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s');
E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign'); E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign');
E('ERR_DNS_SET_SERVERS_FAILED', (err, servers) => E('ERR_DNS_SET_SERVERS_FAILED', (err, servers) =>
`c-ares failed to set servers: "${err}" [${servers}]`); `c-ares failed to set servers: "${err}" [${servers}]`);

View File

@ -1180,6 +1180,7 @@ void DefineCryptoConstants(Local<Object> target) {
"defaultCipherList", "defaultCipherList",
default_cipher_list); default_cipher_list);
#endif #endif
NODE_DEFINE_CONSTANT(target, INT_MAX);
} }
void DefineZlibConstants(Local<Object> target) { void DefineZlibConstants(Local<Object> target) {

View File

@ -5330,7 +5330,6 @@ void PBKDF2(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args); Environment* env = Environment::GetCurrent(args);
const EVP_MD* digest = nullptr; const EVP_MD* digest = nullptr;
const char* type_error = nullptr;
char* pass = nullptr; char* pass = nullptr;
char* salt = nullptr; char* salt = nullptr;
int passlen = -1; int passlen = -1;
@ -5341,54 +5340,19 @@ void PBKDF2(const FunctionCallbackInfo<Value>& args) {
PBKDF2Request* req = nullptr; PBKDF2Request* req = nullptr;
Local<Object> obj; Local<Object> obj;
if (args.Length() != 5 && args.Length() != 6) {
type_error = "Bad parameter";
goto err;
}
THROW_AND_RETURN_IF_NOT_BUFFER(args[0], "Pass phrase");
passlen = Buffer::Length(args[0]); passlen = Buffer::Length(args[0]);
if (passlen < 0) {
type_error = "Bad password";
goto err;
}
THROW_AND_RETURN_IF_NOT_BUFFER(args[1], "Salt");
pass = node::Malloc(passlen); pass = node::Malloc(passlen);
memcpy(pass, Buffer::Data(args[0]), passlen); memcpy(pass, Buffer::Data(args[0]), passlen);
saltlen = Buffer::Length(args[1]); saltlen = Buffer::Length(args[1]);
if (saltlen < 0) {
type_error = "Bad salt";
goto err;
}
salt = node::Malloc(saltlen); salt = node::Malloc(saltlen);
memcpy(salt, Buffer::Data(args[1]), saltlen); memcpy(salt, Buffer::Data(args[1]), saltlen);
if (!args[2]->IsNumber()) {
type_error = "Iterations not a number";
goto err;
}
iter = args[2]->Int32Value(); iter = args[2]->Int32Value();
if (iter < 0) {
type_error = "Bad iterations";
goto err;
}
if (!args[3]->IsNumber()) {
type_error = "Key length not a number";
goto err;
}
raw_keylen = args[3]->NumberValue(); raw_keylen = args[3]->NumberValue();
if (raw_keylen < 0.0 || isnan(raw_keylen) || isinf(raw_keylen) ||
raw_keylen > INT_MAX) {
type_error = "Bad key length";
goto err;
}
keylen = static_cast<int>(raw_keylen); keylen = static_cast<int>(raw_keylen);
@ -5396,8 +5360,10 @@ void PBKDF2(const FunctionCallbackInfo<Value>& args) {
node::Utf8Value digest_name(env->isolate(), args[4]); node::Utf8Value digest_name(env->isolate(), args[4]);
digest = EVP_get_digestbyname(*digest_name); digest = EVP_get_digestbyname(*digest_name);
if (digest == nullptr) { if (digest == nullptr) {
type_error = "Bad digest name"; free(salt);
goto err; free(pass);
args.GetReturnValue().Set(-1);
return;
} }
} }
@ -5443,12 +5409,6 @@ void PBKDF2(const FunctionCallbackInfo<Value>& args) {
else else
args.GetReturnValue().Set(argv[1]); args.GetReturnValue().Set(argv[1]);
} }
return;
err:
free(salt);
free(pass);
return env->ThrowTypeError(type_error);
} }

View File

@ -6,6 +6,8 @@ if (!common.hasCrypto)
const assert = require('assert'); const assert = require('assert');
const crypto = require('crypto'); const crypto = require('crypto');
const { INT_MAX } = process.binding('constants').crypto;
// //
// Test PBKDF2 with RFC 6070 test vectors (except #4) // Test PBKDF2 with RFC 6070 test vectors (except #4)
// //
@ -63,33 +65,17 @@ common.expectsError(
} }
); );
// Should not work with Infinity key length [Infinity, -Infinity, NaN, -1, 4073741824, INT_MAX + 1].forEach((i) => {
assert.throws(function() { common.expectsError(
crypto.pbkdf2('password', 'salt', 1, Infinity, 'sha256', () => {
crypto.pbkdf2('password', 'salt', 1, i, 'sha256',
common.mustNotCall()); common.mustNotCall());
}, /^TypeError: Bad key length$/); }, {
code: 'ERR_OUT_OF_RANGE',
// Should not work with negative Infinity key length type: RangeError,
assert.throws(function() { message: 'The "keylen" argument is out of range'
crypto.pbkdf2('password', 'salt', 1, -Infinity, 'sha256', });
common.mustNotCall()); });
}, /^TypeError: Bad key length$/);
// Should not work with NaN key length
assert.throws(function() {
crypto.pbkdf2('password', 'salt', 1, NaN, 'sha256', common.mustNotCall());
}, /^TypeError: Bad key length$/);
// Should not work with negative key length
assert.throws(function() {
crypto.pbkdf2('password', 'salt', 1, -1, 'sha256', common.mustNotCall());
}, /^TypeError: Bad key length$/);
// Should not work with key length that does not fit into 32 signed bits
assert.throws(function() {
crypto.pbkdf2('password', 'salt', 1, 4073741824, 'sha256',
common.mustNotCall());
}, /^TypeError: Bad key length$/);
// Should not get FATAL ERROR with empty password and salt // Should not get FATAL ERROR with empty password and salt
// https://github.com/nodejs/node/issues/8571 // https://github.com/nodejs/node/issues/8571
@ -114,3 +100,106 @@ common.expectsError(
type: TypeError, type: TypeError,
message: 'The "digest" argument must be one of type string or null' message: 'The "digest" argument must be one of type string or null'
}); });
[1, {}, [], true, undefined, null].forEach((i) => {
common.expectsError(
() => crypto.pbkdf2(i, 'salt', 8, 8, 'sha256', common.mustNotCall()),
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "password" argument must be one of type string, ' +
'Buffer, or TypedArray'
}
);
common.expectsError(
() => crypto.pbkdf2('pass', i, 8, 8, 'sha256', common.mustNotCall()),
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "salt" argument must be one of type string, ' +
'Buffer, or TypedArray'
}
);
common.expectsError(
() => crypto.pbkdf2Sync(i, 'salt', 8, 8, 'sha256'),
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "password" argument must be one of type string, ' +
'Buffer, or TypedArray'
}
);
common.expectsError(
() => crypto.pbkdf2Sync('pass', i, 8, 8, 'sha256'),
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "salt" argument must be one of type string, ' +
'Buffer, or TypedArray'
}
);
});
['test', {}, [], true, undefined, null].forEach((i) => {
common.expectsError(
() => crypto.pbkdf2('pass', 'salt', i, 8, 'sha256', common.mustNotCall()),
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "iterations" argument must be of type number'
}
);
common.expectsError(
() => crypto.pbkdf2Sync('pass', 'salt', i, 8, 'sha256'),
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "iterations" argument must be of type number'
}
);
});
// Any TypedArray should work for password and salt
crypto.pbkdf2(new Uint8Array(10), 'salt', 8, 8, 'sha256', common.mustCall());
crypto.pbkdf2('pass', new Uint8Array(10), 8, 8, 'sha256', common.mustCall());
crypto.pbkdf2(new Uint16Array(10), 'salt', 8, 8, 'sha256', common.mustCall());
crypto.pbkdf2('pass', new Uint16Array(10), 8, 8, 'sha256', common.mustCall());
crypto.pbkdf2(new Uint32Array(10), 'salt', 8, 8, 'sha256', common.mustCall());
crypto.pbkdf2('pass', new Uint32Array(10), 8, 8, 'sha256', common.mustCall());
crypto.pbkdf2(new Float32Array(10), 'salt', 8, 8, 'sha256', common.mustCall());
crypto.pbkdf2('pass', new Float32Array(10), 8, 8, 'sha256', common.mustCall());
crypto.pbkdf2(new Float64Array(10), 'salt', 8, 8, 'sha256', common.mustCall());
crypto.pbkdf2('pass', new Float64Array(10), 8, 8, 'sha256', common.mustCall());
crypto.pbkdf2Sync(new Uint8Array(10), 'salt', 8, 8, 'sha256');
crypto.pbkdf2Sync('pass', new Uint8Array(10), 8, 8, 'sha256');
crypto.pbkdf2Sync(new Uint16Array(10), 'salt', 8, 8, 'sha256');
crypto.pbkdf2Sync('pass', new Uint16Array(10), 8, 8, 'sha256');
crypto.pbkdf2Sync(new Uint32Array(10), 'salt', 8, 8, 'sha256');
crypto.pbkdf2Sync('pass', new Uint32Array(10), 8, 8, 'sha256');
crypto.pbkdf2Sync(new Float32Array(10), 'salt', 8, 8, 'sha256');
crypto.pbkdf2Sync('pass', new Float32Array(10), 8, 8, 'sha256');
crypto.pbkdf2Sync(new Float64Array(10), 'salt', 8, 8, 'sha256');
crypto.pbkdf2Sync('pass', new Float64Array(10), 8, 8, 'sha256');
common.expectsError(
() => crypto.pbkdf2('pass', 'salt', 8, 8, 'md55', common.mustNotCall()),
{
code: 'ERR_CRYPTO_INVALID_DIGEST',
type: TypeError,
message: 'Invalid digest: md55'
}
);
common.expectsError(
() => crypto.pbkdf2Sync('pass', 'salt', 8, 8, 'md55'),
{
code: 'ERR_CRYPTO_INVALID_DIGEST',
type: TypeError,
message: 'Invalid digest: md55'
}
);