From af7e003992c0cdb5aeac55e0ac884d09a7eb1d1b Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Thu, 22 Sep 2016 10:57:35 +0200 Subject: [PATCH 1/4] feat: webcrypto and sha3 In Node.js the underlying hashing functions have not changed, but the browser now uses `webcrypto` instead of JavaScript based methods for `SHA1`, `SHA2-256` and `SHA2-512`. Also `SHA3` support was added in both Node.js and the browser. BREAKING CHANGE: The api was changed to be callback based, as webcrypto only exposes async methods. Closes #10 --- README.md | 46 +++++++++--------------- package.json | 20 ++++++----- src/crypto-browser.js | 66 ++++++++++++++++++++++++++++++++++ src/crypto.js | 32 +++++++++++++++++ src/index.js | 80 +++++++++++++++++++++++++---------------- test/index.spec.js | 83 +++++++++++++++++++++++++------------------ 6 files changed, 225 insertions(+), 102 deletions(-) create mode 100644 src/crypto-browser.js create mode 100644 src/crypto.js diff --git a/README.md b/README.md index c9da2fb..fceceaf 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This module just makes working with multihashes a bit nicer. [js-multihash](//github.com/jbenet/js-multihash) is only for encoding/decoding multihashes, and does not depend on other libs. This module will depend on various implementations for each hash. -For now, it just uses `crypto`, but will use `sha3` and `blake2`, etc. +It currently uses `crypto` and [`sha3`](https://github.com/phusion/node-sha3) in Node.js. In the browser [`webcrypto`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) and [`browserify-sha3`](https://github.com/wanderer/browserify-sha3) are used. ## Table of Contents @@ -80,16 +80,20 @@ You will need to use Node.js `Buffer` API compatible, if you are running inside var multihashing = require('multihashing') var buf = new Buffer('beep boop') -// by default returns a multihash. -multihashing(buf, 'sha1') +multihashing(buf, 'sha1', function (err, multihash) { + // by default calls back with a multihash. +}) // Use `.digest(...)` if you want only the hash digest (drops the prefix indicating the hash type). -multihashing.digest(buf, 'sha1') +multihashing.digest(buf, 'sha1', function (err , digest) { + // digest is the raw digest +}) -// Use `.createHash(...)` for a `crypto.createHash` interface. +// Use `.createHash(...)` for the raw hash functions var h = multihashing.createHash('sha1') -h.update(buf) -h.digest() +h(buf, (err, digest) => { + // digest is a buffer of the sha1 of buf +}) ``` ## Examples @@ -100,39 +104,23 @@ h.digest() > var multihashing = require('multihashing') > var buf = new Buffer('beep boop') -> console.log(multihashing(buf, 'sha1')) +> multihashing(buf, 'sha1'), function (err, mh) { console.log(mh) }) // => -> console.log(multihashing(buf, 'sha2-256')) +> multihashing(buf, 'sha2-256', function (err, mh) { console.log(mh) }) // => -> console.log(multihashing(buf, 'sha2-512')) +> multihashing(buf, 'sha2-512'), function (err, mh) { console.log(mh) }) // => ``` -### Raw digest output - -```js -> var multihashing = require('multihashing') -> var buf = new Buffer('beep boop') - -> console.log(multihashing.digest(buf, 'sha1')) -// => - -> console.log(multihashing.digest(buf, 'sha2-256')) -// => - -> console.log(multihashing.digest(buf, 'sha2-512')) -// => -``` - ## API -### `multihashing(buf, func, length)` +### `multihashing(buf, func, [length,] callback)` -### `digest(buf, func, length)` +### `digest(buf, func, [length,] callback)` -### `createHash(func, length)` +### `createHash(func)` ### `functions` diff --git a/package.json b/package.json index 3db0417..40dfb70 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,17 @@ "description": "multiple hash functions", "main": "lib/index.js", "jsnext:main": "src/index.js", + "browser": { + "./src/crypto.js": "./src/crypto-browser.js" + }, "scripts": { - "test": "aegir-test", - "test:browser": "aegir-test browser", + "test": "PHANTOM=off aegir-test", + "test:browser": "PHANTOM=off aegir-test browser", "test:node": "aegir-test node", "lint": "aegir-lint", - "release": "aegir-release", - "release-minor": "aegir-release minor", - "release-major": "aegir-release major", + "release": "PHANTOM=off aegir-release", + "release-minor": "PHANTOM=off aegir-release minor", + "release-major": "PHANTOM=off aegir-release major", "build": "aegir-build", "coverage": "aegir-coverage", "coverage-publish": "aegir-coverage publish" @@ -33,11 +36,12 @@ "url": "https://github.com/jbenet/js-multihashing/issues" }, "dependencies": { + "browserify-sha3": "0.0.2", "multihashes": "^0.2.0", - "webcrypto": "^0.1.0" + "sha3": "^1.2.0" }, "devDependencies": { - "aegir": "^2.1.1", + "aegir": "^8.1.0", "chai": "^3.5.0", "pre-commit": "^1.1.2" }, @@ -48,4 +52,4 @@ "Juan Batiz-Benet ", "dignifiedquire " ] -} \ No newline at end of file +} diff --git a/src/crypto-browser.js b/src/crypto-browser.js new file mode 100644 index 0000000..582f23e --- /dev/null +++ b/src/crypto-browser.js @@ -0,0 +1,66 @@ +'use strict' + +const SHA3 = require('browserify-sha3') + +const webCrypto = getWebCrypto() + +function getWebCrypto () { + if (typeof window !== 'undefined') { + if (window.crypto) { + return window.crypto.subtle || window.crypto.webkitSubtle + } + + if (window.msCrypto) { + return window.msCrypto.subtle + } + } +} + +function webCryptoHash (type) { + if (!webCrypto) { + throw new Error('Please use a browser with webcrypto support') + } + + return (data, callback) => { + const res = webCrypto.digest({ name: type }, data) + + if (typeof res.then !== 'function') { // IE11 + res.onerror = () => { + callback(`Error hashing data using ${type}`) + } + res.oncomplete = (e) => { + callback(null, e.target.result) + } + return + } + + return res.then((arrbuf) => { + callback(null, new Buffer(new Uint8Array(arrbuf))) + }).catch((err) => callback(err)) + } +} + +function sha1 (buf, callback) { + webCryptoHash('SHA-1')(buf, callback) +} + +function sha2256 (buf, callback) { + webCryptoHash('SHA-256')(buf, callback) +} + +function sha2512 (buf, callback) { + webCryptoHash('SHA-512')(buf, callback) +} + +function sha3 (buf, callback) { + const d = new SHA3.SHA3Hash() + const digest = new Buffer(d.update(buf).digest('hex'), 'hex') + callback(null, digest) +} + +module.exports = { + sha1: sha1, + sha2256: sha2256, + sha2512: sha2512, + sha3: sha3 +} diff --git a/src/crypto.js b/src/crypto.js new file mode 100644 index 0000000..bb08ee4 --- /dev/null +++ b/src/crypto.js @@ -0,0 +1,32 @@ +'use strict' + +const SHA3 = require('sha3') +const crypto = require('crypto') + +function sha1 (buf, callback) { + const digest = crypto.createHash('sha1').update(buf).digest() + callback(null, digest) +} + +function sha2256 (buf, callback) { + const digest = crypto.createHash('sha256').update(buf).digest() + callback(null, digest) +} + +function sha2512 (buf, callback) { + const digest = crypto.createHash('sha512').update(buf).digest() + callback(null, digest) +} + +function sha3 (buf, callback) { + const d = new SHA3.SHA3Hash() + const digest = new Buffer(d.update(buf).digest('hex'), 'hex') + callback(null, digest) +} + +module.exports = { + sha1: sha1, + sha2256: sha2256, + sha2512: sha2512, + sha3: sha3 +} diff --git a/src/index.js b/src/index.js index 5c199a0..62e2ad1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,55 +1,75 @@ 'use strict' const multihash = require('multihashes') -const crypto = require('webcrypto') +const crypto = require('./crypto') -const mh = module.exports = Multihashing +module.exports = Multihashing -mh.Buffer = Buffer // for browser things +function Multihashing (buf, func, length, callback) { + if (typeof length === 'function') { + callback = length + length = undefined + } + + Multihashing.digest(buf, func, length, (err, digest) => { + if (err) { + return callback(err) + } -function Multihashing (buf, func, length) { - return multihash.encode(mh.digest(buf, func, length), func, length) + callback(null, multihash.encode(digest, func, length)) + }) } +Multihashing.Buffer = Buffer // for browser things + // expose multihash itself, to avoid silly double requires. -mh.multihash = multihash +Multihashing.multihash = multihash + +Multihashing.digest = function (buf, func, length, callback) { + if (typeof length === 'function') { + callback = length + length = undefined + } -mh.digest = function (buf, func, length) { - let digest = mh.createHash(func).update(buf).digest() + if (!callback) { + throw new Error('Missing callback') + } + let cb = callback if (length) { - digest = digest.slice(0, length) + cb = (err, digest) => { + if (err) { + return callback(err) + } + + callback(null, digest.slice(0, length)) + } } - return digest + let hash + try { + hash = Multihashing.createHash(func) + } catch (err) { + return cb(err) + } + + hash(buf, cb) } -mh.createHash = function (func, length) { +Multihashing.createHash = function (func) { func = multihash.coerceCode(func) - if (!mh.functions[func]) { + if (!Multihashing.functions[func]) { throw new Error('multihash function ' + func + ' not yet supported') } - return mh.functions[func]() + return Multihashing.functions[func] } -mh.functions = { - 0x11: gsha1, - 0x12: gsha2_256, - 0x13: gsha2_512 - // 0x14: gsha3 // not implemented yet +Multihashing.functions = { + 0x11: crypto.sha1, + 0x12: crypto.sha2256, + 0x13: crypto.sha2512, + 0x14: crypto.sha3 // 0x40: blake2b, // not implemented yet // 0x41: blake2s, // not implemented yet } - -function gsha1 () { - return crypto.createHash('sha1') -} - -function gsha2_256 () { - return crypto.createHash('sha256') -} - -function gsha2_512 () { - return crypto.createHash('sha512') -} diff --git a/test/index.spec.js b/test/index.spec.js index 77bcf26..431b20d 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -5,51 +5,64 @@ const expect = require('chai').expect const multihashing = require('../src') describe('multihashing', () => { - it('sha1', () => { - const buf = new Buffer('beep boop') + function assertEncode (raw, func, encoded) { + it(`encodes in ${func}`, (done) => { + multihashing(raw, func, (err, digest) => { + if (err) { + return done(err) + } - expect( - multihashing(buf, 'sha1') - ).to.be.eql( - new Buffer('11147c8357577f51d4f0a8d393aa1aaafb28863d9421', 'hex') - ) - }) + expect(digest).to.be.eql(encoded) + done() + }) + }) + } - it('sha2-256', () => { - const buf = new Buffer('beep boop') + assertEncode( + new Buffer('beep boop'), + 'sha1', + new Buffer('11147c8357577f51d4f0a8d393aa1aaafb28863d9421', 'hex') + ) - expect( - multihashing(buf, 'sha2-256') - ).to.be.eql( - new Buffer('122090ea688e275d580567325032492b597bc77221c62493e76330b85ddda191ef7c', 'hex') - ) - }) + assertEncode( + new Buffer('beep boop'), + 'sha2-256', + new Buffer('122090ea688e275d580567325032492b597bc77221c62493e76330b85ddda191ef7c', 'hex') + ) - it('sha2-512', () => { - const buf = new Buffer('beep boop') + assertEncode( + new Buffer('beep boop'), + 'sha2-512', + new Buffer('134014f301f31be243f34c5668937883771fa381002f1aaa5f31b3f78e500b66ff2f4f8ea5e3c9f5a61bd073e2452c480484b02e030fb239315a2577f7ae156af177', 'hex') + ) - expect( - multihashing(buf, 'sha2-512') - ).to.be.eql( - new Buffer('134014f301f31be243f34c5668937883771fa381002f1aaa5f31b3f78e500b66ff2f4f8ea5e3c9f5a61bd073e2452c480484b02e030fb239315a2577f7ae156af177', 'hex') - ) - }) + assertEncode( + new Buffer('beep boop'), + 'sha3', + new Buffer('1440e161c54798f78eba3404ac5e7e12d27555b7b810e7fd0db3f25ffa0c785c438331b0fbb6156215f69edf403c642e5280f4521da9bd767296ec81f05100852e78', 'hex') + ) - it('cuts the length', () => { + it('cuts the length', (done) => { const buf = new Buffer('beep boop') - expect( - multihashing(buf, 'sha2-256', 10) - ).to.be.eql( - new Buffer('120a90ea688e275d58056732', 'hex') - ) + multihashing(buf, 'sha2-256', 10, (err, digest) => { + if (err) { + return done(err) + } + + expect( + digest + ).to.be.eql( + new Buffer('120a90ea688e275d58056732', 'hex') + ) + + done() + }) }) it('throws on non implemented func', () => { - expect( - () => multihashing(new Buffer('beep boop'), 'blake2b') - ).to.throw( - /not yet supported/ - ) + multihashing(new Buffer('beep boop'), 'blake2b', (err) => { + expect(err.message).to.match(/not yet supported/) + }) }) }) From dffc5907ad3f815fb450e84ea86ca3ed1a41227b Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Thu, 22 Sep 2016 11:07:47 +0200 Subject: [PATCH 2/4] chore(travis): enable native toolchain support --- .travis.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e1d6320..8677232 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: node_js node_js: - 4 - 5 + - stable # Make sure we have new NPM. before_install: @@ -13,12 +14,20 @@ script: - npm test - npm run coverage -addons: - firefox: 'latest' - before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start after_success: - npm run coverage-publish + +env: + - CXX=g++-4.8 + +addons: + firefox: 'latest' + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 From b0124726b707cdb86966d346ece077053277bcf2 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Thu, 22 Sep 2016 11:38:44 +0200 Subject: [PATCH 3/4] docs(readme): update toc --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fceceaf..8b0ac8f 100644 --- a/README.md +++ b/README.md @@ -22,23 +22,23 @@ It currently uses `crypto` and [`sha3`](https://github.com/phusion/node-sha3) in ## Table of Contents -- [Install](#install) - - [In Node.js through npm](#in-nodejs-through-npm) - - [Use in a browser with browserify, webpack or any other bundler](#use-in-a-browser-with-browserify-webpack-or-any-other-bundler) - - [Use in a browser Using a script tag](#use-in-a-browser-using-a-script-tag) +* [Table of Contents](#table-of-contents) +* [Install](#install) + + [In Node.js through npm](#in-nodejs-through-npm) + + [Use in a browser with browserify, webpack or any other bundler](#use-in-a-browser-with-browserify-webpack-or-any-other-bundler) + + [Use in a browser Using a script tag](#use-in-a-browser-using-a-script-tag) - [Gotchas](#gotchas) -- [Usage](#usage) -- [Examples](#examples) - - [Multihash output](#multihash-output) - - [Raw digest output](#raw-digest-output) -- [API](#api) - - [`multihashing(buf, func, length)`](#multihashingbuf-func-length) - - [`digest(buf, func, length)`](#digestbuf-func-length) - - [`createHash(func, length)`](#createhashfunc-length) - - [`functions`](#functions) -- [Maintainers](#maintainers) -- [Contribute](#contribute) -- [License](#license) +* [Usage](#usage) +* [Examples](#examples) + + [Multihash output](#multihash-output) +* [API](#api) + + [`multihashing(buf, func, [length,] callback)`](#multihashingbuf-func-length-callback) + + [`digest(buf, func, [length,] callback)`](#digestbuf-func-length-callback) + + [`createHash(func)`](#createhashfunc) + + [`functions`](#functions) +* [Maintainers](#maintainers) +* [Contribute](#contribute) +* [License](#license) ## Install From e6667fb18f53a12a35cbc29d3bdc4489b2b8524e Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Sun, 25 Sep 2016 19:24:22 +0200 Subject: [PATCH 4/4] full test coverage and benchmarks added --- benchmarks/hash.js | 32 ++++++++++++++++++++++++++++++++ package.json | 7 +++++-- src/index.js | 4 ++++ test/index.spec.js | 46 +++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 benchmarks/hash.js diff --git a/benchmarks/hash.js b/benchmarks/hash.js new file mode 100644 index 0000000..ba72d79 --- /dev/null +++ b/benchmarks/hash.js @@ -0,0 +1,32 @@ +'use strict' + +const Benchmark = require('benchmark') +const multihashing = require('../src') + +const suite = new Benchmark.Suite('multihashing') +const list = [] + +const algs = ['sha1', 'sha2-256', 'sha2-512', 'sha3'] + +algs.forEach((alg) => { + suite.add(alg, function (d) { + const buf = new Buffer(10 * 1024) + buf.fill(Math.ceil(Math.random() * 100)) + + multihashing(buf, alg, (err, res) => { + if (err) throw err + list.push(res) + d.resolve() + }) + }, { + defer: true + }) +}) +suite +.on('cycle', (event) => { + console.log(String(event.target)) +}) +// run async +.run({ + async: true +}) diff --git a/package.json b/package.json index 40dfb70..ae0cab2 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "lib/index.js", "jsnext:main": "src/index.js", "browser": { - "./src/crypto.js": "./src/crypto-browser.js" + "./src/crypto.js": "./src/crypto-browser.js", + "./lib/crypto.js": "./lib/crypto-browser.js" }, "scripts": { "test": "PHANTOM=off aegir-test", @@ -17,7 +18,8 @@ "release-major": "PHANTOM=off aegir-release major", "build": "aegir-build", "coverage": "aegir-coverage", - "coverage-publish": "aegir-coverage publish" + "coverage-publish": "aegir-coverage publish", + "bench": "node benchmarks/hash.js" }, "pre-commit": [ "lint", @@ -42,6 +44,7 @@ }, "devDependencies": { "aegir": "^8.1.0", + "benchmark": "^2.1.1", "chai": "^3.5.0", "pre-commit": "^1.1.2" }, diff --git a/src/index.js b/src/index.js index 62e2ad1..37b2ae7 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,10 @@ function Multihashing (buf, func, length, callback) { length = undefined } + if (!callback) { + throw new Error('Missing callback') + } + Multihashing.digest(buf, func, length, (err, digest) => { if (err) { return callback(err) diff --git a/test/index.spec.js b/test/index.spec.js index 431b20d..5a0b981 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -60,9 +60,49 @@ describe('multihashing', () => { }) }) - it('throws on non implemented func', () => { - multihashing(new Buffer('beep boop'), 'blake2b', (err) => { - expect(err.message).to.match(/not yet supported/) + it('digest only, without length', (done) => { + const buf = new Buffer('beep boop') + + multihashing.digest(buf, 'sha2-256', (err, digest) => { + if (err) { + return done(err) + } + + expect( + digest + ).to.be.eql( + new Buffer('90ea688e275d580567325032492b597bc77221c62493e76330b85ddda191ef7c', 'hex') + ) + + done() + }) + }) + + describe('invalid arguments', () => { + it('returns an error on non implemented func', (done) => { + multihashing(new Buffer('beep boop'), 'blake2b', (err) => { + expect(err.message).to.match(/not yet supported/) + done() + }) + }) + + it('digest only, with length, returns error on non implmented func', (done) => { + multihashing.digest(new Buffer('beep boop'), 'blake2b', 10, (err) => { + expect(err.message).to.match(/not yet supported/) + done() + }) + }) + + it('throws on missing callback', () => { + expect( + () => multihashing(new Buffer('beep'), 'sha3') + ).to.throw(/Missing callback/) + }) + + it('digest only, throws on missing callback', () => { + expect( + () => multihashing.digest(new Buffer('beep'), 'sha3') + ).to.throw(/Missing callback/) }) }) })