Skip to content

Commit c9b4592

Browse files
bnoordhuistargos
authored andcommitted
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: #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]>
1 parent 4957562 commit c9b4592

File tree

13 files changed

+617
-57
lines changed

13 files changed

+617
-57
lines changed

‎doc/api/crypto.md‎

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,9 +1356,9 @@ password always creates the same key. The low iteration count and
13561356
non-cryptographically secure hash algorithm allow passwords to be tested very
13571357
rapidly.
13581358

1359-
In line with OpenSSL's recommendation to use PBKDF2 instead of
1359+
In line with OpenSSL's recommendation to use a more modern algorithm instead of
13601360
[`EVP_BytesToKey`][] it is recommended that developers derive a key and IV on
1361-
their own using [`crypto.pbkdf2()`][] and to use [`crypto.createCipheriv()`][]
1361+
their own using [`crypto.scrypt()`][] and to use [`crypto.createCipheriv()`][]
13621362
to create the `Cipher` object. Users should not use ciphers with counter mode
13631363
(e.g. CTR, GCM, or CCM) in `crypto.createCipher()`. A warning is emitted when
13641364
they are used in order to avoid the risk of IV reuse that causes
@@ -1458,9 +1458,9 @@ password always creates the same key. The low iteration count and
14581458
non-cryptographically secure hash algorithm allow passwords to be tested very
14591459
rapidly.
14601460

1461-
In line with OpenSSL's recommendation to use PBKDF2 instead of
1461+
In line with OpenSSL's recommendation to use a more modern algorithm instead of
14621462
[`EVP_BytesToKey`][] it is recommended that developers derive a key and IV on
1463-
their own using [`crypto.pbkdf2()`][] and to use [`crypto.createDecipheriv()`][]
1463+
their own using [`crypto.scrypt()`][] and to use [`crypto.createDecipheriv()`][]
14641464
to create the `Decipher` object.
14651465

14661466
### crypto.createDecipheriv(algorithm, key, iv[, options])
@@ -1796,9 +1796,8 @@ The `iterations` argument must be a number set as high as possible. The
17961796
higher the number of iterations, the more secure the derived key will be,
17971797
but will take a longer amount of time to complete.
17981798

1799-
The `salt` should also be as unique as possible. It is recommended that the
1800-
salts are random and their lengths are at least 16 bytes. See
1801-
[NIST SP 800-132][] for details.
1799+
The `salt` should be as unique as possible. It is recommended that a salt is
1800+
random and at least 16 bytes long. See [NIST SP 800-132][] for details.
18021801

18031802
Example:
18041803

@@ -1862,9 +1861,8 @@ The `iterations` argument must be a number set as high as possible. The
18621861
higher the number of iterations, the more secure the derived key will be,
18631862
but will take a longer amount of time to complete.
18641863

1865-
The `salt` should also be as unique as possible. It is recommended that the
1866-
salts are random and their lengths are at least 16 bytes. See
1867-
[NIST SP 800-132][] for details.
1864+
The `salt` should be as unique as possible. It is recommended that a salt is
1865+
random and at least 16 bytes long. See [NIST SP 800-132][] for details.
18681866

18691867
Example:
18701868

@@ -2138,6 +2136,91 @@ threadpool request. To minimize threadpool task length variation, partition
21382136
large `randomFill` requests when doing so as part of fulfilling a client
21392137
request.
21402138

2139+
### crypto.scrypt(password, salt, keylen[, options], callback)
2140+
<!-- YAML
2141+
added: REPLACEME
2142+
-->
2143+
-`password`{string|Buffer|TypedArray}
2144+
-`salt`{string|Buffer|TypedArray}
2145+
-`keylen`{number}
2146+
-`options`{Object}
2147+
-`N`{number} CPU/memory cost parameter. Must be a power of two greater
2148+
than one. **Default:**`16384`.
2149+
-`r`{number} Block size parameter. **Default:**`8`.
2150+
-`p`{number} Parallelization parameter. **Default:**`1`.
2151+
-`maxmem`{number} Memory upper bound. It is an error when (approximately)
2152+
`128*N*r > maxmem`**Default:**`32 * 1024 * 1024`.
2153+
-`callback`{Function}
2154+
-`err`{Error}
2155+
-`derivedKey`{Buffer}
2156+
2157+
Provides an asynchronous [scrypt][] implementation. Scrypt is a password-based
2158+
key derivation function that is designed to be expensive computationally and
2159+
memory-wise in order to make brute-force attacks unrewarding.
2160+
2161+
The `salt` should be as unique as possible. It is recommended that a salt is
2162+
random and at least 16 bytes long. See [NIST SP 800-132][] for details.
2163+
2164+
The `callback` function is called with two arguments: `err` and `derivedKey`.
2165+
`err` is an exception object when key derivation fails, otherwise `err` is
2166+
`null`. `derivedKey` is passed to the callback as a [`Buffer`][].
2167+
2168+
An exception is thrown when any of the input arguments specify invalid values
2169+
or types.
2170+
2171+
```js
2172+
constcrypto=require('crypto');
2173+
// Using the factory defaults.
2174+
crypto.scrypt('secret', 'salt', 64, (err, derivedKey) =>{
2175+
if (err) throw err;
2176+
console.log(derivedKey.toString('hex')); // '3745e48...08d59ae'
2177+
});
2178+
// Using a custom N parameter. Must be a power of two.
2179+
crypto.scrypt('secret', 'salt', 64,{N:1024 }, (err, derivedKey) =>{
2180+
if (err) throw err;
2181+
console.log(derivedKey.toString('hex')); // '3745e48...aa39b34'
2182+
});
2183+
```
2184+
2185+
### crypto.scryptSync(password, salt, keylen[, options])
2186+
<!-- YAML
2187+
added: REPLACEME
2188+
-->
2189+
-`password`{string|Buffer|TypedArray}
2190+
-`salt`{string|Buffer|TypedArray}
2191+
-`keylen`{number}
2192+
-`options`{Object}
2193+
-`N`{number} CPU/memory cost parameter. Must be a power of two greater
2194+
than one. **Default:**`16384`.
2195+
-`r`{number} Block size parameter. **Default:**`8`.
2196+
-`p`{number} Parallelization parameter. **Default:**`1`.
2197+
-`maxmem`{number} Memory upper bound. It is an error when (approximately)
2198+
`128*N*r > maxmem`**Default:**`32 * 1024 * 1024`.
2199+
- Returns:{Buffer}
2200+
2201+
Provides a synchronous [scrypt][] implementation. Scrypt is a password-based
2202+
key derivation function that is designed to be expensive computationally and
2203+
memory-wise in order to make brute-force attacks unrewarding.
2204+
2205+
The `salt` should be as unique as possible. It is recommended that a salt is
2206+
random and at least 16 bytes long. See [NIST SP 800-132][] for details.
2207+
2208+
An exception is thrown when key derivation fails, otherwise the derived key is
2209+
returned as a [`Buffer`][].
2210+
2211+
An exception is thrown when any of the input arguments specify invalid values
2212+
or types.
2213+
2214+
```js
2215+
constcrypto=require('crypto');
2216+
// Using the factory defaults.
2217+
constkey1=crypto.scryptSync('secret', 'salt', 64);
2218+
console.log(key1.toString('hex')); // '3745e48...08d59ae'
2219+
// Using a custom N parameter. Must be a power of two.
2220+
constkey2=crypto.scryptSync('secret', 'salt', 64,{N:1024 });
2221+
console.log(key2.toString('hex')); // '3745e48...aa39b34'
2222+
```
2223+
21412224
### crypto.setEngine(engine[, flags])
21422225
<!-- YAML
21432226
added: v0.11.11
@@ -2645,9 +2728,9 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL.
26452728
[`crypto.createVerify()`]: #crypto_crypto_createverify_algorithm_options
26462729
[`crypto.getCurves()`]: #crypto_crypto_getcurves
26472730
[`crypto.getHashes()`]: #crypto_crypto_gethashes
2648-
[`crypto.pbkdf2()`]: #crypto_crypto_pbkdf2_password_salt_iterations_keylen_digest_callback
26492731
[`crypto.randomBytes()`]: #crypto_crypto_randombytes_size_callback
26502732
[`crypto.randomFill()`]: #crypto_crypto_randomfill_buffer_offset_size_callback
2733+
[`crypto.scrypt()`]: #crypto_crypto_scrypt_password_salt_keylen_options_callback
26512734
[`decipher.final()`]: #crypto_decipher_final_outputencoding
26522735
[`decipher.update()`]: #crypto_decipher_update_data_inputencoding_outputencoding
26532736
[`diffieHellman.setPublicKey()`]: #crypto_diffiehellman_setpublickey_publickey_encoding
@@ -2681,5 +2764,6 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL.
26812764
[RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt
26822765
[RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt
26832766
[initialization vector]: https://en.wikipedia.org/wiki/Initialization_vector
2767+
[scrypt]: https://en.wikipedia.org/wiki/Scrypt
26842768
[stream-writable-write]: stream.html#stream_writable_write_chunk_encoding_callback
26852769
[stream]: stream.html

‎doc/api/errors.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,18 @@ An invalid [crypto digest algorithm][] was specified.
739739
A crypto method was used on an object that was in an invalid state. For
740740
instance, calling [`cipher.getAuthTag()`][] before calling `cipher.final()`.
741741

742+
<aid="ERR_CRYPTO_SCRYPT_INVALID_PARAMETER"></a>
743+
### ERR_CRYPTO_SCRYPT_INVALID_PARAMETER
744+
745+
One or more [`crypto.scrypt()`][] or [`crypto.scryptSync()`][] parameters are
746+
outside their legal range.
747+
748+
<aid="ERR_CRYPTO_SCRYPT_NOT_SUPPORTED"></a>
749+
### ERR_CRYPTO_SCRYPT_NOT_SUPPORTED
750+
751+
Node.js was compiled without `scrypt` support. Not possible with the official
752+
release binaries but can happen with custom builds, including distro builds.
753+
742754
<aid="ERR_CRYPTO_SIGN_KEY_REQUIRED"></a>
743755
### ERR_CRYPTO_SIGN_KEY_REQUIRED
744756

@@ -1750,6 +1762,8 @@ Creation of a [`zlib`][] object failed due to incorrect configuration.
17501762
[`child_process`]: child_process.html
17511763
[`cipher.getAuthTag()`]: crypto.html#crypto_cipher_getauthtag
17521764
[`Class: assert.AssertionError`]: assert.html#assert_class_assert_assertionerror
1765+
[`crypto.scrypt()`]: crypto.html#crypto_crypto_scrypt_password_salt_keylen_options_callback
1766+
[`crypto.scryptSync()`]: crypto.html#crypto_crypto_scryptSync_password_salt_keylen_options
17531767
[`crypto.timingSafeEqual()`]: crypto.html#crypto_crypto_timingsafeequal_a_b
17541768
[`dgram.createSocket()`]: dgram.html#dgram_dgram_createsocket_options_callback
17551769
[`ERR_INVALID_ARG_TYPE`]: #ERR_INVALID_ARG_TYPE

‎lib/crypto.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ const{
5252
pbkdf2,
5353
pbkdf2Sync
5454
}=require('internal/crypto/pbkdf2');
55+
const{
56+
scrypt,
57+
scryptSync
58+
}=require('internal/crypto/scrypt');
5559
const{
5660
DiffieHellman,
5761
DiffieHellmanGroup,
@@ -163,6 +167,8 @@ module.exports = exports ={
163167
randomFill,
164168
randomFillSync,
165169
rng: randomBytes,
170+
scrypt,
171+
scryptSync,
166172
setEngine,
167173
timingSafeEqual,
168174
getFips: !fipsMode ? getFipsDisabled :

‎lib/internal/crypto/scrypt.js‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use strict';
2+
3+
const{ AsyncWrap, Providers }=process.binding('async_wrap');
4+
const{ Buffer }=require('buffer');
5+
const{scrypt: _scrypt}=process.binding('crypto');
6+
const{
7+
ERR_CRYPTO_SCRYPT_INVALID_PARAMETER,
8+
ERR_CRYPTO_SCRYPT_NOT_SUPPORTED,
9+
ERR_INVALID_CALLBACK,
10+
}=require('internal/errors').codes;
11+
const{
12+
checkIsArrayBufferView,
13+
checkIsUint,
14+
getDefaultEncoding,
15+
}=require('internal/crypto/util');
16+
17+
constdefaults={
18+
N: 16384,
19+
r: 8,
20+
p: 1,
21+
maxmem: 32<<20,// 32 MB, matches SCRYPT_MAX_MEM.
22+
};
23+
24+
functionscrypt(password,salt,keylen,options,callback=defaults){
25+
if(callback===defaults){
26+
callback=options;
27+
options=defaults;
28+
}
29+
30+
options=check(password,salt,keylen,options);
31+
const{ N, r, p, maxmem }=options;
32+
({ password, salt, keylen }=options);
33+
34+
if(typeofcallback!=='function')
35+
thrownewERR_INVALID_CALLBACK();
36+
37+
constencoding=getDefaultEncoding();
38+
constkeybuf=Buffer.alloc(keylen);
39+
40+
constwrap=newAsyncWrap(Providers.SCRYPTREQUEST);
41+
wrap.ondone=(ex)=>{// Retains keybuf while request is in flight.
42+
if(ex)returncallback.call(wrap,ex);
43+
if(encoding==='buffer')returncallback.call(wrap,null,keybuf);
44+
callback.call(wrap,null,keybuf.toString(encoding));
45+
};
46+
47+
handleError(keybuf,password,salt,N,r,p,maxmem,wrap);
48+
}
49+
50+
functionscryptSync(password,salt,keylen,options=defaults){
51+
options=check(password,salt,keylen,options);
52+
const{ N, r, p, maxmem }=options;
53+
({ password, salt, keylen }=options);
54+
constkeybuf=Buffer.alloc(keylen);
55+
handleError(keybuf,password,salt,N,r,p,maxmem);
56+
constencoding=getDefaultEncoding();
57+
if(encoding==='buffer')returnkeybuf;
58+
returnkeybuf.toString(encoding);
59+
}
60+
61+
functionhandleError(keybuf,password,salt,N,r,p,maxmem,wrap){
62+
constex=_scrypt(keybuf,password,salt,N,r,p,maxmem,wrap);
63+
64+
if(ex===undefined)
65+
return;
66+
67+
if(ex===null)
68+
thrownewERR_CRYPTO_SCRYPT_INVALID_PARAMETER();// Bad N, r, p, or maxmem.
69+
70+
throwex;// Scrypt operation failed, exception object contains details.
71+
}
72+
73+
functioncheck(password,salt,keylen,options,callback){
74+
if(_scrypt===undefined)
75+
thrownewERR_CRYPTO_SCRYPT_NOT_SUPPORTED();
76+
77+
password=checkIsArrayBufferView('password',password);
78+
salt=checkIsArrayBufferView('salt',salt);
79+
keylen=checkIsUint('keylen',keylen);
80+
81+
let{ N, r, p, maxmem }=defaults;
82+
if(options&&options!==defaults){
83+
if(options.hasOwnProperty('N'))N=checkIsUint('N',options.N);
84+
if(options.hasOwnProperty('r'))r=checkIsUint('r',options.r);
85+
if(options.hasOwnProperty('p'))p=checkIsUint('p',options.p);
86+
if(options.hasOwnProperty('maxmem'))
87+
maxmem=checkIsUint('maxmem',options.maxmem);
88+
if(N===0)N=defaults.N;
89+
if(r===0)r=defaults.r;
90+
if(p===0)p=defaults.p;
91+
if(maxmem===0)maxmem=defaults.maxmem;
92+
}
93+
94+
return{ password, salt, keylen, N, r, p, maxmem };
95+
}
96+
97+
module.exports={ scrypt, scryptSync };

‎lib/internal/errors.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,8 @@ E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error);
503503
E('ERR_CRYPTO_HASH_UPDATE_FAILED','Hash update failed',Error);
504504
E('ERR_CRYPTO_INVALID_DIGEST','Invalid digest: %s',TypeError);
505505
E('ERR_CRYPTO_INVALID_STATE','Invalid state for operation %s',Error);
506-
506+
E('ERR_CRYPTO_SCRYPT_INVALID_PARAMETER','Invalid scrypt parameter',Error);
507+
E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED','Scrypt algorithm not supported',Error);
507508
// Switch to TypeError. The current implementation does not seem right.
508509
E('ERR_CRYPTO_SIGN_KEY_REQUIRED','No key provided to sign',Error);
509510
E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
'lib/internal/crypto/hash.js',
9898
'lib/internal/crypto/pbkdf2.js',
9999
'lib/internal/crypto/random.js',
100+
'lib/internal/crypto/scrypt.js',
100101
'lib/internal/crypto/sig.js',
101102
'lib/internal/crypto/util.js',
102103
'lib/internal/constants.js',

‎src/async_wrap.cc‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ using v8::PromiseHookType;
4545
using v8::PropertyCallbackInfo;
4646
using v8::RetainedObjectInfo;
4747
using v8::String;
48+
using v8::Uint32;
4849
using v8::Undefined;
4950
using v8::Value;
5051

@@ -133,6 +134,23 @@ RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local<Value> wrapper){
133134
// end RetainedAsyncInfo
134135

135136

137+
structAsyncWrapObject : publicAsyncWrap{
138+
staticinlinevoidNew(const FunctionCallbackInfo<Value>& args){
139+
Environment* env = Environment::GetCurrent(args);
140+
CHECK(args.IsConstructCall());
141+
CHECK(env->async_wrap_constructor_template()->HasInstance(args.This()));
142+
CHECK(args[0]->IsUint32());
143+
auto type = static_cast<ProviderType>(args[0].As<Uint32>()->Value());
144+
newAsyncWrapObject(env, args.This(), type);
145+
}
146+
147+
inlineAsyncWrapObject(Environment* env, Local<Object> object,
148+
ProviderType type) : AsyncWrap(env, object, type){}
149+
150+
inlinesize_tself_size() constoverride{returnsizeof(*this)}
151+
};
152+
153+
136154
staticvoidDestroyAsyncIdsCallback(Environment* env, void* data){
137155
Local<Function> fn = env->async_hooks_destroy_function();
138156

@@ -569,6 +587,19 @@ void AsyncWrap::Initialize(Local<Object> target,
569587
env->set_async_hooks_destroy_function(Local<Function>());
570588
env->set_async_hooks_promise_resolve_function(Local<Function>());
571589
env->set_async_hooks_binding(target);
590+
591+
{
592+
auto class_name = FIXED_ONE_BYTE_STRING(env->isolate(), "AsyncWrap");
593+
auto function_template = env->NewFunctionTemplate(AsyncWrapObject::New);
594+
function_template->SetClassName(class_name);
595+
AsyncWrap::AddWrapMethods(env, function_template);
596+
auto instance_template = function_template->InstanceTemplate();
597+
instance_template->SetInternalFieldCount(1);
598+
auto function =
599+
function_template->GetFunction(env->context()).ToLocalChecked();
600+
target->Set(env->context(), class_name, function).FromJust();
601+
env->set_async_wrap_constructor_template(function_template);
602+
}
572603
}
573604

574605

‎src/async_wrap.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ namespace node{
7575
#defineNODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) \
7676
V(PBKDF2REQUEST) \
7777
V(RANDOMBYTESREQUEST) \
78+
V(SCRYPTREQUEST) \
7879
V(TLSWRAP)
7980
#else
8081
#defineNODE_ASYNC_CRYPTO_PROVIDER_TYPES(V)

‎src/env.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ struct PackageConfig{
319319
V(async_hooks_destroy_function, v8::Function) \
320320
V(async_hooks_init_function, v8::Function) \
321321
V(async_hooks_promise_resolve_function, v8::Function) \
322+
V(async_wrap_constructor_template, v8::FunctionTemplate) \
322323
V(buffer_prototype_object, v8::Object) \
323324
V(context, v8::Context) \
324325
V(domain_callback, v8::Function) \

0 commit comments

Comments
(0)