Skip to content

Commit dec5973

Browse files
authored
crypto: implement randomUUIDv7()
Signed-off-by: nabeel378 <mohammadnabeeljameel@gmail.com> PR-URL: #62553 Fixes: #62529 Refs: https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
1 parent a3108ff commit dec5973

File tree

4 files changed

+185
-5
lines changed

4 files changed

+185
-5
lines changed

doc/api/crypto.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5826,6 +5826,25 @@ added:
58265826
Generates a random [RFC 4122][] version 4 UUID. The UUID is generated using a
58275827
cryptographic pseudorandom number generator.
58285828

5829+
### `crypto.randomUUIDv7([options])`
5830+
5831+
<!-- YAML
5832+
added: REPLACEME
5833+
-->
5834+
5835+
* `options` {Object}
5836+
* `disableEntropyCache` {boolean} By default, to improve performance,
5837+
Node.js generates and caches enough
5838+
random data to generate up to 128 random UUIDs. To generate a UUID
5839+
without using the cache, set `disableEntropyCache` to `true`.
5840+
**Default:** `false`.
5841+
* Returns: {string}
5842+
5843+
Generates a random [RFC 9562][] version 7 UUID. The UUID contains a millisecond
5844+
precision Unix timestamp in the most significant 48 bits, followed by
5845+
cryptographically secure random bits for the remaining fields, making it
5846+
suitable for use as a database key with time-based sorting.
5847+
58295848
### `crypto.scrypt(password, salt, keylen[, options], callback)`
58305849

58315850
<!-- YAML
@@ -6864,6 +6883,7 @@ See the [list of SSL OP Flags][] for details.
68646883
[RFC 5280]: https://www.rfc-editor.org/rfc/rfc5280.txt
68656884
[RFC 7517]: https://www.rfc-editor.org/rfc/rfc7517.txt
68666885
[RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt
6886+
[RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt
68676887
[Web Crypto API documentation]: webcrypto.md
68686888
[`BN_is_prime_ex`]: https://www.openssl.org/docs/man1.1.1/man3/BN_is_prime_ex.html
68696889
[`Buffer`]: buffer.md

lib/crypto.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const {
5656
randomFillSync,
5757
randomInt,
5858
randomUUID,
59+
randomUUIDv7,
5960
} = require('internal/crypto/random');
6061
const {
6162
argon2,
@@ -220,6 +221,7 @@ module.exports = {
220221
randomFillSync,
221222
randomInt,
222223
randomUUID,
224+
randomUUIDv7,
223225
scrypt,
224226
scryptSync,
225227
sign: signOneShot,

lib/internal/crypto/random.js

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
BigIntPrototypeToString,
1212
DataView,
1313
DataViewPrototypeGetUint8,
14+
DateNow,
1415
FunctionPrototypeBind,
1516
FunctionPrototypeCall,
1617
MathMin,
@@ -359,7 +360,7 @@ function getHexBytes() {
359360
return hexBytesCache;
360361
}
361362

362-
function serializeUUID(buf, offset = 0) {
363+
function serializeUUID(buf, version, variant, offset = 0) {
363364
const kHexBytes = getHexBytes();
364365
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
365366
return kHexBytes[buf[offset]] +
@@ -370,10 +371,10 @@ function serializeUUID(buf, offset = 0) {
370371
kHexBytes[buf[offset + 4]] +
371372
kHexBytes[buf[offset + 5]] +
372373
'-' +
373-
kHexBytes[(buf[offset + 6] & 0x0f) | 0x40] +
374+
kHexBytes[(buf[offset + 6] & 0x0f) | version] +
374375
kHexBytes[buf[offset + 7]] +
375376
'-' +
376-
kHexBytes[(buf[offset + 8] & 0x3f) | 0x80] +
377+
kHexBytes[(buf[offset + 8] & 0x3f) | variant] +
377378
kHexBytes[buf[offset + 9]] +
378379
'-' +
379380
kHexBytes[buf[offset + 10]] +
@@ -391,15 +392,15 @@ function getBufferedUUID() {
391392

392393
if (uuidBatch === 0) randomFillSync(uuidData);
393394
uuidBatch = (uuidBatch + 1) % kBatchSize;
394-
return serializeUUID(uuidData, uuidBatch * 16);
395+
return serializeUUID(uuidData, 0x40, 0x80, uuidBatch * 16);
395396
}
396397

397398
function getUnbufferedUUID() {
398399
uuidNotBuffered ??= secureBuffer(16);
399400
if (uuidNotBuffered === undefined)
400401
throw new ERR_OPERATION_FAILED('Out of memory');
401402
randomFillSync(uuidNotBuffered);
402-
return serializeUUID(uuidNotBuffered);
403+
return serializeUUID(uuidNotBuffered, 0x40, 0x80);
403404
}
404405

405406
function randomUUID(options) {
@@ -414,6 +415,50 @@ function randomUUID(options) {
414415
return disableEntropyCache ? getUnbufferedUUID() : getBufferedUUID();
415416
}
416417

418+
function writeTimestamp(buf, offset) {
419+
const now = DateNow();
420+
const msb = now / (2 ** 32);
421+
buf[offset] = msb >>> 8;
422+
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;
427+
}
428+
429+
function getBufferedUUIDv7() {
430+
uuidData ??= secureBuffer(16 * kBatchSize);
431+
if (uuidData === undefined)
432+
throw new ERR_OPERATION_FAILED('Out of memory');
433+
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);
439+
}
440+
441+
function getUnbufferedUUIDv7() {
442+
uuidNotBuffered ??= secureBuffer(16);
443+
if (uuidNotBuffered === undefined)
444+
throw new ERR_OPERATION_FAILED('Out of memory');
445+
randomFillSync(uuidNotBuffered, 6);
446+
writeTimestamp(uuidNotBuffered, 0);
447+
return serializeUUID(uuidNotBuffered, 0x70, 0x80);
448+
}
449+
450+
function randomUUIDv7(options) {
451+
if (options !== undefined)
452+
validateObject(options, 'options');
453+
const {
454+
disableEntropyCache = false,
455+
} = options || kEmptyObject;
456+
457+
validateBoolean(disableEntropyCache, 'options.disableEntropyCache');
458+
459+
return disableEntropyCache ? getUnbufferedUUIDv7() : getBufferedUUIDv7();
460+
}
461+
417462
function createRandomPrimeJob(type, size, options) {
418463
validateObject(options, 'options');
419464

@@ -611,6 +656,7 @@ module.exports = {
611656
randomInt,
612657
getRandomValues,
613658
randomUUID,
659+
randomUUIDv7,
614660
generatePrime,
615661
generatePrimeSync,
616662
};
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
const assert = require('assert');
9+
const {
10+
randomUUIDv7,
11+
} = require('crypto');
12+
13+
{
14+
const uuid = randomUUIDv7();
15+
assert.strictEqual(typeof uuid, 'string');
16+
assert.strictEqual(uuid.length, 36);
17+
18+
// UUIDv7 format: xxxxxxxx-xxxx-7xxx-[89ab]xxx-xxxxxxxxxxxx
19+
assert.match(
20+
uuid,
21+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
22+
);
23+
}
24+
25+
{
26+
const uuid = randomUUIDv7();
27+
28+
assert.strictEqual(
29+
Buffer.from(uuid.slice(14, 16), 'hex')[0] & 0xf0, 0x70,
30+
);
31+
32+
assert.strictEqual(
33+
Buffer.from(uuid.slice(19, 21), 'hex')[0] & 0b1100_0000, 0b1000_0000,
34+
);
35+
}
36+
37+
{
38+
const seen = new Set();
39+
for (let i = 0; i < 1000; i++) {
40+
const uuid = randomUUIDv7();
41+
assert(!seen.has(uuid), `Duplicate UUID generated: ${uuid}`);
42+
seen.add(uuid);
43+
}
44+
}
45+
46+
// Timestamp: the embedded timestamp should approximate Date.now().
47+
{
48+
const before = Date.now();
49+
const uuid = randomUUIDv7();
50+
const after = Date.now();
51+
52+
// Extract the 48-bit timestamp from the UUID.
53+
// Bytes 0-3 (chars 0-8) and bytes 4-5 (chars 9-13, skipping the dash).
54+
const hex = uuid.replace(/-/g, '');
55+
const timestampHex = hex.slice(0, 12); // first 48 bits = 12 hex chars
56+
const timestamp = parseInt(timestampHex, 16);
57+
58+
assert(timestamp >= before, `Timestamp ${timestamp} < before ${before}`);
59+
assert(timestamp <= after, `Timestamp ${timestamp} > after ${after}`);
60+
}
61+
62+
{
63+
let prev = randomUUIDv7();
64+
for (let i = 0; i < 100; i++) {
65+
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}`);
73+
prev = curr;
74+
}
75+
}
76+
77+
// Ensure randomUUIDv7 takes no arguments (or ignores them gracefully).
78+
{
79+
const uuid = randomUUIDv7();
80+
assert.match(
81+
uuid,
82+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
83+
);
84+
}
85+
86+
{
87+
const uuidv7Regex =
88+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
89+
90+
assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex);
91+
assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex);
92+
assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex);
93+
assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex);
94+
95+
assert.throws(() => randomUUIDv7(1), {
96+
code: 'ERR_INVALID_ARG_TYPE',
97+
});
98+
99+
assert.throws(() => randomUUIDv7({ disableEntropyCache: '' }), {
100+
code: 'ERR_INVALID_ARG_TYPE',
101+
});
102+
}
103+
104+
{
105+
for (let n = 0; n < 130; n++) {
106+
const uuid = randomUUIDv7();
107+
assert.match(
108+
uuid,
109+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
110+
);
111+
}
112+
}

0 commit comments

Comments
 (0)