| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent ce3f4c8 commit 5b9cb10
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5826,6 +5826,25 @@ added: | |||
| 5826 | 5826 | Generates a random [RFC 4122][] version 4 UUID. The UUID is generated using a | |
| 5827 | 5827 | cryptographic pseudorandom number generator. | |
| 5828 | 5828 | ||
| 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 | + | ||
| 5829 | 5848 | ### `crypto.scrypt(password, salt, keylen[, options], callback)` | |
| 5830 | 5849 | ||
| 5831 | 5850 | <!-- YAML | |
@@ -6864,6 +6883,7 @@ See the [list of SSL OP Flags][] for details. | |||
| 6864 | 6883 | [RFC 5280]: https://www.rfc-editor.org/rfc/rfc5280.txt | |
| 6865 | 6884 | [RFC 7517]: https://www.rfc-editor.org/rfc/rfc7517.txt | |
| 6866 | 6885 | [RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt | |
| 6886 | + [RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt | ||
| 6867 | 6887 | [Web Crypto API documentation]: webcrypto.md | |
| 6868 | 6888 | [`BN_is_prime_ex`]: https://www.openssl.org/docs/man1.1.1/man3/BN_is_prime_ex.html | |
| 6869 | 6889 | [`Buffer`]: buffer.md | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -56,6 +56,7 @@ const { | |||
| 56 | 56 | randomFillSync, | |
| 57 | 57 | randomInt, | |
| 58 | 58 | randomUUID, | |
| 59 | + randomUUIDv7, | ||
| 59 | 60 | } = require('internal/crypto/random'); | |
| 60 | 61 | const { | |
| 61 | 62 | argon2, | |
@@ -220,6 +221,7 @@ module.exports = { | |||
| 220 | 221 | randomFillSync, | |
| 221 | 222 | randomInt, | |
| 222 | 223 | randomUUID, | |
| 224 | + randomUUIDv7, | ||
| 223 | 225 | scrypt, | |
| 224 | 226 | scryptSync, | |
| 225 | 227 | sign: signOneShot, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,6 +11,7 @@ const { | |||
| 11 | 11 | BigIntPrototypeToString, | |
| 12 | 12 | DataView, | |
| 13 | 13 | DataViewPrototypeGetUint8, | |
| 14 | + DateNow, | ||
| 14 | 15 | FunctionPrototypeBind, | |
| 15 | 16 | FunctionPrototypeCall, | |
| 16 | 17 | MathMin, | |
@@ -359,7 +360,7 @@ function getHexBytes() { | |||
| 359 | 360 | return hexBytesCache; | |
| 360 | 361 | } | |
| 361 | 362 | ||
| 362 | - function serializeUUID(buf, offset = 0) { | ||
| 363 | + function serializeUUID(buf, version, variant, offset = 0) { | ||
| 363 | 364 | const kHexBytes = getHexBytes(); | |
| 364 | 365 | // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | |
| 365 | 366 | return kHexBytes[buf[offset]] + | |
@@ -370,10 +371,10 @@ function serializeUUID(buf, offset = 0) { | |||
| 370 | 371 | kHexBytes[buf[offset + 4]] + | |
| 371 | 372 | kHexBytes[buf[offset + 5]] + | |
| 372 | 373 | '-' + | |
| 373 | - kHexBytes[(buf[offset + 6] & 0x0f) | 0x40] + | ||
| 374 | + kHexBytes[(buf[offset + 6] & 0x0f) | version] + | ||
| 374 | 375 | kHexBytes[buf[offset + 7]] + | |
| 375 | 376 | '-' + | |
| 376 | - kHexBytes[(buf[offset + 8] & 0x3f) | 0x80] + | ||
| 377 | + kHexBytes[(buf[offset + 8] & 0x3f) | variant] + | ||
| 377 | 378 | kHexBytes[buf[offset + 9]] + | |
| 378 | 379 | '-' + | |
| 379 | 380 | kHexBytes[buf[offset + 10]] + | |
@@ -391,15 +392,15 @@ function getBufferedUUID() { | |||
| 391 | 392 | ||
| 392 | 393 | if (uuidBatch === 0) randomFillSync(uuidData); | |
| 393 | 394 | uuidBatch = (uuidBatch + 1) % kBatchSize; | |
| 394 | - return serializeUUID(uuidData, uuidBatch * 16); | ||
| 395 | + return serializeUUID(uuidData, 0x40, 0x80, uuidBatch * 16); | ||
| 395 | 396 | } | |
| 396 | 397 | ||
| 397 | 398 | function getUnbufferedUUID() { | |
| 398 | 399 | uuidNotBuffered ??= secureBuffer(16); | |
| 399 | 400 | if (uuidNotBuffered === undefined) | |
| 400 | 401 | throw new ERR_OPERATION_FAILED('Out of memory'); | |
| 401 | 402 | randomFillSync(uuidNotBuffered); | |
| 402 | - return serializeUUID(uuidNotBuffered); | ||
| 403 | + return serializeUUID(uuidNotBuffered, 0x40, 0x80); | ||
| 403 | 404 | } | |
| 404 | 405 | ||
| 405 | 406 | function randomUUID(options) { | |
@@ -414,6 +415,50 @@ function randomUUID(options) { | |||
| 414 | 415 | return disableEntropyCache ? getUnbufferedUUID() : getBufferedUUID(); | |
| 415 | 416 | } | |
| 416 | 417 | ||
| 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 | + | ||
| 417 | 462 | function createRandomPrimeJob(type, size, options) { | |
| 418 | 463 | validateObject(options, 'options'); | |
| 419 | 464 | ||
@@ -611,6 +656,7 @@ module.exports = { | |||
| 611 | 656 | randomInt, | |
| 612 | 657 | getRandomValues, | |
| 613 | 658 | randomUUID, | |
| 659 | + randomUUIDv7, | ||
| 614 | 660 | generatePrime, | |
| 615 | 661 | generatePrimeSync, | |
| 616 | 662 | }; | |
| Original file line number | Diff line number | Diff 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 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments