diff --git a/src/handshake/exchange.js b/src/handshake/exchange.js new file mode 100644 index 0000000..9a8c294 --- /dev/null +++ b/src/handshake/exchange.js @@ -0,0 +1,75 @@ +'use strict' + +const crypto = require('libp2p-crypto') +const debug = require('debug') +const log = debug('libp2p:secio:handshake') +log.error = debug('libp2p:secio:handshake:error') + +module.exports = function exchange (session) { + log('2. exchange - start') + + const eResult = crypto.generateEphemeralKeyPair(session.local.curveT) + session.local.ephemeralPubKey = eResult.key + const genSharedKey = eResult.genSharedKey + + // Gather corpus to sign. + const selectionOut = Buffer.concat([ + proposeOutBytes, + proposeInBytes, + session.local.ephemeralPubKey + ]) + + const exchangeOut = pbm.Exchange({ + epubkey: session.local.ephemeralPubKey, + signature: session.localKey.sign(selectionOut) + }) + + // TODO: write exchangeOut + // TODO: read exchangeIn + const exchangeIn // = ...read + + log('2.1. verify') + + session.remote.ephemeralPubKey = exchangeIn.epubkey + + const selectionIn = Buffer.concat([ + proposeInBytes, + proposeOutBytes, + session.remote.ephemeralPubKey + ]) + + const sigOk = session.remote.permanentPubKey.verify(selectionIn, exchangeIn.signature) + + if (!sigOk) { + throw new Error('Bad signature') + } + + log('2.1. verify - signature verified') + + log('2.2. keys') + + session.sharedSecret = genSharedKey(exchangeIn.epubkey) + + const keys = crypto.keyStretcher(session.local.cipherT, session.local.hashT, session.sharedSecret) + + // use random nonces to decide order. + if (order > 0) { + session.local.keys = keys.k1 + session.remote.keys = keys.k2 + } else if (order < 0) { + // swap + session.local.keys = keys.k2 + session.remote.keys = keys.k1 + } else { + // we should've bailed before this. but if not, bail here. + throw new Error('you are trying to talk to yourself') + } + + log('2.2. keys - shared: %s\n\tlocal: %s\n\tremote: %s', session.sharedSecret, session.local.keys, session.remote.keys) + + log('2.3. mac + cipher') + + // TODO: generate mac and cipher for local and remote + + log('2. exchange - finish') +} diff --git a/src/handshake/finish.js b/src/handshake/finish.js new file mode 100644 index 0000000..7776b12 --- /dev/null +++ b/src/handshake/finish.js @@ -0,0 +1,10 @@ +'use strict' + +const debug = require('debug') +const log = debug('libp2p:secio:handshake') +log.error = debug('libp2p:secio:handshake:error') + +function finish () { + log('3. finish - start') + log('3. finish - finish') +} diff --git a/src/handshake/index.js b/src/handshake/index.js new file mode 100644 index 0000000..37f29b8 --- /dev/null +++ b/src/handshake/index.js @@ -0,0 +1,39 @@ +'use strict' + +const crypto = require('libp2p-crypto') +const debug = require('debug') +const protobuf = require('protocol-buffers') +const path = require('path') +const fs = require('fs') + +const log = debug('libp2p:secio:handshake') +log.error = debug('libp2p:secio:handshake:error') + +const pbm = protobuf(fs.readFileSync(path.join(__dirname, '../secio.proto'))) + +const propose = require('./propose') +const exchange = require('./exchange') +const finish = require('./finish') + +// HandshakeTimeout governs how long the handshake will be allowed to take place for. +// Making this number large means there could be many bogus connections waiting to +// timeout in flight. Typical handshakes take ~3RTTs, so it should be completed within +// seconds across a typical planet in the solar system. +const handshakeTimeout = 30 * 1000 + + +// Performs initial communication over insecure channel to share +// keys, IDs, and initiate communication, assigning all necessary params. +function run (session) { + // step 1. Propose + // -- propose cipher suite + send pubkeys + nonce + propose(session) + + // step 2. Exchange + // -- exchange (signed) ephemeral keys. verify signatures. + exchange(session) + + // step 3. Finish + // -- send expected message to verify encryption works (send local nonce) + finish(session) +} diff --git a/src/handshake.js b/src/handshake/propose.js similarity index 71% rename from src/handshake.js rename to src/handshake/propose.js index 7a113fc..bfc1f38 100644 --- a/src/handshake.js +++ b/src/handshake/propose.js @@ -1,48 +1,23 @@ 'use strict' const forge = require('node-forge') -const crypto = require('libp2p-crypto') const debug = require('debug') const protobuf = require('protocol-buffers') const path = require('path') const fs = require('fs') const PeerId = require('peer-id') +const mh = require('multihashing') const log = debug('libp2p:secio:handshake') log.error = debug('libp2p:secio:handshake:error') -const pbm = protobuf(fs.readFileSync(path.join(__dirname, '../secio.proto'))) -const support = require('./support') - - -// HandshakeTimeout governs how long the handshake will be allowed to take place for. -// Making this number large means there could be many bogus connections waiting to -// timeout in flight. Typical handshakes take ~3RTTs, so it should be completed within -// seconds across a typical planet in the solar system. -const HandshakeTimeout = 30 * 1000 - // nonceSize is the size of our nonces (in bytes) const nonceSize = 16 -// Performs initial communication over insecure channel to share -// keys, IDs, and initiate communication, assigning all necessary params. -function run () { +const pbm = protobuf(fs.readFileSync(path.join(__dirname, '../secio.proto'))) +const support = require('./support') - // step 1. Propose - // -- propose cipher suite + send pubkeys + nonce - propose() - - // step 2. Exchange - // -- exchange (signed) ephemeral keys. verify signatures. - - exchange() - - // step 3. Finish - // -- send expected message to verify encryption works (send local nonce) - finish() -} - -function propose (session) { +module.exports = function propose (session) { log('1. propose - start') const nonceOut = forge.random.getBytesSync(nonceSize) @@ -81,9 +56,9 @@ function propose (session) { // we use the same params for both directions (must choose same curve) // WARNING: if they dont SelectBest the same way, this won't work... - session.remote.curveT = session.local.curveT - session.remote.cipherT = session.local.cipherT - session.remote.hashT = session.local.hashT + session.remote.curveT = session.local.curveT + session.remote.cipherT = session.local.cipherT + session.remote.hashT = session.local.hashT log('1. propose - finish') } @@ -124,13 +99,3 @@ function selectBest (local, remote) { hashT: support.theBest(order, local.hashes, remote.hashes) } } - -function exchange () { - log('2. exchnage - start') - log('2. exchange - finish') -} - -function finish () { - log('3. finish - start') - log('3. finish - finish') -} diff --git a/src/support.js b/src/support.js index 542c765..7063fe3 100644 --- a/src/support.js +++ b/src/support.js @@ -19,5 +19,26 @@ exports.hashes = [ // Determines which algorithm to use. Note: f(a, b) = f(b, a) exports.theBest = (order, p1, p2) => { + let first + let second + if (order < 0) { + first = p2 + second = p1 + } else if (order > 0) { + first = p1 + second = p2 + } else { + return p1[0] + } + + for (let firstCandidate of first) { + for (let secondCandidate of second) { + if (firstCandidate === secondCandidate) { + return firstCandidate + } + } + } + + throw new Error('No algorithms in common!') } diff --git a/test/support.spec.js b/test/support.spec.js new file mode 100644 index 0000000..a0a5de8 --- /dev/null +++ b/test/support.spec.js @@ -0,0 +1,54 @@ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect + +const support = require('../src/support') + +describe('support', () => { + describe('theBest', () => { + it('returns the first matching element, preferring p1', () => { + const order = 1 + const p1 = ['hello', 'world'] + const p2 = ['world', 'hello'] + + expect( + support.theBest(order, p1, p2) + ).to.be.eql( + 'hello' + ) + }) + + it('returns the first matching element, preferring p2', () => { + const order = -1 + const p1 = ['hello', 'world'] + const p2 = ['world', 'hello'] + + expect( + support.theBest(order, p1, p2) + ).to.be.eql( + 'world' + ) + }) + + it('returns the first element if the same', () => { + const order = 0 + const p1 = ['hello', 'world'] + const p2 = p1 + + expect( + support.theBest(order, p1, p2) + ).to.be.eql( + 'hello' + ) + }) + + it('throws if no matching element was found', () => { + expect( + () => support.theBest(1, ['hello'], ['world']) + ).to.throw( + /No algorithms in common/ + ) + }) + }) +})