Skip to content

crypto: add scrypt() and scryptSync() methods #20816

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
20 changes: 20 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,24 @@ 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_PBKDF2_ERROR"></a>
### ERR_CRYPTO_PBKDF2_ERROR

The PBKDF2 algorithm failed for unspecified reasons. OpenSSL does not provide
more details and therefore neither does Node.js.

<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 +1767,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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to actually lazy load this as it is definitely not always required when loading crypto.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could say the same thing for pbkdf2 and others. Comment noted but I'd like to save that for another PR.

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
99 changes: 46 additions & 53 deletions lib/internal/crypto/pbkdf2.js
Original file line number Diff line number Diff line change
@@ -1,85 +1,78 @@
'use strict';

const { AsyncWrap, Providers } = process.binding('async_wrap');
const { Buffer } = require('buffer');
const { INT_MAX, pbkdf2: _pbkdf2 } = process.binding('crypto');
const { validateInt32 } = require('internal/validators');
const {
ERR_CRYPTO_INVALID_DIGEST,
ERR_CRYPTO_PBKDF2_ERROR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_CALLBACK,
ERR_CRYPTO_INVALID_DIGEST,
ERR_OUT_OF_RANGE
} = require('internal/errors').codes;
const {
checkIsArrayBufferView,
getDefaultEncoding,
toBuf
validateArrayBufferView,
} = require('internal/crypto/util');
const {
PBKDF2
} = process.binding('crypto');
const {
INT_MAX
} = process.binding('constants').crypto;

function pbkdf2(password, salt, iterations, keylen, digest, callback) {
if (typeof digest === 'function') {
callback = digest;
digest = undefined;
}

({ password, salt, iterations, keylen, digest } =
check(password, salt, iterations, keylen, digest, callback));

if (typeof callback !== 'function')
throw new ERR_INVALID_CALLBACK();

return _pbkdf2(password, salt, iterations, keylen, digest, callback);
const encoding = getDefaultEncoding();
const keybuf = Buffer.alloc(keylen);

const wrap = new AsyncWrap(Providers.PBKDF2REQUEST);
wrap.ondone = (ok) => { // Retains keybuf while request is in flight.
if (!ok) return callback.call(wrap, new ERR_CRYPTO_PBKDF2_ERROR());
if (encoding === 'buffer') return callback.call(wrap, null, keybuf);
callback.call(wrap, null, keybuf.toString(encoding));
};

handleError(keybuf, password, salt, iterations, digest, wrap);
}

function pbkdf2Sync(password, salt, iterations, keylen, digest) {
return _pbkdf2(password, salt, iterations, keylen, digest);
({ password, salt, iterations, keylen, digest } =
check(password, salt, iterations, keylen, digest, pbkdf2Sync));
const keybuf = Buffer.alloc(keylen);
handleError(keybuf, password, salt, iterations, digest);
const encoding = getDefaultEncoding();
if (encoding === 'buffer') return keybuf;
return keybuf.toString(encoding);
}

function _pbkdf2(password, salt, iterations, keylen, digest, callback) {

if (digest !== null && typeof digest !== 'string')
throw new ERR_INVALID_ARG_TYPE('digest', ['string', 'null'], digest);

password = checkIsArrayBufferView('password', toBuf(password));
salt = checkIsArrayBufferView('salt', toBuf(salt));

if (typeof iterations !== 'number')
throw new ERR_INVALID_ARG_TYPE('iterations', 'number', iterations);

if (iterations < 0)
throw new ERR_OUT_OF_RANGE('iterations',
'a non-negative number',
iterations);
function check(password, salt, iterations, keylen, digest, callback) {
if (typeof digest !== 'string') {
if (digest !== null)
throw new ERR_INVALID_ARG_TYPE('digest', ['string', 'null'], digest);
digest = 'sha1';
}

if (typeof keylen !== 'number')
throw new ERR_INVALID_ARG_TYPE('keylen', 'number', keylen);
password = validateArrayBufferView(password, 'password');
salt = validateArrayBufferView(salt, 'salt');
iterations = validateInt32(iterations, 'iterations', 0, INT_MAX);
keylen = validateInt32(keylen, 'keylen', 0, INT_MAX);

if (keylen < 0 || !Number.isInteger(keylen) || keylen > INT_MAX)
throw new ERR_OUT_OF_RANGE('keylen', `>= 0 && <= ${INT_MAX}`, keylen);
return { password, salt, iterations, keylen, digest };
}

const encoding = getDefaultEncoding();
function handleError(keybuf, password, salt, iterations, digest, wrap) {
const rc = _pbkdf2(keybuf, password, salt, iterations, digest, wrap);

if (encoding === 'buffer') {
const ret = PBKDF2(password, salt, iterations, keylen, digest, callback);
if (ret === -1)
throw new ERR_CRYPTO_INVALID_DIGEST(digest);
return ret;
}
if (rc === -1)
throw new ERR_CRYPTO_INVALID_DIGEST(digest);

// at this point, we need to handle encodings.
if (callback) {
function next(er, ret) {
if (ret)
ret = ret.toString(encoding);
callback(er, ret);
}
if (PBKDF2(password, salt, iterations, keylen, digest, next) === -1)
throw new ERR_CRYPTO_INVALID_DIGEST(digest);
} else {
const ret = PBKDF2(password, salt, iterations, keylen, digest);
if (ret === -1)
throw new ERR_CRYPTO_INVALID_DIGEST(digest);
return ret.toString(encoding);
}
if (rc === false)
throw new ERR_CRYPTO_PBKDF2_ERROR();
}

module.exports = {
Expand Down
Loading