diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index c324b2292b2fb8..e92a55ae5ed605 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -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) { @@ -415,27 +420,45 @@ 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() { @@ -443,7 +466,9 @@ function getUnbufferedUUIDv7() { 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); } diff --git a/test/parallel/test-crypto-randomuuidv7.js b/test/parallel/test-crypto-randomuuidv7.js index 99d052f356721c..27eb086752e292 100644 --- a/test/parallel/test-crypto-randomuuidv7.js +++ b/test/parallel/test-crypto-randomuuidv7.js @@ -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();