diff --git a/LICENSE b/LICENSE index f64ffb04..3a48e037 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Protocol Labs Inc. +Copyright (c) Protocol Labs Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index cdeb9a13..9df12006 100644 --- a/README.md +++ b/README.md @@ -88,20 +88,14 @@ You will need to use Node.js `Buffer` API compatible, if you are running inside const multihashing = require('multihashing-async') const buf = Buffer.from('beep boop') -multihashing(buf, 'sha1', (err, multihash) => { - // by default calls back with a multihash. -}) +const mh = await multihashing(buf, 'sha1') // Use `.digest(...)` if you want only the hash digest (drops the prefix indicating the hash type). -multihashing.digest(buf, 'sha1', (err , digest) => { - // digest is the raw digest -}) +const digest = await multihashing.digest(buf, 'sha1') // Use `.createHash(...)` for the raw hash functions -const h = multihashing.createHash('sha1') -h(buf, (err, digest) => { - // digest is a buffer of the sha1 of buf -}) +const hash = multihashing.createHash('sha1') +const digest = await hash(buf) ``` ## Examples @@ -109,16 +103,19 @@ h(buf, (err, digest) => { ### Multihash output ```js -> const multihashing = require('multihashing-async') -> const buf = Buffer.from('beep boop') +const multihashing = require('multihashing-async') +const buf = Buffer.from('beep boop') -> multihashing(buf, 'sha1', (err, mh) => console.log(mh)) +const mh = await multihashing(buf, 'sha1') +console.log(mh) // => -> multihashing(buf, 'sha2-256', (err, mh) => console.log(mh)) +const mh = await multihashing(buf, 'sha2-256') +console.log(mh) // => -> multihashing(buf, 'sha2-512', (err, mh) => console.log(mh)) +const mh = await multihashing(buf, 'sha2-512') +console.log(mh) // => ``` @@ -137,4 +134,4 @@ Small note: If editing the README, please conform to the [standard-readme](https ## License -[MIT](LICENSE) © 2016 Protocol Labs Inc. +[MIT](LICENSE) © Protocol Labs Inc. diff --git a/benchmarks/hash.js b/benchmarks/hash.js index dfc31bfc..20409ec9 100644 --- a/benchmarks/hash.js +++ b/benchmarks/hash.js @@ -22,19 +22,20 @@ const algs = [ 'keccak-384', 'keccak-512', 'murmur3-32', - 'murmur3-128' + 'murmur3-128', + 'dbl-sha2-256', + 'blake2b-256', + 'blake2b-512', + 'blake2s-256' ] algs.forEach((alg) => { - suite.add(alg, function (d) { + suite.add(alg, async function (d) { const buf = Buffer.alloc(10 * 1024) buf.fill(Math.ceil(Math.random() * 100)) - - multihashing(buf, alg, (err, res) => { - if (err) throw err - list.push(res) - d.resolve() - }) + const res = await multihashing(buf, alg) + list.push(res) + d.resolve() }, { defer: true }) diff --git a/package.json b/package.json index 3e9c4cda..45eec938 100644 --- a/package.json +++ b/package.json @@ -3,19 +3,22 @@ "version": "0.6.0", "description": "multiple hash functions", "keywords": [ - "multihash" + "multihash", + "hash", + "hashing", + "async" ], "homepage": "https://github.com/multiformats/js-multihashing-async", "bugs": "https://github.com/multiformats/js-multihashing-async/issues", "license": "MIT", - "leadMaintainer": "Hugo Dias ", + "leadMaintainer": "Hugo Dias ", "files": [ "src", "dist" ], "main": "src/index.js", "browser": { - "./src/crypto-sha1-2.js": "./src/crypto-sha1-2-browser.js" + "./src/sha.js": "./src/sha.browser.js" }, "repository": "github:multiformats/js-multihashing-async", "scripts": { @@ -32,16 +35,18 @@ }, "dependencies": { "blakejs": "^1.1.0", + "buffer": "^5.2.1", + "err-code": "^1.1.2", "js-sha3": "~0.8.0", "multihashes": "~0.4.13", - "murmurhash3js": "^3.0.1", - "nodeify": "^1.0.1" + "murmurhash3js-revisited": "^3.0.0" }, "devDependencies": { - "aegir": "^18.0.3", + "aegir": "^18.2.2", "benchmark": "^2.1.4", "chai": "^4.1.2", - "dirty-chai": "^2.0.1" + "dirty-chai": "^2.0.1", + "sinon": "^7.2.7" }, "engines": { "node": ">=6.0.0", diff --git a/src/blake.js b/src/blake.js index 2ca0136e..292e07dc 100644 --- a/src/blake.js +++ b/src/blake.js @@ -1,9 +1,8 @@ 'use strict' +const { Buffer } = require('buffer') const blake = require('blakejs') -const toCallback = require('./utils').toCallback - const minB = 0xb201 const minS = 0xb241 @@ -19,11 +18,14 @@ const blake2s = { digest: blake.blake2sFinal } -const makeB2Hash = (size, hf) => toCallback((buf) => { +// Note that although this function doesn't do any asynchronous work, we mark +// the function as async because it must return a Promise to match the API +// for other functions that do perform asynchronous work (see sha.browser.js) +const makeB2Hash = (size, hf) => async (data) => { const ctx = hf.init(size, null) - hf.update(ctx, buf) + hf.update(ctx, data) return Buffer.from(hf.digest(ctx)) -}) +} module.exports = (table) => { for (let i = 0; i < 64; i++) { diff --git a/src/crypto-sha1-2-browser.js b/src/crypto-sha1-2-browser.js deleted file mode 100644 index 83e339da..00000000 --- a/src/crypto-sha1-2-browser.js +++ /dev/null @@ -1,60 +0,0 @@ -/* global self */ - -'use strict' - -const nodeify = require('nodeify') - -const webCrypto = getWebCrypto() - -function getWebCrypto () { - if (self.crypto) { - return self.crypto.subtle || self.crypto.webkitSubtle - } - - if (self.msCrypto) { - return self.msCrypto.subtle - } -} - -function webCryptoHash (type) { - if (!webCrypto) { - throw new Error('Please use a browser with webcrypto support and ensure the code has been delivered securely via HTTPS/TLS and run within a Secure Context') - } - - return (data, callback) => { - const res = webCrypto.digest({ name: type }, data) - - if (typeof res.then !== 'function') { // IE11 - res.onerror = () => { - callback(new Error(`hashing data using ${type}`)) - } - res.oncomplete = (e) => { - callback(null, e.target.result) - } - return - } - - nodeify( - res.then((raw) => Buffer.from(new Uint8Array(raw))), - callback - ) - } -} - -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) -} - -module.exports = { - sha1: sha1, - sha2256: sha2256, - sha2512: sha2512 -} diff --git a/src/crypto-sha1-2.js b/src/crypto-sha1-2.js deleted file mode 100644 index 89bb24b7..00000000 --- a/src/crypto-sha1-2.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' - -const crypto = require('crypto') -const toCallback = require('./utils').toCallback - -const sha1 = toCallback( - (buf) => crypto.createHash('sha1').update(buf).digest() -) - -const sha2256 = toCallback( - (buf) => crypto.createHash('sha256').update(buf).digest() -) - -const sha2512 = toCallback( - (buf) => crypto.createHash('sha512').update(buf).digest() -) - -module.exports = { - sha1: sha1, - sha2256: sha2256, - sha2512: sha2512 -} diff --git a/src/crypto.js b/src/crypto.js index a18d54e3..847e8ad0 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -1,41 +1,62 @@ 'use strict' +const { Buffer } = require('buffer') const sha3 = require('js-sha3') -const murmur3 = require('murmurhash3js') +const mur = require('murmurhash3js-revisited') +const sha = require('./sha') +const { fromNumberTo32BitBuf } = require('./utils') -const utils = require('./utils') -const sha = require('./crypto-sha1-2') +// Note that although this function doesn't do any asynchronous work, we mark +// the function as async because it must return a Promise to match the API +// for other functions that do perform asynchronous work (see sha.browser.js) +const hash = (algorithm) => async (data) => { + switch (algorithm) { + case 'sha3-224': + return Buffer.from(sha3.sha3_224.arrayBuffer(data)) + case 'sha3-256': + return Buffer.from(sha3.sha3_256.arrayBuffer(data)) + case 'sha3-384': + return Buffer.from(sha3.sha3_384.arrayBuffer(data)) + case 'sha3-512': + return Buffer.from(sha3.sha3_512.arrayBuffer(data)) + case 'shake-128': + return Buffer.from(sha3.shake128.create(128).update(data).arrayBuffer()) + case 'shake-256': + return Buffer.from(sha3.shake256.create(256).update(data).arrayBuffer()) + case 'keccak-224': + return Buffer.from(sha3.keccak224.arrayBuffer(data)) + case 'keccak-256': + return Buffer.from(sha3.keccak256.arrayBuffer(data)) + case 'keccak-384': + return Buffer.from(sha3.keccak384.arrayBuffer(data)) + case 'keccak-512': + return Buffer.from(sha3.keccak512.arrayBuffer(data)) + case 'murmur3-128': + return Buffer.from(mur.x64.hash128(data), 'hex') + case 'murmur3-32': + return fromNumberTo32BitBuf(mur.x86.hash32(data)) -const toCallback = utils.toCallback -const toBuf = utils.toBuf -const fromString = utils.fromString -const fromNumberTo32BitBuf = utils.fromNumberTo32BitBuf - -const dblSha2256 = (buf, cb) => { - sha.sha2256(buf, (err, firstHash) => { - if (err) { - cb(err) - } - sha.sha2256((Buffer.from(firstHash)), cb) - }) + default: + throw new TypeError(`${algorithm} is not a supported algorithm`) + } } module.exports = { - sha1: sha.sha1, - sha2256: sha.sha2256, - sha2512: sha.sha2512, - sha3512: toCallback(toBuf(sha3.sha3_512)), - sha3384: toCallback(toBuf(sha3.sha3_384)), - sha3256: toCallback(toBuf(sha3.sha3_256)), - sha3224: toCallback(toBuf(sha3.sha3_224)), - shake128: toCallback(toBuf(sha3.shake_128, 128)), - shake256: toCallback(toBuf(sha3.shake_256, 256)), - keccak224: toCallback(toBuf(sha3.keccak_224)), - keccak256: toCallback(toBuf(sha3.keccak_256)), - keccak384: toCallback(toBuf(sha3.keccak_384)), - keccak512: toCallback(toBuf(sha3.keccak_512)), - murmur3128: toCallback(toBuf(fromString(murmur3.x64.hash128))), - murmur332: toCallback(fromNumberTo32BitBuf(fromString(murmur3.x86.hash32))), - addBlake: require('./blake'), - dblSha2256: dblSha2256 + sha1: sha('sha1'), + sha2256: sha('sha2-256'), + sha2512: sha('sha2-512'), + dblSha2256: sha('dbl-sha2-256'), + sha3224: hash('sha3-224'), + sha3256: hash('sha3-256'), + sha3384: hash('sha3-384'), + sha3512: hash('sha3-512'), + shake128: hash('shake-128'), + shake256: hash('shake-256'), + keccak224: hash('keccak-224'), + keccak256: hash('keccak-256'), + keccak384: hash('keccak-384'), + keccak512: hash('keccak-512'), + murmur3128: hash('murmur3-128'), + murmur332: hash('murmur3-32'), + addBlake: require('./blake') } diff --git a/src/index.js b/src/index.js index 01052f92..33886715 100644 --- a/src/index.js +++ b/src/index.js @@ -1,37 +1,20 @@ 'use strict' +const { Buffer } = require('buffer') +const errcode = require('err-code') const multihash = require('multihashes') const crypto = require('./crypto') -module.exports = Multihashing - /** - * Hash the given `buf` using the algorithm specified - * by `func`. - * + * Hash the given `buf` using the algorithm specified by `alg`. * @param {Buffer} buf - The value to hash. - * @param {number|string} func - The algorithm to use. + * @param {number|string} alg - The algorithm to use eg 'sha1' * @param {number} [length] - Optionally trim the result to this length. - * @param {function(Error, Buffer)} callback - * @returns {undefined} + * @returns {Promise} */ -function Multihashing (buf, func, length, callback) { - if (typeof length === 'function') { - callback = length - length = undefined - } - - if (!callback) { - throw new Error('Missing callback') - } - - Multihashing.digest(buf, func, length, (err, digest) => { - if (err) { - return callback(err) - } - - callback(null, multihash.encode(digest, func, length)) - }) +async function Multihashing (buf, alg, length) { + const digest = await Multihashing.digest(buf, alg, length) + return multihash.encode(digest, alg, length) } /** @@ -48,54 +31,34 @@ Multihashing.multihash = multihash /** * @param {Buffer} buf - The value to hash. - * @param {number|string} func - The algorithm to use. + * @param {number|string} alg - The algorithm to use eg 'sha1' * @param {number} [length] - Optionally trim the result to this length. - * @param {function(Error, Buffer)} callback - * @returns {undefined} + * @returns {Promise} */ -Multihashing.digest = function (buf, func, length, callback) { - if (typeof length === 'function') { - callback = length - length = undefined - } - - if (!callback) { - throw new Error('Missing callback') - } - - let cb = callback - if (length) { - cb = (err, digest) => { - if (err) { - return callback(err) - } - - callback(null, digest.slice(0, length)) - } - } - - let hash - try { - hash = Multihashing.createHash(func) - } catch (err) { - return cb(err) - } - - hash(buf, cb) +Multihashing.digest = async (buf, alg, length) => { + const hash = Multihashing.createHash(alg) + const digest = await hash(buf) + return length ? digest.slice(0, length) : digest } /** - * @param {string|number} func + * Creates a function that hashes with the given algorithm + * + * @param {string|number} alg - The algorithm to use eg 'sha1' * - * @returns {function} - The to `func` corresponding hash function. + * @returns {function} - The hash function corresponding to `alg` */ -Multihashing.createHash = function (func) { - func = multihash.coerceCode(func) - if (!Multihashing.functions[func]) { - throw new Error('multihash function ' + func + ' not yet supported') +Multihashing.createHash = function (alg) { + if (!alg) { + throw errcode('hash algorithm must be specified', 'ERR_HASH_ALGORITHM_NOT_SPECIFIED') } - return Multihashing.functions[func] + alg = multihash.coerceCode(alg) + if (!Multihashing.functions[alg]) { + throw errcode(`multihash function '${alg}' not yet supported`, 'ERR_HASH_ALGORITHM_NOT_SUPPORTED') + } + + return Multihashing.functions[alg] } /** @@ -140,10 +103,10 @@ Multihashing.functions = { // add blake functions crypto.addBlake(Multihashing.functions) -Multihashing.validate = (data, hash, callback) => { - let algo = multihash.decode(hash).name - Multihashing(data, algo, (err, newHash) => { - if (err) return callback(err) - callback(err, Buffer.compare(hash, newHash) === 0) - }) +Multihashing.validate = async (buf, hash) => { + const newHash = await Multihashing(buf, multihash.decode(hash).name) + + return Buffer.compare(hash, newHash) === 0 } + +module.exports = Multihashing diff --git a/src/sha.browser.js b/src/sha.browser.js new file mode 100644 index 00000000..996eba08 --- /dev/null +++ b/src/sha.browser.js @@ -0,0 +1,30 @@ +'use strict' + +const { Buffer } = require('buffer') + +const crypto = self.crypto || self.msCrypto + +module.exports = (algorithm) => { + if (typeof self === 'undefined' || (!self.crypto && !self.msCrypto)) { + throw new Error( + 'Please use a browser with webcrypto support and ensure the code has been delivered securely via HTTPS/TLS and run within a Secure Context' + ) + } + + return async (data) => { + switch (algorithm) { + case 'sha1': + return Buffer.from(await crypto.subtle.digest({ name: 'SHA-1' }, data)) + case 'sha2-256': + return Buffer.from(await crypto.subtle.digest({ name: 'SHA-256' }, data)) + case 'sha2-512': + return Buffer.from(await crypto.subtle.digest({ name: 'SHA-512' }, data)) + case 'dbl-sha2-256': { + const d = await crypto.subtle.digest({ name: 'SHA-256' }, data) + return Buffer.from(await crypto.subtle.digest({ name: 'SHA-256' }, d)) + } + default: + throw new Error(`${algorithm} is not a supported algorithm`) + } + } +} diff --git a/src/sha.js b/src/sha.js new file mode 100644 index 00000000..94e609d4 --- /dev/null +++ b/src/sha.js @@ -0,0 +1,22 @@ +'use strict' +const crypto = require('crypto') + +// Note that although this function doesn't do any asynchronous work, we mark +// the function as async because it must return a Promise to match the API +// for other functions that do perform asynchronous work (see sha.browser.js) +module.exports = (algorithm) => async (data) => { + switch (algorithm) { + case 'sha1': + return crypto.createHash('sha1').update(data).digest() + case 'sha2-256': + return crypto.createHash('sha256').update(data).digest() + case 'sha2-512': + return crypto.createHash('sha512').update(data).digest() + case 'dbl-sha2-256': { + const first = crypto.createHash('sha256').update(data).digest() + return crypto.createHash('sha256').update(first).digest() + } + default: + throw new Error(`${algorithm} is not a supported algorithm`) + } +} diff --git a/src/utils.js b/src/utils.js index 668501f7..03415260 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,31 +1,8 @@ 'use strict' -exports.toCallback = (doWork) => { - return function (input, callback) { - let res - try { - res = doWork(input) - } catch (err) { - process.nextTick(callback, err) - return - } +const { Buffer } = require('buffer') - process.nextTick(callback, null, res) - } -} - -exports.toBuf = (doWork, other) => (input) => { - let result = doWork(input, other) - return Buffer.from(result, 'hex') -} - -exports.fromString = (doWork, other) => (_input) => { - const input = Buffer.isBuffer(_input) ? _input.toString() : _input - return doWork(input, other) -} - -exports.fromNumberTo32BitBuf = (doWork, other) => (input) => { - let number = doWork(input, other) +const fromNumberTo32BitBuf = (number) => { const bytes = new Array(4) for (let i = 0; i < 4; i++) { @@ -35,3 +12,7 @@ exports.fromNumberTo32BitBuf = (doWork, other) => (input) => { return Buffer.from(bytes) } + +module.exports = { + fromNumberTo32BitBuf +} diff --git a/test/index.spec.js b/test/index.spec.js index 73411c6b..e292af37 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,102 +1,100 @@ /* eslint-env mocha */ 'use strict' +const { Buffer } = require('buffer') const chai = require('chai') const dirtyChai = require('dirty-chai') chai.use(dirtyChai) const expect = chai.expect +const sinon = require('sinon') const multihashing = require('../src') const fixtures = require('./fixtures/encodes') describe('multihashing', () => { - fixtures.forEach((fixture) => { + for (const fixture of fixtures) { const raw = fixture[0] const func = fixture[1] const encoded = fixture[2] - it(`encodes in ${func}`, (done) => { - multihashing(Buffer.from(raw), func, (err, digest) => { - if (err) { - return done(err) - } - - expect( - digest.toString('hex') - ).to.eql(encoded) - done() - }) + it(`encodes in ${func}`, async function () { + const digest = await multihashing(Buffer.from(raw), func) + expect(digest.toString('hex')).to.eql(encoded) }) - }) + } - it('cuts the length', (done) => { + it('cuts the length', async () => { const buf = Buffer.from('beep boop') - multihashing(buf, 'sha2-256', 10, (err, digest) => { - if (err) { - return done(err) - } - - expect(digest) - .to.eql(Buffer.from('120a90ea688e275d58056732', 'hex')) - - done() - }) + const digest = await multihashing(buf, 'sha2-256', 10) + expect(digest) + .to.eql(Buffer.from('120a90ea688e275d58056732', 'hex')) }) - it('digest only, without length', (done) => { + it('digest only, without length', async () => { const buf = Buffer.from('beep boop') - multihashing.digest(buf, 'sha2-256', (err, digest) => { - if (err) { - return done(err) - } + const digest = await multihashing.digest(buf, 'sha2-256') + expect( + digest + ).to.eql( + Buffer.from('90ea688e275d580567325032492b597bc77221c62493e76330b85ddda191ef7c', 'hex') + ) + }) +}) - expect( - digest - ).to.eql( - Buffer.from('90ea688e275d580567325032492b597bc77221c62493e76330b85ddda191ef7c', 'hex') - ) +describe('validate', () => { + it('true on pass', async () => { + const hash = await multihashing(Buffer.from('test'), 'sha2-256') + const validation = await multihashing.validate(Buffer.from('test'), hash) - done() - }) + return expect(validation).to.eql(true) }) - describe('invalid arguments', () => { - it('throws on missing callback', () => { - expect( - () => multihashing(Buffer.from('beep'), 'sha3') - ).to.throw(/Missing callback/) - }) - - it('digest only, throws on missing callback', () => { - expect( - () => multihashing.digest(Buffer.from('beep'), 'sha3') - ).to.throw(/Missing callback/) - }) + it('false on fail', async () => { + const hash = await multihashing(Buffer.from('test'), 'sha2-256') + const validation = await multihashing.validate(Buffer.from('test-fail'), hash) + return expect(validation).to.eql(false) }) }) -describe('validate', () => { - it('true on pass', done => { - multihashing(Buffer.from('test'), 'sha2-256', (err, hash) => { - if (err) throw done(err) - multihashing.validate(Buffer.from('test'), hash, (err, bool) => { - if (err) throw done(err) - expect(bool).to.eql(true) - done() +describe('error handling', () => { + const methods = { + multihashing: multihashing, + digest: multihashing.digest, + createHash: (buff, alg) => multihashing.createHash(alg) + } + + for (const [name, fn] of Object.entries(methods)) { + describe(name, () => { + it('throws an error when there is no hashing algorithm specified', async () => { + const buf = Buffer.from('beep boop') + + try { + await fn(buf) + } catch (err) { + expect(err).to.exist() + expect(err.code).to.eql('ERR_HASH_ALGORITHM_NOT_SPECIFIED') + return + } + expect.fail('Did not throw') }) - }) - }) - it('false on fail', done => { - multihashing(Buffer.from('test'), 'sha2-256', (err, hash) => { - if (err) throw done(err) - multihashing.validate(Buffer.from('test-fail'), hash, (err, bool) => { - if (err) throw done(err) - expect(bool).to.eql(false) - done() + it('throws an error when the hashing algorithm is not supported', async () => { + const buf = Buffer.from('beep boop') + + const stub = sinon.stub(require('multihashes'), 'coerceCode').returns('snake-oil') + try { + await fn(buf, 'snake-oil') + } catch (err) { + expect(err).to.exist() + expect(err.code).to.eql('ERR_HASH_ALGORITHM_NOT_SUPPORTED') + return + } finally { + stub.restore() + } + expect.fail('Did not throw') }) }) - }) + } })