diff --git a/.docs/implementation-coverage.md b/.docs/implementation-coverage.md index b7f67f85..5d0cc4d9 100644 --- a/.docs/implementation-coverage.md +++ b/.docs/implementation-coverage.md @@ -156,6 +156,7 @@ These algorithms provide quantum-resistant cryptography. - ✅ `crypto.randomFillSync(buffer[, offset][, size])` - ✅ `crypto.randomInt([min, ]max[, callback])` - ✅ `crypto.randomUUID([options])` + - ✅ `crypto.randomUUIDv7([options])` - ✅ `crypto.scrypt(password, salt, keylen[, options], callback)` - ✅ `crypto.scryptSync(password, salt, keylen[, options])` - `-` `crypto.secureHeapUsed()` not applicable to RN @@ -306,6 +307,7 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs - ✅ `crypto.subtle` - ✅ `crypto.getRandomValues(typedArray)` - ✅ `crypto.randomUUID()` + - ✅ `crypto.randomUUIDv7()` _(extension; not in WebCrypto spec)_ - ✅ Class: `CryptoKey` - ✅ `cryptoKey.algorithm` - ✅ `cryptoKey.extractable` @@ -378,19 +380,25 @@ These ciphers are **not available in Node.js** but are provided by RNQC via libs ## `subtle.digest` -| Algorithm | Status | -| ----------- | :----: | -| `cSHAKE128` | ✅ | -| `cSHAKE256` | ✅ | -| `SHA-1` | ✅ | -| `SHA-256` | ✅ | -| `SHA-384` | ✅ | -| `SHA-512` | ✅ | -| `SHA3-256` | ✅ | -| `SHA3-384` | ✅ | -| `SHA3-512` | ✅ | - -> **Note:** `cSHAKE128` and `cSHAKE256` provide SHAKE128/SHAKE256 (XOF) functionality with empty customization, matching Node.js behavior. The `length` parameter (in bytes, must be a multiple of 8) is required to specify the output length. +| Algorithm | Status | +| --------------- | :----: | +| `cSHAKE128` | ✅ | +| `cSHAKE256` | ✅ | +| `KT128` | ✅ | +| `KT256` | ✅ | +| `SHA-1` | ✅ | +| `SHA-256` | ✅ | +| `SHA-384` | ✅ | +| `SHA-512` | ✅ | +| `SHA3-256` | ✅ | +| `SHA3-384` | ✅ | +| `SHA3-512` | ✅ | +| `TurboSHAKE128` | ✅ | +| `TurboSHAKE256` | ✅ | + +> **Note:** `cSHAKE128` and `cSHAKE256` provide SHAKE128/SHAKE256 (XOF) functionality with empty customization, matching Node.js behavior. The `outputLength` parameter (in bytes, must be a multiple of 8) is required to specify the output length. +> +> **TurboSHAKE128/256** (RFC 9861) and **KangarooTwelve** (`KT128`, `KT256`) are extendable-output functions (XOFs) requiring an `outputLength` parameter. TurboSHAKE additionally accepts a `domainSeparation` byte; KangarooTwelve accepts a `customization` byte string. ## `subtle.encrypt` diff --git a/docs/content/docs/api/random.mdx b/docs/content/docs/api/random.mdx index 8a273893..540cc9a0 100644 --- a/docs/content/docs/api/random.mdx +++ b/docs/content/docs/api/random.mdx @@ -22,8 +22,9 @@ Standard random number generators (like `Math.random()`) are **Pseudo-Random Num Cryptographically secure systems require **CSPRNGs (Cryptographically Strong PRNGs)**. These are designed to be unpredictable even if an attacker knows the algorithm. RNQC delegates randomness to the underlying Operating System's entropy pool: -* **iOS/macOS**: `SecRandomCopyBytes` -* **Android**: `SecureRandom` + +- **iOS/macOS**: `SecRandomCopyBytes` +- **Android**: `SecureRandom` This ensures that generated keys, salts, and nonces are secure. @@ -40,7 +41,10 @@ Generates cryptographically strong pseudo-random data. @@ -66,17 +70,27 @@ randomBytes(256, (err, buf) => { --- ### randomFill(buffer[, offset][, size], callback) + ### randomFillSync(buffer[, offset][, size]) -Populates an *existing* buffer with random data. Works correctly with TypedArray views over larger ArrayBuffers — `offset` and `size` are relative to the view, not the underlying buffer. +Populates an _existing_ buffer with random data. Works correctly with TypedArray views over larger ArrayBuffers — `offset` and `size` are relative to the view, not the underlying buffer. **Parameters:** @@ -90,9 +104,12 @@ Returns a random integer `n` such that `min <= n < max`. The implementation avoi @@ -114,14 +131,18 @@ const m = randomInt(10, 50); ### randomUUID([options]) -Generates a random RFC 4122 Version 4 UUID. +Generates a random RFC 9562 Version 4 UUID. **Parameters:** @@ -129,6 +150,39 @@ Generates a random RFC 4122 Version 4 UUID. --- +### randomUUIDv7([options]) + +Generates a random RFC 9562 §5.7 Version 7 UUID. Layout: 48-bit big-endian Unix-ms timestamp prefix, 4-bit version (`7`), 2-bit variant (`10`), and 74 bits of CSPRNG output. + +The timestamp prefix makes v7 UUIDs **lexicographically sortable by creation time**, which makes them well-suited as primary keys, idempotency tokens, and ordered identifiers. + +**Parameters:** + + + +**Returns:** `string` e.g. `'017f22e2-79b0-7cc3-98c4-dc0c0c07398f'` + +**Example:** + +```ts twoslash +// @noErrors +import { randomUUIDv7 } from 'react-native-quick-crypto'; + +const id = randomUUIDv7(); +// '0193b6f6-a8d0-7abc-8def-0123456789ab' +``` + +--- + ## Real-World Examples ### API Key Generation @@ -139,11 +193,12 @@ Generating a URL-safe random string. import { randomBytes } from 'react-native-quick-crypto'; function generateApiKey(lengthBytes = 32): string { - const buffer = randomBytes(lengthBytes); - return buffer.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); + const buffer = randomBytes(lengthBytes); + return buffer + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); } ``` @@ -157,7 +212,7 @@ import { randomBytes } from 'react-native-quick-crypto'; const NONCE_SIZE = 12; function generateNonce(): Buffer { - return randomBytes(NONCE_SIZE); + return randomBytes(NONCE_SIZE); } ``` @@ -169,14 +224,14 @@ Shuffling an array using the Fisher-Yates algorithm with CSPRNG. import { randomInt } from 'react-native-quick-crypto'; async function secureShuffle(array: T[]): Promise { - const arr = [...array]; - for (let i = arr.length - 1; i > 0; i--) { - const j = await new Promise((resolve, reject) => { - randomInt(0, i + 1, (err, n) => err ? reject(err) : resolve(n)); - }); - [arr[i], arr[j]] = [arr[j], arr[i]]; - } - return arr; + const arr = [...array]; + for (let i = arr.length - 1; i > 0; i--) { + const j = await new Promise((resolve, reject) => { + randomInt(0, i + 1, (err, n) => (err ? reject(err) : resolve(n))); + }); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; } ``` @@ -185,4 +240,5 @@ async function secureShuffle(array: T[]): Promise { ## Security Considerations ### Blocking the Event Loop + `randomBytes` (synchronous) taps into system sources. While generally fast, requesting large amounts of entropy on a constrained device could potentially block the Main/UI thread. For generating 4KB or less (keys, nonces), sync is fine. For larger buffers, use the asynchronous version or `randomUUID`. diff --git a/docs/data/coverage.ts b/docs/data/coverage.ts index 91936377..376be4da 100644 --- a/docs/data/coverage.ts +++ b/docs/data/coverage.ts @@ -218,6 +218,18 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'x25519', status: 'implemented' }, { name: 'x448', status: 'implemented' }, { name: 'dh', status: 'missing' }, + { name: 'slh-dsa-sha2-128s', status: 'implemented' }, + { name: 'slh-dsa-sha2-128f', status: 'implemented' }, + { name: 'slh-dsa-sha2-192s', status: 'implemented' }, + { name: 'slh-dsa-sha2-192f', status: 'implemented' }, + { name: 'slh-dsa-sha2-256s', status: 'implemented' }, + { name: 'slh-dsa-sha2-256f', status: 'implemented' }, + { name: 'slh-dsa-shake-128s', status: 'implemented' }, + { name: 'slh-dsa-shake-128f', status: 'implemented' }, + { name: 'slh-dsa-shake-192s', status: 'implemented' }, + { name: 'slh-dsa-shake-192f', status: 'implemented' }, + { name: 'slh-dsa-shake-256s', status: 'implemented' }, + { name: 'slh-dsa-shake-256f', status: 'implemented' }, ], }, { @@ -232,6 +244,18 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'x25519', status: 'implemented' }, { name: 'x448', status: 'implemented' }, { name: 'dh', status: 'missing' }, + { name: 'slh-dsa-sha2-128s', status: 'implemented' }, + { name: 'slh-dsa-sha2-128f', status: 'implemented' }, + { name: 'slh-dsa-sha2-192s', status: 'implemented' }, + { name: 'slh-dsa-sha2-192f', status: 'implemented' }, + { name: 'slh-dsa-sha2-256s', status: 'implemented' }, + { name: 'slh-dsa-sha2-256f', status: 'implemented' }, + { name: 'slh-dsa-shake-128s', status: 'implemented' }, + { name: 'slh-dsa-shake-128f', status: 'implemented' }, + { name: 'slh-dsa-shake-192s', status: 'implemented' }, + { name: 'slh-dsa-shake-192f', status: 'implemented' }, + { name: 'slh-dsa-shake-256s', status: 'implemented' }, + { name: 'slh-dsa-shake-256f', status: 'implemented' }, ], }, { @@ -258,6 +282,7 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'randomFill / randomFillSync', status: 'implemented' }, { name: 'randomInt', status: 'implemented' }, { name: 'randomUUID', status: 'implemented' }, + { name: 'randomUUIDv7', status: 'implemented' }, { name: 'scrypt', status: 'implemented' }, { name: 'secureHeapUsed', @@ -279,6 +304,18 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'Ed25519', status: 'implemented' }, { name: 'Ed448', status: 'implemented' }, { name: 'HMAC', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-128s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-128f', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-192s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-192f', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-256s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-256f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-128s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-128f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-192s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-192f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-256s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-256f', status: 'implemented' }, ], }, { @@ -290,6 +327,18 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'Ed25519', status: 'implemented' }, { name: 'Ed448', status: 'implemented' }, { name: 'HMAC', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-128s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-128f', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-192s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-192f', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-256s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-256f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-128s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-128f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-192s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-192f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-256s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-256f', status: 'implemented' }, ], }, { name: 'timingSafeEqual', status: 'implemented' }, @@ -351,6 +400,8 @@ export const COVERAGE_DATA: CoverageCategory[] = [ subItems: [ { name: 'cSHAKE128', status: 'implemented' }, { name: 'cSHAKE256', status: 'implemented' }, + { name: 'KT128', status: 'implemented' }, + { name: 'KT256', status: 'implemented' }, { name: 'SHA-1', status: 'implemented' }, { name: 'SHA-256', status: 'implemented' }, { name: 'SHA-384', status: 'implemented' }, @@ -358,6 +409,8 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'SHA3-256', status: 'implemented' }, { name: 'SHA3-384', status: 'implemented' }, { name: 'SHA3-512', status: 'implemented' }, + { name: 'TurboSHAKE128', status: 'implemented' }, + { name: 'TurboSHAKE256', status: 'implemented' }, ], }, { @@ -448,6 +501,66 @@ export const COVERAGE_DATA: CoverageCategory[] = [ status: 'partial', note: 'spki, pkcs8, jwk', }, + { + name: 'SLH-DSA-SHA2-128s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHA2-128f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHA2-192s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHA2-192f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHA2-256s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHA2-256f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-128s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-128f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-192s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-192f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-256s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-256f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, ], }, { @@ -466,6 +579,18 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'RSA-OAEP', status: 'implemented' }, { name: 'RSA-PSS', status: 'implemented' }, { name: 'RSASSA-PKCS1-v1_5', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-128s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-128f', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-192s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-192f', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-256s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-256f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-128s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-128f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-192s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-192f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-256s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-256f', status: 'implemented' }, { name: 'X25519', status: 'implemented' }, { name: 'X448', status: 'implemented' }, { name: 'AES-CTR', status: 'implemented' }, @@ -561,6 +686,66 @@ export const COVERAGE_DATA: CoverageCategory[] = [ status: 'partial', note: 'spki, pkcs8, jwk', }, + { + name: 'SLH-DSA-SHA2-128s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHA2-128f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHA2-192s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHA2-192f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHA2-256s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHA2-256f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-128s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-128f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-192s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-192f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-256s', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, + { + name: 'SLH-DSA-SHAKE-256f', + status: 'partial', + note: 'spki, pkcs8, raw-public, raw-seed', + }, { name: 'X25519', status: 'partial', @@ -587,6 +772,18 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'ML-DSA-87', status: 'implemented' }, { name: 'RSA-PSS', status: 'implemented' }, { name: 'RSASSA-PKCS1-v1_5', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-128s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-128f', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-192s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-192f', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-256s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-256f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-128s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-128f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-192s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-192f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-256s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-256f', status: 'implemented' }, ], }, { @@ -615,6 +812,18 @@ export const COVERAGE_DATA: CoverageCategory[] = [ { name: 'ML-DSA-87', status: 'implemented' }, { name: 'RSA-PSS', status: 'implemented' }, { name: 'RSASSA-PKCS1-v1_5', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-128s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-128f', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-192s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-192f', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-256s', status: 'implemented' }, + { name: 'SLH-DSA-SHA2-256f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-128s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-128f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-192s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-192f', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-256s', status: 'implemented' }, + { name: 'SLH-DSA-SHAKE-256f', status: 'implemented' }, ], }, { diff --git a/example/src/tests/random/random_tests.ts b/example/src/tests/random/random_tests.ts index 9c6e1d1c..76cf9ee0 100644 --- a/example/src/tests/random/random_tests.ts +++ b/example/src/tests/random/random_tests.ts @@ -788,3 +788,77 @@ test( }); }, ); + +// --- randomUUID (RFC 9562 §5.4 — v4) --- + +const UUID_V4_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +test(SUITE, 'randomUUID returns RFC 9562 v4 string', () => { + const id = crypto.randomUUID(); + expect(id).to.match(UUID_V4_RE); +}); + +test(SUITE, 'randomUUID accepts disableEntropyCache option', () => { + const id = crypto.randomUUID({ disableEntropyCache: true }); + expect(id).to.match(UUID_V4_RE); +}); + +test(SUITE, 'randomUUID values are unique', () => { + const ids = new Set(); + for (let i = 0; i < 100; i++) ids.add(crypto.randomUUID()); + expect(ids.size).to.equal(100); +}); + +// --- randomUUIDv7 (RFC 9562 §5.7) --- + +const UUID_V7_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +function uuidV7Timestamp(id: string): number { + // First 12 hex chars = 48-bit ms timestamp. + return parseInt(id.replace(/-/g, '').slice(0, 12), 16); +} + +test(SUITE, 'randomUUIDv7 returns RFC 9562 v7 string', () => { + const id = crypto.randomUUIDv7(); + expect(id).to.match(UUID_V7_RE); +}); + +test(SUITE, 'randomUUIDv7 version=7 and variant=10', () => { + const id = crypto.randomUUIDv7(); + const hex = id.replace(/-/g, ''); + expect(parseInt(hex[12]!, 16)).to.equal(7); + // variant nibble: top 2 bits must be 10xx, i.e. 8/9/a/b + const v = parseInt(hex[16]!, 16); + expect(v >= 0x8 && v <= 0xb).to.equal(true); +}); + +test(SUITE, 'randomUUIDv7 timestamp matches Date.now()', () => { + const before = Date.now(); + const id = crypto.randomUUIDv7(); + const after = Date.now(); + const ts = uuidV7Timestamp(id); + expect(ts >= before && ts <= after).to.equal(true); +}); + +test(SUITE, 'randomUUIDv7 timestamps are monotonic', () => { + const ids: string[] = []; + for (let i = 0; i < 50; i++) ids.push(crypto.randomUUIDv7()); + for (let i = 1; i < ids.length; i++) { + expect(uuidV7Timestamp(ids[i]!) >= uuidV7Timestamp(ids[i - 1]!)).to.equal( + true, + ); + } +}); + +test(SUITE, 'randomUUIDv7 accepts disableEntropyCache option', () => { + const id = crypto.randomUUIDv7({ disableEntropyCache: true }); + expect(id).to.match(UUID_V7_RE); +}); + +test(SUITE, 'randomUUIDv7 values are unique', () => { + const ids = new Set(); + for (let i = 0; i < 100; i++) ids.add(crypto.randomUUIDv7()); + expect(ids.size).to.equal(100); +}); diff --git a/packages/react-native-quick-crypto/src/random.ts b/packages/react-native-quick-crypto/src/random.ts index 579b4354..fabc6c55 100644 --- a/packages/react-native-quick-crypto/src/random.ts +++ b/packages/react-native-quick-crypto/src/random.ts @@ -350,14 +350,28 @@ for (let i = 0; i < 256; ++i) { byteToHex.push((i + 0x100).toString(16).slice(1)); } -// Based on https://github.com/uuidjs/uuid/blob/main/src/v4.js -export function randomUUID() { - const size = 16; - const buffer = new Buffer(size); - randomFillSync(buffer, 0, size); - - // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` - buffer[6] = (buffer[6]! & 0x0f) | 0x40; +export interface RandomUUIDOptions { + // Accepted for Node.js parity. RNQC does not buffer entropy, so this is a + // no-op: every UUID already pulls fresh bytes from the OS CSPRNG. + disableEntropyCache?: boolean; +} + +function validateRandomUUIDOptions(options?: RandomUUIDOptions): void { + if (options === undefined) return; + if (typeof options !== 'object' || options === null) { + throw new TypeError('options must be an object'); + } + if ( + options.disableEntropyCache !== undefined && + typeof options.disableEntropyCache !== 'boolean' + ) { + throw new TypeError('options.disableEntropyCache must be a boolean'); + } +} + +// RFC 9562 variant 10xx is shared by v4 and v7. +function serializeUUID(buffer: Buffer, version: number): string { + buffer[6] = (buffer[6]! & 0x0f) | (version << 4); buffer[8] = (buffer[8]! & 0x3f) | 0x80; return ( @@ -383,3 +397,31 @@ export function randomUUID() { byteToHex[buffer[15]!] ).toLowerCase(); } + +// RFC 9562 §5.4 — random UUID (v4). +export function randomUUID(options?: RandomUUIDOptions): string { + validateRandomUUIDOptions(options); + const buffer = new Buffer(16); + randomFillSync(buffer, 0, 16); + return serializeUUID(buffer, 4); +} + +// RFC 9562 §5.7 — Unix-ms timestamped UUID (v7). +// Layout: 48-bit big-endian Unix-ms timestamp | 4-bit version (7) | +// 12 bits random | 2-bit variant (10) | 62 bits random. +export function randomUUIDv7(options?: RandomUUIDOptions): string { + validateRandomUUIDOptions(options); + const buffer = new Buffer(16); + randomFillSync(buffer, 6, 10); + + const now = Date.now(); + const msb = Math.floor(now / 0x100000000); + buffer[0] = (msb >>> 8) & 0xff; + buffer[1] = msb & 0xff; + buffer[2] = (now >>> 24) & 0xff; + buffer[3] = (now >>> 16) & 0xff; + buffer[4] = (now >>> 8) & 0xff; + buffer[5] = now & 0xff; + + return serializeUUID(buffer, 7); +}