crypto: add buffering to randomInt · nodejs/node@960c6be

@@ -2,6 +2,10 @@

2233

const {

44

Array,

5+

ArrayPrototypeForEach,

6+

ArrayPrototypePush,

7+

ArrayPrototypeShift,

8+

ArrayPrototypeSplice,

59

BigInt,

610

FunctionPrototypeBind,

711

FunctionPrototypeCall,

@@ -186,6 +190,13 @@ function randomFill(buf, offset, size, callback) {

186190

// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);

187191

const RAND_MAX = 0xFFFF_FFFF_FFFF;

188192193+

// Cache random data to use in randomInt. The cache size must be evenly

194+

// divisible by 6 because each attempt to obtain a random int uses 6 bytes.

195+

const randomCache = new FastBuffer(6 * 1024);

196+

let randomCacheOffset = randomCache.length;

197+

let asyncCacheFillInProgress = false;

198+

const asyncCachePendingTasks = [];

199+189200

// Generates an integer in [min, max) range where min is inclusive and max is

190201

// exclusive.

191202

function randomInt(min, max, callback) {

@@ -230,33 +241,59 @@ function randomInt(min, max, callback) {

230241

// than or equal to 0 and less than randLimit.

231242

const randLimit = RAND_MAX - (RAND_MAX % range);

232243233-

if (isSync) {

234-

// Sync API

235-

while (true) {

236-

const x = randomBytes(6).readUIntBE(0, 6);

237-

if (x >= randLimit) {

238-

// Try again.

239-

continue;

240-

}

241-

return (x % range) + min;

244+

// If we don't have a callback, or if there is still data in the cache, we can

245+

// do this synchronously, which is super fast.

246+

while (isSync || (randomCacheOffset < randomCache.length)) {

247+

if (randomCacheOffset === randomCache.length) {

248+

// This might block the thread for a bit, but we are in sync mode.

249+

randomFillSync(randomCache);

250+

randomCacheOffset = 0;

251+

}

252+253+

const x = randomCache.readUIntBE(randomCacheOffset, 6);

254+

randomCacheOffset += 6;

255+256+

if (x < randLimit) {

257+

const n = (x % range) + min;

258+

if (isSync) return n;

259+

process.nextTick(callback, undefined, n);

260+

return;

242261

}

243-

} else {

244-

// Async API

245-

const pickAttempt = () => {

246-

randomBytes(6, (err, bytes) => {

247-

if (err) return callback(err);

248-

const x = bytes.readUIntBE(0, 6);

249-

if (x >= randLimit) {

250-

// Try again.

251-

return pickAttempt();

252-

}

253-

const n = (x % range) + min;

254-

callback(null, n);

255-

});

256-

};

257-258-

pickAttempt();

259262

}

263+264+

// At this point, we are in async mode with no data in the cache. We cannot

265+

// simply refill the cache, because another async call to randomInt might

266+

// already be doing that. Instead, queue this call for when the cache has

267+

// been refilled.

268+

ArrayPrototypePush(asyncCachePendingTasks, { min, max, callback });

269+

asyncRefillRandomIntCache();

270+

}

271+272+

function asyncRefillRandomIntCache() {

273+

if (asyncCacheFillInProgress)

274+

return;

275+276+

asyncCacheFillInProgress = true;

277+

randomFill(randomCache, (err) => {

278+

asyncCacheFillInProgress = false;

279+280+

const tasks = asyncCachePendingTasks;

281+

const errorReceiver = err && ArrayPrototypeShift(tasks);

282+

if (!err)

283+

randomCacheOffset = 0;

284+285+

// Restart all pending tasks. If an error occurred, we only notify a single

286+

// callback (errorReceiver) about it. This way, every async call to

287+

// randomInt has a chance of being successful, and it avoids complex

288+

// exception handling here.

289+

ArrayPrototypeForEach(ArrayPrototypeSplice(tasks, 0), (task) => {

290+

randomInt(task.min, task.max, task.callback);

291+

});

292+293+

// This is the only call that might throw, and is therefore done at the end.

294+

if (errorReceiver)

295+

errorReceiver.callback(err);

296+

});

260297

}

261298262299