diff --git a/README_js.md b/README_js.md index 4baf44c0..d105544c 100644 --- a/README_js.md +++ b/README_js.md @@ -8,7 +8,7 @@ Simple, fast generation of [RFC4122](http://www.ietf.org/rfc/rfc4122.txt) UUIDS. Features: -* Support for version 1, 4 and 5 UUIDs +* Support for version 1, 3, 4 and 5 UUIDs * Cross-platform * Uses cryptographically-strong random number APIs (when available) * Zero-dependency, small footprint (... but not [this small](https://gist.github.com/982883)) @@ -28,6 +28,25 @@ const uuidv1 = require('uuid/v1'); uuidv1(); // RESULT ``` +Version 3 (namespace): + +```javascript --context +const uuidv3 = require('uuid/v3'); + +// ... using predefined DNS namespace (for domain names) +uuidv3('hello.example.com', uuidv3.DNS); // RESULT + +// ... using predefined URL namespace (for, well, URLs) +uuidv3('http://example.com/hello', uuidv3.URL); // RESULT + +// ... using a custom namespace +// +// Note: Custom namespaces should be a UUID string specific to your application! +// E.g. the one here was generated using this modules `uuid` CLI. +const MY_NAMESPACE = '1b671a64-40d5-491e-99b0-da01ff1f3341'; +uuidv3('Hello, World!', MY_NAMESPACE); // RESULT +``` + Version 4 (random): ```javascript --context @@ -67,6 +86,15 @@ uuidv1(); // -> v1 UUID ``` +For version 3 uuids: + +```html + + +``` + For version 4 uuids: ```html @@ -135,6 +163,40 @@ uuidv1(null, arr, 0); // RESULT uuidv1(null, arr, 16); // RESULT ``` +### Version 3 + +```javascript +const uuidv3 = require('uuid/v3'); + +// Incantations +uuidv3(name, namespace); +uuidv3(name, namespace, buffer); +uuidv3(name, namespace, buffer, offset); +``` + +Generate and return a RFC4122 v3 UUID. + +* `name` - (String | Array[]) "name" to create UUID with +* `namespace` - (String | Array[]) "namespace" UUID either as a String or Array[16] of byte values +* `buffer` - (Array | Buffer) Array or buffer where UUID bytes are to be written. +* `offset` - (Number) Starting index in `buffer` at which to begin writing. Default = 0 + +Returns `buffer`, if specified, otherwise the string form of the UUID + +Example: + +```javascript --run +// Generate a unique namespace (typically you would do this once, outside of +// your project, then bake this value into your code) +const uuidv4 = require('uuid/v4'); +const uuidv3 = require('uuid/v3'); +const MY_NAMESPACE = uuidv4(); // RESULT + +// Generate a couple namespace uuids +uuidv3('hello', MY_NAMESPACE); // RESULT +uuidv3('world', MY_NAMESPACE); // RESULT +``` + ### Version 4 ```javascript diff --git a/bin/uuid b/bin/uuid index 2fd26d76..502626e6 100755 --- a/bin/uuid +++ b/bin/uuid @@ -5,6 +5,7 @@ function usage() { console.log('Usage:'); console.log(' uuid'); console.log(' uuid v1'); + console.log(' uuid v3 '); console.log(' uuid v4'); console.log(' uuid v5 '); console.log(' uuid --help'); @@ -25,6 +26,20 @@ switch (version) { console.log(uuidV1()); break; + case 'v3': + var uuidV3 = require('../v3'); + + var name = args.shift(); + var namespace = args.shift(); + assert(name != null, 'v3 name not specified'); + assert(namespace != null, 'v3 namespace not specified'); + + if (namespace == 'URL') namespace = uuidV3.URL; + if (namespace == 'DNS') namespace = uuidV3.DNS; + + console.log(uuidV3(name, namespace)); + break; + case 'v4': var uuidV4 = require('../v4'); console.log(uuidV4()); diff --git a/lib/md5-browser.js b/lib/md5-browser.js new file mode 100644 index 00000000..d7f82dff --- /dev/null +++ b/lib/md5-browser.js @@ -0,0 +1,218 @@ +/* + * Browser-compatible JavaScript MD5 + * + * Modification of JavaScript MD5 + * https://github.com/blueimp/JavaScript-MD5 + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + * + * Based on + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +'use strict;' + +function md5(bytes) { + + 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); + } + + return md5ToHexEncodedArray( + wordsToMd5( + bytesToWords(bytes) + , bytes.length * 8) + ) +} + + + /* +* Convert an array of little-endian words to an array of bytes +*/ +function md5ToHexEncodedArray(input) { + var i + var x + var output = [] + var length32 = input.length * 32 + var hexTab = '0123456789abcdef' + var hex + + for (i = 0; i < length32; i += 8) { + x = (input[i >> 5] >>> (i % 32)) & 0xFF + + hex = parseInt(hexTab.charAt((x >>> 4) & 0x0F) + hexTab.charAt(x & 0x0F), 16) + + output.push(hex) + } + return output +} + + /* +* Calculate the MD5 of an array of little-endian words, and a bit length. +*/ +function wordsToMd5 (x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << (len % 32) + x[(((len + 64) >>> 9) << 4) + 14] = len + + var i + var olda + var oldb + var oldc + var oldd + var a = 1732584193 + var b = -271733879 + var c = -1732584194 + + var d = 271733878 + + for (i = 0; i < x.length; i += 16) { + olda = a + oldb = b + oldc = c + oldd = d + + a = md5ff(a, b, c, d, x[i], 7, -680876936) + d = md5ff(d, a, b, c, x[i + 1], 12, -389564586) + c = md5ff(c, d, a, b, x[i + 2], 17, 606105819) + b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330) + a = md5ff(a, b, c, d, x[i + 4], 7, -176418897) + d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426) + c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341) + b = md5ff(b, c, d, a, x[i + 7], 22, -45705983) + a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416) + d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417) + c = md5ff(c, d, a, b, x[i + 10], 17, -42063) + b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162) + a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682) + d = md5ff(d, a, b, c, x[i + 13], 12, -40341101) + c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290) + b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329) + + a = md5gg(a, b, c, d, x[i + 1], 5, -165796510) + d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632) + c = md5gg(c, d, a, b, x[i + 11], 14, 643717713) + b = md5gg(b, c, d, a, x[i], 20, -373897302) + a = md5gg(a, b, c, d, x[i + 5], 5, -701558691) + d = md5gg(d, a, b, c, x[i + 10], 9, 38016083) + c = md5gg(c, d, a, b, x[i + 15], 14, -660478335) + b = md5gg(b, c, d, a, x[i + 4], 20, -405537848) + a = md5gg(a, b, c, d, x[i + 9], 5, 568446438) + d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690) + c = md5gg(c, d, a, b, x[i + 3], 14, -187363961) + b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501) + a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467) + d = md5gg(d, a, b, c, x[i + 2], 9, -51403784) + c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473) + b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734) + + a = md5hh(a, b, c, d, x[i + 5], 4, -378558) + d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463) + c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562) + b = md5hh(b, c, d, a, x[i + 14], 23, -35309556) + a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060) + d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353) + c = md5hh(c, d, a, b, x[i + 7], 16, -155497632) + b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640) + a = md5hh(a, b, c, d, x[i + 13], 4, 681279174) + d = md5hh(d, a, b, c, x[i], 11, -358537222) + c = md5hh(c, d, a, b, x[i + 3], 16, -722521979) + b = md5hh(b, c, d, a, x[i + 6], 23, 76029189) + a = md5hh(a, b, c, d, x[i + 9], 4, -640364487) + d = md5hh(d, a, b, c, x[i + 12], 11, -421815835) + c = md5hh(c, d, a, b, x[i + 15], 16, 530742520) + b = md5hh(b, c, d, a, x[i + 2], 23, -995338651) + + a = md5ii(a, b, c, d, x[i], 6, -198630844) + d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415) + c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905) + b = md5ii(b, c, d, a, x[i + 5], 21, -57434055) + a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571) + d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606) + c = md5ii(c, d, a, b, x[i + 10], 15, -1051523) + b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799) + a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359) + d = md5ii(d, a, b, c, x[i + 15], 10, -30611744) + c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380) + b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649) + a = md5ii(a, b, c, d, x[i + 4], 6, -145523070) + d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379) + c = md5ii(c, d, a, b, x[i + 2], 15, 718787259) + b = md5ii(b, c, d, a, x[i + 9], 21, -343485551) + + a = safeAdd(a, olda) + b = safeAdd(b, oldb) + c = safeAdd(c, oldc) + d = safeAdd(d, oldd) + } + return [a, b, c, d] +} + + /* +* Convert an array bytes to an array of little-endian words +* Characters >255 have their high-byte silently ignored. +*/ +function bytesToWords(input) { + + var i + var output = [] + output[(input.length >> 2) - 1] = undefined + for (i = 0; i < output.length; i += 1) { + output[i] = 0 + } + var length8 = input.length * 8 + for (i = 0; i < length8; i += 8) { + output[i >> 5] |= (input[(i / 8)] & 0xFF) << (i % 32) + } + + return output +} + +/* +* Add integers, wrapping at 2^32. This uses 16-bit operations internally +* to work around bugs in some JS interpreters. +*/ +function safeAdd (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF) + var msw = (x >> 16) + (y >> 16) + (lsw >> 16) + return (msw << 16) | (lsw & 0xFFFF) +} + +/* +* Bitwise rotate a 32-bit number to the left. +*/ +function bitRotateLeft (num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)) +} + +/* +* These functions implement the four basic operations the algorithm uses. +*/ +function md5cmn (q, a, b, x, s, t) { + return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b) +} +function md5ff (a, b, c, d, x, s, t) { + return md5cmn((b & c) | ((~b) & d), a, b, x, s, t) +} +function md5gg (a, b, c, d, x, s, t) { + return md5cmn((b & d) | (c & (~d)), a, b, x, s, t) +} +function md5hh (a, b, c, d, x, s, t) { + return md5cmn(b ^ c ^ d, a, b, x, s, t) +} +function md5ii (a, b, c, d, x, s, t) { + return md5cmn(c ^ (b | (~d)), a, b, x, s, t) +} + +module.exports = md5; \ No newline at end of file diff --git a/lib/md5.js b/lib/md5.js new file mode 100644 index 00000000..1645d6bb --- /dev/null +++ b/lib/md5.js @@ -0,0 +1,21 @@ +'use strict'; + +var crypto = require('crypto'); + +function md5(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('md5').update(bytes).digest(); +} + +module.exports = md5; diff --git a/lib/v35.js b/lib/v35.js new file mode 100644 index 00000000..3e01871c --- /dev/null +++ b/lib/v35.js @@ -0,0 +1,57 @@ +var bytesToUuid = require('./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; +} + + + +module.exports = function(name, version, hashfunc){ + + var generateUUID = function (value, namespace, buf, offset) { + var off = buf && offset || 0; + + if (typeof(value) == 'string') value = stringToBytes(value); + if (typeof(namespace) == 'string') namespace = uuidToBytes(namespace); + + if (!Array.isArray(value)) throw TypeError('value must be an array of bytes'); + if (!Array.isArray(namespace) || namespace.length != 16) throw TypeError('namespace must be uuid string or an Array of 16 byte values'); + + // Per 4.3 + var bytes = hashfunc(namespace.concat(value)); + bytes[6] = (bytes[6] & 0x0f) | version; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + if (buf) { + for (var idx = 0; idx < 16; ++idx) { + buf[off+idx] = bytes[idx]; + } + } + + return buf || bytesToUuid(bytes); + } + + generateUUID.name = name + + // Pre-defined namespaces, per Appendix C + generateUUID.DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + generateUUID.URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; + + return generateUUID; + +} diff --git a/package.json b/package.json index 220ec747..aa8786b6 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ }, "browser": { "./lib/rng.js": "./lib/rng-browser.js", - "./lib/sha1.js": "./lib/sha1-browser.js" + "./lib/sha1.js": "./lib/sha1-browser.js", + "./lib/md5.js": "./lib/md5-browser.js" }, "repository": { "type": "git", diff --git a/test/test.js b/test/test.js index 56b13e3b..c35a1eea 100644 --- a/test/test.js +++ b/test/test.js @@ -9,21 +9,21 @@ var HASH_SAMPLES = [ { input: '', sha1: 'da39a3ee5e6b4b0d3255bfef95601890afd80709', - md5: '', + md5: 'd41d8cd98f00b204e9800998ecf8427e', }, // 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: '', + md5: 'e8098ec21950f841731d28749129d3ee', }, // 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: '', + md5: '231b309e277b6be8bb3d6c688b7f098b', } ]; @@ -63,6 +63,46 @@ test('sha1 browser', function() { }); }); +test('md5 node', function() { + var md5 = require('../lib/md5'); + + 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(md5(sample.input))), sample.md5); + }); +}); + +test('md5 browser', function() { + var md5 = require('../lib/md5-browser'); + + HASH_SAMPLES.forEach(function(sample) { + assert.equal(hashToHex(md5(sample.input)), sample.md5); + }); +}); + +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'); + + // 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); + 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); + 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'); diff --git a/v3.js b/v3.js new file mode 100644 index 00000000..ee7e14c0 --- /dev/null +++ b/v3.js @@ -0,0 +1,4 @@ +var v35 = require('./lib/v35.js'); +var md5 = require('./lib/md5'); + +module.exports = v35('v3', 0x30, md5); \ No newline at end of file diff --git a/v5.js b/v5.js index 07eaf747..358f6570 100644 --- a/v5.js +++ b/v5.js @@ -1,50 +1,3 @@ -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) { - var off = buf && offset || 0; - - 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 uuid string or an Array of 16 byte values'); - - // Per 4.3 - var bytes = sha1(namespace.concat(name)); - bytes[6] = (bytes[6] & 0x0f) | 0x50; - bytes[8] = (bytes[8] & 0x3f) | 0x80; - - if (buf) { - for (var idx = 0; idx < 16; ++idx) { - buf[off+idx] = bytes[idx]; - } - } - - 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; +var v35 = require('./lib/v35.js'); +var sha1 = require('./lib/sha1'); +module.exports = v35('v5', 0x50, sha1); \ No newline at end of file