From 3a5842b141a6e5de0ae338f391661e6b84b167c9 Mon Sep 17 00:00:00 2001 From: Christoph Tavan Date: Thu, 23 Jan 2020 14:38:38 +0100 Subject: [PATCH] feat: remove insecure fallback random number generator BREAKING CHANGE: Remove builtin support for insecure random number generators in the browser. Users who want that will have to supply their own random number generator function. Fixes #173. --- README.md | 8 +++---- README_js.md | 8 +++---- src/rng-browser.js | 46 +++++++++++-------------------------- src/rng.js | 2 +- src/v1.js | 2 +- test/unit/rng.test.js | 17 ++++---------- test/unit/v1-random.test.js | 34 +++++++++++++++++++++++++++ test/unit/v1-rng.test.js | 34 +++++++++++++++++++++++++++ 8 files changed, 96 insertions(+), 55 deletions(-) create mode 100644 test/unit/v1-random.test.js create mode 100644 test/unit/v1-rng.test.js diff --git a/README.md b/README.md index d3cbfca8..f405894f 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Features: - Support for version 1, 3, 4 and 5 UUIDs - Cross-platform: CommonJS build for Node.js and [ECMAScript Modules](#ecmascript-modules) for the browser. -- Uses cryptographically-strong random number APIs (when available) -- Zero-dependency, small footprint (... but not [this small](https://gist.github.com/982883)) +- Uses cryptographically-strong random number APIs +- Zero-dependency, small footprint ## Quickstart - Node.js/CommonJS @@ -201,12 +201,12 @@ uuid.v1(options, buffer, offset); Generate and return a RFC4122 v1 (timestamp-based) UUID. - `options` - (Object) Optional uuid state to apply. Properties may include: - - `node` - (Array) Node id as Array of 6 bytes (per 4.1.6). Default: Randomly generated ID. See note 1. - `clockseq` - (Number between 0 - 0x3fff) RFC clock sequence. Default: An internally maintained clockseq is used. - `msecs` - (Number) Time in milliseconds since unix Epoch. Default: The current time is used. - `nsecs` - (Number between 0-9999) additional time, in 100-nanosecond units. Ignored if `msecs` is unspecified. Default: internal uuid counter is used, as per 4.2.1.2. - + - `random` - (Number[16]) Array of 16 numbers (0-255) to use for initialization of `node` and `clockseq` as described above. Takes precedence over `options.rng`. + - `rng` - (Function) Random # generator function that returns an Array[16] of byte values (0-255). Alternative to `options.random`. - `buffer` - (Array | Buffer) Array or buffer where UUID bytes are to be written. - `offset` - (Number) Starting index in `buffer` at which to begin writing. diff --git a/README_js.md b/README_js.md index 3d0aad2e..504d5e57 100644 --- a/README_js.md +++ b/README_js.md @@ -24,8 +24,8 @@ Features: - Support for version 1, 3, 4 and 5 UUIDs - Cross-platform: CommonJS build for Node.js and [ECMAScript Modules](#ecmascript-modules) for the browser. -- Uses cryptographically-strong random number APIs (when available) -- Zero-dependency, small footprint (... but not [this small](https://gist.github.com/982883)) +- Uses cryptographically-strong random number APIs +- Zero-dependency, small footprint ## Quickstart - Node.js/CommonJS @@ -196,12 +196,12 @@ uuid.v1(options, buffer, offset); Generate and return a RFC4122 v1 (timestamp-based) UUID. - `options` - (Object) Optional uuid state to apply. Properties may include: - - `node` - (Array) Node id as Array of 6 bytes (per 4.1.6). Default: Randomly generated ID. See note 1. - `clockseq` - (Number between 0 - 0x3fff) RFC clock sequence. Default: An internally maintained clockseq is used. - `msecs` - (Number) Time in milliseconds since unix Epoch. Default: The current time is used. - `nsecs` - (Number between 0-9999) additional time, in 100-nanosecond units. Ignored if `msecs` is unspecified. Default: internal uuid counter is used, as per 4.2.1.2. - + - `random` - (Number[16]) Array of 16 numbers (0-255) to use for initialization of `node` and `clockseq` as described above. Takes precedence over `options.rng`. + - `rng` - (Function) Random # generator function that returns an Array[16] of byte values (0-255). Alternative to `options.random`. - `buffer` - (Array | Buffer) Array or buffer where UUID bytes are to be written. - `offset` - (Number) Starting index in `buffer` at which to begin writing. diff --git a/src/rng-browser.js b/src/rng-browser.js index 1a96ee5f..d91b4bc2 100644 --- a/src/rng-browser.js +++ b/src/rng-browser.js @@ -1,41 +1,21 @@ -// Unique ID creation requires a high quality random # generator. In the -// browser this is a little complicated due to unknown quality of Math.random() -// and inconsistent support for the `crypto` API. We do the best we can via -// feature-detection +// Unique ID creation requires a high quality random # generator. In the browser we therefore +// require the crypto API and do not support built-in fallback to lower quality random number +// generators (like Math.random()). -// getRandomValues needs to be invoked in a context where "this" is a Crypto -// implementation. Also, find the complete implementation of crypto on IE11. +// getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. Also, +// find the complete implementation of crypto (msCrypto) on IE11. var getRandomValues = (typeof crypto != 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto)) || (typeof msCrypto != 'undefined' && typeof window.msCrypto.getRandomValues == 'function' && msCrypto.getRandomValues.bind(msCrypto)); -let rng; - -if (getRandomValues) { - // WHATWG crypto RNG - http://wiki.whatwg.org/wiki/Crypto - var rnds8 = new Uint8Array(16); // eslint-disable-line no-undef - - rng = function whatwgRNG() { - getRandomValues(rnds8); - return rnds8; - }; -} else { - // Math.random()-based (RNG) - // - // If all else fails, use Math.random(). It's fast, but is of unspecified - // quality. - var rnds = new Array(16); - - rng = function mathRNG() { - for (var i = 0, r; i < 16; i++) { - if ((i & 0x03) === 0) r = Math.random() * 0x100000000; - rnds[i] = (r >>> ((i & 0x03) << 3)) & 0xff; - } - - return rnds; - }; +var rnds8 = new Uint8Array(16); // eslint-disable-line no-undef +export default function rng() { + if (!getRandomValues) { + throw new Error( + 'uuid: This browser does not seem to support crypto.getRandomValues(). If you need to support this browser, please provide a custom random number generator through options.rng.', + ); + } + return getRandomValues(rnds8); } - -export default rng; diff --git a/src/rng.js b/src/rng.js index de783100..33513bb8 100644 --- a/src/rng.js +++ b/src/rng.js @@ -1,5 +1,5 @@ import crypto from 'crypto'; -export default function nodeRNG() { +export default function rng() { return crypto.randomBytes(16); } diff --git a/src/v1.js b/src/v1.js index 4a02c1c9..594616e7 100644 --- a/src/v1.js +++ b/src/v1.js @@ -26,7 +26,7 @@ function v1(options, buf, offset) { // specified. We do this lazily to minimize issues related to insufficient // system entropy. See #189 if (node == null || clockseq == null) { - var seedBytes = rng(); + var seedBytes = options.random || (options.rng || rng)(); if (node == null) { // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) node = _nodeId = [ diff --git a/test/unit/rng.test.js b/test/unit/rng.test.js index f804c906..f7cd6364 100644 --- a/test/unit/rng.test.js +++ b/test/unit/rng.test.js @@ -3,9 +3,7 @@ import rng from '../../src/rng.js'; import rngBrowser from '../../src/rng-browser.js'; describe('rng', () => { - test('nodeRNG', () => { - assert.equal(rng.name, 'nodeRNG'); - + test('Node.js RNG', () => { var bytes = rng(); assert.equal(bytes.length, 16); @@ -14,15 +12,10 @@ describe('rng', () => { } }); - test('mathRNG', () => { - assert.equal(rngBrowser.name, 'mathRNG'); - - var bytes = rng(); - assert.equal(bytes.length, 16); - - for (var i = 0; i < bytes.length; i++) { - assert.equal(typeof bytes[i], 'number'); - } + test('Browser without crypto.getRandomValues()', () => { + assert.throws(() => { + rngBrowser(); + }); }); // Test of whatwgRNG missing for now since with esmodules we can no longer manipulate the diff --git a/test/unit/v1-random.test.js b/test/unit/v1-random.test.js new file mode 100644 index 00000000..e74dfc50 --- /dev/null +++ b/test/unit/v1-random.test.js @@ -0,0 +1,34 @@ +import assert from 'assert'; +import v1 from '../../src/v1.js'; + +// Since the clockseq is cached in the module this test must run in a separate file in order to +// initialize the v1 clockseq with controlled random data. +describe('v1', () => { + const randomBytesFixture = [ + 0x10, + 0x91, + 0x56, + 0xbe, + 0xc4, + 0xfb, + 0xc1, + 0xea, + 0x71, + 0xb4, + 0xef, + 0xe1, + 0x67, + 0x1c, + 0x58, + 0x36, + ]; + + test('explicit options.random produces expected id', () => { + const id = v1({ + msecs: 1321651533573, + nsecs: 5432, + random: randomBytesFixture, + }); + assert.strictEqual(id, 'd9428888-122b-11e1-81ea-119156bec4fb'); + }); +}); diff --git a/test/unit/v1-rng.test.js b/test/unit/v1-rng.test.js new file mode 100644 index 00000000..7e15ab88 --- /dev/null +++ b/test/unit/v1-rng.test.js @@ -0,0 +1,34 @@ +import assert from 'assert'; +import v1 from '../../src/v1.js'; + +// Since the clockseq is cached in the module this test must run in a separate file in order to +// initialize the v1 clockseq with controlled random data. +describe('v1', () => { + const randomBytesFixture = [ + 0x10, + 0x91, + 0x56, + 0xbe, + 0xc4, + 0xfb, + 0xc1, + 0xea, + 0x71, + 0xb4, + 0xef, + 0xe1, + 0x67, + 0x1c, + 0x58, + 0x36, + ]; + + test('explicit options.random produces expected id', () => { + const id = v1({ + msecs: 1321651533573, + nsecs: 5432, + rng: () => randomBytesFixture, + }); + assert.strictEqual(id, 'd9428888-122b-11e1-81ea-119156bec4fb'); + }); +});