From 4b9830d3dfb2b315373ae8bc1e5cbcd449e20834 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Sun, 22 May 2016 22:39:36 +0200 Subject: [PATCH 1/2] Add base line test and fix issues with handshaking --- package.json | 7 ++-- src/etm.js | 38 ++++++++++--------- src/handshake/exchange.js | 36 ++++++++++-------- src/handshake/finish.js | 6 +-- src/handshake/index.js | 4 +- src/handshake/propose.js | 37 ++++++++++++------- src/index.js | 10 ++++- src/support.js | 13 ++++--- test/index.spec.js | 78 +++++++++++++++++++++++++++++++++------ 9 files changed, 156 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index 6a24e2f..3e5dc79 100644 --- a/package.json +++ b/package.json @@ -28,20 +28,21 @@ "debug": "^2.2.0", "duplexify": "^3.4.3", "length-prefixed-stream": "^1.5.0", - "libp2p-crypto": "^0.3.1", + "libp2p-crypto": "^0.4.0", "multihashing": "^0.2.1", "node-forge": "^0.6.39", "peer-id": "^0.6.6", "protocol-buffers": "^3.1.6", "readable-stream": "1.1.13", - "run-series": "^1.1.4" + "run-series": "^1.1.4", + "through2": "^2.0.1" }, "devDependencies": { "aegir": "^3.0.4", "bl": "^1.1.2", "chai": "^3.5.0", "pre-commit": "^1.1.3", - "through2": "^2.0.1" + "stream-pair": "^1.0.3" }, "pre-commit": [ "lint", diff --git a/src/etm.js b/src/etm.js index 6ee77ec..45ea158 100644 --- a/src/etm.js +++ b/src/etm.js @@ -2,38 +2,36 @@ const through = require('through2') const lpm = require('length-prefixed-stream') -const forge = require('node-forge') -const createBuffer = forge.util.createBuffer +const toForgeBuffer = require('./support').toForgeBuffer exports.writer = function etmWriter (insecure, cipher, mac) { const encode = lpm.encode() const pt = through(function (chunk, enc, cb) { - cipher.update(createBuffer(chunk.toString('binary'))) + cipher.update(toForgeBuffer(chunk)) if (cipher.output.length() > 0) { const data = new Buffer(cipher.output.getBytes(), 'binary') mac.update(data) - this.push(Buffer.concat([ - data, - new Buffer(mac.digest(), 'binary') - ])) + const macBuffer = new Buffer(mac.getMac().getBytes(), 'binary') + + this.push(Buffer.concat([data, macBuffer])) // reset hmac - mac.start() + mac.start(null, null) } cb() }) - return insecure.pipe(pt).pipe(encode) + pt.pipe(encode).pipe(insecure) + + return pt } exports.reader = function etmReader (insecure, decipher, mac) { const decode = lpm.decode() const pt = through(function (chunk, enc, cb) { const l = chunk.length - - // TODO: check that this mac.getMac().length() is correct const macSize = mac.getMac().length() if (l < macSize) { @@ -44,17 +42,19 @@ exports.reader = function etmReader (insecure, decipher, mac) { const data = chunk.slice(0, mark) const macd = chunk.slice(mark) - mac.update(data) - const expected = new Buffer(mac.digest(), 'binary') - // reset hmac - mac.start() + // Clear out any previous data + mac.start(null, null) + mac.update(data) + const expected = new Buffer(mac.getMac().getBytes(), 'binary') + // reset hmac + mac.start(null, null) if (!macd.equals(expected)) { - return cb(new Error(`MAC Invalid: ${macd} != ${expected}`)) + return cb(new Error(`MAC Invalid: ${macd.toString('hex')} != ${expected.toString('hex')}`)) } // all good, decrypt - decipher.update(data) + decipher.update(toForgeBuffer(data)) if (decipher.output.length() > 0) { const data = new Buffer(decipher.output.getBytes(), 'binary') @@ -64,5 +64,7 @@ exports.reader = function etmReader (insecure, decipher, mac) { cb() }) - return insecure.pipe(decode).pipe(pt) + insecure.pipe(decode).pipe(pt) + + return pt } diff --git a/src/handshake/exchange.js b/src/handshake/exchange.js index 997d8f9..4c6e5f7 100644 --- a/src/handshake/exchange.js +++ b/src/handshake/exchange.js @@ -6,8 +6,8 @@ const fs = require('fs') const path = require('path') const protobuf = require('protocol-buffers') -const log = debug('libp2p:secio:handshake') -log.error = debug('libp2p:secio:handshake:error') +const log = debug('libp2p:secio:2-exchange') +log.error = debug('libp2p:secio:2-exchange:error') const pbm = protobuf(fs.readFileSync(path.join(__dirname, '../secio.proto'))) @@ -16,28 +16,32 @@ const support = require('../support') // step 2. Exchange // -- exchange (signed) ephemeral keys. verify signatures. module.exports = function exchange (session, cb) { - log('2. exchange - start') + log('start') + + let genSharedKey + let exchangeOut - let eResult try { - eResult = crypto.generateEphemeralKeyPair(session.local.curveT) + const eResult = crypto.generateEphemeralKeyPair(session.local.curveT) + session.local.ephemeralPubKey = eResult.key + genSharedKey = eResult.genSharedKey + exchangeOut = makeExchange(session) + log('exout', exchangeOut.toString('hex')) } catch (err) { + log.error(err) return cb(err) } - session.local.ephemeralPubKey = eResult.key - const genSharedKey = eResult.genSharedKey - const exchangeOut = makeExchange(session) - session.insecureLp.write(exchangeOut) session.insecureLp.once('data', (chunk) => { const exchangeIn = pbm.Exchange.decode(chunk) - + log('exIn', exchangeIn) try { verify(session, exchangeIn) keys(session, exchangeIn, genSharedKey) macAndCipher(session) } catch (err) { + log.error(err) return cb(err) } @@ -47,23 +51,25 @@ module.exports = function exchange (session, cb) { } function makeExchange (session) { + log('make exchange') // Gather corpus to sign. const selectionOut = Buffer.concat([ session.proposal.out, session.proposal.in, session.local.ephemeralPubKey ]) - return pbm.Exchange({ - epubkey: session.local.ephemeralPubKey, - signature: session.localKey.sign(selectionOut) - }) + const epubkey = session.local.ephemeralPubKey + const signature = session.localKey.sign(selectionOut) + + log('exOut', epubkey, signature) + + return pbm.Exchange.encode({epubkey, signature}) } function verify (session, exchangeIn) { log('2.1. verify') session.remote.ephemeralPubKey = exchangeIn.epubkey - const selectionIn = Buffer.concat([ session.proposal.in, session.proposal.out, diff --git a/src/handshake/finish.js b/src/handshake/finish.js index 9609b64..47c9fd4 100644 --- a/src/handshake/finish.js +++ b/src/handshake/finish.js @@ -2,8 +2,8 @@ const duplexify = require('duplexify') const debug = require('debug') -const log = debug('libp2p:secio:handshake') -log.error = debug('libp2p:secio:handshake:error') +const log = debug('libp2p:secio') +log.error = debug('libp2p:secio:error') const etm = require('../etm') @@ -16,7 +16,7 @@ module.exports = function finish (session, cb) { const r = etm.reader(session.insecure, session.remote.cipher, session.remote.mac) session.secure = duplexify(w, r) - session.secure.write(session.proposal.in.rand) + session.secure.write(session.proposal.randIn) // read our nonce back session.secure.once('data', (nonceOut2) => { diff --git a/src/handshake/index.js b/src/handshake/index.js index 3725f64..993fcc5 100644 --- a/src/handshake/index.js +++ b/src/handshake/index.js @@ -3,8 +3,8 @@ const debug = require('debug') const series = require('run-series') -const log = debug('libp2p:secio:handshake') -log.error = debug('libp2p:secio:handshake:error') +const log = debug('libp2p:secio') +log.error = debug('libp2p:secio:error') const propose = require('./propose') const exchange = require('./exchange') diff --git a/src/handshake/propose.js b/src/handshake/propose.js index 5908894..2403274 100644 --- a/src/handshake/propose.js +++ b/src/handshake/propose.js @@ -9,8 +9,8 @@ const PeerId = require('peer-id') const mh = require('multihashing') const crypto = require('libp2p-crypto') -const log = debug('libp2p:secio:handshake') -log.error = debug('libp2p:secio:handshake:error') +const log = debug('libp2p:secio') +log.error = debug('libp2p:secio:error') // nonceSize is the size of our nonces (in bytes) const nonceSize = 16 @@ -23,17 +23,20 @@ const support = require('../support') module.exports = function propose (session, cb) { log('1. propose - start') - const nonceOut = forge.random.getBytesSync(nonceSize) + const nonceOut = new Buffer(forge.random.getBytesSync(nonceSize), 'binary') const proposeOut = makeProposal(session, nonceOut) + session.proposal.out = proposeOut session.proposal.nonceOut = nonceOut session.insecureLp.write(proposeOut) session.insecureLp.once('data', (chunk) => { - const proposeIn = readProposal(chunk) - session.proposal.in = proposeIn + let proposeIn try { + proposeIn = readProposal(chunk) + session.proposal.in = chunk + session.proposal.randIn = proposeIn.rand identify(session, proposeIn) } catch (err) { return cb(err) @@ -54,12 +57,12 @@ module.exports = function propose (session, cb) { // Generate and send Hello packet. // Hello = (rand, PublicKey, Supported) function makeProposal (session, nonceOut) { - session.local.permanentPubKey = session.localKey.getPublic() + session.local.permanentPubKey = session.localKey.public const myPubKeyBytes = session.local.permanentPubKey.bytes - return pbm.Propose({ + return pbm.Propose.encode({ rand: nonceOut, - pubKey: myPubKeyBytes, + pubkey: myPubKeyBytes, exchanges: support.exchanges.join(','), ciphers: support.ciphers.join(','), hashes: support.hashes.join(',') @@ -67,14 +70,14 @@ function makeProposal (session, nonceOut) { } function readProposal (bytes) { - return pbm.Proposal.decode(bytes) + return pbm.Propose.decode(bytes) } function identify (session, proposeIn) { log('1.1 identify') - session.remote.permanentPubKey = crypto.unmarshalPublicKey(proposeIn.pubKey) - session.remotePeer = PeerId.createFromPubKey(session.remote.permanentPubKey) + session.remote.permanentPubKey = crypto.unmarshalPublicKey(proposeIn.pubkey) + session.remotePeer = PeerId.createFromPubKey(proposeIn.pubkey.toString('base64')) log('1.1 identify - %s - identified remote peer as %s', session.localPeer.toB58String(), session.remotePeer.toB58String()) } @@ -91,7 +94,7 @@ function selection (session, nonceOut, proposeIn) { } const remote = { - pubKeyBytes: proposeIn.pubKey, + pubKeyBytes: proposeIn.pubkey, exchanges: proposeIn.exchanges.split(','), hashes: proposeIn.hashes.split(','), ciphers: proposeIn.ciphers.split(','), @@ -113,8 +116,14 @@ function selection (session, nonceOut, proposeIn) { } function selectBest (local, remote) { - const oh1 = digest(Buffer.concat(remote.pubKeyBytes, local.nonce)) - const oh2 = digest(Buffer.concat(local.pubKeyBytes, remote.nonce)) + const oh1 = digest(Buffer.concat([ + remote.pubKeyBytes, + local.nonce + ])) + const oh2 = digest(Buffer.concat([ + local.pubKeyBytes, + remote.nonce + ])) const order = Buffer.compare(oh1, oh2) if (order === 0) { diff --git a/src/index.js b/src/index.js index 7e8c2b2..5d88842 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ exports.SecureSession = class SecureSession { this.remote = {} this.proposal = {} this.insecure = insecure + this.secure = null const e = lpstream.encode() const d = lpstream.decode() this.insecureLp = duplexify(e, d) @@ -39,9 +40,16 @@ exports.SecureSession = class SecureSession { } } + secureStream (cb) { + this.handshake((err) => { + if (err) return cb(err) + + cb(null, this.secure) + }) + } + handshake (cb) { // TODO: figure out how to best handle the handshake timeout - // TODO: better locking if (this._handshakeLock) { return cb(new Error('handshake already in progress')) } diff --git a/src/support.js b/src/support.js index f6ec72f..b23b157 100644 --- a/src/support.js +++ b/src/support.js @@ -10,8 +10,7 @@ exports.exchanges = [ exports.ciphers = [ 'AES-256', - 'AES-128', - 'Blowfish' + 'AES-128' ] exports.hashes = [ @@ -57,6 +56,10 @@ const hashMap = { SHA512: forge.md.sha512.create() } +const toForgeBuffer = exports.toForgeBuffer = (buf) => ( + forge.util.createBuffer(buf.toString('binary')) +) + function makeMac (hashType, key) { const hash = hashMap[hashType] @@ -65,7 +68,7 @@ function makeMac (hashType, key) { } const mac = forge.hmac.create() - mac.start(hash, key) + mac.start(hash, toForgeBuffer(key)) return mac } @@ -73,8 +76,8 @@ function makeCipher (cipherType, iv, key) { 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', key) - cipher.start({iv}) + const cipher = forge.cipher.createCipher('AES-CTR', toForgeBuffer(key)) + cipher.start({iv: toForgeBuffer(iv)}) return cipher } diff --git a/test/index.spec.js b/test/index.spec.js index d2c4e8e..b394aa5 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -5,22 +5,18 @@ const expect = require('chai').expect const through = require('through2') const bl = require('bl') const PeerId = require('peer-id') -const duplexify = require('duplexify') +const crypto = require('libp2p-crypto') +const streamPair = require('stream-pair') -const secio = require('../src') +const SecureSession = require('../src').SecureSession describe('libp2p-secio', () => { - it('exists', () => { - expect(secio).to.exist - }) - describe('insecure length prefixed stream', () => { it('encodes', (done) => { const id = PeerId.create({bits: 64}) const key = {} - const pt = through() - const insecure = duplexify(pt, pt) - const s = new secio.SecureSession(id, key, insecure) + const insecure = through() + const s = new SecureSession(id, key, insecure) // encoded on raw s.insecure.pipe(bl((err, res) => { @@ -37,9 +33,8 @@ describe('libp2p-secio', () => { it('decodes', (done) => { const id = PeerId.create({bits: 64}) const key = {} - const pt = through() - const insecure = duplexify(pt, pt) - const s = new secio.SecureSession(id, key, insecure) + const insecure = through() + const s = new SecureSession(id, key, insecure) // encoded on raw s.insecureLp.pipe(bl((err, res) => { @@ -52,5 +47,64 @@ describe('libp2p-secio', () => { s.insecure.write('\u0005world') s.insecureLp.end() }) + + it('all together now', (done) => { + const pair = streamPair.create() + + createSession(pair, (err, local) => { + if (err) throw err + createSession(pair.other, (err, remote) => { + if (err) throw err + + remote.session.insecureLp.pipe(bl((err, res) => { + if (err) throw err + expect(res.toString()).to.be.eql('hello world') + done() + })) + + local.session.insecureLp.write('hello ') + local.session.insecureLp.write('world') + pair.end() + }) + }) + }) + }) + + it('upgrades a connection', (done) => { + const pair = streamPair.create() + + createSession(pair, (err, local) => { + if (err) throw err + createSession(pair.other, (err, remote) => { + if (err) throw err + + local.session.secureStream((err, localSecure) => { + if (err) throw err + + localSecure.write('hello world') + }) + + remote.session.secureStream((err, remoteSecure) => { + if (err) throw err + remoteSecure.once('data', (chunk) => { + expect(chunk.toString()).to.be.eql('hello world') + done() + }) + }) + }) + }) }) }) + +function createSession (insecure, cb) { + crypto.generateKeyPair('RSA', 2048, (err, key) => { + if (err) return cb(err) + const id = PeerId.createFromPrivKey(key.bytes) + cb(null, { + id, + key, + insecure, + session: new SecureSession(id, key, insecure) + }) + }) +} From cecf41926fc691eaf9fcc345dd6a6c6483fffc13 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 23 May 2016 12:42:21 +0200 Subject: [PATCH 2/2] fix: clean up logging --- src/handshake/exchange.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/handshake/exchange.js b/src/handshake/exchange.js index 4c6e5f7..88d1322 100644 --- a/src/handshake/exchange.js +++ b/src/handshake/exchange.js @@ -6,8 +6,8 @@ const fs = require('fs') const path = require('path') const protobuf = require('protocol-buffers') -const log = debug('libp2p:secio:2-exchange') -log.error = debug('libp2p:secio:2-exchange:error') +const log = debug('libp2p:secio') +log.error = debug('libp2p:secio:error') const pbm = protobuf(fs.readFileSync(path.join(__dirname, '../secio.proto'))) @@ -16,7 +16,7 @@ const support = require('../support') // step 2. Exchange // -- exchange (signed) ephemeral keys. verify signatures. module.exports = function exchange (session, cb) { - log('start') + log('2. exchange - start') let genSharedKey let exchangeOut @@ -26,22 +26,19 @@ module.exports = function exchange (session, cb) { session.local.ephemeralPubKey = eResult.key genSharedKey = eResult.genSharedKey exchangeOut = makeExchange(session) - log('exout', exchangeOut.toString('hex')) } catch (err) { - log.error(err) return cb(err) } session.insecureLp.write(exchangeOut) session.insecureLp.once('data', (chunk) => { const exchangeIn = pbm.Exchange.decode(chunk) - log('exIn', exchangeIn) + try { verify(session, exchangeIn) keys(session, exchangeIn, genSharedKey) macAndCipher(session) } catch (err) { - log.error(err) return cb(err) } @@ -51,7 +48,6 @@ module.exports = function exchange (session, cb) { } function makeExchange (session) { - log('make exchange') // Gather corpus to sign. const selectionOut = Buffer.concat([ session.proposal.out, @@ -61,8 +57,6 @@ function makeExchange (session) { const epubkey = session.local.ephemeralPubKey const signature = session.localKey.sign(selectionOut) - log('exOut', epubkey, signature) - return pbm.Exchange.encode({epubkey, signature}) }