Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -5826,6 +5826,25 @@ added:
Generates a random [RFC 4122][] version 4 UUID. The UUID is generated using a
cryptographic pseudorandom number generator.

### `crypto.randomUUIDv7([options])`

<!-- YAML
added: REPLACEME
-->

* `options` {Object}
* `disableEntropyCache` {boolean} By default, to improve performance,
Node.js generates and caches enough
random data to generate up to 128 random UUIDs. To generate a UUID
without using the cache, set `disableEntropyCache` to `true`.
**Default:** `false`.
* Returns: {string}

Generates a random [RFC 9562][] version 7 UUID. The UUID contains a millisecond
precision Unix timestamp in the most significant 48 bits, followed by
cryptographically secure random bits for the remaining fields, making it
suitable for use as a database key with time-based sorting.

### `crypto.scrypt(password, salt, keylen[, options], callback)`

<!-- YAML
Expand Down Expand Up @@ -6864,6 +6883,7 @@ See the [list of SSL OP Flags][] for details.
[RFC 5280]: https://www.rfc-editor.org/rfc/rfc5280.txt
[RFC 7517]: https://www.rfc-editor.org/rfc/rfc7517.txt
[RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt
[RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt
[Web Crypto API documentation]: webcrypto.md
[`BN_is_prime_ex`]: https://www.openssl.org/docs/man1.1.1/man3/BN_is_prime_ex.html
[`Buffer`]: buffer.md
Expand Down
2 changes: 2 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const {
randomFillSync,
randomInt,
randomUUID,
randomUUIDv7,
} = require('internal/crypto/random');
const {
argon2,
Expand Down Expand Up @@ -220,6 +221,7 @@ module.exports = {
randomFillSync,
randomInt,
randomUUID,
randomUUIDv7,
scrypt,
scryptSync,
sign: signOneShot,
Expand Down
56 changes: 51 additions & 5 deletions lib/internal/crypto/random.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
BigIntPrototypeToString,
DataView,
DataViewPrototypeGetUint8,
DateNow,
FunctionPrototypeBind,
FunctionPrototypeCall,
MathMin,
Expand Down Expand Up @@ -359,7 +360,7 @@ function getHexBytes() {
return hexBytesCache;
}

function serializeUUID(buf, offset = 0) {
function serializeUUID(buf, version, variant, offset = 0) {
const kHexBytes = getHexBytes();
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
return kHexBytes[buf[offset]] +
Expand All @@ -370,10 +371,10 @@ function serializeUUID(buf, offset = 0) {
kHexBytes[buf[offset + 4]] +
kHexBytes[buf[offset + 5]] +
'-' +
kHexBytes[(buf[offset + 6] & 0x0f) | 0x40] +
kHexBytes[(buf[offset + 6] & 0x0f) | version] +
kHexBytes[buf[offset + 7]] +
'-' +
kHexBytes[(buf[offset + 8] & 0x3f) | 0x80] +
kHexBytes[(buf[offset + 8] & 0x3f) | variant] +
kHexBytes[buf[offset + 9]] +
'-' +
kHexBytes[buf[offset + 10]] +
Expand All @@ -391,15 +392,15 @@ function getBufferedUUID() {

if (uuidBatch === 0) randomFillSync(uuidData);
uuidBatch = (uuidBatch + 1) % kBatchSize;
return serializeUUID(uuidData, uuidBatch * 16);
return serializeUUID(uuidData, 0x40, 0x80, uuidBatch * 16);
}

function getUnbufferedUUID() {
uuidNotBuffered ??= secureBuffer(16);
if (uuidNotBuffered === undefined)
throw new ERR_OPERATION_FAILED('Out of memory');
randomFillSync(uuidNotBuffered);
return serializeUUID(uuidNotBuffered);
return serializeUUID(uuidNotBuffered, 0x40, 0x80);
}

function randomUUID(options) {
Expand All @@ -414,6 +415,50 @@ function randomUUID(options) {
return disableEntropyCache ? getUnbufferedUUID() : getBufferedUUID();
}

function writeTimestamp(buf, offset) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could add a monotonic counter here to ensure sub-millisecond sort order

Ref: https://datatracker.ietf.org/doc/html/rfc9562#name-monotonicity-and-counters

Copy link
Copy Markdown
Member

@Renegade334 Renegade334 Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That can be a follow-up. It would reduce the UUID entropy so would probably need to be optional.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!

const now = DateNow();
const msb = now / (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;
}

function getBufferedUUIDv7() {
uuidData ??= secureBuffer(16 * kBatchSize);
if (uuidData === 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);
}

function getUnbufferedUUIDv7() {
uuidNotBuffered ??= secureBuffer(16);
if (uuidNotBuffered === undefined)
throw new ERR_OPERATION_FAILED('Out of memory');
randomFillSync(uuidNotBuffered, 6);
writeTimestamp(uuidNotBuffered, 0);
return serializeUUID(uuidNotBuffered, 0x70, 0x80);
}

function randomUUIDv7(options) {
if (options !== undefined)
validateObject(options, 'options');
const {
disableEntropyCache = false,
} = options || kEmptyObject;

validateBoolean(disableEntropyCache, 'options.disableEntropyCache');

return disableEntropyCache ? getUnbufferedUUIDv7() : getBufferedUUIDv7();
}

function createRandomPrimeJob(type, size, options) {
validateObject(options, 'options');

Expand Down Expand Up @@ -611,6 +656,7 @@ module.exports = {
randomInt,
getRandomValues,
randomUUID,
randomUUIDv7,
generatePrime,
generatePrimeSync,
};
112 changes: 112 additions & 0 deletions test/parallel/test-crypto-randomuuidv7.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use strict';

const common = require('../common');

if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const {
randomUUIDv7,
} = require('crypto');

{
const uuid = randomUUIDv7();
assert.strictEqual(typeof uuid, 'string');
assert.strictEqual(uuid.length, 36);

// UUIDv7 format: xxxxxxxx-xxxx-7xxx-[89ab]xxx-xxxxxxxxxxxx
assert.match(
uuid,
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
}

{
const uuid = randomUUIDv7();

assert.strictEqual(
Buffer.from(uuid.slice(14, 16), 'hex')[0] & 0xf0, 0x70,
);

assert.strictEqual(
Buffer.from(uuid.slice(19, 21), 'hex')[0] & 0b1100_0000, 0b1000_0000,
);
}

{
const seen = new Set();
for (let i = 0; i < 1000; i++) {
const uuid = randomUUIDv7();
assert(!seen.has(uuid), `Duplicate UUID generated: ${uuid}`);
seen.add(uuid);
}
}

// Timestamp: the embedded timestamp should approximate Date.now().
{
const before = Date.now();
const uuid = randomUUIDv7();
const after = Date.now();

// Extract the 48-bit timestamp from the UUID.
// Bytes 0-3 (chars 0-8) and bytes 4-5 (chars 9-13, skipping the dash).
const hex = uuid.replace(/-/g, '');
const timestampHex = hex.slice(0, 12); // first 48 bits = 12 hex chars
const timestamp = parseInt(timestampHex, 16);

assert(timestamp >= before, `Timestamp ${timestamp} < before ${before}`);
assert(timestamp <= after, `Timestamp ${timestamp} > after ${after}`);
}

{
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}`);
prev = curr;
}
}

// Ensure randomUUIDv7 takes no arguments (or ignores them gracefully).
{
const uuid = randomUUIDv7();
assert.match(
uuid,
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
}

{
const uuidv7Regex =
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;

assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex);
assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex);
assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex);
assert.match(randomUUIDv7({ disableEntropyCache: true }), uuidv7Regex);

assert.throws(() => randomUUIDv7(1), {
code: 'ERR_INVALID_ARG_TYPE',
});

assert.throws(() => randomUUIDv7({ disableEntropyCache: '' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
}

{
for (let n = 0; n < 130; n++) {
const uuid = randomUUIDv7();
assert.match(
uuid,
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
}
}
Loading