crypto: add randomFill and randomFillSync

crypto.randomFill and crypto.randomFillSync are similar to
crypto.randomBytes, but allow passing in a buffer as the first
argument. This allows us to reuse buffers to prevent having to
create a new one on every call.

PR-URL: https://github.com/nodejs/node/pull/10209
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
Evan Lucas 2016-12-09 17:13:14 -06:00
parent ade80eeb1a
commit d06eb530a1
5 changed files with 433 additions and 14 deletions

View File

@ -1713,6 +1713,66 @@ This should normally never take longer than a few milliseconds. The only time
when generating the random bytes may conceivably block for a longer period of when generating the random bytes may conceivably block for a longer period of
time is right after boot, when the whole system is still low on entropy. time is right after boot, when the whole system is still low on entropy.
### crypto.randomFillSync(buf[, offset][, size])
<!-- YAML
added: REPLACEME
-->
* `buf` {Buffer|Uint8Array} Must be supplied.
* `offset` {number} Defaults to `0`.
* `size` {number} Defaults to `buf.length - offset`.
Synchronous version of [`crypto.randomFill()`][].
Returns `buf`
```js
const buf = Buffer.alloc(10);
console.log(crypto.randomFillSync(buf).toString('hex'));
crypto.randomFillSync(buf, 5);
console.log(buf.toString('hex'));
// The above is equivalent to the following:
crypto.randomFillSync(buf, 5, 5);
console.log(buf.toString('hex'));
```
### crypto.randomFill(buf[, offset][, size], callback)
<!-- YAML
added: REPLACEME
-->
* `buf` {Buffer|Uint8Array} Must be supplied.
* `offset` {number} Defaults to `0`.
* `size` {number} Defaults to `buf.length - offset`.
* `callback` {Function} `function(err, buf) {}`.
This function is similar to [`crypto.randomBytes()`][] but requires the first
argument to be a [`Buffer`][] that will be filled. It also
requires that a callback is passed in.
If the `callback` function is not provided, an error will be thrown.
```js
const buf = Buffer.alloc(10);
crypto.randomFill(buf, (err, buf) => {
if (err) throw err;
console.log(buf.toString('hex'));
});
crypto.randomFill(buf, 5, (err, buf) => {
if (err) throw err;
console.log(buf.toString('hex'));
});
// The above is equivalent to the following:
crypto.randomFill(buf, 5, 5, (err, buf) => {
if (err) throw err;
console.log(buf.toString('hex'));
});
```
### crypto.setEngine(engine[, flags]) ### crypto.setEngine(engine[, flags])
<!-- YAML <!-- YAML
added: v0.11.11 added: v0.11.11
@ -2152,6 +2212,8 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL.
[`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.pbkdf2()`]: #crypto_crypto_pbkdf2_password_salt_iterations_keylen_digest_callback
[`crypto.randomBytes()`]: #crypto_crypto_randombytes_size_callback
[`crypto.randomFill()`]: #crypto_crypto_randombytesbuffer_buf_size_offset_cb
[`decipher.final()`]: #crypto_decipher_final_output_encoding [`decipher.final()`]: #crypto_decipher_final_output_encoding
[`decipher.update()`]: #crypto_decipher_update_data_input_encoding_output_encoding [`decipher.update()`]: #crypto_decipher_update_data_input_encoding_output_encoding
[`diffieHellman.setPublicKey()`]: #crypto_diffiehellman_setpublickey_public_key_encoding [`diffieHellman.setPublicKey()`]: #crypto_diffiehellman_setpublickey_public_key_encoding

View File

@ -40,8 +40,10 @@ const setFipsCrypto = binding.setFipsCrypto;
const timingSafeEqual = binding.timingSafeEqual; const timingSafeEqual = binding.timingSafeEqual;
const Buffer = require('buffer').Buffer; const Buffer = require('buffer').Buffer;
const kBufferMaxLength = require('buffer').kMaxLength;
const stream = require('stream'); const stream = require('stream');
const util = require('util'); const util = require('util');
const { isUint8Array } = process.binding('util');
const LazyTransform = require('internal/streams/lazy_transform'); const LazyTransform = require('internal/streams/lazy_transform');
const DH_GENERATOR = 2; const DH_GENERATOR = 2;
@ -696,6 +698,74 @@ exports.setEngine = function setEngine(id, flags) {
return binding.setEngine(id, flags); return binding.setEngine(id, flags);
}; };
const kMaxUint32 = Math.pow(2, 32) - 1;
function randomFillSync(buf, offset = 0, size) {
if (!isUint8Array(buf)) {
throw new TypeError('"buf" argument must be a Buffer or Uint8Array');
}
assertOffset(offset, buf.length);
if (size === undefined) size = buf.length - offset;
assertSize(size, offset, buf.length);
return binding.randomFill(buf, offset, size);
}
exports.randomFillSync = randomFillSync;
function randomFill(buf, offset, size, cb) {
if (!isUint8Array(buf)) {
throw new TypeError('"buf" argument must be a Buffer or Uint8Array');
}
if (typeof offset === 'function') {
cb = offset;
offset = 0;
size = buf.length;
} else if (typeof size === 'function') {
cb = size;
size = buf.length - offset;
} else if (typeof cb !== 'function') {
throw new TypeError('"cb" argument must be a function');
}
assertOffset(offset, buf.length);
assertSize(size, offset, buf.length);
return binding.randomFill(buf, offset, size, cb);
}
exports.randomFill = randomFill;
function assertOffset(offset, length) {
if (typeof offset !== 'number' || offset !== offset) {
throw new TypeError('offset must be a number');
}
if (offset > kMaxUint32 || offset < 0) {
throw new TypeError('offset must be a uint32');
}
if (offset > kBufferMaxLength || offset > length) {
throw new RangeError('offset out of range');
}
}
function assertSize(size, offset, length) {
if (typeof size !== 'number' || size !== size) {
throw new TypeError('size must be a number');
}
if (size > kMaxUint32 || size < 0) {
throw new TypeError('size must be a uint32');
}
if (size + offset > length || size > kBufferMaxLength) {
throw new RangeError('buffer too small');
}
}
exports.randomBytes = exports.pseudoRandomBytes = randomBytes; exports.randomBytes = exports.pseudoRandomBytes = randomBytes;
exports.rng = exports.prng = randomBytes; exports.rng = exports.prng = randomBytes;

View File

@ -5574,11 +5574,18 @@ void PBKDF2(const FunctionCallbackInfo<Value>& args) {
// Only instantiate within a valid HandleScope. // Only instantiate within a valid HandleScope.
class RandomBytesRequest : public AsyncWrap { class RandomBytesRequest : public AsyncWrap {
public: public:
RandomBytesRequest(Environment* env, Local<Object> object, size_t size) enum FreeMode { FREE_DATA, DONT_FREE_DATA };
RandomBytesRequest(Environment* env,
Local<Object> object,
size_t size,
char* data,
FreeMode free_mode)
: AsyncWrap(env, object, AsyncWrap::PROVIDER_CRYPTO), : AsyncWrap(env, object, AsyncWrap::PROVIDER_CRYPTO),
error_(0), error_(0),
size_(size), size_(size),
data_(node::Malloc(size)) { data_(data),
free_mode_(free_mode) {
Wrap(object, this); Wrap(object, this);
} }
@ -5599,9 +5606,15 @@ class RandomBytesRequest : public AsyncWrap {
return data_; return data_;
} }
inline void set_data(char* data) {
data_ = data;
}
inline void release() { inline void release() {
free(data_);
size_ = 0; size_ = 0;
if (free_mode_ == FREE_DATA) {
free(data_);
}
} }
inline void return_memory(char** d, size_t* len) { inline void return_memory(char** d, size_t* len) {
@ -5627,6 +5640,7 @@ class RandomBytesRequest : public AsyncWrap {
unsigned long error_; // NOLINT(runtime/int) unsigned long error_; // NOLINT(runtime/int)
size_t size_; size_t size_;
char* data_; char* data_;
const FreeMode free_mode_;
}; };
@ -5665,7 +5679,18 @@ void RandomBytesCheck(RandomBytesRequest* req, Local<Value> argv[2]) {
size_t size; size_t size;
req->return_memory(&data, &size); req->return_memory(&data, &size);
argv[0] = Null(req->env()->isolate()); argv[0] = Null(req->env()->isolate());
argv[1] = Buffer::New(req->env(), data, size).ToLocalChecked(); Local<Value> buffer =
req->object()->Get(req->env()->context(),
req->env()->buffer_string()).ToLocalChecked();
if (buffer->IsUint8Array()) {
CHECK_LE(req->size(), Buffer::Length(buffer));
char* buf = Buffer::Data(buffer);
memcpy(buf, data, req->size());
argv[1] = buffer;
} else {
argv[1] = Buffer::New(req->env(), data, size).ToLocalChecked();
}
} }
} }
@ -5684,11 +5709,22 @@ void RandomBytesAfter(uv_work_t* work_req, int status) {
} }
void RandomBytesProcessSync(Environment* env,
RandomBytesRequest* req,
Local<Value> argv[2]) {
env->PrintSyncTrace();
RandomBytesWork(req->work_req());
RandomBytesCheck(req, argv);
delete req;
if (!argv[0]->IsNull())
env->isolate()->ThrowException(argv[0]);
}
void RandomBytes(const FunctionCallbackInfo<Value>& args) { void RandomBytes(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args); Environment* env = Environment::GetCurrent(args);
// maybe allow a buffer to write to? cuts down on object creation
// when generating random data in a loop
if (!args[0]->IsUint32()) { if (!args[0]->IsUint32()) {
return env->ThrowTypeError("size must be a number >= 0"); return env->ThrowTypeError("size must be a number >= 0");
} }
@ -5698,7 +5734,13 @@ void RandomBytes(const FunctionCallbackInfo<Value>& args) {
return env->ThrowRangeError("size is not a valid Smi"); return env->ThrowRangeError("size is not a valid Smi");
Local<Object> obj = env->NewInternalFieldObject(); Local<Object> obj = env->NewInternalFieldObject();
RandomBytesRequest* req = new RandomBytesRequest(env, obj, size); char* data = node::Malloc(size);
RandomBytesRequest* req =
new RandomBytesRequest(env,
obj,
size,
data,
RandomBytesRequest::FREE_DATA);
if (args[1]->IsFunction()) { if (args[1]->IsFunction()) {
obj->Set(env->ondone_string(), args[1]); obj->Set(env->ondone_string(), args[1]);
@ -5711,15 +5753,55 @@ void RandomBytes(const FunctionCallbackInfo<Value>& args) {
RandomBytesAfter); RandomBytesAfter);
args.GetReturnValue().Set(obj); args.GetReturnValue().Set(obj);
} else { } else {
env->PrintSyncTrace();
Local<Value> argv[2]; Local<Value> argv[2];
RandomBytesWork(req->work_req()); RandomBytesProcessSync(env, req, argv);
RandomBytesCheck(req, argv); if (argv[0]->IsNull())
delete req; args.GetReturnValue().Set(argv[1]);
}
}
if (!argv[0]->IsNull())
env->isolate()->ThrowException(argv[0]); void RandomBytesBuffer(const FunctionCallbackInfo<Value>& args) {
else Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsUint8Array());
CHECK(args[1]->IsUint32());
CHECK(args[2]->IsUint32());
int64_t offset = args[1]->IntegerValue();
int64_t size = args[2]->IntegerValue();
Local<Object> obj = env->NewInternalFieldObject();
obj->Set(env->context(), env->buffer_string(), args[0]).FromJust();
char* data = Buffer::Data(args[0]);
data += offset;
RandomBytesRequest* req =
new RandomBytesRequest(env,
obj,
size,
data,
RandomBytesRequest::DONT_FREE_DATA);
if (args[3]->IsFunction()) {
obj->Set(env->context(),
FIXED_ONE_BYTE_STRING(args.GetIsolate(), "ondone"),
args[3]).FromJust();
if (env->in_domain()) {
obj->Set(env->context(),
env->domain_string(),
env->domain_array()->Get(0)).FromJust();
}
uv_queue_work(env->event_loop(),
req->work_req(),
RandomBytesWork,
RandomBytesAfter);
args.GetReturnValue().Set(obj);
} else {
Local<Value> argv[2];
RandomBytesProcessSync(env, req, argv);
if (argv[0]->IsNull())
args.GetReturnValue().Set(argv[1]); args.GetReturnValue().Set(argv[1]);
} }
} }
@ -6144,6 +6226,7 @@ void InitCrypto(Local<Object> target,
env->SetMethod(target, "setFipsCrypto", SetFipsCrypto); env->SetMethod(target, "setFipsCrypto", SetFipsCrypto);
env->SetMethod(target, "PBKDF2", PBKDF2); env->SetMethod(target, "PBKDF2", PBKDF2);
env->SetMethod(target, "randomBytes", RandomBytes); env->SetMethod(target, "randomBytes", RandomBytes);
env->SetMethod(target, "randomFill", RandomBytesBuffer);
env->SetMethod(target, "timingSafeEqual", TimingSafeEqual); env->SetMethod(target, "timingSafeEqual", TimingSafeEqual);
env->SetMethod(target, "getSSLCiphers", GetSSLCiphers); env->SetMethod(target, "getSSLCiphers", GetSSLCiphers);
env->SetMethod(target, "getCiphers", GetCiphers); env->SetMethod(target, "getCiphers", GetCiphers);

View File

@ -50,6 +50,208 @@ const expectedErrorRegexp = /^TypeError: size must be a number >= 0$/;
}); });
}); });
{
const buf = Buffer.alloc(10);
const before = buf.toString('hex');
const after = crypto.randomFillSync(buf).toString('hex');
assert.notStrictEqual(before, after);
}
{
const buf = new Uint8Array(new Array(10).fill(0));
const before = Buffer.from(buf).toString('hex');
crypto.randomFillSync(buf);
const after = Buffer.from(buf).toString('hex');
assert.notStrictEqual(before, after);
}
{
const buf = Buffer.alloc(10);
const before = buf.toString('hex');
crypto.randomFill(buf, common.mustCall((err, buf) => {
assert.ifError(err);
const after = buf.toString('hex');
assert.notStrictEqual(before, after);
}));
}
{
const buf = new Uint8Array(new Array(10).fill(0));
const before = Buffer.from(buf).toString('hex');
crypto.randomFill(buf, common.mustCall((err, buf) => {
assert.ifError(err);
const after = Buffer.from(buf).toString('hex');
assert.notStrictEqual(before, after);
}));
}
{
const buf = Buffer.alloc(10);
const before = buf.toString('hex');
crypto.randomFillSync(buf, 5, 5);
const after = buf.toString('hex');
assert.notStrictEqual(before, after);
assert.deepStrictEqual(before.slice(0, 5), after.slice(0, 5));
}
{
const buf = new Uint8Array(new Array(10).fill(0));
const before = Buffer.from(buf).toString('hex');
crypto.randomFillSync(buf, 5, 5);
const after = Buffer.from(buf).toString('hex');
assert.notStrictEqual(before, after);
assert.deepStrictEqual(before.slice(0, 5), after.slice(0, 5));
}
{
const buf = Buffer.alloc(10);
const before = buf.toString('hex');
crypto.randomFillSync(buf, 5);
const after = buf.toString('hex');
assert.notStrictEqual(before, after);
assert.deepStrictEqual(before.slice(0, 5), after.slice(0, 5));
}
{
const buf = Buffer.alloc(10);
const before = buf.toString('hex');
crypto.randomFill(buf, 5, 5, common.mustCall((err, buf) => {
assert.ifError(err);
const after = buf.toString('hex');
assert.notStrictEqual(before, after);
assert.deepStrictEqual(before.slice(0, 5), after.slice(0, 5));
}));
}
{
const buf = new Uint8Array(new Array(10).fill(0));
const before = Buffer.from(buf).toString('hex');
crypto.randomFill(buf, 5, 5, common.mustCall((err, buf) => {
assert.ifError(err);
const after = Buffer.from(buf).toString('hex');
assert.notStrictEqual(before, after);
assert.deepStrictEqual(before.slice(0, 5), after.slice(0, 5));
}));
}
{
const bufs = [
Buffer.alloc(10),
new Uint8Array(new Array(10).fill(0))
];
for (const buf of bufs) {
const len = Buffer.byteLength(buf);
assert.strictEqual(len, 10, `Expected byteLength of 10, got ${len}`);
assert.throws(() => {
crypto.randomFillSync(buf, 'test');
}, /offset must be a number/);
assert.throws(() => {
crypto.randomFillSync(buf, NaN);
}, /offset must be a number/);
assert.throws(() => {
crypto.randomFill(buf, 'test', common.noop);
}, /offset must be a number/);
assert.throws(() => {
crypto.randomFill(buf, NaN, common.noop);
}, /offset must be a number/);
const max = require('buffer').kMaxLength + 1;
assert.throws(() => {
crypto.randomFillSync(buf, 11);
}, /offset out of range/);
assert.throws(() => {
crypto.randomFillSync(buf, max);
}, /offset out of range/);
assert.throws(() => {
crypto.randomFill(buf, 11, common.noop);
}, /offset out of range/);
assert.throws(() => {
crypto.randomFill(buf, max, common.noop);
}, /offset out of range/);
assert.throws(() => {
crypto.randomFillSync(buf, 0, 'test');
}, /size must be a number/);
assert.throws(() => {
crypto.randomFillSync(buf, 0, NaN);
}, /size must be a number/);
assert.throws(() => {
crypto.randomFill(buf, 0, 'test', common.noop);
}, /size must be a number/);
assert.throws(() => {
crypto.randomFill(buf, 0, NaN, common.noop);
}, /size must be a number/);
{
const size = (-1 >>> 0) + 1;
assert.throws(() => {
crypto.randomFillSync(buf, 0, -10);
}, /size must be a uint32/);
assert.throws(() => {
crypto.randomFillSync(buf, 0, size);
}, /size must be a uint32/);
assert.throws(() => {
crypto.randomFill(buf, 0, -10, common.noop);
}, /size must be a uint32/);
assert.throws(() => {
crypto.randomFill(buf, 0, size, common.noop);
}, /size must be a uint32/);
}
assert.throws(() => {
crypto.randomFillSync(buf, -10);
}, /offset must be a uint32/);
assert.throws(() => {
crypto.randomFill(buf, -10, common.noop);
}, /offset must be a uint32/);
assert.throws(() => {
crypto.randomFillSync(buf, 1, 10);
}, /buffer too small/);
assert.throws(() => {
crypto.randomFill(buf, 1, 10, common.noop);
}, /buffer too small/);
assert.throws(() => {
crypto.randomFillSync(buf, 0, 12);
}, /buffer too small/);
assert.throws(() => {
crypto.randomFill(buf, 0, 12, common.noop);
}, /buffer too small/);
{
// Offset is too big
const offset = (-1 >>> 0) + 1;
assert.throws(() => {
crypto.randomFillSync(buf, offset, 10);
}, /offset must be a uint32/);
assert.throws(() => {
crypto.randomFill(buf, offset, 10, common.noop);
}, /offset must be a uint32/);
}
}
}
// #5126, "FATAL ERROR: v8::Object::SetIndexedPropertiesToExternalArrayData() // #5126, "FATAL ERROR: v8::Object::SetIndexedPropertiesToExternalArrayData()
// length exceeds max acceptable value" // length exceeds max acceptable value"
assert.throws(function() { assert.throws(function() {

View File

@ -38,6 +38,8 @@ global.domain = require('domain');
// should not throw a 'TypeError: undefined is not a function' exception // should not throw a 'TypeError: undefined is not a function' exception
crypto.randomBytes(8); crypto.randomBytes(8);
crypto.randomBytes(8, common.noop); crypto.randomBytes(8, common.noop);
const buf = Buffer.alloc(8);
crypto.randomFillSync(buf);
crypto.pseudoRandomBytes(8); crypto.pseudoRandomBytes(8);
crypto.pseudoRandomBytes(8, common.noop); crypto.pseudoRandomBytes(8, common.noop);
crypto.pbkdf2('password', 'salt', 8, 8, 'sha1', common.noop); crypto.pbkdf2('password', 'salt', 8, 8, 'sha1', common.noop);