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
53 changes: 39 additions & 14 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,55 @@ 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)
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;
const seed = ((uuidDataV7[offset + 6] & 0x0f) << 8) | uuidDataV7[offset + 7];
advanceV7(seed);
writeTimestampAndCounterV7(uuidDataV7, offset);
return serializeUUID(uuidDataV7, 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);
const seed = ((uuidNotBuffered[6] & 0x0f) << 8) | uuidNotBuffered[7];
advanceV7(seed);
writeTimestampAndCounterV7(uuidNotBuffered, 0);
return serializeUUID(uuidNotBuffered, 0x70, 0x80);
}

Expand Down
29 changes: 21 additions & 8 deletions test/parallel/test-crypto-randomuuidv7.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,37 @@ const {
const timestamp = parseInt(timestampHex, 16);

assert(timestamp >= before, `Timestamp ${timestamp} < before ${before}`);
assert(timestamp <= after, `Timestamp ${timestamp} > after ${after}`);
// The monotonic counter may have overflowed in a prior call and advanced
// v7LastTimestamp by 1 ms beyond wall-clock time (RFC 9562 §6.2 allows this).
assert(timestamp <= after + 1, `Timestamp ${timestamp} > after+1 ${after + 1}`);
}

{
let prev = randomUUIDv7();
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}`);
// 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 burst = [];
for (let i = 0; i < 500; i++) {
burst.push(randomUUIDv7());
}
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 Down
Loading