From a1167c0084adc5b8dac5ccd22bcfb42fab21a94f Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Thu, 3 Nov 2016 10:23:33 +0100 Subject: [PATCH] Async Crypto Endeavour (+benchmarks!) (#19) * feat: add benchmarks and integrate with new libp2p-crypto * start removing node-forge * less forge * cleanup and fixes * benchmarked and tested * remove unused .aegir * Replace lib multihashing with multihashing-async * cleanup package.json * Using string instead of buffer for proto file * chore: fix benchmarks * docs(readme): update README URLs based on HTTP redirects * ready for next aegir * update aegir * ready * chore: update deps --- .aegir.js | 16 ---- .gitignore | 1 - .travis.yml | 20 +++-- README.md | 4 + benchmarks/send.js | 87 ++++++++++++++++++ package.json | 33 ++++--- src/etm.js | 98 ++++++++++---------- src/handshake/crypto.js | 169 +++++++++++++++++++++-------------- src/handshake/exchange.js | 24 ++--- src/handshake/finish.js | 6 +- src/handshake/index.js | 2 +- src/handshake/propose.js | 21 +++-- src/handshake/secio.proto | 12 --- src/handshake/secio.proto.js | 14 +++ src/index.js | 10 ++- src/state.js | 5 ++ src/support.js | 108 +++++++++++----------- test/index.spec.js | 105 ++++++++++++++-------- 18 files changed, 443 insertions(+), 292 deletions(-) delete mode 100644 .aegir.js create mode 100644 benchmarks/send.js delete mode 100644 src/handshake/secio.proto create mode 100644 src/handshake/secio.proto.js diff --git a/.aegir.js b/.aegir.js deleted file mode 100644 index e3151ab..0000000 --- a/.aegir.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' - -const path = require('path') - -module.exports = { - webpack: { - resolve: { - alias: { - 'node-forge': path.resolve( - path.dirname(require.resolve('libp2p-crypto')), - '../vendor/forge.bundle.js' - ) - } - } - } -} diff --git a/.gitignore b/.gitignore index 254988d..fb8d1c9 100644 --- a/.gitignore +++ b/.gitignore @@ -31,5 +31,4 @@ build # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules -lib dist diff --git a/.travis.yml b/.travis.yml index 68f6dd8..4fe6550 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,15 @@ sudo: false language: node_js -node_js: - - 4 - - 5 - - stable +matrix: + include: + - node_js: 4 + env: CXX=g++-4.8 + - node_js: 6 + env: + - SAUCE=true + - CXX=g++-4.8 + - node_js: stable + env: CXX=g++-4.8 # Make sure we have new NPM. before_install: @@ -14,7 +20,6 @@ script: - npm test - npm run coverage - before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start @@ -22,13 +27,10 @@ before_script: after_success: - npm run coverage-publish -env: - - CXX=g++-4.8 - addons: firefox: 'latest' apt: sources: - ubuntu-toolchain-r-test packages: - - g++-4.8 + - g++-4.8 \ No newline at end of file diff --git a/README.md b/README.md index be9d930..cc741b8 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ [![Travis CI](https://travis-ci.org/libp2p/js-libp2p-secio.svg?branch=master)](https://travis-ci.org/libp2p/js-libp2p-secio) [![Circle CI](https://circleci.com/gh/libp2p/js-libp2p-secio.svg?style=svg)](https://circleci.com/gh/libp2p/js-libp2p-secio) [![Dependency Status](https://david-dm.org/libp2p/js-libp2p-secio.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-secio) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) +![](https://img.shields.io/badge/npm-%3E%3D3.0.0-orange.svg?style=flat-square) +![](https://img.shields.io/badge/Node.js-%3E%3D4.0.0-orange.svg?style=flat-square) + +[![Sauce Test Status](https://saucelabs.com/browser-matrix/libp2p-js-secio.svg)](https://saucelabs.com/u/libp2p-js-secio) > Secio implementation in JavaScript diff --git a/benchmarks/send.js b/benchmarks/send.js new file mode 100644 index 0000000..097655c --- /dev/null +++ b/benchmarks/send.js @@ -0,0 +1,87 @@ +'use strict' + +const Benchmark = require('benchmark') +const pull = require('pull-stream') +const pair = require('pull-pair/duplex') +const PeerId = require('peer-id') +const crypto = require('libp2p-crypto') + +const secio = require('../src') + +const suite = new Benchmark.Suite('secio') +const ids = [] + +suite.add('createKey', function (d) { + crypto.generateKeyPair('RSA', 2048, (err, key) => { + if (err) { + throw err + } + PeerId.createFromPrivKey(key.bytes, (err, id) => { + if (err) { + throw err + } + ids.push(id) + d.resolve() + }) + }) +}, { + defer: true +}) +.add('send', function (deferred) { + const p = pair() + + createSession(p[0], (err, local) => { + if (err) { + throw err + } + createSession(p[1], (err, remote) => { + if (err) { + throw err + } + sendMessages(local, remote) + }) + }) + + function sendMessages (local, remote) { + pull( + pull.infinite(), + pull.take(100), + pull.map((val) => Buffer(val.toString())), + local + ) + + pull( + remote, + pull.take(100), + pull.collect((err, chunks) => { + if (err) throw err + if (chunks.length !== 100) throw new Error('Did not receive enough chunks') + deferred.resolve() + }) + ) + } +}, { + defer: true +}) +.on('cycle', (event) => { + console.log(String(event.target)) +}) +// run async +.run({ + async: true +}) + +function createSession (insecure, cb) { + crypto.generateKeyPair('RSA', 2048, (err, key) => { + if (err) { + return cb(err) + } + PeerId.createFromPrivKey(key.bytes, (err, id) => { + if (err) { + return cb(err) + } + + cb(null, secio.encrypt(id, key, insecure)) + }) + }) +} diff --git a/package.json b/package.json index 9aeb8b3..8246197 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,7 @@ "name": "libp2p-secio", "version": "0.5.0", "description": "Secio implementation in JavaScript", - "main": "lib/index.js", - "jsnext:main": "src/index.js", + "main": "src/index.js", "scripts": { "lint": "aegir-lint", "build": "aegir-build", @@ -14,7 +13,8 @@ "release-minor": "aegir-release --type minor", "release-major": "aegir-release --type major", "coverage": "aegir-coverage", - "coverage-publish": "aegir-coverage publish" + "coverage-publish": "aegir-coverage publish", + "bench": "node benchmarks/send.js" }, "keywords": [ "IPFS", @@ -22,37 +22,36 @@ "crypto", "rsa" ], - "author": "Friedel Ziegelmayer ", + "author": "Friedel Ziegelmayer ", "license": "MIT", "dependencies": { + "async": "^2.1.2", "debug": "^2.2.0", - "interface-connection": "^0.2.1", - "libp2p-crypto": "^0.6.1", - "multihashing": "^0.2.1", - "node-forge": "^0.6.42", - "peer-id": "^0.7.0", + "interface-connection": "^0.3.0", + "libp2p-crypto": "^0.7.0", + "multihashing-async": "^0.2.0", + "peer-id": "^0.8.0", "protocol-buffers": "^3.1.6", "pull-defer": "^0.2.2", "pull-handshake": "^1.1.4", "pull-length-prefixed": "^1.2.0", - "pull-stream": "^3.4.3", - "pull-through": "^1.0.18", - "run-series": "^1.1.4" + "pull-stream": "^3.5.0" }, "devDependencies": { - "aegir": "^8.0.0", + "aegir": "^9.0.1", + "benchmark": "^2.1.2", "chai": "^3.5.0", - "multistream-select": "^0.11.0", + "multistream-select": "^0.11.1", "pre-commit": "^1.1.3", "pull-pair": "^1.1.0", - "run-parallel": "^1.1.6" + "pull-pushable": "^2.0.1" }, "pre-commit": [ "lint", "test" ], "engines": { - "node": "^4.0.0" + "node": ">=4.0.0" }, "repository": { "type": "git", @@ -68,4 +67,4 @@ "Richard Littauer ", "greenkeeperio-bot " ] -} \ No newline at end of file +} diff --git a/src/etm.js b/src/etm.js index a55d931..0880c34 100644 --- a/src/etm.js +++ b/src/etm.js @@ -1,73 +1,67 @@ 'use strict' -const through = require('pull-through') const pull = require('pull-stream') const lp = require('pull-length-prefixed') -const toForgeBuffer = require('./support').toForgeBuffer - const lpOpts = { fixed: true, bytes: 4 } exports.createBoxStream = (cipher, mac) => { - const pt = through(function (chunk) { - cipher.update(toForgeBuffer(chunk)) - - if (cipher.output.length() > 0) { - const data = new Buffer(cipher.output.getBytes(), 'binary') - mac.update(data.toString('binary')) - const macBuffer = new Buffer(mac.digest().getBytes(), 'binary') - - this.queue(Buffer.concat([data, macBuffer])) - // reset hmac - mac.start(null, null) - } - }) - return pull( - pt, + pull.asyncMap((chunk, cb) => { + cipher.encrypt(chunk, (err, data) => { + if (err) { + return cb(err) + } + + mac.digest(data, (err, digest) => { + if (err) { + return cb(err) + } + + cb(null, Buffer.concat([data, digest])) + }) + }) + }), lp.encode(lpOpts) ) } exports.createUnboxStream = (decipher, mac) => { - const pt = through(function (chunk) { - const l = chunk.length - const macSize = mac.getMac().length() - - if (l < macSize) { - return this.emit('error', new Error(`buffer (${l}) shorter than MAC size (${macSize})`)) - } - - const mark = l - macSize - const data = chunk.slice(0, mark) - const macd = chunk.slice(mark) - - // Clear out any previous data - mac.start(null, null) - - mac.update(data.toString('binary')) - const expected = new Buffer(mac.getMac().getBytes(), 'binary') - - // reset hmac - mac.start(null, null) - if (!macd.equals(expected)) { - return this.emit('error', new Error(`MAC Invalid: ${macd.toString('hex')} != ${expected.toString('hex')}`)) - } - - // all good, decrypt - decipher.update(toForgeBuffer(data)) - - if (decipher.output.length() > 0) { - const data = new Buffer(decipher.output.getBytes(), 'binary') - this.queue(data) - } - }) - return pull( lp.decode(lpOpts), - pt + pull.asyncMap((chunk, cb) => { + const l = chunk.length + const macSize = mac.length + + if (l < macSize) { + return cb(new Error(`buffer (${l}) shorter than MAC size (${macSize})`)) + } + + const mark = l - macSize + const data = chunk.slice(0, mark) + const macd = chunk.slice(mark) + + mac.digest(data, (err, expected) => { + if (err) { + return cb(err) + } + + if (!macd.equals(expected)) { + return cb(new Error(`MAC Invalid: ${macd.toString('hex')} != ${expected.toString('hex')}`)) + } + + // all good, decrypt + decipher.decrypt(data, (err, decrypted) => { + if (err) { + return cb(err) + } + + cb(null, decrypted) + }) + }) + }) ) } diff --git a/src/handshake/crypto.js b/src/handshake/crypto.js index 7187b95..09dc488 100644 --- a/src/handshake/crypto.js +++ b/src/handshake/crypto.js @@ -1,15 +1,15 @@ 'use strict' const protobuf = require('protocol-buffers') -const path = require('path') -const fs = require('fs') const PeerId = require('peer-id') const crypto = require('libp2p-crypto') +const parallel = require('async/parallel') +const waterfall = require('async/waterfall') const debug = require('debug') const log = debug('libp2p:secio') log.error = debug('libp2p:secio:error') -const pbm = protobuf(fs.readFileSync(path.join(__dirname, 'secio.proto'))) +const pbm = protobuf(require('./secio.proto')) const support = require('../support') @@ -29,27 +29,38 @@ exports.createProposal = (state) => { return state.proposalEncoded.out } -exports.createExchange = (state) => { - const res = crypto.generateEphemeralKeyPair(state.protocols.local.curveT) - state.ephemeralKey.local = res.key - state.shared.generate = res.genSharedKey +exports.createExchange = (state, callback) => { + crypto.generateEphemeralKeyPair(state.protocols.local.curveT, (err, res) => { + if (err) { + return callback(err) + } - // Gather corpus to sign. - const selectionOut = Buffer.concat([ - state.proposalEncoded.out, - state.proposalEncoded.in, - state.ephemeralKey.local - ]) + state.ephemeralKey.local = res.key + state.shared.generate = res.genSharedKey - state.exchange.out = { - epubkey: state.ephemeralKey.local, - signature: new Buffer(state.key.local.sign(selectionOut), 'binary') - } + // Gather corpus to sign. + const selectionOut = Buffer.concat([ + state.proposalEncoded.out, + state.proposalEncoded.in, + state.ephemeralKey.local + ]) - return pbm.Exchange.encode(state.exchange.out) + state.key.local.sign(selectionOut, (err, sig) => { + if (err) { + return callback(err) + } + + state.exchange.out = { + epubkey: state.ephemeralKey.local, + signature: sig + } + + callback(null, pbm.Exchange.encode(state.exchange.out)) + }) + }) } -exports.identify = (state, msg) => { +exports.identify = (state, msg, callback) => { log('1.1 identify') state.proposalEncoded.in = msg @@ -57,12 +68,19 @@ exports.identify = (state, msg) => { const pubkey = state.proposal.in.pubkey state.key.remote = crypto.unmarshalPublicKey(pubkey) - state.id.remote = PeerId.createFromPubKey(pubkey.toString('base64')) + PeerId.createFromPubKey(pubkey.toString('base64'), (err, remoteId) => { + if (err) { + return callback(err) + } - log('1.1 identify - %s - identified remote peer as %s', state.id.local.toB58String(), state.id.remote.toB58String()) + state.id.remote = remoteId + + log('1.1 identify - %s - identified remote peer as %s', state.id.local.toB58String(), state.id.remote.toB58String()) + callback() + }) } -exports.selectProtocols = (state) => { +exports.selectProtocols = (state, callback) => { log('1.2 selection') const local = { @@ -81,25 +99,30 @@ exports.selectProtocols = (state) => { nonce: state.proposal.in.rand } - let selected = support.selectBest(local, remote) - // we use the same params for both directions (must choose same curve) - // WARNING: if they dont SelectBest the same way, this won't work... - state.protocols.remote = { - order: selected.order, - curveT: selected.curveT, - cipherT: selected.cipherT, - hashT: selected.hashT - } + support.selectBest(local, remote, (err, selected) => { + if (err) { + return callback(err) + } + // we use the same params for both directions (must choose same curve) + // WARNING: if they dont SelectBest the same way, this won't work... + state.protocols.remote = { + order: selected.order, + curveT: selected.curveT, + cipherT: selected.cipherT, + hashT: selected.hashT + } - state.protocols.local = { - order: selected.order, - curveT: selected.curveT, - cipherT: selected.cipherT, - hashT: selected.hashT - } + state.protocols.local = { + order: selected.order, + curveT: selected.curveT, + cipherT: selected.cipherT, + hashT: selected.hashT + } + callback() + }) } -exports.verify = (state, msg) => { +exports.verify = (state, msg, callback) => { log('2.1. verify') state.exchange.in = pbm.Exchange.decode(msg) @@ -111,43 +134,57 @@ exports.verify = (state, msg) => { state.ephemeralKey.remote ]) - const sigOk = state.key.remote.verify(selectionIn, state.exchange.in.signature) + state.key.remote.verify(selectionIn, state.exchange.in.signature, (err, sigOk) => { + if (err) { + return callback(err) + } - if (!sigOk) { - throw new Error('Bad signature') - } + if (!sigOk) { + return callback(new Error('Bad signature')) + } - log('2.1. verify - signature verified') + log('2.1. verify - signature verified') + callback() + }) } -exports.generateKeys = (state) => { +exports.generateKeys = (state, callback) => { log('2.2. keys') - state.shared.secret = state.shared.generate(state.exchange.in.epubkey) + waterfall([ + (cb) => state.shared.generate(state.exchange.in.epubkey, cb), + (secret, cb) => { + state.shared.secret = secret - const keys = crypto.keyStretcher( - state.protocols.local.cipherT, - state.protocols.local.hashT, - state.shared.secret - ) + crypto.keyStretcher( + state.protocols.local.cipherT, + state.protocols.local.hashT, + state.shared.secret, + cb + ) + }, + (keys, cb) => { + // use random nonces to decide order. + if (state.protocols.local.order > 0) { + state.protocols.local.keys = keys.k1 + state.protocols.remote.keys = keys.k2 + } else if (state.protocols.local.order < 0) { + // swap + state.protocols.local.keys = keys.k2 + state.protocols.remote.keys = keys.k1 + } else { + // we should've bailed before state. but if not, bail here. + return cb(new Error('you are trying to talk to yourself')) + } - // use random nonces to decide order. - if (state.protocols.local.order > 0) { - state.protocols.local.keys = keys.k1 - state.protocols.remote.keys = keys.k2 - } else if (state.protocols.local.order < 0) { - // swap - state.protocols.local.keys = keys.k2 - state.protocols.remote.keys = keys.k1 - } else { - // we should've bailed before state. but if not, bail here. - throw new Error('you are trying to talk to yourself') - } + log('2.3. mac + cipher') - log('2.3. mac + cipher') - - support.makeMacAndCipher(state.protocols.local) - support.makeMacAndCipher(state.protocols.remote) + parallel([ + (cb) => support.makeMacAndCipher(state.protocols.local, cb), + (cb) => support.makeMacAndCipher(state.protocols.remote, cb) + ], cb) + } + ], callback) } exports.verifyNonce = (state, n2) => { diff --git a/src/handshake/exchange.js b/src/handshake/exchange.js index 79d51fc..4ed867e 100644 --- a/src/handshake/exchange.js +++ b/src/handshake/exchange.js @@ -1,6 +1,7 @@ 'use strict' const debug = require('debug') +const waterfall = require('async/waterfall') const support = require('../support') const crypto = require('./crypto') @@ -14,21 +15,22 @@ module.exports = function exchange (state, cb) { log('2. exchange - start') log('2. exchange - writing exchange') - support.write(state, crypto.createExchange(state)) - support.read(state.shake, (err, msg) => { + waterfall([ + (cb) => crypto.createExchange(state, cb), + (ex, cb) => { + support.write(state, ex) + support.read(state.shake, cb) + }, + (msg, cb) => { + log('2. exchange - reading exchange') + crypto.verify(state, msg, cb) + }, + (cb) => crypto.generateKeys(state, cb) + ], (err) => { if (err) { return cb(err) } - log('2. exchange - reading exchange') - - try { - crypto.verify(state, msg) - crypto.generateKeys(state) - } catch (err) { - return cb(err) - } - log('2. exchange - finish') cb() }) diff --git a/src/handshake/finish.js b/src/handshake/finish.js index 92874b1..55533e4 100644 --- a/src/handshake/finish.js +++ b/src/handshake/finish.js @@ -17,7 +17,11 @@ module.exports = function finish (state, cb) { const proto = state.protocols const stream = state.shake.rest() - const shake = handshake({timeout: state.timeout}) + const shake = handshake({timeout: state.timeout}, (err) => { + if (err) { + throw err + } + }) pull( stream, diff --git a/src/handshake/index.js b/src/handshake/index.js index 75894b5..3882d08 100644 --- a/src/handshake/index.js +++ b/src/handshake/index.js @@ -1,6 +1,6 @@ 'use strict' -const series = require('run-series') +const series = require('async/series') const propose = require('./propose') const exchange = require('./exchange') diff --git a/src/handshake/propose.js b/src/handshake/propose.js index 7023d8f..7b0689b 100644 --- a/src/handshake/propose.js +++ b/src/handshake/propose.js @@ -1,6 +1,7 @@ 'use strict' const debug = require('debug') +const waterfall = require('async/waterfall') const support = require('../support') const crypto = require('./crypto') @@ -15,22 +16,20 @@ module.exports = function propose (state, cb) { log('1. propose - writing proposal') support.write(state, crypto.createProposal(state)) - support.read(state.shake, (err, msg) => { + + waterfall([ + (cb) => support.read(state.shake, cb), + (msg, cb) => { + log('1. propose - reading proposal', msg) + crypto.identify(state, msg, cb) + }, + (cb) => crypto.selectProtocols(state, cb) + ], (err) => { if (err) { return cb(err) } - log('1. propose - reading proposal', msg) - - try { - crypto.identify(state, msg) - crypto.selectProtocols(state) - } catch (err) { - return cb(err) - } - log('1. propose - finish') - cb() }) } diff --git a/src/handshake/secio.proto b/src/handshake/secio.proto deleted file mode 100644 index c85692f..0000000 --- a/src/handshake/secio.proto +++ /dev/null @@ -1,12 +0,0 @@ -message Propose { - optional bytes rand = 1; - optional bytes pubkey = 2; - optional string exchanges = 3; - optional string ciphers = 4; - optional string hashes = 5; -} - -message Exchange { - optional bytes epubkey = 1; - optional bytes signature = 2; -} diff --git a/src/handshake/secio.proto.js b/src/handshake/secio.proto.js new file mode 100644 index 0000000..cf1b972 --- /dev/null +++ b/src/handshake/secio.proto.js @@ -0,0 +1,14 @@ +'use strict' + +module.exports = `message Propose { + optional bytes rand = 1; + optional bytes pubkey = 2; + optional string exchanges = 3; + optional string ciphers = 4; + optional string hashes = 5; +} + +message Exchange { + optional bytes epubkey = 1; + optional bytes signature = 2; +}` diff --git a/src/index.js b/src/index.js index 424b3c7..4276fcf 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,7 @@ const State = require('./state') module.exports = { tag: '/secio/1.0.0', - encrypt (local, key, insecure) { + encrypt (local, key, insecure, callback) { if (!local) { throw new Error('no local id provided') } @@ -21,7 +21,13 @@ module.exports = { throw new Error('no insecure stream provided') } - const state = new State(local, key) + if (!callback) { + callback = (err) => { + throw err + } + } + + const state = new State(local, key, callback) pull( insecure, diff --git a/src/state.js b/src/state.js index abedf02..47c333f 100644 --- a/src/state.js +++ b/src/state.js @@ -5,6 +5,11 @@ const deferred = require('pull-defer') class State { constructor (id, key, timeout, cb) { + if (typeof timeout === 'function') { + cb = timeout + timeout = undefined + } + this.setup() this.id.local = id this.key.local = key diff --git a/src/support.js b/src/support.js index efbbd06..02ebe4f 100644 --- a/src/support.js +++ b/src/support.js @@ -1,9 +1,10 @@ 'use strict' -const mh = require('multihashing') -const forge = require('node-forge') +const mh = require('multihashing-async') const lp = require('pull-length-prefixed') const pull = require('pull-stream') +const crypto = require('libp2p-crypto') +const parallel = require('async/parallel') exports.exchanges = [ 'P-256', @@ -47,78 +48,73 @@ exports.theBest = (order, p1, p2) => { throw new Error('No algorithms in common!') } -exports.makeMacAndCipher = (target) => { - target.mac = makeMac(target.hashT, target.keys.macKey) - target.cipher = makeCipher(target.cipherT, target.keys.iv, target.keys.cipherKey) +exports.makeMacAndCipher = (target, callback) => { + parallel([ + (cb) => makeMac(target.hashT, target.keys.macKey, cb), + (cb) => makeCipher(target.cipherT, target.keys.iv, target.keys.cipherKey, cb) + ], (err, macAndCipher) => { + if (err) { + return callback(err) + } + + target.mac = macAndCipher[0] + target.cipher = macAndCipher[1] + callback() + }) } -const hashMap = { - SHA1: 'sha1', - SHA256: 'sha256', - // workaround for https://github.com/digitalbazaar/forge/issues/401 - SHA512: forge.md.sha512.create() +function makeMac (hash, key, callback) { + crypto.hmac.create(hash, key, callback) } -const toForgeBuffer = exports.toForgeBuffer = (buf) => ( - forge.util.createBuffer(buf.toString('binary')) -) - -function makeMac (hashType, key) { - const hash = hashMap[hashType] - - if (!hash) { - throw new Error(`unsupported hash type: ${hashType}`) - } - - const mac = forge.hmac.create() - mac.start(hash, toForgeBuffer(key)) - return mac -} - -function makeCipher (cipherType, iv, key) { +function makeCipher (cipherType, iv, key, callback) { if (cipherType === 'AES-128' || cipherType === 'AES-256') { - // aes in counter (CTR) mode because that is what - // is used in go (cipher.NewCTR) - const cipher = forge.cipher.createCipher('AES-CTR', toForgeBuffer(key)) - cipher.start({iv: toForgeBuffer(iv)}) - return cipher + return crypto.aes.create(key, iv, callback) } - // TODO: Blowfish is not supported in node-forge, figure out if - // it's needed and if so find a library for it. - - throw new Error(`unrecognized cipher type: ${cipherType}`) + // TODO: figure out if Blowfish is needed and if so find a library for it. + callback(new Error(`unrecognized cipher type: ${cipherType}`)) } exports.randomBytes = (nonceSize) => { - return new Buffer(forge.random.getBytesSync(nonceSize), 'binary') + return Buffer.from(crypto.webcrypto.getRandomValues(new Uint8Array(nonceSize))) } -exports.selectBest = (local, remote) => { - const oh1 = exports.digest(Buffer.concat([ +exports.selectBest = (local, remote, cb) => { + exports.digest(Buffer.concat([ remote.pubKeyBytes, local.nonce - ])) - const oh2 = exports.digest(Buffer.concat([ - local.pubKeyBytes, - remote.nonce - ])) - const order = Buffer.compare(oh1, oh2) + ]), (err, oh1) => { + if (err) { + return cb(err) + } - if (order === 0) { - throw new Error('you are trying to talk to yourself') - } + exports.digest(Buffer.concat([ + local.pubKeyBytes, + remote.nonce + ]), (err, oh2) => { + if (err) { + return cb(err) + } - return { - curveT: exports.theBest(order, local.exchanges, remote.exchanges), - cipherT: exports.theBest(order, local.ciphers, remote.ciphers), - hashT: exports.theBest(order, local.hashes, remote.hashes), - order - } + const order = Buffer.compare(oh1, oh2) + + if (order === 0) { + cb(new Error('you are trying to talk to yourself')) + } + + cb(null, { + curveT: exports.theBest(order, local.exchanges, remote.exchanges), + cipherT: exports.theBest(order, local.ciphers, remote.ciphers), + hashT: exports.theBest(order, local.hashes, remote.hashes), + order + }) + }) + }) } -exports.digest = (buf) => { - return mh.digest(buf, 'sha2-256', buf.length) +exports.digest = (buf, cb) => { + mh.digest(buf, 'sha2-256', buf.length, cb) } exports.write = function write (state, msg, cb) { diff --git a/test/index.spec.js b/test/index.spec.js index faee8ac..d8a61fd 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,3 +1,4 @@ +/* eslint max-nested-callbacks: ["error", 8] */ /* eslint-env mocha */ 'use strict' @@ -5,8 +6,8 @@ const pair = require('pull-pair/duplex') const expect = require('chai').expect const PeerId = require('peer-id') const crypto = require('libp2p-crypto') -const parallel = require('run-parallel') -const series = require('run-series') +const parallel = require('async/parallel') +const series = require('async/series') const ms = require('multistream-select') const pull = require('pull-stream') const Listener = ms.Listener @@ -21,23 +22,31 @@ describe('libp2p-secio', () => { it('upgrades a connection', (done) => { const p = pair() + createSession(p[0], (err, local) => { + if (err) { + return done(err) + } - const local = createSession(p[0]) - const remote = createSession(p[1]) + createSession(p[1], (err, remote) => { + if (err) { + return done(err) + } - pull( - pull.values(['hello world']), - local - ) + pull( + pull.values([new Buffer('hello world')]), + local + ) - pull( - remote, - pull.collect((err, chunks) => { - expect(err).to.not.exist - expect(chunks).to.be.eql([new Buffer('hello world')]) - done() + pull( + remote, + pull.collect((err, chunks) => { + expect(err).to.not.exist + expect(chunks).to.be.eql([new Buffer('hello world')]) + done() + }) + ) }) - ) + }) }) it('works over multistream', (done) => { @@ -45,8 +54,7 @@ describe('libp2p-secio', () => { const listener = new Listener() const dialer = new Dialer() - let local - let remote + series([ (cb) => parallel([ (cb) => listener.handle(p[0], cb), @@ -54,35 +62,58 @@ describe('libp2p-secio', () => { ], cb), (cb) => { listener.addHandler('/banana/1.0.0', (conn) => { - local = createSession(conn) - pull( - local, - pull.collect((err, chunks) => { - expect(err).to.not.exist - expect(chunks).to.be.eql([new Buffer('hello world')]) - done() - }) - ) + createSession(conn, (err, local) => { + if (err) { + return done(err) + } + pull( + local, + pull.collect((err, chunks) => { + expect(err).to.not.exist + expect(chunks).to.be.eql([new Buffer('hello world')]) + done() + }) + ) + }) }) cb() }, (cb) => dialer.select('/banana/1.0.0', (err, conn) => { - remote = createSession(conn) - pull( - pull.values(['hello world']), - remote - ) - cb(err) + if (err) { + return cb(err) + } + + createSession(conn, (err, remote) => { + if (err) { + return cb(err) + } + pull( + pull.values([new Buffer('hello world')]), + remote + ) + cb() + }) }) ], (err) => { - if (err) throw err + if (err) { + throw err + } }) }) }) -function createSession (insecure) { - const key = crypto.generateKeyPair('RSA', 2048) - const id = PeerId.createFromPrivKey(key.bytes) +function createSession (insecure, cb) { + crypto.generateKeyPair('RSA', 2048, (err, key) => { + if (err) { + return cb(err) + } - return secio.encrypt(id, key, insecure) + key.public.hash((err, digest) => { + if (err) { + return cb(err) + } + + cb(null, secio.encrypt(new PeerId(digest, key), key, insecure)) + }) + }) }