diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index c324b2292b2fb8..af3a0c285c8b64 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,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); } @@ -452,11 +499,14 @@ function randomUUIDv7(options) { validateObject(options, 'options'); const { disableEntropyCache = false, + monotonic = true, } = 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) { diff --git a/test/parallel/test-crypto-randomuuidv7.js b/test/parallel/test-crypto-randomuuidv7.js index 99d052f356721c..27005a00929998 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(); @@ -92,6 +105,11 @@ 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', }); @@ -99,6 +117,10 @@ const { assert.throws(() => randomUUIDv7({ disableEntropyCache: '' }), { code: 'ERR_INVALID_ARG_TYPE', }); + + assert.throws(() => randomUUIDv7({ monotonic: 1 }), { + code: 'ERR_INVALID_ARG_TYPE', + }); } {