Skip to content

Commit

Permalink
V5 support (#188)
Browse files Browse the repository at this point in the history
* First pass at v5 support

* test v5 against known results

* specify string encoding of UTF8 (#190)

* specify string encoding of UTF8

Node changed the default from `binary` to `utf8` some time back; this change just specifies `utf8`for older versions of node.

https://nodejs.org/dist/latest-v4.x/docs/api/buffer.html#buffer_class_method_buffer_from_str_encoding
https://nodejs.org/dist/latest-v7.x/docs/api/buffer.html#buffer_class_method_buffer_from_string_encoding

* support node <4 Buffer API

* rm blueimp-md5 dependency ('cuz we don't actually depend on it)

* {}'s
  • Loading branch information
broofa committed May 12, 2017
1 parent f37f96a commit 1d56dc9
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 12 deletions.
46 changes: 46 additions & 0 deletions .eslintrc.json
@@ -0,0 +1,46 @@
{
"root": true,
"env": {
"browser": true,
"commonjs": true,
"node": true,
"mocha": true
},
"extends": ["eslint:recommended"],
"installedESLint": true,
"rules": {
"array-bracket-spacing": ["warn", "never"],
"arrow-body-style": ["warn", "as-needed"],
"arrow-parens": ["warn", "as-needed"],
"arrow-spacing": "warn",
"brace-style": "warn",
"camelcase": "warn",
"comma-spacing": ["warn", {"after": true}],
"dot-notation": "warn",
"indent": ["warn", 2, {
"SwitchCase": 1,
"FunctionDeclaration": {"parameters": 1},
"MemberExpression": 1,
"CallExpression": {"arguments": 1}
}],
"key-spacing": ["warn", {"beforeColon": false, "afterColon": true, "mode": "minimum"}],
"keyword-spacing": "warn",
"no-console": "off",
"no-empty": "off",
"no-multi-spaces": "warn",
"no-redeclare": "off",
"no-restricted-globals": ["warn", "Promise"],
"no-trailing-spaces": "warn",
"no-undef": "error",
"no-unused-vars": ["warn", {"args": "none"}],
"padded-blocks": ["warn", "never"],
"object-curly-spacing": ["warn", "never"],
"quotes": ["warn", "single"],
"react/prop-types": "off",
"react/jsx-no-bind": "off",
"semi": ["warn", "always"],
"space-before-blocks": ["warn", "always"],
"space-before-function-paren": ["warn", "never"],
"space-in-parens": ["warn", "never"]
}
}
21 changes: 19 additions & 2 deletions README.md
Expand Up @@ -4,10 +4,12 @@ Simple, fast generation of [RFC4122](http://www.ietf.org/rfc/rfc4122.txt) UUIDS.

Features:

* Generate RFC4122 version 1 or version 4 UUIDs
* Generate RFC4122 version 1, 4 or 5 UUIDs
* Runs in node.js and browsers
* Cryptographically strong random number generation on supporting platforms
* Small footprint (Want something smaller? [Check this out](https://gist.github.com/982883)!)
* Small footprint (Want something smaller? [Check this out](https://gist.github.com/982883))

\["Why no version 3?" Per RFC4122, "If backward compatibility is not an issue, SHA-1 is preferred."... I.e. use v5.]

## Quickstart - CommonJS (Recommended)

Expand All @@ -20,9 +22,24 @@ npm install uuid
const uuidV1 = require('uuid/v1');
uuidV1(); // -> '6c84fb90-12c4-11e1-840d-7b25c5ee775a'


// Generate a v4 UUID (random)
const uuidV4 = require('uuid/v4');
uuidV4(); // -> '110ec58a-a0f2-4ac4-8393-c866d813b8d1'


// Generate a v5 UUID (namespace)
const uuidV5 = require('uuid/v5');

// ... using predefined DNS namespace (for domain names)
uuidV5('hello.example.com', v5.DNS)); // -> 'fdda765f-fc57-5604-a269-52a7df8164ec'

// ... using predefined URL namespace (for, well, URLs)
uuidV5('http://example.com/hello', v5.URL); // -> '3bbcee75-cecc-5b56-8031-b6641c1ed1f1'

// ... using a custom namespace
const MY_NAMESPACE = '(previously generated unique uuid string)';
uuidV5('hello', MY_NAMESPACE); // -> '90123e1c-7512-523e-bb28-76fab9f2f73d'
```

## Quickstart - Pre-packaged for browsers (Not recommended)
Expand Down
2 changes: 1 addition & 1 deletion lib/bytesToUuid.js
Expand Up @@ -10,7 +10,7 @@ for (var i = 0; i < 256; ++i) {
function bytesToUuid(buf, offset) {
var i = offset || 0;
var bth = byteToHex;
return bth[buf[i++]] + bth[buf[i++]] +
return bth[buf[i++]] + bth[buf[i++]] +
bth[buf[i++]] + bth[buf[i++]] + '-' +
bth[buf[i++]] + bth[buf[i++]] + '-' +
bth[buf[i++]] + bth[buf[i++]] + '-' +
Expand Down
4 changes: 2 additions & 2 deletions lib/rng-browser.js
Expand Up @@ -7,7 +7,7 @@ var rng;
var crypto = global.crypto || global.msCrypto; // for IE 11
if (crypto && crypto.getRandomValues) {
// WHATWG crypto RNG - http://wiki.whatwg.org/wiki/Crypto
var rnds8 = new Uint8Array(16);
var rnds8 = new Uint8Array(16); // eslint-disable-line no-undef
rng = function whatwgRNG() {
crypto.getRandomValues(rnds8);
return rnds8;
Expand All @@ -19,7 +19,7 @@ if (!rng) {
//
// If all else fails, use Math.random(). It's fast, but is of unspecified
// quality.
var rnds = new Array(16);
var rnds = new Array(16);
rng = function() {
for (var i = 0, r; i < 16; i++) {
if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
Expand Down
2 changes: 1 addition & 1 deletion lib/rng.js
Expand Up @@ -5,6 +5,6 @@ var rb = require('crypto').randomBytes;

function rng() {
return rb(16);
};
}

module.exports = rng;
85 changes: 85 additions & 0 deletions lib/sha1-browser.js
@@ -0,0 +1,85 @@
// Adapted from Chris Veness' SHA1 code at
// http://www.movable-type.co.uk/scripts/sha1.html
'use strict';

function f(s, x, y, z) {
switch (s) {
case 0: return (x & y) ^ (~x & z);
case 1: return x ^ y ^ z;
case 2: return (x & y) ^ (x & z) ^ (y & z);
case 3: return x ^ y ^ z;
}
}

function ROTL(x, n) {
return (x << n) | (x>>> (32 - n));
}

function sha1(bytes) {
var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
var H = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];

if (typeof(bytes) == 'string') {
var msg = unescape(encodeURIComponent(bytes)); // UTF8 escape
bytes = new Array(msg.length);
for (var i = 0; i < msg.length; i++) bytes[i] = msg.charCodeAt(i);
}

bytes.push(0x80);

var l = bytes.length/4 + 2;
var N = Math.ceil(l/16);
var M = new Array(N);

for (var i=0; i<N; i++) {
M[i] = new Array(16);
for (var j=0; j<16; j++) {
M[i][j] =
bytes[i * 64 + j * 4] << 24 |
bytes[i * 64 + j * 4 + 1] << 16 |
bytes[i * 64 + j * 4 + 2] << 8 |
bytes[i * 64 + j * 4 + 3];
}
}

M[N - 1][14] = ((bytes.length - 1) * 8) /
Math.pow(2, 32); M[N - 1][14] = Math.floor(M[N - 1][14]);
M[N - 1][15] = ((bytes.length - 1) * 8) & 0xffffffff;

for (var i=0; i<N; i++) {
var W = new Array(80);

for (var t=0; t<16; t++) W[t] = M[i][t];
for (var t=16; t<80; t++) {
W[t] = ROTL(W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16], 1);
}

var a = H[0], b = H[1], c = H[2], d = H[3], e = H[4];

for (var t=0; t<80; t++) {
var s = Math.floor(t/20);
var T = ROTL(a, 5) + f(s, b, c, d) + e + K[s] + W[t] >>> 0;
e = d;
d = c;
c = ROTL(b, 30) >>> 0;
b = a;
a = T;
}

H[0] = (H[0] + a) >>> 0;
H[1] = (H[1] + b) >>> 0;
H[2] = (H[2] + c) >>> 0;
H[3] = (H[3] + d) >>> 0;
H[4] = (H[4] + e) >>> 0;
}

return [
H[0] >> 24 & 0xff, H[0] >> 16 & 0xff, H[0] >> 8 & 0xff, H[0] & 0xff,
H[1] >> 24 & 0xff, H[1] >> 16 & 0xff, H[1] >> 8 & 0xff, H[1] & 0xff,
H[2] >> 24 & 0xff, H[2] >> 16 & 0xff, H[2] >> 8 & 0xff, H[2] & 0xff,
H[3] >> 24 & 0xff, H[3] >> 16 & 0xff, H[3] >> 8 & 0xff, H[3] & 0xff,
H[4] >> 24 & 0xff, H[4] >> 16 & 0xff, H[4] >> 8 & 0xff, H[4] & 0xff
];
}

module.exports = sha1;
21 changes: 21 additions & 0 deletions lib/sha1.js
@@ -0,0 +1,21 @@
'use strict';

var crypto = require('crypto');

function sha1(bytes) {
// support modern Buffer API
if (typeof Buffer.from === 'function') {
if (Array.isArray(bytes)) bytes = Buffer.from(bytes);
else if (typeof bytes === 'string') bytes = Buffer.from(bytes, 'utf8');
}

// support pre-v4 Buffer API
else {
if (Array.isArray(bytes)) bytes = new Buffer(bytes);
else if (typeof bytes === 'string') bytes = new Buffer(bytes, 'utf8');
}

return crypto.createHash('sha1').update(bytes).digest();
}

module.exports = sha1;
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -18,10 +18,11 @@
"test": "mocha test/test.js"
},
"browser": {
"./lib/rng.js": "./lib/rng-browser.js"
"./lib/rng.js": "./lib/rng-browser.js",
"./lib/sha1.js": "./lib/sha1-browser.js"
},
"repository": {
"type": "git",
"url": "https://github.com/kelektiv/node-uuid.git"
}
}
}
57 changes: 56 additions & 1 deletion test/test.js
Expand Up @@ -5,6 +5,34 @@ var uuid = require('../');
// Verify ordering of v1 ids created with explicit times
var TIME = 1321644961388; // 2011-11-18 11:36:01.388-08:00

var HASH_SAMPLES = [
{
input: '',
sha1: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
md5: '',
},

// Extended ascii chars
{
input: '\t\b\f !\"#$%&\'()*+,-.\/0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u00A1\u00A2\u00A3\u00A4\u00A5\u00A6\u00A7\u00A8\u00A9\u00AA\u00AB\u00AC\u00AE\u00AF\u00B0\u00B1\u00B2\u00B3\u00B4\u00B5\u00B6\u00B7\u00B8\u00B9\u00BA\u00BB\u00BC\u00BD\u00BE\u00BF\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF',
sha1: 'ca4a426a3d536f14cfd79011e79e10d64de950a0',
md5: '',
},

// A sampling from the Unicode BMP
{
input: '\u00A5\u0104\u018F\u0256\u02B1o\u0315\u038E\u0409\u0500\u0531\u05E1\u05B6\u0920\u0903\u09A4\u0983\u0A20\u0A02\u0AA0\u0A83\u0B06\u0C05\u0C03\u1401\u16A0',
sha1: 'f2753ebc390e5f637e333c2a4179644a93ae9f65',
md5: '',
}
];

function hashToHex(hash) {
return hash.map(function(b) {
return (0x100 + b).toString(16).slice(-2);
}).join('');
}

function compare(name, ids) {
test(name, function() {
// avoid .map for older browsers
Expand All @@ -18,6 +46,33 @@ function compare(name, ids) {
});
}

test('sha1 node', function() {
var sha1 = require('../lib/sha1');

HASH_SAMPLES.forEach(function(sample) {
// Convert the sha1 Buffer to an Array here so we can call map() on it in hashToHex
assert.equal(hashToHex(Array.prototype.slice.apply(sha1(sample.input))), sample.sha1);
});
});

test('sha1 browser', function() {
var sha1 = require('../lib/sha1-browser');

HASH_SAMPLES.forEach(function(sample) {
assert.equal(hashToHex(sha1(sample.input)), sample.sha1);
});
});

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');
});


// Verify ordering of v1 ids created using default behavior
compare('uuids with current time', [
uuid.v1(),
Expand Down Expand Up @@ -80,7 +135,7 @@ test('explicit options product expected id', function() {
msecs: 1321651533573,
nsecs: 5432,
clockseq: 0x385c,
node: [ 0x61, 0xcd, 0x3c, 0xbb, 0x32, 0x10 ]
node: [0x61, 0xcd, 0x3c, 0xbb, 0x32, 0x10]
});
assert(id == 'd9428888-122b-11e1-b85c-61cd3cbb3210', 'Explicit options produce expected id');
});
Expand Down
3 changes: 0 additions & 3 deletions v1.js
@@ -1,6 +1,3 @@
// Unique ID creation requires a high quality random # generator. We feature
// detect to determine the best RNG source, normalizing to a function that
// returns 128-bits of randomness, since that's what's usually required
var rng = require('./lib/rng');
var bytesToUuid = require('./lib/bytesToUuid');

Expand Down
42 changes: 42 additions & 0 deletions v5.js
@@ -0,0 +1,42 @@
var sha1 = require('./lib/sha1-browser');
var bytesToUuid = require('./lib/bytesToUuid');

function uuidToBytes(uuid) {
// Note: We assume we're being passed a valid uuid string
var bytes = [];
uuid.replace(/[a-fA-F0-9]{2}/g, function(hex) {
bytes.push(parseInt(hex, 16));
});

return bytes;
}

function stringToBytes(str) {
str = unescape(encodeURIComponent(str)); // UTF8 escape
var bytes = new Array(str.length);
for (var i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
}

function v5(name, namespace, buf, offset) {
if (typeof(name) == 'string') name = stringToBytes(name);
if (typeof(namespace) == 'string') namespace = uuidToBytes(namespace);

if (!Array.isArray(name)) throw TypeError('name must be an array of bytes');
if (!Array.isArray(namespace) || namespace.length != 16) throw TypeError('namespace must be an array of bytes');

// Per 4.3
var bytes = sha1(namespace.concat(name));
bytes[6] = (bytes[6] & 0x0f) | 0x50;
bytes[8] = (bytes[8] & 0x3f) | 0x80;

return buf || bytesToUuid(bytes);
}

// Pre-defined namespaces, per Appendix C
v5.DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
v5.URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';

module.exports = v5;

0 comments on commit 1d56dc9

Please sign in to comment.