Skip to content

Commit e264acf

Browse files
paddyoPatrick Housley
and
Patrick Housley
authored
fix: traceids not random when using webcrypto (#825)
Co-authored-by: Patrick Housley <[email protected]>
1 parent c3b0235 commit e264acf

File tree

2 files changed

+97
-3
lines changed

2 files changed

+97
-3
lines changed

src/common/ids/unique-id.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ export function generateUuid () {
4343
let randomValueTable
4444
let randomValueIndex = 0
4545
if (crypto && crypto.getRandomValues) {
46+
// For a UUID, we only need 30 characters since two characters are pre-defined
4647
// eslint-disable-next-line
47-
randomValueTable = crypto.getRandomValues(new Uint8Array(31))
48+
randomValueTable = crypto.getRandomValues(new Uint8Array(30))
4849
}
4950

5051
return uuidv4Template.split('').map(templateInput => {
5152
if (templateInput === 'x') {
52-
return getRandomValue(randomValueTable, ++randomValueIndex).toString(16)
53+
return getRandomValue(randomValueTable, randomValueIndex++).toString(16)
5354
} else if (templateInput === 'y') {
5455
// this is the uuid variant per spec (8, 9, a, b)
5556
// % 4, then shift to get values 8-11
@@ -73,7 +74,7 @@ export function generateRandomHexString (length) {
7374
let randomValueIndex = 0
7475
if (crypto && crypto.getRandomValues) {
7576
// eslint-disable-next-line
76-
randomValueTable = crypto.getRandomValues(new Uint8Array(31))
77+
randomValueTable = crypto.getRandomValues(new Uint8Array(length))
7778
}
7879

7980
const chars = []

src/common/ids/unique-id.test.js

+93
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,96 @@ test('generateTraceId should generate a 32 character hex string', () => {
5656

5757
expect(/^[0-9a-f]{32}$/.test(id)).toEqual(true)
5858
})
59+
60+
describe('#826 trace id should be unique', () => {
61+
test('should not generate subsequent ids that are the same', () => {
62+
expect(uniqueId.generateTraceId()).not.toEqual(uniqueId.generateTraceId())
63+
})
64+
})
65+
66+
describe('rough randomness tests', () => {
67+
/**
68+
* This tests the distribution of each hex character. The distribution of each character
69+
* should approach 1.0 to show that each character is being used as evenly as possible to
70+
* produce the resulting string.
71+
*/
72+
test('should have sufficient distribution of 8 bit character usage', () => {
73+
const tally = new Map()
74+
const sampleSize = 100000 // A large sample size but not so large that it causes the test to take forever
75+
76+
for (let i = 0; i < sampleSize; i++) {
77+
const chars = uniqueId.generateTraceId().split('')
78+
chars.forEach(char => {
79+
if (!tally.has(char)) {
80+
tally.set(char, 1)
81+
} else {
82+
tally.set(char, (tally.get(char)) + 1)
83+
}
84+
})
85+
}
86+
87+
tally.forEach((distributionCount) => {
88+
/*
89+
Take the square root of the distribution count as a variance stabilization:
90+
https://en.wikipedia.org/wiki/Variance-stabilizing_transformation
91+
92+
Divide by the total number of possible values accounting for placement within the
93+
resulting string:
94+
16 === the number of unique hex characters
95+
32 === the length of the string returned by our random function
96+
sampleSize === the total number of times we are running the function
97+
98+
Subtract 1 to make the result into a percentage.
99+
*/
100+
const distributionDeviation = 1 - Math.sqrt(distributionCount) / (16 * 32 * sampleSize)
101+
expect(distributionDeviation).toBeGreaterThanOrEqual(0.999)
102+
})
103+
})
104+
105+
/**
106+
* This tests the distribution of each hex character. The distribution of each character
107+
* should approach 1.0 to show that each character is being used as evenly as possible to
108+
* produce the resulting string.
109+
*/
110+
test('should have sufficient distribution of sequential characters', () => {
111+
const tally = new Map()
112+
const sampleSize = 100000 // A large sample size but not so large that it causes the test to take forever
113+
114+
for (let i = 0; i < sampleSize; i++) {
115+
const chars = uniqueId.generateTraceId().split('')
116+
117+
chars.forEach((char, index) => {
118+
let charSequence
119+
120+
if (index === 0) {
121+
charSequence = `_${char}`
122+
} else {
123+
charSequence = `${chars[index - 1]}${char}`
124+
}
125+
126+
if (!tally.has(charSequence)) {
127+
tally.set(charSequence, 1)
128+
} else {
129+
tally.set(charSequence, (tally.get(charSequence)) + 1)
130+
}
131+
})
132+
}
133+
134+
tally.forEach((distributionCount) => {
135+
/*
136+
Take the square root of the distribution count as a variance stabilization:
137+
https://en.wikipedia.org/wiki/Variance-stabilizing_transformation
138+
139+
Divide by the total number of possible values accounting for placement within the
140+
resulting string:
141+
16 === the number of unique hex characters
142+
32 === the length of the string returned by our random function
143+
sampleSize === the total number of times we are running the function
144+
145+
Subtract 1 to make the result into a percentage.
146+
*/
147+
const distributionDeviation = 1 - Math.sqrt(distributionCount) / (16 * 32 * sampleSize)
148+
expect(distributionDeviation).toBeGreaterThanOrEqual(0.999)
149+
})
150+
})
151+
})

0 commit comments

Comments
 (0)