Files
node/lib/internal/crypto/random.js
T
Jiawen Geng 38aa1f3881 crypto: avoid reusing randomInt() cache bytes
When an asynchronous randomInt() call finds the cache empty, it queues
itself and starts randomFill() on the shared cache buffer. A synchronous
call made before that job completes refills the same buffer with
randomFillSync() and returns values from offset 0. The completion
callback then reset the offset to 0, so the queued call and later calls
returned the bytes that synchronous calls had already used. The
threadpool job and randomFillSync() could also write the buffer at the
same time.

Fill a separate buffer asynchronously and copy it into the cache once
the job is done.

Fixes: https://github.com/nodejs/node/issues/66595
Refs: https://github.com/nodejs/node/pull/35110
Signed-off-by: Jiawen Geng <technicalcute@gmail.com>
Assisted-by: claude:opus-5.5
PR-URL: https://github.com/nodejs/node/pull/66597
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
2026-10-15 09:46:38 +00:00

707 lines
19 KiB
JavaScript

'use strict';
const {
Array,
ArrayBufferPrototypeGetByteLength,
ArrayPrototypeForEach,
ArrayPrototypePush,
ArrayPrototypeShift,
ArrayPrototypeSplice,
BigInt,
BigIntPrototypeToString,
DataView,
DataViewPrototypeGetByteLength,
DataViewPrototypeGetUint8,
DateNow,
FunctionPrototypeBind,
FunctionPrototypeCall,
MathMin,
NumberIsNaN,
NumberIsSafeInteger,
NumberPrototypeToString,
StringFromCharCodeApply,
StringPrototypePadStart,
TypedArrayPrototypeGetBuffer,
TypedArrayPrototypeGetByteLength,
TypedArrayPrototypeGetLength,
TypedArrayPrototypeGetSymbolToStringTag,
TypedArrayPrototypeSet,
Uint8Array,
} = primordials;
const {
RandomBytesJob,
RandomPrimeJob,
CheckPrimeJob,
kCryptoJobAsync,
kCryptoJobSync,
randomFillSync: randomFillSyncImpl,
secureBuffer,
} = internalBinding('crypto');
const {
kEmptyObject,
lazyDOMException,
} = require('internal/util');
const { Buffer, kMaxLength } = require('buffer');
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_OPERATION_FAILED,
ERR_OUT_OF_RANGE,
},
} = require('internal/errors');
const {
validateNumber,
validateBoolean,
validateFunction,
validateInt32,
validateObject,
} = require('internal/validators');
const {
isArrayBufferView,
isAnyArrayBuffer,
isDataView,
isSharedArrayBuffer,
isTypedArray,
isFloat16Array,
isFloat32Array,
isFloat64Array,
} = require('internal/util/types');
const { FastBuffer } = require('internal/buffer');
const kMaxInt32 = 2 ** 31 - 1;
const kMaxPossibleLength = MathMin(kMaxLength, kMaxInt32);
function getByteLength(buf) {
if (isArrayBufferView(buf)) {
return isDataView(buf) ?
DataViewPrototypeGetByteLength(buf) : TypedArrayPrototypeGetByteLength(buf);
}
if (isSharedArrayBuffer(buf))
return TypedArrayPrototypeGetByteLength(new Uint8Array(buf));
return ArrayBufferPrototypeGetByteLength(buf);
}
function getElementSize(buf) {
switch (TypedArrayPrototypeGetSymbolToStringTag(buf)) {
case 'Int16Array':
case 'Uint16Array':
case 'Float16Array':
return 2;
case 'Int32Array':
case 'Uint32Array':
case 'Float32Array':
return 4;
case 'Float64Array':
case 'BigInt64Array':
case 'BigUint64Array':
return 8;
default:
return 1;
}
}
function assertOffset(offset, elementSize, length) {
validateNumber(offset, 'offset');
offset *= elementSize;
const maxLength = MathMin(length, kMaxPossibleLength);
if (NumberIsNaN(offset) || offset > maxLength || offset < 0) {
throw new ERR_OUT_OF_RANGE('offset', `>= 0 && <= ${maxLength}`, offset);
}
return offset >>> 0; // Convert to uint32.
}
function assertSize(size, elementSize, offset, length) {
validateNumber(size, 'size');
size *= elementSize;
if (NumberIsNaN(size) || size > kMaxPossibleLength || size < 0) {
throw new ERR_OUT_OF_RANGE('size',
`>= 0 && <= ${kMaxPossibleLength}`, size);
}
if (size + offset > length) {
throw new ERR_OUT_OF_RANGE('size + offset', `<= ${length}`, size + offset);
}
return size >>> 0; // Convert to uint32.
}
function randomBytes(size, callback) {
size = assertSize(size, 1, 0, Infinity);
if (callback !== undefined) {
validateFunction(callback, 'callback');
}
const buf = new FastBuffer(size);
if (callback === undefined) {
randomFillSync(TypedArrayPrototypeGetBuffer(buf), 0, size);
return buf;
}
// Keep the callback as a regular function so this is propagated.
randomFill(TypedArrayPrototypeGetBuffer(buf), 0, size, function(error) {
if (error) return FunctionPrototypeCall(callback, this, error);
FunctionPrototypeCall(callback, this, null, buf);
});
}
function randomFillSync(buf, offset = 0, size) {
if (!isAnyArrayBuffer(buf) && !isArrayBufferView(buf)) {
throw new ERR_INVALID_ARG_TYPE(
'buf',
['ArrayBuffer', 'ArrayBufferView'],
buf);
}
const elementSize = getElementSize(buf);
const byteLength = getByteLength(buf);
offset = assertOffset(offset, elementSize, byteLength);
if (size === undefined) {
size = byteLength - offset;
} else {
size = assertSize(size, elementSize, offset, byteLength);
}
if (size === 0)
return buf;
randomFillSyncImpl(buf, offset, size);
return buf;
}
function randomFill(buf, offset, size, callback) {
if (!isAnyArrayBuffer(buf) && !isArrayBufferView(buf)) {
throw new ERR_INVALID_ARG_TYPE(
'buf',
['ArrayBuffer', 'ArrayBufferView'],
buf);
}
const elementSize = getElementSize(buf);
if (typeof offset === 'function') {
callback = offset;
offset = 0;
// Size is a length here, assertSize() call turns it into a number of bytes
size = isTypedArray(buf) ? TypedArrayPrototypeGetLength(buf) : undefined;
} else if (typeof size === 'function') {
callback = size;
size = (isTypedArray(buf) ? TypedArrayPrototypeGetLength(buf) : getByteLength(buf)) - offset;
} else {
validateFunction(callback, 'callback');
}
const byteLength = getByteLength(buf);
offset = assertOffset(offset, elementSize, byteLength);
if (size === undefined) {
size = byteLength - offset;
} else {
size = assertSize(size, elementSize, offset, byteLength);
}
if (size === 0) {
callback(null, buf);
return;
}
const job = new RandomBytesJob(
kCryptoJobAsync,
buf,
offset,
size);
job.ondone = FunctionPrototypeBind(onJobDone, job, buf, callback);
job.run();
}
// Largest integer we can read from a buffer.
// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
const RAND_MAX = 0xFFFF_FFFF_FFFF;
// Cache random data to use in randomInt. The cache size must be evenly
// divisible by 6 because each attempt to obtain a random int uses 6 bytes.
const randomCache = new FastBuffer(6 * 1024);
let randomCacheOffset = randomCache.length;
// Asynchronous refills write into this buffer, never into randomCache.
let asyncRandomCache;
let asyncCacheFillInProgress = false;
const asyncCachePendingTasks = [];
// Generates an integer in [min, max) range where min is inclusive and max is
// exclusive.
function randomInt(min, max, callback) {
// Detect optional min syntax
// randomInt(max)
// randomInt(max, callback)
const minNotSpecified = typeof max === 'undefined' ||
typeof max === 'function';
if (minNotSpecified) {
callback = max;
max = min;
min = 0;
}
const isSync = typeof callback === 'undefined';
if (!isSync) {
validateFunction(callback, 'callback');
}
if (!NumberIsSafeInteger(min)) {
throw new ERR_INVALID_ARG_TYPE('min', 'a safe integer', min);
}
if (!NumberIsSafeInteger(max)) {
throw new ERR_INVALID_ARG_TYPE('max', 'a safe integer', max);
}
if (max <= min) {
throw new ERR_OUT_OF_RANGE(
'max', `greater than the value of "min" (${min})`, max,
);
}
// First we generate a random int between [0..range)
const range = max - min;
if (!(range <= RAND_MAX)) {
throw new ERR_OUT_OF_RANGE(`max${minNotSpecified ? '' : ' - min'}`,
`<= ${RAND_MAX}`, range);
}
// For (x % range) to produce an unbiased value greater than or equal to 0 and
// less than range, x must be drawn randomly from the set of integers greater
// than or equal to 0 and less than randLimit.
const randLimit = RAND_MAX - (RAND_MAX % range);
// If we don't have a callback, or if there is still data in the cache, we can
// do this synchronously, which is super fast.
while (isSync || (randomCacheOffset < randomCache.length)) {
if (randomCacheOffset === randomCache.length) {
// This might block the thread for a bit, but we are in sync mode.
randomFillSync(randomCache);
randomCacheOffset = 0;
}
const x = randomCache.readUIntBE(randomCacheOffset, 6);
randomCacheOffset += 6;
if (x < randLimit) {
const n = (x % range) + min;
if (isSync) return n;
process.nextTick(callback, undefined, n);
return;
}
}
// At this point, we are in async mode with no data in the cache. We cannot
// simply refill the cache, because another async call to randomInt might
// already be doing that. Instead, queue this call for when the cache has
// been refilled.
ArrayPrototypePush(asyncCachePendingTasks, { min, max, callback });
asyncRefillRandomIntCache();
}
function asyncRefillRandomIntCache() {
if (asyncCacheFillInProgress)
return;
asyncCacheFillInProgress = true;
// Synchronous calls may refill and read randomCache while this job runs.
// Filling a separate buffer and copying it in afterwards ensures that
// resetting the offset never hands out bytes they have already used.
asyncRandomCache ??= new FastBuffer(randomCache.length);
randomFill(asyncRandomCache, (err) => {
asyncCacheFillInProgress = false;
const tasks = asyncCachePendingTasks;
const errorReceiver = err && ArrayPrototypeShift(tasks);
if (!err) {
TypedArrayPrototypeSet(randomCache, asyncRandomCache);
randomCacheOffset = 0;
}
// Restart all pending tasks. If an error occurred, we only notify a single
// callback (errorReceiver) about it. This way, every async call to
// randomInt has a chance of being successful, and it avoids complex
// exception handling here.
ArrayPrototypeForEach(ArrayPrototypeSplice(tasks, 0), (task) => {
randomInt(task.min, task.max, task.callback);
});
// This is the only call that might throw, and is therefore done at the end.
if (errorReceiver)
errorReceiver.callback(err);
});
}
function onJobDone(buf, callback, error) {
if (error) return FunctionPrototypeCall(callback, this, error);
FunctionPrototypeCall(callback, this, null, buf);
}
// Really just the Web Crypto API alternative
// to require('crypto').randomFillSync() with an
// additional limitation that the input buffer is
// not allowed to exceed 65536 bytes, and can only
// be an integer-type TypedArray.
function getRandomValues(data) {
if (!isTypedArray(data) ||
isFloat16Array(data) ||
isFloat32Array(data) ||
isFloat64Array(data)) {
// Ordinarily this would be an ERR_INVALID_ARG_TYPE. However,
// the Web Crypto API and web platform tests expect this to
// be a DOMException with type TypeMismatchError.
throw lazyDOMException(
'The data argument must be an integer-type TypedArray',
'TypeMismatchError');
}
const byteLength = TypedArrayPrototypeGetByteLength(data);
if (byteLength > 65536) {
const { QuotaExceededError } = internalBinding('messaging');
throw new QuotaExceededError(
'The requested length exceeds 65,536 bytes');
}
randomFillSync(data, 0);
return data;
}
// Implements an RFC 4122 version 4 random UUID.
// To improve performance, random data is generated in batches
// large enough to cover kBatchSize UUID's at a time. The uuidData
// buffer is reused. Each call to randomUUID() consumes 16 bytes
// from the buffer.
const kBatchSize = 128;
let uuidData;
let uuidNotBuffered;
let uuidBatch = 0;
let hexBytesCache;
function getHexBytes() {
if (hexBytesCache === undefined) {
hexBytesCache = new Array(256);
for (let i = 0; i < hexBytesCache.length; i++) {
const hex = NumberPrototypeToString(i, 16);
hexBytesCache[i] = StringPrototypePadStart(hex, 2, '0');
}
}
return hexBytesCache;
}
function serializeUUID(buf, version, variant, offset = 0) {
const kHexBytes = getHexBytes();
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
return kHexBytes[buf[offset]] +
kHexBytes[buf[offset + 1]] +
kHexBytes[buf[offset + 2]] +
kHexBytes[buf[offset + 3]] +
'-' +
kHexBytes[buf[offset + 4]] +
kHexBytes[buf[offset + 5]] +
'-' +
kHexBytes[(buf[offset + 6] & 0x0f) | version] +
kHexBytes[buf[offset + 7]] +
'-' +
kHexBytes[(buf[offset + 8] & 0x3f) | variant] +
kHexBytes[buf[offset + 9]] +
'-' +
kHexBytes[buf[offset + 10]] +
kHexBytes[buf[offset + 11]] +
kHexBytes[buf[offset + 12]] +
kHexBytes[buf[offset + 13]] +
kHexBytes[buf[offset + 14]] +
kHexBytes[buf[offset + 15]];
}
function getBufferedUUID() {
uuidData ??= secureBuffer(16 * kBatchSize);
if (uuidData === undefined)
throw new ERR_OPERATION_FAILED('Out of memory');
if (uuidBatch === 0) randomFillSync(uuidData);
uuidBatch = (uuidBatch + 1) % kBatchSize;
return serializeUUID(uuidData, 0x40, 0x80, uuidBatch * 16);
}
function getUnbufferedUUID() {
uuidNotBuffered ??= secureBuffer(16);
if (uuidNotBuffered === undefined)
throw new ERR_OPERATION_FAILED('Out of memory');
randomFillSync(uuidNotBuffered);
return serializeUUID(uuidNotBuffered, 0x40, 0x80);
}
function randomUUID(options) {
if (options !== undefined)
validateObject(options, 'options');
const {
disableEntropyCache = false,
} = options || kEmptyObject;
validateBoolean(disableEntropyCache, 'options.disableEntropyCache');
return disableEntropyCache ? getUnbufferedUUID() : getBufferedUUID();
}
function writeTimestamp(buf, offset) {
const now = DateNow();
const msb = now / (2 ** 32);
buf[offset] = msb >>> 8;
buf[offset + 1] = msb;
buf[offset + 2] = now >>> 24;
buf[offset + 3] = now >>> 16;
buf[offset + 4] = now >>> 8;
buf[offset + 5] = now;
}
function getBufferedUUIDv7() {
uuidData ??= secureBuffer(16 * kBatchSize);
if (uuidData === undefined)
throw new ERR_OPERATION_FAILED('Out of memory');
if (uuidBatch === 0) randomFillSync(uuidData);
uuidBatch = (uuidBatch + 1) % kBatchSize;
const offset = uuidBatch * 16;
writeTimestamp(uuidData, offset);
return serializeUUID(uuidData, 0x70, 0x80, offset);
}
function getUnbufferedUUIDv7() {
uuidNotBuffered ??= secureBuffer(16);
if (uuidNotBuffered === undefined)
throw new ERR_OPERATION_FAILED('Out of memory');
randomFillSync(uuidNotBuffered, 6);
writeTimestamp(uuidNotBuffered, 0);
return serializeUUID(uuidNotBuffered, 0x70, 0x80);
}
function randomUUIDv7(options) {
if (options !== undefined)
validateObject(options, 'options');
const {
disableEntropyCache = false,
} = options || kEmptyObject;
validateBoolean(disableEntropyCache, 'options.disableEntropyCache');
return disableEntropyCache ? getUnbufferedUUIDv7() : getBufferedUUIDv7();
}
function createRandomPrimeJob(type, size, options) {
validateObject(options, 'options');
const {
safe = false,
bigint = false,
} = options;
let {
add,
rem,
} = options;
validateBoolean(safe, 'options.safe');
validateBoolean(bigint, 'options.bigint');
if (add !== undefined) {
if (typeof add === 'bigint') {
add = unsignedBigIntToBuffer(add, 'options.add');
} else if (!isAnyArrayBuffer(add) && !isArrayBufferView(add)) {
throw new ERR_INVALID_ARG_TYPE(
'options.add',
[
'ArrayBuffer',
'TypedArray',
'Buffer',
'DataView',
'bigint',
],
add);
}
}
if (rem !== undefined) {
if (typeof rem === 'bigint') {
rem = unsignedBigIntToBuffer(rem, 'options.rem');
} else if (!isAnyArrayBuffer(rem) && !isArrayBufferView(rem)) {
throw new ERR_INVALID_ARG_TYPE(
'options.rem',
[
'ArrayBuffer',
'TypedArray',
'Buffer',
'DataView',
'bigint',
],
rem);
}
}
const job = new RandomPrimeJob(type, size, safe, add, rem);
job.result = bigint ? arrayBufferToUnsignedBigInt : (p) => p;
return job;
}
function generatePrime(size, options, callback) {
validateInt32(size, 'size', 1);
if (typeof options === 'function') {
callback = options;
options = kEmptyObject;
}
validateFunction(callback, 'callback');
const job = createRandomPrimeJob(kCryptoJobAsync, size, options);
job.ondone = (err, prime) => {
if (err) {
callback(err);
return;
}
callback(
undefined,
job.result(prime));
};
job.run();
}
function generatePrimeSync(size, options = kEmptyObject) {
validateInt32(size, 'size', 1);
const job = createRandomPrimeJob(kCryptoJobSync, size, options);
const { 0: err, 1: prime } = job.run();
if (err)
throw err;
return job.result(prime);
}
/**
* 48 is the ASCII code for '0', 97 is the ASCII code for 'a'.
* @param {number} number An integer between 0 and 15.
* @returns {number} corresponding to the ASCII code of the hex representation
* of the parameter.
*/
const numberToHexCharCode = (number) => (number < 10 ? 48 : 87) + number;
/**
* @param {ArrayBuffer} buf An ArrayBuffer.
* @returns {bigint}
*/
function arrayBufferToUnsignedBigInt(buf) {
const length = ArrayBufferPrototypeGetByteLength(buf);
const chars = Array(length * 2);
const view = new DataView(buf);
for (let i = 0; i < length; i++) {
const val = DataViewPrototypeGetUint8(view, i);
chars[2 * i] = numberToHexCharCode(val >> 4);
chars[2 * i + 1] = numberToHexCharCode(val & 0xf);
}
return BigInt(`0x${StringFromCharCodeApply(chars)}`);
}
function unsignedBigIntToBuffer(bigint, name) {
if (bigint < 0) {
throw new ERR_OUT_OF_RANGE(name, '>= 0', bigint);
}
const hex = BigIntPrototypeToString(bigint, 16);
const padded = StringPrototypePadStart(hex, hex.length + (hex.length % 2), 0);
return Buffer.from(padded, 'hex');
}
function checkPrime(candidate, options = kEmptyObject, callback) {
if (typeof candidate === 'bigint')
candidate = unsignedBigIntToBuffer(candidate, 'candidate');
if (!isAnyArrayBuffer(candidate) && !isArrayBufferView(candidate)) {
throw new ERR_INVALID_ARG_TYPE(
'candidate',
[
'ArrayBuffer',
'TypedArray',
'Buffer',
'DataView',
'bigint',
],
candidate,
);
}
if (typeof options === 'function') {
callback = options;
options = kEmptyObject;
}
validateFunction(callback, 'callback');
validateObject(options, 'options');
let {
checks = 0,
} = options;
// The checks option is unsigned but must fit into a signed C int for OpenSSL.
validateInt32(checks, 'options.checks', 0);
// Coerce -0 to +0.
checks += 0;
const job = new CheckPrimeJob(kCryptoJobAsync, candidate, checks);
job.ondone = callback;
job.run();
}
function checkPrimeSync(candidate, options = kEmptyObject) {
if (typeof candidate === 'bigint')
candidate = unsignedBigIntToBuffer(candidate, 'candidate');
if (!isAnyArrayBuffer(candidate) && !isArrayBufferView(candidate)) {
throw new ERR_INVALID_ARG_TYPE(
'candidate',
[
'ArrayBuffer',
'TypedArray',
'Buffer',
'DataView',
'bigint',
],
candidate,
);
}
validateObject(options, 'options');
let {
checks = 0,
} = options;
// The checks option is unsigned but must fit into a signed C int for OpenSSL.
validateInt32(checks, 'options.checks', 0);
// Coerce -0 to +0.
checks += 0;
const job = new CheckPrimeJob(kCryptoJobSync, candidate, checks);
const { 0: err, 1: result } = job.run();
if (err)
throw err;
return result;
}
module.exports = {
checkPrime,
checkPrimeSync,
randomBytes,
randomFill,
randomFillSync,
randomInt,
getRandomValues,
randomUUID,
randomUUIDv7,
generatePrime,
generatePrimeSync,
};