Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit b267f6b

Browse filesBrowse files
nabeel378aduh95
authored andcommitted
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 736ed8a commit b267f6b
Copy full SHA for b267f6b

4 files changed

+185-5Lines changed: 185 additions & 5 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎doc/api/crypto.md‎

Copy file name to clipboardExpand all lines: doc/api/crypto.md
+20Lines changed: 20 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -5805,6 +5805,25 @@ added:
58055805
Generates a random [RFC 4122][] version 4 UUID. The UUID is generated using a
58065806
cryptographic pseudorandom number generator.
58075807

5808+
### `crypto.randomUUIDv7([options])`
5809+
5810+
<!-- YAML
5811+
added: REPLACEME
5812+
-->
5813+
5814+
* `options` {Object}
5815+
* `disableEntropyCache` {boolean} By default, to improve performance,
5816+
Node.js generates and caches enough
5817+
random data to generate up to 128 random UUIDs. To generate a UUID
5818+
without using the cache, set `disableEntropyCache` to `true`.
5819+
**Default:** `false`.
5820+
* Returns: {string}
5821+
5822+
Generates a random [RFC 9562][] version 7 UUID. The UUID contains a millisecond
5823+
precision Unix timestamp in the most significant 48 bits, followed by
5824+
cryptographically secure random bits for the remaining fields, making it
5825+
suitable for use as a database key with time-based sorting.
5826+
58085827
### `crypto.scrypt(password, salt, keylen[, options], callback)`
58095828

58105829
<!-- YAML
@@ -6844,6 +6863,7 @@ See the [list of SSL OP Flags][] for details.
68446863
[RFC 5280]: https://www.rfc-editor.org/rfc/rfc5280.txt
68456864
[RFC 7517]: https://www.rfc-editor.org/rfc/rfc7517.txt
68466865
[RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt
6866+
[RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt
68476867
[Web Crypto API documentation]: webcrypto.md
68486868
[`BN_is_prime_ex`]: https://www.openssl.org/docs/man1.1.1/man3/BN_is_prime_ex.html
68496869
[`Buffer`]: buffer.md
Collapse file

‎lib/crypto.js‎

Copy file name to clipboardExpand all lines: lib/crypto.js
+2Lines 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,
Collapse file

‎lib/internal/crypto/random.js‎

Copy file name to clipboardExpand all lines: lib/internal/crypto/random.js
+51-5Lines 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
};
Collapse file
+112Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.