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);
+}