Skip to content

Commit

Permalink
crypto: add scrypt() and scryptSync() methods
Browse files Browse the repository at this point in the history
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: #8417
PR-URL: #20816
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: Colin Ihrig <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Tobias Nießen <[email protected]>
  • Loading branch information
bnoordhuis committed Jun 13, 2018
1 parent 58176e3 commit 371103d
Show file tree
Hide file tree
Showing 13 changed files with 617 additions and 57 deletions.
106 changes: 95 additions & 11 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
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
(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
Expand Down Expand Up @@ -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
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
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.

### crypto.createDecipheriv(algorithm, key, iv[, options])
Expand Down Expand Up @@ -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,
but will take a longer amount of time to complete.

The `salt` should also be as unique as possible. It is recommended that the
salts are random and their lengths are at least 16 bytes. See
[NIST SP 800-132][] for details.
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.

Example:

Expand Down Expand Up @@ -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,
but will take a longer amount of time to complete.

The `salt` should also be as unique as possible. It is recommended that the
salts are random and their lengths are at least 16 bytes. See
[NIST SP 800-132][] for details.
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.

Example:

Expand Down Expand Up @@ -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
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])
<!-- YAML
added: v0.11.11
Expand Down Expand Up @@ -2650,9 +2733,9 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL.
[`crypto.createVerify()`]: #crypto_crypto_createverify_algorithm_options
[`crypto.getCurves()`]: #crypto_crypto_getcurves
[`crypto.getHashes()`]: #crypto_crypto_gethashes
[`crypto.pbkdf2()`]: #crypto_crypto_pbkdf2_password_salt_iterations_keylen_digest_callback
[`crypto.randomBytes()`]: #crypto_crypto_randombytes_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.update()`]: #crypto_decipher_update_data_inputencoding_outputencoding
[`diffieHellman.setPublicKey()`]: #crypto_diffiehellman_setpublickey_publickey_encoding
Expand Down Expand Up @@ -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 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt
[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]: stream.html
14 changes: 14 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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>
### ERR_CRYPTO_SIGN_KEY_REQUIRED

Expand Down Expand Up @@ -1749,6 +1761,8 @@ Creation of a [`zlib`][] object failed due to incorrect configuration.
[`child_process`]: child_process.html
[`cipher.getAuthTag()`]: crypto.html#crypto_cipher_getauthtag
[`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
[`dgram.createSocket()`]: dgram.html#dgram_dgram_createsocket_options_callback
[`ERR_INVALID_ARG_TYPE`]: #ERR_INVALID_ARG_TYPE
Expand Down
6 changes: 6 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ const {
pbkdf2,
pbkdf2Sync
} = require('internal/crypto/pbkdf2');
const {
scrypt,
scryptSync
} = require('internal/crypto/scrypt');
const {
DiffieHellman,
DiffieHellmanGroup,
Expand Down Expand Up @@ -163,6 +167,8 @@ module.exports = exports = {
randomFill,
randomFillSync,
rng: randomBytes,
scrypt,
scryptSync,
setEngine,
timingSafeEqual,
getFips: !fipsMode ? getFipsDisabled :
Expand Down
97 changes: 97 additions & 0 deletions lib/internal/crypto/scrypt.js
Original file line number Diff line number Diff line change
@@ -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 };
3 changes: 2 additions & 1 deletion lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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_INVALID_DIGEST', 'Invalid digest: %s', TypeError);
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.
E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error);
E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
'lib/internal/crypto/hash.js',
'lib/internal/crypto/pbkdf2.js',
'lib/internal/crypto/random.js',
'lib/internal/crypto/scrypt.js',
'lib/internal/crypto/sig.js',
'lib/internal/crypto/util.js',
'lib/internal/constants.js',
Expand Down
31 changes: 31 additions & 0 deletions src/async_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ using v8::PromiseHookType;
using v8::PropertyCallbackInfo;
using v8::RetainedObjectInfo;
using v8::String;
using v8::Uint32;
using v8::Undefined;
using v8::Value;

Expand Down Expand Up @@ -133,6 +134,23 @@ RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local<Value> wrapper) {
// 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) {
Local<Function> fn = env->async_hooks_destroy_function();

Expand Down Expand Up @@ -569,6 +587,19 @@ void AsyncWrap::Initialize(Local<Object> target,
env->set_async_hooks_destroy_function(Local<Function>());
env->set_async_hooks_promise_resolve_function(Local<Function>());
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);
}
}


Expand Down
1 change: 1 addition & 0 deletions src/async_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ namespace node {
#define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) \
V(PBKDF2REQUEST) \
V(RANDOMBYTESREQUEST) \
V(SCRYPTREQUEST) \
V(TLSWRAP)
#else
#define NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V)
Expand Down
1 change: 1 addition & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ struct PackageConfig {
V(async_hooks_destroy_function, v8::Function) \
V(async_hooks_init_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(context, v8::Context) \
V(domain_callback, v8::Function) \
Expand Down
Loading

0 comments on commit 371103d

Please sign in to comment.