Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -5838,6 +5838,10 @@ added: REPLACEME
random data to generate up to 128 random UUIDs. To generate a UUID
without using the cache, set `disableEntropyCache` to `true`.
**Default:** `false`.
* `monotonic` {boolean} When `true`, guarantees that UUIDs generated
within the same millisecond are strictly increasing by using a counter
in the `rand_a` field, as described in [RFC 9562][] Section 6.2.
**Default:** `false`.
* Returns: {string}

Generates a random [RFC 9562][] version 7 UUID. The UUID contains a millisecond
Expand Down
84 changes: 67 additions & 17 deletions lib/internal/crypto/random.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,11 @@ let uuidData;
let uuidNotBuffered;
let uuidBatch = 0;

let uuidDataV7;
let uuidBatchV7 = 0;
let v7LastTimestamp = -1;
let v7Counter = 0;

let hexBytesCache;
function getHexBytes() {
if (hexBytesCache === undefined) {
Expand Down Expand Up @@ -415,35 +420,77 @@ function randomUUID(options) {
return disableEntropyCache ? getUnbufferedUUID() : getBufferedUUID();
}

function writeTimestamp(buf, offset) {
function advanceV7(seed) {
const now = DateNow();
const msb = now / (2 ** 32);
if (now > v7LastTimestamp) {
v7LastTimestamp = now;
v7Counter = seed & 0xFFF;
} else {
v7Counter++;
if (v7Counter > 0xFFF) {
v7LastTimestamp++;
v7Counter = 0;
}
}
}

function writeTimestampAndCounterV7(buf, offset) {
const ts = v7LastTimestamp;
const msb = ts / (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;
buf[offset + 2] = ts >>> 24;
buf[offset + 3] = ts >>> 16;
buf[offset + 4] = ts >>> 8;
buf[offset + 5] = ts;
buf[offset + 6] = (v7Counter >>> 8) & 0x0f;
buf[offset + 7] = v7Counter & 0xff;
}

function getBufferedUUIDv7() {
uuidData ??= secureBuffer(16 * kBatchSize);
if (uuidData === undefined)
function getBufferedUUIDv7(monotonic) {
uuidDataV7 ??= secureBuffer(16 * kBatchSize);
if (uuidDataV7 === 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);
if (uuidBatchV7 === 0) randomFillSync(uuidDataV7);
uuidBatchV7 = (uuidBatchV7 + 1) % kBatchSize;
const offset = uuidBatchV7 * 16;
if (monotonic) {
const seed = ((uuidDataV7[offset + 6] & 0x0f) << 8) | uuidDataV7[offset + 7];
advanceV7(seed);
writeTimestampAndCounterV7(uuidDataV7, offset);
} else {
const now = DateNow();
const msb = now / (2 ** 32);
uuidDataV7[offset] = msb >>> 8;
uuidDataV7[offset + 1] = msb;
uuidDataV7[offset + 2] = now >>> 24;
uuidDataV7[offset + 3] = now >>> 16;
uuidDataV7[offset + 4] = now >>> 8;
uuidDataV7[offset + 5] = now;
}
return serializeUUID(uuidDataV7, 0x70, 0x80, offset);
}

function getUnbufferedUUIDv7() {
function getUnbufferedUUIDv7(monotonic) {
uuidNotBuffered ??= secureBuffer(16);
if (uuidNotBuffered === undefined)
throw new ERR_OPERATION_FAILED('Out of memory');
randomFillSync(uuidNotBuffered, 6);
writeTimestamp(uuidNotBuffered, 0);
if (monotonic) {
const seed = ((uuidNotBuffered[6] & 0x0f) << 8) | uuidNotBuffered[7];
advanceV7(seed);
writeTimestampAndCounterV7(uuidNotBuffered, 0);
} else {
const now = DateNow();
const msb = now / (2 ** 32);
uuidNotBuffered[0] = msb >>> 8;
uuidNotBuffered[1] = msb;
uuidNotBuffered[2] = now >>> 24;
uuidNotBuffered[3] = now >>> 16;
uuidNotBuffered[4] = now >>> 8;
uuidNotBuffered[5] = now;
}
return serializeUUID(uuidNotBuffered, 0x70, 0x80);
}

Expand All @@ -452,11 +499,14 @@ function randomUUIDv7(options) {
validateObject(options, 'options');
const {
disableEntropyCache = false,
monotonic = false,
} = options || kEmptyObject;

validateBoolean(disableEntropyCache, 'options.disableEntropyCache');
validateBoolean(monotonic, 'options.monotonic');

return disableEntropyCache ? getUnbufferedUUIDv7() : getBufferedUUIDv7();
return disableEntropyCache ?
getUnbufferedUUIDv7(monotonic) : getBufferedUUIDv7(monotonic);
}

function createRandomPrimeJob(type, size, options) {
Expand Down
40 changes: 31 additions & 9 deletions test/parallel/test-crypto-randomuuidv7.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,33 @@ const {
}

{
let prev = randomUUIDv7();
const opts = { monotonic: true };
let prev = randomUUIDv7(opts);
for (let i = 0; i < 100; i++) {
const curr = randomUUIDv7();
// UUIDs with later timestamps must sort after earlier ones.
// Within the same millisecond, ordering depends on random bits,
// so we only assert >= on the timestamp portion.
const prevTs = parseInt(prev.replace(/-/g, '').slice(0, 12), 16);
const currTs = parseInt(curr.replace(/-/g, '').slice(0, 12), 16);
assert(currTs >= prevTs,
`Timestamp went backwards: ${currTs} < ${prevTs}`);
const curr = randomUUIDv7(opts);
// With a monotonic counter in rand_a, each UUID must be strictly greater
// than the previous regardless of whether the timestamp changed.
assert(curr > prev,
`UUID ordering violated: ${curr} <= ${prev}`);
prev = curr;
}
}

// Sub-millisecond ordering: a tight synchronous burst exercises the counter
// increment path and must also produce strictly increasing UUIDs.
{
const opts = { monotonic: true };
const burst = [];
for (let i = 0; i < 500; i++) {
burst.push(randomUUIDv7(opts));
}
for (let i = 1; i < burst.length; i++) {
assert(burst[i] > burst[i - 1],
`Sub-millisecond ordering violated at index ${i}: ` +
`${burst[i]} <= ${burst[i - 1]}`);
}
}

// Ensure randomUUIDv7 takes no arguments (or ignores them gracefully).
{
const uuid = randomUUIDv7();
Expand All @@ -92,13 +105,22 @@ const {
assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex);
assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex);

// monotonic: false — rand_a is random; UUIDs must still be valid but are not
// guaranteed to be strictly ordered within the same millisecond.
assert.match(randomUUIDv7({ monotonic: false }), uuidv7Regex);
assert.match(randomUUIDv7({ monotonic: false, disableEntropyCache: true }), uuidv7Regex);

assert.throws(() => randomUUIDv7(1), {
code: 'ERR_INVALID_ARG_TYPE',
});

assert.throws(() => randomUUIDv7({ disableEntropyCache: '' }), {
code: 'ERR_INVALID_ARG_TYPE',
});

assert.throws(() => randomUUIDv7({ monotonic: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});
}

{
Expand Down