crypto: add scrypt() and scryptSync() methods

Scrypt is a password-based key derivation function that is designed to
be expensive both computationally and memory-wise in order to make
brute-force attacks unrewarding.

OpenSSL has had support for the scrypt algorithm since v1.1.0.  Add a
Node.js API modeled after `crypto.pbkdf2()` and `crypto.pbkdf2Sync()`.

Changes:

* Introduce helpers for copying buffers, collecting openssl errors, etc.

* Add new infrastructure for offloading crypto to a worker thread.

* Add a `AsyncWrap` JS class to simplify pbkdf2(), randomBytes() and
  scrypt().

Fixes: https://github.com/nodejs/node/issues/8417
PR-URL: https://github.com/nodejs/node/pull/20816
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
This commit is contained in:
Ben Noordhuis 2018-05-18 11:05:20 +02:00
parent 58176e352c
commit 371103dae8
13 changed files with 620 additions and 60 deletions

View File

@ -1361,9 +1361,9 @@ password always creates the same key. The low iteration count and
non-cryptographically secure hash algorithm allow passwords to be tested very non-cryptographically secure hash algorithm allow passwords to be tested very
rapidly. rapidly.
In line with OpenSSL's recommendation to use PBKDF2 instead of In line with OpenSSL's recommendation to use a more modern algorithm instead of
[`EVP_BytesToKey`][] it is recommended that developers derive a key and IV on [`EVP_BytesToKey`][] it is recommended that developers derive a key and IV on
their own using [`crypto.pbkdf2()`][] and to use [`crypto.createCipheriv()`][] their own using [`crypto.scrypt()`][] and to use [`crypto.createCipheriv()`][]
to create the `Cipher` object. Users should not use ciphers with counter mode to create the `Cipher` object. Users should not use ciphers with counter mode
(e.g. CTR, GCM, or CCM) in `crypto.createCipher()`. A warning is emitted when (e.g. CTR, GCM, or CCM) in `crypto.createCipher()`. A warning is emitted when
they are used in order to avoid the risk of IV reuse that causes they are used in order to avoid the risk of IV reuse that causes
@ -1463,9 +1463,9 @@ password always creates the same key. The low iteration count and
non-cryptographically secure hash algorithm allow passwords to be tested very non-cryptographically secure hash algorithm allow passwords to be tested very
rapidly. rapidly.
In line with OpenSSL's recommendation to use PBKDF2 instead of In line with OpenSSL's recommendation to use a more modern algorithm instead of
[`EVP_BytesToKey`][] it is recommended that developers derive a key and IV on [`EVP_BytesToKey`][] it is recommended that developers derive a key and IV on
their own using [`crypto.pbkdf2()`][] and to use [`crypto.createDecipheriv()`][] their own using [`crypto.scrypt()`][] and to use [`crypto.createDecipheriv()`][]
to create the `Decipher` object. to create the `Decipher` object.
### crypto.createDecipheriv(algorithm, key, iv[, options]) ### crypto.createDecipheriv(algorithm, key, iv[, options])
@ -1801,9 +1801,8 @@ 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,
but will take a longer amount of time to complete. but will take a longer amount of time to complete.
The `salt` should also be as unique as possible. It is recommended that the The `salt` should be as unique as possible. It is recommended that a salt is
salts are random and their lengths are at least 16 bytes. See random and at least 16 bytes long. See [NIST SP 800-132][] for details.
[NIST SP 800-132][] for details.
Example: Example:
@ -1867,9 +1866,8 @@ 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,
but will take a longer amount of time to complete. but will take a longer amount of time to complete.
The `salt` should also be as unique as possible. It is recommended that the The `salt` should be as unique as possible. It is recommended that a salt is
salts are random and their lengths are at least 16 bytes. See random and at least 16 bytes long. See [NIST SP 800-132][] for details.
[NIST SP 800-132][] for details.
Example: Example:
@ -2143,6 +2141,91 @@ threadpool request. To minimize threadpool task length variation, partition
large `randomFill` requests when doing so as part of fulfilling a client large `randomFill` requests when doing so as part of fulfilling a client
request. request.
### crypto.scrypt(password, salt, keylen[, options], callback)
<!-- YAML
added: REPLACEME
-->
- `password` {string|Buffer|TypedArray}
- `salt` {string|Buffer|TypedArray}
- `keylen` {number}
- `options` {Object}
- `N` {number} CPU/memory cost parameter. Must be a power of two greater
than one. **Default:** `16384`.
- `r` {number} Block size parameter. **Default:** `8`.
- `p` {number} Parallelization parameter. **Default:** `1`.
- `maxmem` {number} Memory upper bound. It is an error when (approximately)
`128*N*r > maxmem` **Default:** `32 * 1024 * 1024`.
- `callback` {Function}
- `err` {Error}
- `derivedKey` {Buffer}
Provides an asynchronous [scrypt][] implementation. Scrypt is a password-based
key derivation function that is designed to be expensive computationally and
memory-wise in order to make brute-force attacks unrewarding.
The `salt` should be as unique as possible. It is recommended that a salt is
random and at least 16 bytes long. See [NIST SP 800-132][] for details.
The `callback` function is called with two arguments: `err` and `derivedKey`.
`err` is an exception object when key derivation fails, otherwise `err` is
`null`. `derivedKey` is passed to the callback as a [`Buffer`][].
An exception is thrown when any of the input arguments specify invalid values
or types.
```js
const crypto = require('crypto');
// Using the factory defaults.
crypto.scrypt('secret', 'salt', 64, (err, derivedKey) => {
if (err) throw err;
console.log(derivedKey.toString('hex')); // '3745e48...08d59ae'
});
// Using a custom N parameter. Must be a power of two.
crypto.scrypt('secret', 'salt', 64, { N: 1024 }, (err, derivedKey) => {
if (err) throw err;
console.log(derivedKey.toString('hex')); // '3745e48...aa39b34'
});
```
### crypto.scryptSync(password, salt, keylen[, options])
<!-- YAML
added: REPLACEME
-->
- `password` {string|Buffer|TypedArray}
- `salt` {string|Buffer|TypedArray}
- `keylen` {number}
- `options` {Object}
- `N` {number} CPU/memory cost parameter. Must be a power of two greater
than one. **Default:** `16384`.
- `r` {number} Block size parameter. **Default:** `8`.
- `p` {number} Parallelization parameter. **Default:** `1`.
- `maxmem` {number} Memory upper bound. It is an error when (approximately)
`128*N*r > maxmem` **Default:** `32 * 1024 * 1024`.
- Returns: {Buffer}
Provides a synchronous [scrypt][] implementation. Scrypt is a password-based
key derivation function that is designed to be expensive computationally and
memory-wise in order to make brute-force attacks unrewarding.
The `salt` should be as unique as possible. It is recommended that a salt is
random and at least 16 bytes long. See [NIST SP 800-132][] for details.
An exception is thrown when key derivation fails, otherwise the derived key is
returned as a [`Buffer`][].
An exception is thrown when any of the input arguments specify invalid values
or types.
```js
const crypto = require('crypto');
// Using the factory defaults.
const key1 = crypto.scryptSync('secret', 'salt', 64);
console.log(key1.toString('hex')); // '3745e48...08d59ae'
// Using a custom N parameter. Must be a power of two.
const key2 = crypto.scryptSync('secret', 'salt', 64, { N: 1024 });
console.log(key2.toString('hex')); // '3745e48...aa39b34'
```
### crypto.setEngine(engine[, flags]) ### crypto.setEngine(engine[, flags])
<!-- YAML <!-- YAML
added: v0.11.11 added: v0.11.11
@ -2650,9 +2733,9 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL.
[`crypto.createVerify()`]: #crypto_crypto_createverify_algorithm_options [`crypto.createVerify()`]: #crypto_crypto_createverify_algorithm_options
[`crypto.getCurves()`]: #crypto_crypto_getcurves [`crypto.getCurves()`]: #crypto_crypto_getcurves
[`crypto.getHashes()`]: #crypto_crypto_gethashes [`crypto.getHashes()`]: #crypto_crypto_gethashes
[`crypto.pbkdf2()`]: #crypto_crypto_pbkdf2_password_salt_iterations_keylen_digest_callback
[`crypto.randomBytes()`]: #crypto_crypto_randombytes_size_callback [`crypto.randomBytes()`]: #crypto_crypto_randombytes_size_callback
[`crypto.randomFill()`]: #crypto_crypto_randomfill_buffer_offset_size_callback [`crypto.randomFill()`]: #crypto_crypto_randomfill_buffer_offset_size_callback
[`crypto.scrypt()`]: #crypto_crypto_scrypt_password_salt_keylen_options_callback
[`decipher.final()`]: #crypto_decipher_final_outputencoding [`decipher.final()`]: #crypto_decipher_final_outputencoding
[`decipher.update()`]: #crypto_decipher_update_data_inputencoding_outputencoding [`decipher.update()`]: #crypto_decipher_update_data_inputencoding_outputencoding
[`diffieHellman.setPublicKey()`]: #crypto_diffiehellman_setpublickey_publickey_encoding [`diffieHellman.setPublicKey()`]: #crypto_diffiehellman_setpublickey_publickey_encoding
@ -2686,5 +2769,6 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL.
[RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt [RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt
[RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt [RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt
[initialization vector]: https://en.wikipedia.org/wiki/Initialization_vector [initialization vector]: https://en.wikipedia.org/wiki/Initialization_vector
[scrypt]: https://en.wikipedia.org/wiki/Scrypt
[stream-writable-write]: stream.html#stream_writable_write_chunk_encoding_callback [stream-writable-write]: stream.html#stream_writable_write_chunk_encoding_callback
[stream]: stream.html [stream]: stream.html

View File

@ -739,6 +739,18 @@ An invalid [crypto digest algorithm][] was specified.
A crypto method was used on an object that was in an invalid state. For A crypto method was used on an object that was in an invalid state. For
instance, calling [`cipher.getAuthTag()`][] before calling `cipher.final()`. instance, calling [`cipher.getAuthTag()`][] before calling `cipher.final()`.
<a id="ERR_CRYPTO_SCRYPT_INVALID_PARAMETER"></a>
### ERR_CRYPTO_SCRYPT_INVALID_PARAMETER
One or more [`crypto.scrypt()`][] or [`crypto.scryptSync()`][] parameters are
outside their legal range.
<a id="ERR_CRYPTO_SCRYPT_NOT_SUPPORTED"></a>
### ERR_CRYPTO_SCRYPT_NOT_SUPPORTED
Node.js was compiled without `scrypt` support. Not possible with the official
release binaries but can happen with custom builds, including distro builds.
<a id="ERR_CRYPTO_SIGN_KEY_REQUIRED"></a> <a id="ERR_CRYPTO_SIGN_KEY_REQUIRED"></a>
### ERR_CRYPTO_SIGN_KEY_REQUIRED ### ERR_CRYPTO_SIGN_KEY_REQUIRED
@ -1749,6 +1761,8 @@ Creation of a [`zlib`][] object failed due to incorrect configuration.
[`child_process`]: child_process.html [`child_process`]: child_process.html
[`cipher.getAuthTag()`]: crypto.html#crypto_cipher_getauthtag [`cipher.getAuthTag()`]: crypto.html#crypto_cipher_getauthtag
[`Class: assert.AssertionError`]: assert.html#assert_class_assert_assertionerror [`Class: assert.AssertionError`]: assert.html#assert_class_assert_assertionerror
[`crypto.scrypt()`]: crypto.html#crypto_crypto_scrypt_password_salt_keylen_options_callback
[`crypto.scryptSync()`]: crypto.html#crypto_crypto_scryptSync_password_salt_keylen_options
[`crypto.timingSafeEqual()`]: crypto.html#crypto_crypto_timingsafeequal_a_b [`crypto.timingSafeEqual()`]: crypto.html#crypto_crypto_timingsafeequal_a_b
[`dgram.createSocket()`]: dgram.html#dgram_dgram_createsocket_options_callback [`dgram.createSocket()`]: dgram.html#dgram_dgram_createsocket_options_callback
[`ERR_INVALID_ARG_TYPE`]: #ERR_INVALID_ARG_TYPE [`ERR_INVALID_ARG_TYPE`]: #ERR_INVALID_ARG_TYPE

View File

@ -52,6 +52,10 @@ const {
pbkdf2, pbkdf2,
pbkdf2Sync pbkdf2Sync
} = require('internal/crypto/pbkdf2'); } = require('internal/crypto/pbkdf2');
const {
scrypt,
scryptSync
} = require('internal/crypto/scrypt');
const { const {
DiffieHellman, DiffieHellman,
DiffieHellmanGroup, DiffieHellmanGroup,
@ -163,6 +167,8 @@ module.exports = exports = {
randomFill, randomFill,
randomFillSync, randomFillSync,
rng: randomBytes, rng: randomBytes,
scrypt,
scryptSync,
setEngine, setEngine,
timingSafeEqual, timingSafeEqual,
getFips: !fipsMode ? getFipsDisabled : getFips: !fipsMode ? getFipsDisabled :

View File

@ -0,0 +1,97 @@
'use strict';
const { AsyncWrap, Providers } = process.binding('async_wrap');
const { Buffer } = require('buffer');
const { scrypt: _scrypt } = process.binding('crypto');
const {
ERR_CRYPTO_SCRYPT_INVALID_PARAMETER,
ERR_CRYPTO_SCRYPT_NOT_SUPPORTED,
ERR_INVALID_CALLBACK,
} = require('internal/errors').codes;
const {
checkIsArrayBufferView,
checkIsUint,
getDefaultEncoding,
} = require('internal/crypto/util');
const defaults = {
N: 16384,
r: 8,
p: 1,
maxmem: 32 << 20, // 32 MB, matches SCRYPT_MAX_MEM.
};
function scrypt(password, salt, keylen, options, callback = defaults) {
if (callback === defaults) {
callback = options;
options = defaults;
}
options = check(password, salt, keylen, options);
const { N, r, p, maxmem } = options;
({ password, salt, keylen } = options);
if (typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK();
const encoding = getDefaultEncoding();
const keybuf = Buffer.alloc(keylen);
const wrap = new AsyncWrap(Providers.SCRYPTREQUEST);
wrap.ondone = (ex) => { // Retains keybuf while request is in flight.
if (ex) return callback.call(wrap, ex);
if (encoding === 'buffer') return callback.call(wrap, null, keybuf);
callback.call(wrap, null, keybuf.toString(encoding));
};
handleError(keybuf, password, salt, N, r, p, maxmem, wrap);
}
function scryptSync(password, salt, keylen, options = defaults) {
options = check(password, salt, keylen, options);
const { N, r, p, maxmem } = options;
({ password, salt, keylen } = options);
const keybuf = Buffer.alloc(keylen);
handleError(keybuf, password, salt, N, r, p, maxmem);
const encoding = getDefaultEncoding();
if (encoding === 'buffer') return keybuf;
return keybuf.toString(encoding);
}
function handleError(keybuf, password, salt, N, r, p, maxmem, wrap) {
const ex = _scrypt(keybuf, password, salt, N, r, p, maxmem, wrap);
if (ex === undefined)
return;
if (ex === null)
throw new ERR_CRYPTO_SCRYPT_INVALID_PARAMETER(); // Bad N, r, p, or maxmem.
throw ex; // Scrypt operation failed, exception object contains details.
}
function check(password, salt, keylen, options, callback) {
if (_scrypt === undefined)
throw new ERR_CRYPTO_SCRYPT_NOT_SUPPORTED();
password = checkIsArrayBufferView('password', password);
salt = checkIsArrayBufferView('salt', salt);
keylen = checkIsUint('keylen', keylen);
let { N, r, p, maxmem } = defaults;
if (options && options !== defaults) {
if (options.hasOwnProperty('N')) N = checkIsUint('N', options.N);
if (options.hasOwnProperty('r')) r = checkIsUint('r', options.r);
if (options.hasOwnProperty('p')) p = checkIsUint('p', options.p);
if (options.hasOwnProperty('maxmem'))
maxmem = checkIsUint('maxmem', options.maxmem);
if (N === 0) N = defaults.N;
if (r === 0) r = defaults.r;
if (p === 0) p = defaults.p;
if (maxmem === 0) maxmem = defaults.maxmem;
}
return { password, salt, keylen, N, r, p, maxmem };
}
module.exports = { scrypt, scryptSync };

View File

@ -500,7 +500,8 @@ E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error);
E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error); E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error);
E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError); E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError);
E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error); E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error);
E('ERR_CRYPTO_SCRYPT_INVALID_PARAMETER', 'Invalid scrypt parameter', Error);
E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);
// Switch to TypeError. The current implementation does not seem right. // Switch to TypeError. The current implementation does not seem right.
E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error); E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error);
E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH', E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',

View File

@ -97,6 +97,7 @@
'lib/internal/crypto/hash.js', 'lib/internal/crypto/hash.js',
'lib/internal/crypto/pbkdf2.js', 'lib/internal/crypto/pbkdf2.js',
'lib/internal/crypto/random.js', 'lib/internal/crypto/random.js',
'lib/internal/crypto/scrypt.js',
'lib/internal/crypto/sig.js', 'lib/internal/crypto/sig.js',
'lib/internal/crypto/util.js', 'lib/internal/crypto/util.js',
'lib/internal/constants.js', 'lib/internal/constants.js',

View File

@ -45,6 +45,7 @@ using v8::PromiseHookType;
using v8::PropertyCallbackInfo; using v8::PropertyCallbackInfo;
using v8::RetainedObjectInfo; using v8::RetainedObjectInfo;
using v8::String; using v8::String;
using v8::Uint32;
using v8::Undefined; using v8::Undefined;
using v8::Value; using v8::Value;
@ -133,6 +134,23 @@ RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local<Value> wrapper) {
// end RetainedAsyncInfo // end RetainedAsyncInfo
struct AsyncWrapObject : public AsyncWrap {
static inline void New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args.IsConstructCall());
CHECK(env->async_wrap_constructor_template()->HasInstance(args.This()));
CHECK(args[0]->IsUint32());
auto type = static_cast<ProviderType>(args[0].As<Uint32>()->Value());
new AsyncWrapObject(env, args.This(), type);
}
inline AsyncWrapObject(Environment* env, Local<Object> object,
ProviderType type) : AsyncWrap(env, object, type) {}
inline size_t self_size() const override { return sizeof(*this); }
};
static void DestroyAsyncIdsCallback(Environment* env, void* data) { static void DestroyAsyncIdsCallback(Environment* env, void* data) {
Local<Function> fn = env->async_hooks_destroy_function(); Local<Function> fn = env->async_hooks_destroy_function();
@ -569,6 +587,19 @@ void AsyncWrap::Initialize(Local<Object> target,
env->set_async_hooks_destroy_function(Local<Function>()); env->set_async_hooks_destroy_function(Local<Function>());
env->set_async_hooks_promise_resolve_function(Local<Function>()); env->set_async_hooks_promise_resolve_function(Local<Function>());
env->set_async_hooks_binding(target); env->set_async_hooks_binding(target);
{
auto class_name = FIXED_ONE_BYTE_STRING(env->isolate(), "AsyncWrap");
auto function_template = env->NewFunctionTemplate(AsyncWrapObject::New);
function_template->SetClassName(class_name);
AsyncWrap::AddWrapMethods(env, function_template);
auto instance_template = function_template->InstanceTemplate();
instance_template->SetInternalFieldCount(1);
auto function =
function_template->GetFunction(env->context()).ToLocalChecked();
target->Set(env->context(), class_name, function).FromJust();
env->set_async_wrap_constructor_template(function_template);
}
} }

View File

@ -75,6 +75,7 @@ namespace node {
#define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) \ #define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) \
V(PBKDF2REQUEST) \ V(PBKDF2REQUEST) \
V(RANDOMBYTESREQUEST) \ V(RANDOMBYTESREQUEST) \
V(SCRYPTREQUEST) \
V(TLSWRAP) V(TLSWRAP)
#else #else
#define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) #define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V)

View File

@ -319,6 +319,7 @@ struct PackageConfig {
V(async_hooks_destroy_function, v8::Function) \ V(async_hooks_destroy_function, v8::Function) \
V(async_hooks_init_function, v8::Function) \ V(async_hooks_init_function, v8::Function) \
V(async_hooks_promise_resolve_function, v8::Function) \ V(async_hooks_promise_resolve_function, v8::Function) \
V(async_wrap_constructor_template, v8::FunctionTemplate) \
V(buffer_prototype_object, v8::Object) \ V(buffer_prototype_object, v8::Object) \
V(context, v8::Context) \ V(context, v8::Context) \
V(domain_callback, v8::Function) \ V(domain_callback, v8::Function) \

View File

@ -78,6 +78,7 @@ using v8::Isolate;
using v8::Local; using v8::Local;
using v8::Maybe; using v8::Maybe;
using v8::MaybeLocal; using v8::MaybeLocal;
using v8::NewStringType;
using v8::Null; using v8::Null;
using v8::Object; using v8::Object;
using v8::ObjectTemplate; using v8::ObjectTemplate;
@ -204,57 +205,75 @@ static int NoPasswordCallback(char* buf, int size, int rwflag, void* u) {
} }
struct CryptoErrorVector : public std::vector<std::string> {
inline void Capture() {
clear();
while (auto err = ERR_get_error()) {
char buf[256];
ERR_error_string_n(err, buf, sizeof(buf));
push_back(buf);
}
std::reverse(begin(), end());
}
inline Local<Value> ToException(
Environment* env,
Local<String> exception_string = Local<String>()) const {
if (exception_string.IsEmpty()) {
CryptoErrorVector copy(*this);
if (copy.empty()) copy.push_back("no error"); // But possibly a bug...
// Use last element as the error message, everything else goes
// into the .opensslErrorStack property on the exception object.
auto exception_string =
String::NewFromUtf8(env->isolate(), copy.back().data(),
NewStringType::kNormal, copy.back().size())
.ToLocalChecked();
copy.pop_back();
return copy.ToException(env, exception_string);
}
Local<Value> exception_v = Exception::Error(exception_string);
CHECK(!exception_v.IsEmpty());
if (!empty()) {
Local<Array> array = Array::New(env->isolate(), size());
CHECK(!array.IsEmpty());
for (const std::string& string : *this) {
const size_t index = &string - &front();
Local<String> value =
String::NewFromUtf8(env->isolate(), string.data(),
NewStringType::kNormal, string.size())
.ToLocalChecked();
array->Set(env->context(), index, value).FromJust();
}
CHECK(exception_v->IsObject());
Local<Object> exception = exception_v.As<Object>();
exception->Set(env->context(),
env->openssl_error_stack(), array).FromJust();
}
return exception_v;
}
};
void ThrowCryptoError(Environment* env, void ThrowCryptoError(Environment* env,
unsigned long err, // NOLINT(runtime/int) unsigned long err, // NOLINT(runtime/int)
const char* default_message = nullptr) { const char* message = nullptr) {
char message_buffer[128] = {0};
if (err != 0 || message == nullptr) {
ERR_error_string_n(err, message_buffer, sizeof(message_buffer));
message = message_buffer;
}
HandleScope scope(env->isolate()); HandleScope scope(env->isolate());
Local<String> message; auto exception_string =
String::NewFromUtf8(env->isolate(), message, NewStringType::kNormal)
if (err != 0 || default_message == nullptr) {
char errmsg[128] = { 0 };
ERR_error_string_n(err, errmsg, sizeof(errmsg));
message = String::NewFromUtf8(env->isolate(), errmsg,
v8::NewStringType::kNormal)
.ToLocalChecked(); .ToLocalChecked();
} else { CryptoErrorVector errors;
message = String::NewFromUtf8(env->isolate(), default_message, errors.Capture();
v8::NewStringType::kNormal) auto exception = errors.ToException(env, exception_string);
.ToLocalChecked();
}
Local<Value> exception_v = Exception::Error(message);
CHECK(!exception_v.IsEmpty());
Local<Object> exception = exception_v.As<Object>();
std::vector<Local<String>> errors;
for (;;) {
unsigned long err = ERR_get_error(); // NOLINT(runtime/int)
if (err == 0) {
break;
}
char tmp_str[256];
ERR_error_string_n(err, tmp_str, sizeof(tmp_str));
errors.push_back(String::NewFromUtf8(env->isolate(), tmp_str,
v8::NewStringType::kNormal)
.ToLocalChecked());
}
// ERR_get_error returns errors in order of most specific to least
// specific. We wish to have the reverse ordering:
// opensslErrorStack: [
// 'error:0906700D:PEM routines:PEM_ASN1_read_bio:ASN1 lib',
// 'error:0D07803A:asn1 encoding routines:ASN1_ITEM_EX_D2I:nested asn1 err'
// ]
if (!errors.empty()) {
std::reverse(errors.begin(), errors.end());
Local<Array> errors_array = Array::New(env->isolate(), errors.size());
for (size_t i = 0; i < errors.size(); i++) {
errors_array->Set(env->context(), i, errors[i]).FromJust();
}
exception->Set(env->context(), env->openssl_error_stack(), errors_array)
.FromJust();
}
env->isolate()->ThrowException(exception); env->isolate()->ThrowException(exception);
} }
@ -4529,6 +4548,43 @@ bool ECDH::IsKeyPairValid() {
} }
struct CryptoJob : public ThreadPoolWork {
Environment* const env;
std::unique_ptr<AsyncWrap> async_wrap;
inline explicit CryptoJob(Environment* env) : ThreadPoolWork(env), env(env) {}
inline void AfterThreadPoolWork(int status) final;
virtual void AfterThreadPoolWork() = 0;
static inline void Run(std::unique_ptr<CryptoJob> job, Local<Value> wrap);
};
void CryptoJob::AfterThreadPoolWork(int status) {
CHECK(status == 0 || status == UV_ECANCELED);
std::unique_ptr<CryptoJob> job(this);
if (status == UV_ECANCELED) return;
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
CHECK_EQ(false, async_wrap->persistent().IsWeak());
AfterThreadPoolWork();
}
void CryptoJob::Run(std::unique_ptr<CryptoJob> job, Local<Value> wrap) {
CHECK(wrap->IsObject());
CHECK_EQ(nullptr, job->async_wrap);
job->async_wrap.reset(Unwrap<AsyncWrap>(wrap.As<Object>()));
CHECK_EQ(false, job->async_wrap->persistent().IsWeak());
job->ScheduleWork();
job.release(); // Run free, little job!
}
inline void CopyBuffer(Local<Value> buf, std::vector<char>* vec) {
vec->clear();
if (auto p = Buffer::Data(buf)) vec->assign(p, p + Buffer::Length(buf));
}
class PBKDF2Request : public AsyncWrap, public ThreadPoolWork { class PBKDF2Request : public AsyncWrap, public ThreadPoolWork {
public: public:
PBKDF2Request(Environment* env, PBKDF2Request(Environment* env,
@ -4870,6 +4926,98 @@ void RandomBytesBuffer(const FunctionCallbackInfo<Value>& args) {
} }
#ifndef OPENSSL_NO_SCRYPT
struct ScryptJob : public CryptoJob {
unsigned char* keybuf_data;
size_t keybuf_size;
std::vector<char> pass;
std::vector<char> salt;
uint32_t N;
uint32_t r;
uint32_t p;
uint32_t maxmem;
CryptoErrorVector errors;
inline explicit ScryptJob(Environment* env) : CryptoJob(env) {}
inline ~ScryptJob() override {
Cleanse();
}
inline bool Validate() {
if (1 == EVP_PBE_scrypt(nullptr, 0, nullptr, 0, N, r, p, maxmem,
nullptr, 0)) {
return true;
} else {
// Note: EVP_PBE_scrypt() does not always put errors on the error stack.
errors.Capture();
return false;
}
}
inline void DoThreadPoolWork() override {
auto salt_data = reinterpret_cast<const unsigned char*>(salt.data());
if (1 != EVP_PBE_scrypt(pass.data(), pass.size(), salt_data, salt.size(),
N, r, p, maxmem, keybuf_data, keybuf_size)) {
errors.Capture();
}
}
inline void AfterThreadPoolWork() override {
Local<Value> arg = ToResult();
async_wrap->MakeCallback(env->ondone_string(), 1, &arg);
}
inline Local<Value> ToResult() const {
if (errors.empty()) return Undefined(env->isolate());
return errors.ToException(env);
}
inline void Cleanse() {
OPENSSL_cleanse(pass.data(), pass.size());
OPENSSL_cleanse(salt.data(), salt.size());
pass.clear();
salt.clear();
}
};
void Scrypt(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsArrayBufferView()); // keybuf; wrap object retains ref.
CHECK(args[1]->IsArrayBufferView()); // pass
CHECK(args[2]->IsArrayBufferView()); // salt
CHECK(args[3]->IsUint32()); // N
CHECK(args[4]->IsUint32()); // r
CHECK(args[5]->IsUint32()); // p
CHECK(args[6]->IsUint32()); // maxmem
CHECK(args[7]->IsObject() || args[7]->IsUndefined()); // wrap object
std::unique_ptr<ScryptJob> job(new ScryptJob(env));
job->keybuf_data = reinterpret_cast<unsigned char*>(Buffer::Data(args[0]));
job->keybuf_size = Buffer::Length(args[0]);
CopyBuffer(args[1], &job->pass);
CopyBuffer(args[2], &job->salt);
job->N = args[3].As<Uint32>()->Value();
job->r = args[4].As<Uint32>()->Value();
job->p = args[5].As<Uint32>()->Value();
job->maxmem = args[6].As<Uint32>()->Value();
if (!job->Validate()) {
// EVP_PBE_scrypt() does not always put errors on the error stack
// and therefore ToResult() may or may not return an exception
// object. Return a sentinel value to inform JS land it should
// throw an ERR_CRYPTO_SCRYPT_PARAMETER_ERROR on our behalf.
auto result = job->ToResult();
if (result->IsUndefined()) result = Null(args.GetIsolate());
return args.GetReturnValue().Set(result);
}
if (args[7]->IsObject()) return ScryptJob::Run(std::move(job), args[7]);
env->PrintSyncTrace();
job->DoThreadPoolWork();
args.GetReturnValue().Set(job->ToResult());
}
#endif // OPENSSL_NO_SCRYPT
void GetSSLCiphers(const FunctionCallbackInfo<Value>& args) { void GetSSLCiphers(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args); Environment* env = Environment::GetCurrent(args);
@ -5293,6 +5441,9 @@ void Initialize(Local<Object> target,
PublicKeyCipher::Cipher<PublicKeyCipher::kPublic, PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
EVP_PKEY_verify_recover_init, EVP_PKEY_verify_recover_init,
EVP_PKEY_verify_recover>); EVP_PKEY_verify_recover>);
#ifndef OPENSSL_NO_SCRYPT
env->SetMethod(target, "scrypt", Scrypt);
#endif // OPENSSL_NO_SCRYPT
Local<FunctionTemplate> pb = FunctionTemplate::New(env->isolate()); Local<FunctionTemplate> pb = FunctionTemplate::New(env->isolate());
pb->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "PBKDF2")); pb->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "PBKDF2"));

View File

@ -523,6 +523,8 @@ class InternalCallbackScope {
class ThreadPoolWork { class ThreadPoolWork {
public: public:
explicit inline ThreadPoolWork(Environment* env) : env_(env) {} explicit inline ThreadPoolWork(Environment* env) : env_(env) {}
inline virtual ~ThreadPoolWork() = default;
inline void ScheduleWork(); inline void ScheduleWork();
inline int CancelWork(); inline int CancelWork();

View File

@ -0,0 +1,165 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const crypto = require('crypto');
if (typeof process.binding('crypto').scrypt !== 'function')
common.skip('no scrypt support');
const good = [
// Zero-length key is legal, functions as a parameter validation check.
{
pass: '',
salt: '',
keylen: 0,
N: 16,
p: 1,
r: 1,
expected: '',
},
// Test vectors from https://tools.ietf.org/html/rfc7914#page-13 that
// should pass. Note that the test vector with N=1048576 is omitted
// because it takes too long to complete and uses over 1 GB of memory.
{
pass: '',
salt: '',
keylen: 64,
N: 16,
p: 1,
r: 1,
expected:
'77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442' +
'fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906',
},
{
pass: 'password',
salt: 'NaCl',
keylen: 64,
N: 1024,
p: 16,
r: 8,
expected:
'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b373162' +
'2eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640',
},
{
pass: 'pleaseletmein',
salt: 'SodiumChloride',
keylen: 64,
N: 16384,
p: 1,
r: 8,
expected:
'7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2' +
'd5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887',
},
];
// Test vectors that should fail.
const bad = [
{ N: 1, p: 1, r: 1 }, // N < 2
{ N: 3, p: 1, r: 1 }, // Not power of 2.
{ N: 2 ** 16, p: 1, r: 1 }, // N >= 2**(r*16)
{ N: 2, p: 2 ** 30, r: 1 }, // p > (2**30-1)/r
];
// Test vectors where 128*N*r exceeds maxmem.
const toobig = [
{ N: 2 ** 20, p: 1, r: 8 },
{ N: 2 ** 10, p: 1, r: 8, maxmem: 2 ** 20 },
];
const badargs = [
{
args: [],
expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"password"/ },
},
{
args: [null],
expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"password"/ },
},
{
args: [''],
expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"salt"/ },
},
{
args: ['', null],
expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"salt"/ },
},
{
args: ['', ''],
expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"keylen"/ },
},
{
args: ['', '', null],
expected: { code: 'ERR_INVALID_ARG_TYPE', message: /"keylen"/ },
},
{
args: ['', '', .42],
expected: { code: 'ERR_OUT_OF_RANGE', message: /"keylen"/ },
},
{
args: ['', '', -42],
expected: { code: 'ERR_OUT_OF_RANGE', message: /"keylen"/ },
},
];
for (const options of good) {
const { pass, salt, keylen, expected } = options;
const actual = crypto.scryptSync(pass, salt, keylen, options);
assert.strictEqual(actual.toString('hex'), expected);
crypto.scrypt(pass, salt, keylen, options, common.mustCall((err, actual) => {
assert.ifError(err);
assert.strictEqual(actual.toString('hex'), expected);
}));
}
for (const options of bad) {
const expected = {
code: 'ERR_CRYPTO_SCRYPT_INVALID_PARAMETER',
message: 'Invalid scrypt parameter',
type: Error,
};
common.expectsError(() => crypto.scrypt('pass', 'salt', 1, options, () => {}),
expected);
common.expectsError(() => crypto.scryptSync('pass', 'salt', 1, options),
expected);
}
for (const options of toobig) {
const expected = {
message: /error:[^:]+:digital envelope routines:EVP_PBE_scrypt:memory limit exceeded/,
type: Error,
};
common.expectsError(() => crypto.scrypt('pass', 'salt', 1, options, () => {}),
expected);
common.expectsError(() => crypto.scryptSync('pass', 'salt', 1, options),
expected);
}
{
const defaults = { N: 16384, p: 1, r: 8 };
const expected = crypto.scryptSync('pass', 'salt', 1, defaults);
const actual = crypto.scryptSync('pass', 'salt', 1);
assert.deepStrictEqual(actual.toString('hex'), expected.toString('hex'));
crypto.scrypt('pass', 'salt', 1, common.mustCall((err, actual) => {
assert.ifError(err);
assert.deepStrictEqual(actual.toString('hex'), expected.toString('hex'));
}));
}
for (const { args, expected } of badargs) {
common.expectsError(() => crypto.scrypt(...args), expected);
common.expectsError(() => crypto.scryptSync(...args), expected);
}
{
const expected = { code: 'ERR_INVALID_CALLBACK' };
common.expectsError(() => crypto.scrypt('', '', 42, null), expected);
common.expectsError(() => crypto.scrypt('', '', 42, {}, null), expected);
common.expectsError(() => crypto.scrypt('', '', 42, {}), expected);
common.expectsError(() => crypto.scrypt('', '', 42, {}, {}), expected);
}

View File

@ -122,6 +122,12 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check
crypto.randomBytes(1, common.mustCall(function rb() { crypto.randomBytes(1, common.mustCall(function rb() {
testInitialized(this, 'RandomBytes'); testInitialized(this, 'RandomBytes');
})); }));
if (typeof process.binding('crypto').scrypt === 'function') {
crypto.scrypt('password', 'salt', 8, common.mustCall(function() {
testInitialized(this, 'AsyncWrap');
}));
}
} }