Skip to content

Commit 5e312c1

Browse files
committed
crypto: add uuidv7 monotonic counter
1 parent dec5973 commit 5e312c1

File tree

2 files changed

+57
-21
lines changed

2 files changed

+57
-21
lines changed

lib/internal/crypto/random.js

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,11 @@ let uuidData;
348348
let uuidNotBuffered;
349349
let uuidBatch = 0;
350350

351+
let uuidDataV7;
352+
let uuidBatchV7 = 0;
353+
let v7LastTimestamp = -1;
354+
let v7Counter = 0;
355+
351356
let hexBytesCache;
352357
function getHexBytes() {
353358
if (hexBytesCache === undefined) {
@@ -415,35 +420,55 @@ function randomUUID(options) {
415420
return disableEntropyCache ? getUnbufferedUUID() : getBufferedUUID();
416421
}
417422

418-
function writeTimestamp(buf, offset) {
423+
function advanceV7(seed) {
419424
const now = DateNow();
420-
const msb = now / (2 ** 32);
425+
if (now > v7LastTimestamp) {
426+
v7LastTimestamp = now;
427+
v7Counter = seed & 0xFFF;
428+
} else {
429+
v7Counter++;
430+
if (v7Counter > 0xFFF) {
431+
v7LastTimestamp++;
432+
v7Counter = 0;
433+
}
434+
}
435+
}
436+
437+
function writeTimestampAndCounterV7(buf, offset) {
438+
const ts = v7LastTimestamp;
439+
const msb = ts / (2 ** 32);
421440
buf[offset] = msb >>> 8;
422441
buf[offset + 1] = msb;
423-
buf[offset + 2] = now >>> 24;
424-
buf[offset + 3] = now >>> 16;
425-
buf[offset + 4] = now >>> 8;
426-
buf[offset + 5] = now;
442+
buf[offset + 2] = ts >>> 24;
443+
buf[offset + 3] = ts >>> 16;
444+
buf[offset + 4] = ts >>> 8;
445+
buf[offset + 5] = ts;
446+
buf[offset + 6] = (v7Counter >>> 8) & 0x0f;
447+
buf[offset + 7] = v7Counter & 0xff;
427448
}
428449

429450
function getBufferedUUIDv7() {
430-
uuidData ??= secureBuffer(16 * kBatchSize);
431-
if (uuidData === undefined)
451+
uuidDataV7 ??= secureBuffer(16 * kBatchSize);
452+
if (uuidDataV7 === undefined)
432453
throw new ERR_OPERATION_FAILED('Out of memory');
433454

434-
if (uuidBatch === 0) randomFillSync(uuidData);
435-
uuidBatch = (uuidBatch + 1) % kBatchSize;
436-
const offset = uuidBatch * 16;
437-
writeTimestamp(uuidData, offset);
438-
return serializeUUID(uuidData, 0x70, 0x80, offset);
455+
if (uuidBatchV7 === 0) randomFillSync(uuidDataV7);
456+
uuidBatchV7 = (uuidBatchV7 + 1) % kBatchSize;
457+
const offset = uuidBatchV7 * 16;
458+
const seed = ((uuidDataV7[offset + 6] & 0x0f) << 8) | uuidDataV7[offset + 7];
459+
advanceV7(seed);
460+
writeTimestampAndCounterV7(uuidDataV7, offset);
461+
return serializeUUID(uuidDataV7, 0x70, 0x80, offset);
439462
}
440463

441464
function getUnbufferedUUIDv7() {
442465
uuidNotBuffered ??= secureBuffer(16);
443466
if (uuidNotBuffered === undefined)
444467
throw new ERR_OPERATION_FAILED('Out of memory');
445468
randomFillSync(uuidNotBuffered, 6);
446-
writeTimestamp(uuidNotBuffered, 0);
469+
const seed = ((uuidNotBuffered[6] & 0x0f) << 8) | uuidNotBuffered[7];
470+
advanceV7(seed);
471+
writeTimestampAndCounterV7(uuidNotBuffered, 0);
447472
return serializeUUID(uuidNotBuffered, 0x70, 0x80);
448473
}
449474

test/parallel/test-crypto-randomuuidv7.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,28 @@ const {
6363
let prev = randomUUIDv7();
6464
for (let i = 0; i < 100; i++) {
6565
const curr = randomUUIDv7();
66-
// UUIDs with later timestamps must sort after earlier ones.
67-
// Within the same millisecond, ordering depends on random bits,
68-
// so we only assert >= on the timestamp portion.
69-
const prevTs = parseInt(prev.replace(/-/g, '').slice(0, 12), 16);
70-
const currTs = parseInt(curr.replace(/-/g, '').slice(0, 12), 16);
71-
assert(currTs >= prevTs,
72-
`Timestamp went backwards: ${currTs} < ${prevTs}`);
66+
// With a monotonic counter in rand_a, each UUID must be strictly greater
67+
// than the previous regardless of whether the timestamp changed.
68+
assert(curr > prev,
69+
`UUID ordering violated: ${curr} <= ${prev}`);
7370
prev = curr;
7471
}
7572
}
7673

74+
// Sub-millisecond ordering: a tight synchronous burst exercises the counter
75+
// increment path and must also produce strictly increasing UUIDs.
76+
{
77+
const burst = [];
78+
for (let i = 0; i < 500; i++) {
79+
burst.push(randomUUIDv7());
80+
}
81+
for (let i = 1; i < burst.length; i++) {
82+
assert(burst[i] > burst[i - 1],
83+
`Sub-millisecond ordering violated at index ${i}: ` +
84+
`${burst[i]} <= ${burst[i - 1]}`);
85+
}
86+
}
87+
7788
// Ensure randomUUIDv7 takes no arguments (or ignores them gracefully).
7889
{
7990
const uuid = randomUUIDv7();

0 commit comments

Comments
 (0)