diff --git a/README.md b/README.md index 3bcb44e4..965ddbc9 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,8 @@ const mh = await multihashing(buf, 'sha1') const digest = await multihashing.digest(buf, 'sha1') // Use `.createHash(...)` for the raw hash functions -const h = multihashing.createHash('sha1') -const digest = await h(buf) +const hash = multihashing.createHash('sha1') +const digest = await hash(buf) ``` ## Examples diff --git a/package.json b/package.json index 8d80d23e..f4444341 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,10 @@ "browser": { "./src/sha.js": "./src/sha.browser.js" }, + "pre-push": [ + "lint", + "test" + ], "scripts": { "test": "aegir test", "test:browser": "aegir test -t browser", @@ -37,9 +41,11 @@ "repository": "github:multiformats/js-multihashing-async", "dependencies": { "blakejs": "^1.1.0", + "err-code": "^1.1.2", "js-sha3": "~0.8.0", "multihashes": "~0.4.13", - "murmurhash3js-revisited": "^3.0.0" + "murmurhash3js-revisited": "^3.0.0", + "sinon": "^7.2.7" }, "devDependencies": { "aegir": "^18.0.3", diff --git a/src/blake.js b/src/blake.js index d64fb705..ef55f25b 100644 --- a/src/blake.js +++ b/src/blake.js @@ -17,16 +17,13 @@ const blake2s = { digest: blake.blake2sFinal } -const makeB2Hash = (size, hf) => (data) => { - return new Promise((resolve, reject) => { - try { - const ctx = hf.init(size, null) - hf.update(ctx, data) - resolve(Buffer.from(hf.digest(ctx))) - } catch (error) { - reject(error) - } - }) +// 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, data) + return Buffer.from(hf.digest(ctx)) } module.exports = (table) => { diff --git a/src/crypto.js b/src/crypto.js index b6cb9628..894a9b49 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -5,42 +5,39 @@ const mur = require('murmurhash3js-revisited') const sha = require('./sha') const { fromNumberTo32BitBuf } = require('./utils') -const hash = (algorithm) => (data) => { - return new Promise((resolve, reject) => { - try { - switch (algorithm) { - case 'sha3-224': - return resolve(Buffer.from(sha3.sha3_224.arrayBuffer(data))) - case 'sha3-256': - return resolve(Buffer.from(sha3.sha3_256.arrayBuffer(data))) - case 'sha3-384': - return resolve(Buffer.from(sha3.sha3_384.arrayBuffer(data))) - case 'sha3-512': - return resolve(Buffer.from(sha3.sha3_512.arrayBuffer(data))) - case 'shake-128': - return resolve(Buffer.from(sha3.shake128.create(128).update(data).arrayBuffer())) - case 'shake-256': - return resolve(Buffer.from(sha3.shake256.create(256).update(data).arrayBuffer())) - case 'keccak-224': - return resolve(Buffer.from(sha3.keccak224.arrayBuffer(data))) - case 'keccak-256': - return resolve(Buffer.from(sha3.keccak256.arrayBuffer(data))) - case 'keccak-384': - return resolve(Buffer.from(sha3.keccak384.arrayBuffer(data))) - case 'keccak-512': - return resolve(Buffer.from(sha3.keccak512.arrayBuffer(data))) - case 'murmur3-128': - return resolve(Buffer.from(mur.x64.hash128(data), 'hex')) - case 'murmur3-32': - return resolve(fromNumberTo32BitBuf(mur.x86.hash32(data))) +// 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)) - default: - throw new TypeError(`${algorithm} is not a supported algorithm`) - } - } catch (error) { - return reject(error) - } - }) + default: + throw new TypeError(`${algorithm} is not a supported algorithm`) + } } module.exports = { diff --git a/src/index.js b/src/index.js index 07c54ea9..90ac4dde 100644 --- a/src/index.js +++ b/src/index.js @@ -1,19 +1,19 @@ 'use strict' +const errcode = require('err-code') const multihash = require('multihashes') const crypto = require('./crypto') /** - * 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. * @returns {Promise} */ -function Multihashing (buf, func, length) { - return Multihashing.digest(buf, func, length) - .then(digest => 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) } /** @@ -30,38 +30,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. - * @returns {Promise} + * @returns {Promise} */ -Multihashing.digest = (buf, func, length) => { - try { - return Multihashing.createHash(func)(buf) - .then(digest => { - if (length) { - return digest.slice(0, length) - } - return digest - }) - } catch (err) { - return Promise.reject(err) - } +Multihashing.digest = async (buf, alg, length) => { + const hash = Multihashing.createHash(alg) + const digest = await hash(buf) + return length ? digest.slice(0, length) : digest } /** - * Creates a function that hashs with the provided algorithm + * Creates a function that hashes with the given algorithm * - * @param {string|number} func + * @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') + } + + alg = multihash.coerceCode(alg) + if (!Multihashing.functions[alg]) { + throw errcode(`multihash function '${alg}' not yet supported`, 'ERR_HASH_ALGORITHM_NOT_SUPPORTED') } - return Multihashing.functions[func] + return Multihashing.functions[alg] } /** diff --git a/src/sha.browser.js b/src/sha.browser.js index ad39bb65..3658dcc5 100644 --- a/src/sha.browser.js +++ b/src/sha.browser.js @@ -8,18 +8,17 @@ module.exports = (algorithm) => { ) } - return (data) => { + return async (data) => { switch (algorithm) { case 'sha1': - return crypto.subtle.digest({ name: 'SHA-1' }, data).then(Buffer.from) + return Buffer.from(await crypto.subtle.digest({ name: 'SHA-1' }, data)) case 'sha2-256': - return crypto.subtle.digest({ name: 'SHA-256' }, data).then(Buffer.from) + return Buffer.from(await crypto.subtle.digest({ name: 'SHA-256' }, data)) case 'sha2-512': - return crypto.subtle.digest({ name: 'SHA-512' }, data).then(Buffer.from) + return Buffer.from(await crypto.subtle.digest({ name: 'SHA-512' }, data)) case 'dbl-sha2-256': { - return crypto.subtle.digest({ name: 'SHA-256' }, data) - .then(d => crypto.subtle.digest({ name: 'SHA-256' }, d)) - .then(Buffer.from) + const d = await crypto.subtle.digest({ name: 'SHA-256' }, data) + return Buffer.from(await crypto.subtle.digest({ name: 'SHA-256' }, d)) } default: throw new TypeError(`${algorithm} is not a supported algorithm`) diff --git a/src/sha.js b/src/sha.js index 3516fad9..aeb866c6 100644 --- a/src/sha.js +++ b/src/sha.js @@ -1,25 +1,22 @@ 'use strict' const crypto = require('crypto') -module.exports = (algorithm) => (data) => { - return new Promise((resolve, reject) => { - try { - switch (algorithm) { - case 'sha1': - return resolve(crypto.createHash('sha1').update(data).digest()) - case 'sha2-256': - return resolve(crypto.createHash('sha256').update(data).digest()) - case 'sha2-512': - return resolve(crypto.createHash('sha512').update(data).digest()) - case 'dbl-sha2-256': { - const first = crypto.createHash('sha256').update(data).digest() - return resolve(crypto.createHash('sha256').update(first).digest()) - } - default: - throw new TypeError(`${algorithm} is not a supported algorithm`) - } - } catch (error) { - return reject(error) +// 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 TypeError(`${algorithm} is not a supported algorithm`) + } } diff --git a/test/index.spec.js b/test/index.spec.js index 537d270d..1b9d1f58 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -5,12 +5,13 @@ 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] @@ -19,7 +20,7 @@ describe('multihashing', () => { const digest = await multihashing(Buffer.from(raw), func) expect(digest.toString('hex')).to.eql(encoded) }) - }) + } it('cuts the length', async () => { const buf = Buffer.from('beep boop') @@ -40,3 +41,44 @@ describe('multihashing', () => { ) }) }) + +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('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') + }) + }) + } +})