Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dubious rng #225

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 23 additions & 22 deletions lib/rng-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,31 @@
// 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

var getRandomValues = typeof(crypto) != 'undefined' && crypto.getRandomValues;
var rnds8 = new Uint8Array(16); // eslint-disable-line no-undef
var rnds = new Array(16);
var rng;

if (getRandomValues) {
// WHATWG crypto RNG - http://wiki.whatwg.org/wiki/Crypto
var rnds8 = new Uint8Array(16); // eslint-disable-line no-undef

module.exports = 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);
function whatwgRNG() {
getRandomValues(rnds8);
return rnds8;
}

module.exports = 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;
}
function mathRNG() {
if (!rng._dubious) throw new Error('No good RNG available. See https://goo.gl/vz3J87');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lint?

for (var i = 0, r; i < 16; i++) {
if (i % 4 === 0) r = Math.random() * 0x100000000 | 0;
rnds[i] = r & 0xff;
r >>>= 8;
}

return rnds;
};
return rnds;
}


module.exports = rng = getRandomValues ? whatwgRNG : mathRNG;

// Hook for allowing the use of Math.random() as an RNG source
rng.allowDubiousRNG = function(flag) {
rng._dubious = flag;
};
8 changes: 6 additions & 2 deletions lib/rng.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
// this is pretty straight-forward - we use the crypto API.

var crypto = require('crypto');

module.exports = function nodeRNG() {
var rng = function nodeRNG() {
return crypto.randomBytes(16);
};

// Shim to keep API consistent across rng and rng-browser
rng.allowDubiousRNG = function() {};

module.exports = rng;
108 changes: 58 additions & 50 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
var assert = require('assert');
var crypto = require('crypto');

var uuid = require('../');
var crypto = require('crypto');
var uuidv1 = require('../v1');
var uuidv3 = require('../v3');
var uuidv4 = require('../v4');
var uuidv5 = require('../v5');

// Verify ordering of v1 ids created with explicit times
var TIME = 1321644961388; // 2011-11-18 11:36:01.388-08:00
Expand Down Expand Up @@ -35,6 +36,13 @@ function hashToHex(hash) {
}).join('');
}

// Remove a module from the CommonJS cache
function unrequire(mod) {
Object.keys(require.cache).forEach(function(path) {
if (path.indexOf(mod) >= 0) delete require.cache[path];
});
}

function compare(name, ids) {
test(name, function() {
// avoid .map for older browsers
Expand All @@ -51,6 +59,7 @@ function compare(name, ids) {
test('nodeRNG', function() {
var rng = require('../lib/rng');
assert.equal(rng.name, 'nodeRNG');
assert(rng.allowDubiousRNG, 'Supports dubious RNG API');

var bytes = rng();
assert.equal(bytes.length, 16);
Expand All @@ -63,6 +72,10 @@ test('nodeRNG', function() {
test('mathRNG', function() {
var rng = require('../lib/rng-browser');
assert.equal(rng.name, 'mathRNG');
assert(rng.allowDubiousRNG, 'Supports dubious RNG API');
assert.throws(rng);
rng.allowDubiousRNG(true);
assert.doesNotThrow(rng);

var bytes = rng();
assert.equal(bytes.length, 16);
Expand All @@ -73,27 +86,19 @@ test('mathRNG', function() {
});

test('cryptoRNG', function() {
var randomFillSync = crypto.randomFillSync;

Object.keys(require.cache).forEach(function(path) {
if (/rng-browser/.test(path)) delete require.cache[path];
});
// Unrequire so we can test with a `crypto` shim
unrequire('rng-browser');

// We shim the web crypto API to trigger cryptoRNG code path in rng module,
// then unshim once we've required it
global.crypto = {
getRandomValues: function(arr) {
var bytes = crypto.randomBytes(arr.length);
for (var i = 0; i < arr.length; i++) {
arr[i] = bytes[i];
}
return arr;
}
};
global.crypto = {getRandomValues: function(arr) {}};
var rng = require('../lib/rng-browser');
delete global.crypto;

assert.equal(rng.name, 'whatwgRNG');

// Unrequire again since we didn't actually provide a good RNG function
unrequire('rng-browser');
});

test('sha1 node', function() {
Expand Down Expand Up @@ -131,50 +136,46 @@ test('md5 browser', function() {
});

test('v3', function() {
var v3 = require('../v3');

// Expect to get the same results as http://tools.adjet.org/uuid-v3
assert.equal(v3('hello.example.com', v3.DNS), '9125a8dc-52ee-365b-a5aa-81b0b3681cf6');
assert.equal(v3('http://example.com/hello', v3.URL), 'c6235813-3ba4-3801-ae84-e0a6ebb7d138');
assert.equal(v3('hello', '0f5abcd1-c194-47f3-905b-2df7263a084b'), 'a981a0c2-68b1-35dc-bcfc-296e52ab01ec');
assert.equal(uuidv3('hello.example.com', uuidv3.DNS), '9125a8dc-52ee-365b-a5aa-81b0b3681cf6');
assert.equal(uuidv3('http://example.com/hello', uuidv3.URL), 'c6235813-3ba4-3801-ae84-e0a6ebb7d138');
assert.equal(uuidv3('hello', '0f5abcd1-c194-47f3-905b-2df7263a084b'), 'a981a0c2-68b1-35dc-bcfc-296e52ab01ec');

// test the buffer functionality
var buf = new Array(16);
var testBuf = [0x91, 0x25, 0xa8, 0xdc, 0x52, 0xee, 0x36, 0x5b, 0xa5, 0xaa, 0x81, 0xb0, 0xb3, 0x68, 0x1c, 0xf6];
v3('hello.example.com', v3.DNS, buf);
uuidv3('hello.example.com', uuidv3.DNS, buf);
assert.ok(buf.length === testBuf.length && buf.every(function(elem, idx) {
return elem === testBuf[idx];
}));

// test offsets as well
buf = new Array(19);
for (var i=0; i<3; ++i) buf[i] = 'landmaster';
v3('hello.example.com', v3.DNS, buf, 3);
uuidv3('hello.example.com', uuidv3.DNS, buf, 3);
assert.ok(buf.length === testBuf.length+3 && buf.every(function(elem, idx) {
return (idx >= 3) ? (elem === testBuf[idx-3]) : (elem === 'landmaster');
}), 'hello');
});

test('v5', function() {
var v5 = require('../v5');

// Expect to get the same results as http://tools.adjet.org/uuid-v5
assert.equal(v5('hello.example.com', v5.DNS), 'fdda765f-fc57-5604-a269-52a7df8164ec');
assert.equal(v5('http://example.com/hello', v5.URL), '3bbcee75-cecc-5b56-8031-b6641c1ed1f1');
assert.equal(v5('hello', '0f5abcd1-c194-47f3-905b-2df7263a084b'), '90123e1c-7512-523e-bb28-76fab9f2f73d');
assert.equal(uuidv5('hello.example.com', uuidv5.DNS), 'fdda765f-fc57-5604-a269-52a7df8164ec');
assert.equal(uuidv5('http://example.com/hello', uuidv5.URL), '3bbcee75-cecc-5b56-8031-b6641c1ed1f1');
assert.equal(uuidv5('hello', '0f5abcd1-c194-47f3-905b-2df7263a084b'), '90123e1c-7512-523e-bb28-76fab9f2f73d');

// test the buffer functionality
var buf = new Array(16);
var testBuf = [0xfd, 0xda, 0x76, 0x5f, 0xfc, 0x57, 0x56, 0x04, 0xa2, 0x69, 0x52, 0xa7, 0xdf, 0x81, 0x64, 0xec];
v5('hello.example.com', v5.DNS, buf);
uuidv5('hello.example.com', uuidv5.DNS, buf);
assert.ok(buf.length === testBuf.length && buf.every(function(elem, idx) {
return elem === testBuf[idx];
}));

// test offsets as well
buf = new Array(19);
for (var i=0; i<3; ++i) buf[i] = 'landmaster';
v5('hello.example.com', v5.DNS, buf, 3);
uuidv5('hello.example.com', uuidv5.DNS, buf, 3);
assert.ok(buf.length === testBuf.length+3 && buf.every(function(elem, idx) {
return (idx >= 3) ? (elem === testBuf[idx-3]) : (elem === 'landmaster');
}));
Expand All @@ -183,25 +184,25 @@ test('v5', function() {

// Verify ordering of v1 ids created using default behavior
compare('uuids with current time', [
uuid.v1(),
uuid.v1(),
uuid.v1(),
uuid.v1(),
uuid.v1()
uuidv1(),
uuidv1(),
uuidv1(),
uuidv1(),
uuidv1()
]);

// Verify ordering of v1 ids created with explicit times
compare('uuids with time option', [
uuid.v1({msecs: TIME - 10*3600*1000}),
uuid.v1({msecs: TIME - 1}),
uuid.v1({msecs: TIME}),
uuid.v1({msecs: TIME + 1}),
uuid.v1({msecs: TIME + 28*24*3600*1000})
uuidv1({msecs: TIME - 10*3600*1000}),
uuidv1({msecs: TIME - 1}),
uuidv1({msecs: TIME}),
uuidv1({msecs: TIME + 1}),
uuidv1({msecs: TIME + 28*24*3600*1000})
]);

test('msec', function() {
assert(
uuid.v1({msecs: TIME}) !== uuid.v1({msecs: TIME}),
uuidv1({msecs: TIME}) !== uuidv1({msecs: TIME}),
'IDs created at same msec are different'
);
});
Expand All @@ -210,7 +211,7 @@ test('exception thrown when > 10k ids created in 1ms', function() {
// Verify throw if too many ids created
var thrown = false;
try {
uuid.v1({msecs: TIME, nsecs: 10000});
uuidv1({msecs: TIME, nsecs: 10000});
} catch (e) {
thrown = true;
}
Expand All @@ -219,8 +220,8 @@ test('exception thrown when > 10k ids created in 1ms', function() {

test('clock regression by msec', function() {
// Verify clock regression bumps clockseq
var uidt = uuid.v1({msecs: TIME});
var uidtb = uuid.v1({msecs: TIME - 1});
var uidt = uuidv1({msecs: TIME});
var uidtb = uuidv1({msecs: TIME - 1});
assert(
parseInt(uidtb.split('-')[3], 16) - parseInt(uidt.split('-')[3], 16) === 1,
'Clock regression by msec increments the clockseq'
Expand All @@ -229,8 +230,8 @@ test('clock regression by msec', function() {

test('clock regression by nsec', function() {
// Verify clock regression bumps clockseq
var uidtn = uuid.v1({msecs: TIME, nsecs: 10});
var uidtnb = uuid.v1({msecs: TIME, nsecs: 9});
var uidtn = uuidv1({msecs: TIME, nsecs: 10});
var uidtnb = uuidv1({msecs: TIME, nsecs: 9});
assert(
parseInt(uidtnb.split('-')[3], 16) - parseInt(uidtn.split('-')[3], 16) === 1,
'Clock regression by nsec increments the clockseq'
Expand All @@ -239,7 +240,7 @@ test('clock regression by nsec', function() {

test('explicit options product expected id', function() {
// Verify explicit options produce expected id
var id = uuid.v1({
var id = uuidv1({
msecs: 1321651533573,
nsecs: 5432,
clockseq: 0x385c,
Expand All @@ -250,11 +251,18 @@ test('explicit options product expected id', function() {

test('ids spanning 1ms boundary are 100ns apart', function() {
// Verify adjacent ids across a msec boundary are 1 time unit apart
var u0 = uuid.v1({msecs: TIME, nsecs: 9999});
var u1 = uuid.v1({msecs: TIME + 1, nsecs: 0});
var u0 = uuidv1({msecs: TIME, nsecs: 9999});
var u1 = uuidv1({msecs: TIME + 1, nsecs: 0});

var before = u0.split('-')[0];
var after = u1.split('-')[0];
var dt = parseInt(after, 16) - parseInt(before, 16);
assert(dt === 1, 'Ids spanning 1ms boundary are 100ns apart');
});

test('legacy API', function() {
var uuid = require('..');
assert.equal(uuid.v1, uuidv1, 'uuid.v1 = uuid/v1');
assert.equal(uuid.v4, uuidv4, 'uuid.v4 = uuid/v4');
assert.equal(uuid, uuidv4, 'uuid = uuid/v4');
});
8 changes: 4 additions & 4 deletions v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@ 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 rnds = 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 = [
seedBytes[0] | 0x01,
seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]
rnds[0] | 0x01,
rnds[1], rnds[2], rnds[3], rnds[4], rnds[5]
];
}
if (clockseq == null) {
// Per 4.2.2, randomize (14 bit) clockseq
clockseq = _clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff;
clockseq = _clockseq = (rnds[6] << 8 | rnds[7]) & 0x3fff;
}
}

Expand Down