From 5b694a16faa791c0ab990ae6556069bc39321a9d Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Tue, 24 May 2022 20:39:50 -0600 Subject: [PATCH 1/4] Extract UUID generation - Allows for future upgrades with more versions (e.g. namespaces) - Splits earlier when pure random is requested, skipping the PRNG - Uses browser crypto.randomUUID --- app/assets/javascripts/lib/utils/uuids.js | 45 ++++++++++++++++++----- spec/frontend/lib/utils/uuids_spec.js | 16 +++++--- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/lib/utils/uuids.js b/app/assets/javascripts/lib/utils/uuids.js index 98fe4bf96646c6..ebbed3bcd2b6ee 100644 --- a/app/assets/javascripts/lib/utils/uuids.js +++ b/app/assets/javascripts/lib/utils/uuids.js @@ -16,6 +16,16 @@ import { isString } from 'lodash'; import stringHash from 'string-hash'; import { v4 } from 'uuid'; +const getRandomUUIDFunction = () => window.crypto.randomUUID.bind(window.crypto); + +function arrayOf(length) { + return { + using(generator) { + return Array(length).fill(0).map(generator); + }, + }; +} + function getSeed(seeds) { return seeds.reduce((seedling, seed, i) => { let thisSeed = 0; @@ -36,7 +46,9 @@ function getPseudoRandomNumberGenerator(...seeds) { if (seeds.length) { seedNumber = getSeed(seeds); } else { - seedNumber = Math.floor(Math.random() * 10 ** 15); + throw new Error( + 'Seeding the random number generator requires initial seed values' /* eslint-disable-line @gitlab/require-i18n-strings */, + ); } return new MersenneTwister(seedNumber); @@ -57,6 +69,26 @@ function randomValuesForUuid(prng) { return randomValues; } +function generate({ version, seeds, count = 1 }) { + const unimplemented = (v, t) => { + throw new Error(`${t} v${v} uuids are not yet implemented`); + }; + const versions = { + 4: { + random: () => arrayOf(count).using(getRandomUUIDFunction()), + seeded: ({ seeds: seedValues }) => { + const rng = getPseudoRandomNumberGenerator(...seedValues); + + return arrayOf(count).using(() => v4({ random: randomValuesForUuid(rng) })); + }, + }, + }; + const type = seeds.length ? 'seeded' : 'random'; + const generator = versions[version]?.[type] ?? (() => unimplemented(version, type)); + + return generator({ seeds }); +} + /** * Get an array of UUIDv4s * @param {Object} [options={}] @@ -65,12 +97,7 @@ function randomValuesForUuid(prng) { * @returns {UUIDv4[]} An array of UUIDv4s */ export function uuids({ seeds = [], count = 1 } = {}) { - const rng = getPseudoRandomNumberGenerator(...seeds); - return ( - // Create an array the same size as the number of UUIDs requested - Array(count) - .fill(0) - // Replace each slot in the array with a UUID which needs 16 (pseudo)random values to generate - .map(() => v4({ random: randomValuesForUuid(rng) })) - ); + const version = 4; // The only one we handle right now; also the only one the browser provides for free + + return generate({ version, seeds, count }); } diff --git a/spec/frontend/lib/utils/uuids_spec.js b/spec/frontend/lib/utils/uuids_spec.js index a7770d37566f46..4a8fa8f30c475e 100644 --- a/spec/frontend/lib/utils/uuids_spec.js +++ b/spec/frontend/lib/utils/uuids_spec.js @@ -55,11 +55,17 @@ describe('UUIDs Util', () => { ); describe('unseeded UUID randomness', () => { - const nonRandom = Array(6) - .fill(0) - .map((_, i) => uuids({ seeds: [i] })[0]); - const random = uuids({ count: 6 }); - const moreRandom = uuids({ count: 6 }); + let nonRandom; + let random; + let moreRandom; + + beforeAll(() => { + nonRandom = Array(6) + .fill(0) + .map((_, i) => uuids({ seeds: [i] })[0]); + random = uuids({ count: 6 }); + moreRandom = uuids({ count: 6 }); + }); it('is different from a seeded result', () => { random.forEach((id, i) => { -- GitLab From c93866aff90a64a9972f8dad6c06ee9d2eb4492b Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Thu, 26 May 2022 13:11:51 -0600 Subject: [PATCH 2/4] Add the `crypto` object to the global object for all tests --- spec/frontend/__helpers__/shared_test_setup.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js index 011e1142c76801..a8f8881c832305 100644 --- a/spec/frontend/__helpers__/shared_test_setup.js +++ b/spec/frontend/__helpers__/shared_test_setup.js @@ -1,4 +1,5 @@ /* Common setup for both unit and integration test environments */ +import crypto from 'crypto'; import { config as testUtilsConfig } from '@vue/test-utils'; import * as jqueryMatchers from 'custom-jquery-matchers'; import Vue from 'vue'; @@ -69,6 +70,9 @@ Object.assign(global, { cancelIdleCallback(id) { clearTimeout(id); }, + crypto: { + randomUUID: crypto.webcrypto.randomUUID, + }, }); beforeEach(() => { -- GitLab From 6d72e023a6c42d2b8894f7c6c5163143cd42ed07 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 1 Jun 2022 12:26:35 -0600 Subject: [PATCH 3/4] Better variable names for clarity and readability Thanks @sheldonled --- app/assets/javascripts/lib/utils/uuids.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/lib/utils/uuids.js b/app/assets/javascripts/lib/utils/uuids.js index ebbed3bcd2b6ee..a2c2b0d925e025 100644 --- a/app/assets/javascripts/lib/utils/uuids.js +++ b/app/assets/javascripts/lib/utils/uuids.js @@ -20,8 +20,8 @@ const getRandomUUIDFunction = () => window.crypto.randomUUID.bind(window.crypto) function arrayOf(length) { return { - using(generator) { - return Array(length).fill(0).map(generator); + using(uuidGenerator) { + return Array(length).fill(0).map(uuidGenerator); }, }; } @@ -70,8 +70,8 @@ function randomValuesForUuid(prng) { } function generate({ version, seeds, count = 1 }) { - const unimplemented = (v, t) => { - throw new Error(`${t} v${v} uuids are not yet implemented`); + const unimplemented = (ver, type) => { + throw new Error(`${type} v${ver} uuids are not yet implemented`); }; const versions = { 4: { -- GitLab From 71c1fd1ccd24e3592449bd319bc72bc69a173b37 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Tue, 14 Jun 2022 00:23:56 -0600 Subject: [PATCH 4/4] Remove any code to handle versions other than v4 --- app/assets/javascripts/lib/utils/uuids.js | 32 +++++++---------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/lib/utils/uuids.js b/app/assets/javascripts/lib/utils/uuids.js index a2c2b0d925e025..5ee29c4b0e0c97 100644 --- a/app/assets/javascripts/lib/utils/uuids.js +++ b/app/assets/javascripts/lib/utils/uuids.js @@ -69,26 +69,6 @@ function randomValuesForUuid(prng) { return randomValues; } -function generate({ version, seeds, count = 1 }) { - const unimplemented = (ver, type) => { - throw new Error(`${type} v${ver} uuids are not yet implemented`); - }; - const versions = { - 4: { - random: () => arrayOf(count).using(getRandomUUIDFunction()), - seeded: ({ seeds: seedValues }) => { - const rng = getPseudoRandomNumberGenerator(...seedValues); - - return arrayOf(count).using(() => v4({ random: randomValuesForUuid(rng) })); - }, - }, - }; - const type = seeds.length ? 'seeded' : 'random'; - const generator = versions[version]?.[type] ?? (() => unimplemented(version, type)); - - return generator({ seeds }); -} - /** * Get an array of UUIDv4s * @param {Object} [options={}] @@ -97,7 +77,15 @@ function generate({ version, seeds, count = 1 }) { * @returns {UUIDv4[]} An array of UUIDv4s */ export function uuids({ seeds = [], count = 1 } = {}) { - const version = 4; // The only one we handle right now; also the only one the browser provides for free + const versionFour = { + random: () => arrayOf(count).using(getRandomUUIDFunction()), + seeded: ({ seeds: seedValues }) => { + const rng = getPseudoRandomNumberGenerator(...seedValues); + + return arrayOf(count).using(() => v4({ random: randomValuesForUuid(rng) })); + }, + }; + const type = seeds.length ? 'seeded' : 'random'; - return generate({ version, seeds, count }); + return versionFour[type]({ seeds }); } -- GitLab