Skip to content

Commit 4cf34f9

Browse files
authored
feat: Random uuid for telemetry package. (#689)
Originally this implementation was done for the telemetry package. When implementing the browser package we needed the same implementation. Currently this cannot be easily shared, but a task has been created to followup and change that. This is now a copy of the version in the browser package. Subsequent PRs will make use of this functionality.
1 parent 8ad7d46 commit 4cf34f9

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* eslint-disable no-bitwise */
2+
import { fallbackUuidV4, formatDataAsUuidV4 } from '../src/randomUuidV4';
3+
4+
it('formats conformant UUID', () => {
5+
// For this test we remove the random component and just inspect the variant and version.
6+
const idA = formatDataAsUuidV4(Array(16).fill(0x00));
7+
const idB = formatDataAsUuidV4(Array(16).fill(0xff));
8+
const idC = fallbackUuidV4();
9+
10+
// 32 characters and 4 dashes
11+
expect(idC).toHaveLength(36);
12+
const versionA = idA[14];
13+
const versionB = idB[14];
14+
const versionC = idB[14];
15+
16+
expect(versionA).toEqual('4');
17+
expect(versionB).toEqual('4');
18+
expect(versionC).toEqual('4');
19+
20+
// Keep only the top 2 bits.
21+
const specifierA = parseInt(idA[19], 16) & 0xc;
22+
const specifierB = parseInt(idB[19], 16) & 0xc;
23+
const specifierC = parseInt(idC[19], 16) & 0xc;
24+
25+
// bit 6 should be 0 and bit 8 should be one, which is 0x8
26+
expect(specifierA).toEqual(0x8);
27+
expect(specifierB).toEqual(0x8);
28+
expect(specifierC).toEqual(0x8);
29+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// This implementation is the same as in the browser package. Eventually we
2+
// will want a common package for this type of code. (SDK-905)
3+
4+
// The implementation in this file generates UUIDs in v4 format and is suitable
5+
// for use as a UUID in LaunchDarkly events. It is not a rigorous implementation.
6+
7+
// It uses crypto.randomUUID when available.
8+
// If crypto.randomUUID is not available, then it uses random values and forms
9+
// the UUID itself.
10+
// When possible it uses crypto.getRandomValues, but it can use Math.random
11+
// if crypto.getRandomValues is not available.
12+
13+
// UUIDv4 Struct definition.
14+
// https://www.rfc-archive.org/getrfc.php?rfc=4122
15+
// Appendix A. Appendix A - Sample Implementation
16+
const timeLow = {
17+
start: 0,
18+
end: 3,
19+
};
20+
const timeMid = {
21+
start: 4,
22+
end: 5,
23+
};
24+
const timeHiAndVersion = {
25+
start: 6,
26+
end: 7,
27+
};
28+
const clockSeqHiAndReserved = {
29+
start: 8,
30+
end: 8,
31+
};
32+
const clockSeqLow = {
33+
start: 9,
34+
end: 9,
35+
};
36+
const nodes = {
37+
start: 10,
38+
end: 15,
39+
};
40+
41+
function getRandom128bit(): number[] {
42+
if (crypto && crypto.getRandomValues) {
43+
const typedArray = new Uint8Array(16);
44+
crypto.getRandomValues(typedArray);
45+
return [...typedArray.values()];
46+
}
47+
const values = [];
48+
for (let index = 0; index < 16; index += 1) {
49+
// Math.random is 0-1 with inclusive min and exclusive max.
50+
values.push(Math.floor(Math.random() * 256));
51+
}
52+
return values;
53+
}
54+
55+
function hex(bytes: number[], range: { start: number; end: number }): string {
56+
let strVal = '';
57+
for (let index = range.start; index <= range.end; index += 1) {
58+
strVal += bytes[index].toString(16).padStart(2, '0');
59+
}
60+
return strVal;
61+
}
62+
63+
/**
64+
* Given a list of 16 random bytes generate a UUID in v4 format.
65+
*
66+
* Note: The input bytes are modified to conform to the requirements of UUID v4.
67+
*
68+
* @param bytes A list of 16 bytes.
69+
* @returns A UUID v4 string.
70+
*/
71+
export function formatDataAsUuidV4(bytes: number[]): string {
72+
// https://www.rfc-archive.org/getrfc.php?rfc=4122
73+
// 4.4. Algorithms for Creating a UUID from Truly Random or
74+
// Pseudo-Random Numbers
75+
76+
// Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and
77+
// one, respectively.
78+
// eslint-disable-next-line no-bitwise, no-param-reassign
79+
bytes[clockSeqHiAndReserved.start] = (bytes[clockSeqHiAndReserved.start] | 0x80) & 0xbf;
80+
// Set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to
81+
// the 4-bit version number from Section 4.1.3.
82+
// eslint-disable-next-line no-bitwise, no-param-reassign
83+
bytes[timeHiAndVersion.start] = (bytes[timeHiAndVersion.start] & 0x0f) | 0x40;
84+
85+
return (
86+
`${hex(bytes, timeLow)}-${hex(bytes, timeMid)}-${hex(bytes, timeHiAndVersion)}-` +
87+
`${hex(bytes, clockSeqHiAndReserved)}${hex(bytes, clockSeqLow)}-${hex(bytes, nodes)}`
88+
);
89+
}
90+
91+
export function fallbackUuidV4(): string {
92+
const bytes = getRandom128bit();
93+
return formatDataAsUuidV4(bytes);
94+
}
95+
96+
export default function randomUuidV4(): string {
97+
if (typeof crypto !== undefined && typeof crypto.randomUUID === 'function') {
98+
return crypto.randomUUID();
99+
}
100+
101+
return fallbackUuidV4();
102+
}

0 commit comments

Comments
 (0)