nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
547 lines (463 loc) • 15.9 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/crypto/random.js
import {
RandomBytesJob,
RandomPrimeJob,
CheckPrimeJob,
kCryptoJobAsync,
kCryptoJobSync,
secureBuffer,
} from "nstdlib/stub/binding/crypto";
import { kEmptyObject, lazyDOMException } from "nstdlib/lib/internal/util";
import { Buffer, kMaxLength } from "nstdlib/lib/buffer";
import { codes as __codes__ } from "nstdlib/lib/internal/errors";
import {
validateNumber,
validateBoolean,
validateFunction,
validateInt32,
validateObject,
} from "nstdlib/lib/internal/validators";
import {
isArrayBufferView,
isAnyArrayBuffer,
isTypedArray,
isFloat32Array,
isFloat64Array,
} from "nstdlib/lib/internal/util/types";
import { FastBuffer } from "nstdlib/lib/internal/buffer";
const { ERR_INVALID_ARG_TYPE, ERR_OPERATION_FAILED, ERR_OUT_OF_RANGE } =
__codes__;
const kMaxInt32 = 2 ** 31 - 1;
const kMaxPossibleLength = Math.min(kMaxLength, kMaxInt32);
function assertOffset(offset, elementSize, length) {
validateNumber(offset, "offset");
offset *= elementSize;
const maxLength = Math.min(length, kMaxPossibleLength);
if (Number.isNaN(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 (Number.isNaN(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(buf.buffer, 0, size);
return buf;
}
// Keep the callback as a regular function so this is propagated.
randomFill(buf.buffer, 0, size, function (error) {
if (error) return Function.prototype.call.call(callback, this, error);
Function.prototype.call.call(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 = buf.BYTES_PER_ELEMENT || 1;
offset = assertOffset(offset, elementSize, buf.byteLength);
if (size === undefined) {
size = buf.byteLength - offset;
} else {
size = assertSize(size, elementSize, offset, buf.byteLength);
}
if (size === 0) return buf;
const job = new RandomBytesJob(kCryptoJobSync, buf, offset, size);
const err = job.run()[0];
if (err) throw err;
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 = buf.BYTES_PER_ELEMENT || 1;
if (typeof offset === "function") {
callback = offset;
offset = 0;
// Size is a length here, assertSize() call turns it into a number of bytes
size = buf.length;
} else if (typeof size === "function") {
callback = size;
size = buf.length - offset;
} else {
validateFunction(callback, "callback");
}
offset = assertOffset(offset, elementSize, buf.byteLength);
if (size === undefined) {
size = buf.byteLength - offset;
} else {
size = assertSize(size, elementSize, offset, buf.byteLength);
}
if (size === 0) {
callback(null, buf);
return;
}
const job = new RandomBytesJob(kCryptoJobAsync, buf, offset, size);
job.ondone = Function.prototype.bind.call(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;
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 (!Number.isSafeInteger(min)) {
throw new ERR_INVALID_ARG_TYPE("min", "a safe integer", min);
}
if (!Number.isSafeInteger(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.
Array.prototype.push.call(asyncCachePendingTasks, { min, max, callback });
asyncRefillRandomIntCache();
}
function asyncRefillRandomIntCache() {
if (asyncCacheFillInProgress) return;
asyncCacheFillInProgress = true;
randomFill(randomCache, (err) => {
asyncCacheFillInProgress = false;
const tasks = asyncCachePendingTasks;
const errorReceiver = err && Array.prototype.shift.call(tasks);
if (!err) 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.
Array.prototype.forEach.call(
Array.prototype.splice.call(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 Function.prototype.call.call(callback, this, error);
Function.prototype.call.call(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) || 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",
);
}
if (data.byteLength > 65536) {
throw lazyDOMException(
"The requested length exceeds 65,536 bytes",
"QuotaExceededError",
);
}
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 = Number.prototype.toString.call(i, 16);
hexBytesCache[i] = String.prototype.padStart.call(hex, 2, "0");
}
}
return hexBytesCache;
}
function serializeUUID(buf, 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) | 0x40] +
kHexBytes[buf[offset + 7]] +
"-" +
kHexBytes[(buf[offset + 8] & 0x3f) | 0x80] +
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, uuidBatch * 16);
}
function getUnbufferedUUID() {
uuidNotBuffered ??= secureBuffer(16);
if (uuidNotBuffered === undefined)
throw new ERR_OPERATION_FAILED("Out of memory");
randomFillSync(uuidNotBuffered);
return serializeUUID(uuidNotBuffered);
}
function randomUUID(options) {
if (options !== undefined) validateObject(options, "options");
const { disableEntropyCache = false } = options || kEmptyObject;
validateBoolean(disableEntropyCache, "options.disableEntropyCache");
return disableEntropyCache ? getUnbufferedUUID() : getBufferedUUID();
}
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.
* @return {bigint}
*/
function arrayBufferToUnsignedBigInt(buf) {
const length = Object.getOwnPropertyDescriptor(
ArrayBufferView.prototype,
"byteLength",
).get(buf);
const chars = Array(length * 2);
const view = new DataView(buf);
for (let i = 0; i < length; i++) {
const val = DataView.prototype.getUint8.call(view, i);
chars[2 * i] = numberToHexCharCode(val >> 4);
chars[2 * i + 1] = numberToHexCharCode(val & 0xf);
}
return BigInt(`0x${String.fromCharCode.apply(chars)}`);
}
function unsignedBigIntToBuffer(bigint, name) {
if (bigint < 0) {
throw new ERR_OUT_OF_RANGE(name, ">= 0", bigint);
}
const hex = BigInt.prototype.toString.call(bigint, 16);
const padded = String.prototype.padStart.call(
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");
const { checks = 0 } = options;
// The checks option is unsigned but must fit into a signed C int for OpenSSL.
validateInt32(checks, "options.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");
const { checks = 0 } = options;
// The checks option is unsigned but must fit into a signed C int for OpenSSL.
validateInt32(checks, "options.checks", 0);
const job = new CheckPrimeJob(kCryptoJobSync, candidate, checks);
const { 0: err, 1: result } = job.run();
if (err) throw err;
return result;
}
export { checkPrime };
export { checkPrimeSync };
export { randomBytes };
export { randomFill };
export { randomFillSync };
export { randomInt };
export { getRandomValues };
export { randomUUID };
export { generatePrime };
export { generatePrimeSync };