GeezerVison is showing github.com — crypto: implement randomUUIDv7() · nodejs/node@b267f6b
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file 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 number | Diff line number | Diff line change |
|---|
@@ -5805,6 +5805,25 @@ added: | 5805 | 5805 | Generates a random [RFC 4122][] version 4 UUID. The UUID is generated using a | 5806 | 5806 | cryptographic pseudorandom number generator. | 5807 | 5807 |
| | 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 | + | 5808 | 5827 | ### `crypto.scrypt(password, salt, keylen[, options], callback)` | 5809 | 5828 |
| 5810 | 5829 | <!-- YAML | @@ -6844,6 +6863,7 @@ See the [list of SSL OP Flags][] for details. | 6844 | 6863 | [RFC 5280]: https://www.rfc-editor.org/rfc/rfc5280.txt | 6845 | 6864 | [RFC 7517]: https://www.rfc-editor.org/rfc/rfc7517.txt | 6846 | 6865 | [RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt | | 6866 | +[RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt | 6847 | 6867 | [Web Crypto API documentation]: webcrypto.md | 6848 | 6868 | [`BN_is_prime_ex`]: https://www.openssl.org/docs/man1.1.1/man3/BN_is_prime_ex.html | 6849 | 6869 | [`Buffer`]: buffer.md | |
Collapse file Copy file name to clipboardExpand all lines: lib/crypto.js +2Lines changed: 2 additions & 0 deletions | 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, | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/crypto/random.js +51-5Lines changed: 51 additions & 5 deletions | 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 | }; |
Collapse file Copy file name to clipboard +112Lines changed: 112 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
|