feat(secio): implement with pull-streams, ensure interop with go

This commit is contained in:
Friedel Ziegelmayer 2016-06-13 19:10:38 +02:00 committed by David Dias
parent b948ad62dd
commit 10a4cf0337
13 changed files with 497 additions and 452 deletions

View File

@ -4,14 +4,14 @@
[![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/)
[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
[![Coverage Status](https://coveralls.io/repos/github/ipfs/js-libp2p-secio/badge.svg?branch=master)](https://coveralls.io/github/ipfs/js-libp2p-secio?branch=master)
[![Travis CI](https://travis-ci.org/ipfs/js-libp2p-secio.svg?branch=master)](https://travis-ci.org/ipfs/js-libp2p-secio)
[![Circle CI](https://circleci.com/gh/ipfs/js-libp2p-secio.svg?style=svg)](https://circleci.com/gh/ipfs/js-libp2p-secio)
[![Dependency Status](https://david-dm.org/ipfs/js-libp2p-secio.svg?style=flat-square)](https://david-dm.org/ipfs/js-libp2p-secio) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard)
[![Coverage Status](https://coveralls.io/repos/github/libp2p/js-libp2p-secio/badge.svg?branch=master)](https://coveralls.io/github/libp2p/js-libp2p-secio?branch=master)
[![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)
> Secio implementation in JavaScript
This repo contains the JavaScript implementation of secio, an encryption protocol used in libp2p. This is based on this [go implementation](https://github.com/ipfs/go-libp2p-secio).
This repo contains the JavaScript implementation of secio, an encryption protocol used in libp2p. This is based on this [go implementation](https://github.com/libp2p/go-libp2p-secio).
## Table of Contents
@ -24,17 +24,55 @@ This repo contains the JavaScript implementation of secio, an encryption protoco
## Install
```sh
npm install --save libp2p-secio
npm install libp2p-secio
```
## Usage
```js
const libp2pSecio = require('libp2p-secio')
const secio = require('libp2p-secio')
```
## API
### `SecureSession`
#### `constructor(id, key, insecure)`
- `id: PeerId` - The id of the node.
- `key: RSAPrivateKey` - The private key of the node.
- `insecure: PullStream` - The insecure connection.
### `.secure`
Returns the `insecure` connection provided, wrapped with secio. This is a pull-stream.
### This module uses `pull-streams`
We expose a streaming interface based on `pull-streams`, rather then on the Node.js core streams implementation (aka Node.js streams). `pull-streams` offers us a better mechanism for error handling and flow control guarantees. If you would like to know more about why we did this, see the discussion at this [issue](https://github.com/ipfs/js-ipfs/issues/362).
You can learn more about pull-streams at:
- [The history of Node.js streams, nodebp April 2014](https://www.youtube.com/watch?v=g5ewQEuXjsQ)
- [The history of streams, 2016](http://dominictarr.com/post/145135293917/history-of-streams)
- [pull-streams, the simple streaming primitive](http://dominictarr.com/post/149248845122/pull-streams-pull-streams-are-a-very-simple)
- [pull-streams documentation](https://pull-stream.github.io/)
#### Converting `pull-streams` to Node.js Streams
If you are a Node.js streams user, you can convert a pull-stream to a Node.js stream using the module [`pull-stream-to-stream`](https://github.com/dominictarr/pull-stream-to-stream), giving you an instance of a Node.js stream that is linked to the pull-stream. For example:
```js
const pullToStream = require('pull-stream-to-stream')
const nodeStreamInstance = pullToStream(pullStreamInstance)
// nodeStreamInstance is an instance of a Node.js Stream
```
To learn more about this utility, visit https://pull-stream.github.io/#pull-stream-to-stream.
## Contribute
Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/js-libp2p-secio/issues)!

View File

@ -25,27 +25,27 @@
"author": "Friedel Ziegelmayer <dignifiedqurie@gmail.com>",
"license": "MIT",
"dependencies": {
"async-buffered-reader": "^1.2.1",
"debug": "^2.2.0",
"duplexify": "^3.4.3",
"length-prefixed-stream": "^1.5.0",
"interface-connection": "^0.2.1",
"libp2p-crypto": "^0.5.0",
"multihashing": "^0.2.1",
"node-forge": "^0.6.39",
"node-forge": "^0.6.42",
"peer-id": "^0.7.0",
"protocol-buffers": "^3.1.6",
"readable-stream": "2.1.4",
"run-series": "^1.1.4",
"through2": "^2.0.1"
"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"
},
"devDependencies": {
"aegir": "^6.0.0",
"bl": "^1.1.2",
"aegir": "^8.0.0",
"chai": "^3.5.0",
"multistream-select": "^0.10.0",
"multistream-select": "^0.11.0",
"pre-commit": "^1.1.3",
"run-parallel": "^1.1.6",
"stream-pair": "^1.0.3"
"pull-pair": "^1.1.0",
"run-parallel": "^1.1.6"
},
"pre-commit": [
"lint",
@ -65,4 +65,4 @@
"contributors": [
"dignifiedquire <dignifiedquire@gmail.com>"
]
}
}

View File

@ -1,41 +1,44 @@
'use strict'
const through = require('through2')
const lpm = require('length-prefixed-stream')
const through = require('pull-through')
const pull = require('pull-stream')
const lp = require('pull-length-prefixed')
const toForgeBuffer = require('./support').toForgeBuffer
exports.writer = function etmWriter (insecure, cipher, mac) {
const encode = lpm.encode()
const pt = through(function (chunk, enc, cb) {
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)
const macBuffer = new Buffer(mac.getMac().getBytes(), 'binary')
mac.update(data.toString('binary'))
const macBuffer = new Buffer(mac.digest().getBytes(), 'binary')
this.push(Buffer.concat([data, macBuffer]))
this.queue(Buffer.concat([data, macBuffer]))
// reset hmac
mac.start(null, null)
}
cb()
})
pt.pipe(encode).pipe(insecure)
return pt
return pull(
pt,
lp.encode(lpOpts)
)
}
exports.reader = function etmReader (insecure, decipher, mac) {
const decode = lpm.decode()
const pt = through(function (chunk, enc, cb) {
exports.createUnboxStream = (decipher, mac) => {
const pt = through(function (chunk) {
const l = chunk.length
const macSize = mac.getMac().length()
if (l < macSize) {
return cb(new Error(`buffer (${l}) shorter than MAC size (${macSize})`))
return this.emit('error', new Error(`buffer (${l}) shorter than MAC size (${macSize})`))
}
const mark = l - macSize
@ -45,12 +48,13 @@ exports.reader = function etmReader (insecure, decipher, mac) {
// Clear out any previous data
mac.start(null, null)
mac.update(data)
mac.update(data.toString('binary'))
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.toString('hex')} != ${expected.toString('hex')}`))
return this.emit('error', new Error(`MAC Invalid: ${macd.toString('hex')} != ${expected.toString('hex')}`))
}
// all good, decrypt
@ -58,13 +62,12 @@ exports.reader = function etmReader (insecure, decipher, mac) {
if (decipher.output.length() > 0) {
const data = new Buffer(decipher.output.getBytes(), 'binary')
this.push(data)
this.queue(data)
}
cb()
})
insecure.pipe(decode).pipe(pt)
return pt
return pull(
lp.decode(lpOpts),
pt
)
}

163
src/handshake/crypto.js Normal file
View File

@ -0,0 +1,163 @@
'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 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 support = require('../support')
// nonceSize is the size of our nonces (in bytes)
const nonceSize = 16
exports.createProposal = (state) => {
state.proposal.out = {
rand: support.randomBytes(nonceSize),
pubkey: state.key.local.public.bytes,
exchanges: support.exchanges.join(','),
ciphers: support.ciphers.join(','),
hashes: support.hashes.join(',')
}
state.proposalEncoded.out = pbm.Propose.encode(state.proposal.out)
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
// Gather corpus to sign.
const selectionOut = Buffer.concat([
state.proposalEncoded.out,
state.proposalEncoded.in,
state.ephemeralKey.local
])
state.exchange.out = {
epubkey: state.ephemeralKey.local,
signature: new Buffer(state.key.local.sign(selectionOut), 'binary')
}
return pbm.Exchange.encode(state.exchange.out)
}
exports.identify = (state, msg) => {
log('1.1 identify')
state.proposalEncoded.in = msg
state.proposal.in = pbm.Propose.decode(msg)
const pubkey = state.proposal.in.pubkey
console.log(state.proposal.in)
state.key.remote = crypto.unmarshalPublicKey(pubkey)
state.id.remote = PeerId.createFromPubKey(pubkey.toString('base64'))
log('1.1 identify - %s - identified remote peer as %s', state.id.local.toB58String(), state.id.remote.toB58String())
}
exports.selectProtocols = (state) => {
log('1.2 selection')
const local = {
pubKeyBytes: state.key.local.public.bytes,
exchanges: support.exchanges,
hashes: support.hashes,
ciphers: support.ciphers,
nonce: state.proposal.out.rand
}
const remote = {
pubKeyBytes: state.proposal.in.pubkey,
exchanges: state.proposal.in.exchanges.split(','),
hashes: state.proposal.in.hashes.split(','),
ciphers: state.proposal.in.ciphers.split(','),
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
}
state.protocols.local = {
order: selected.order,
curveT: selected.curveT,
cipherT: selected.cipherT,
hashT: selected.hashT
}
}
exports.verify = (state, msg) => {
log('2.1. verify')
state.exchange.in = pbm.Exchange.decode(msg)
state.ephemeralKey.remote = state.exchange.in.epubkey
const selectionIn = Buffer.concat([
state.proposalEncoded.in,
state.proposalEncoded.out,
state.ephemeralKey.remote
])
const sigOk = state.key.remote.verify(selectionIn, state.exchange.in.signature)
if (!sigOk) {
throw new Error('Bad signature')
}
log('2.1. verify - signature verified')
}
exports.generateKeys = (state) => {
log('2.2. keys')
state.shared.secret = state.shared.generate(state.exchange.in.epubkey)
const keys = crypto.keyStretcher(
state.protocols.local.cipherT,
state.protocols.local.hashT,
state.shared.secret
)
// 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')
support.makeMacAndCipher(state.protocols.local)
support.makeMacAndCipher(state.protocols.remote)
}
exports.verifyNonce = (state, n2) => {
const n1 = state.proposal.out.rand
if (n1.equals(n2)) return
throw new Error(
`Failed to read our encrypted nonce: ${n1.toString('hex')} != ${n2.toString('hex')}`
)
}

View File

@ -1,43 +1,30 @@
'use strict'
const crypto = require('libp2p-crypto')
const debug = require('debug')
const fs = require('fs')
const path = require('path')
const protobuf = require('protocol-buffers')
const support = require('../support')
const crypto = require('./crypto')
const log = debug('libp2p:secio')
log.error = debug('libp2p:secio:error')
const pbm = protobuf(fs.readFileSync(path.join(__dirname, '../secio.proto')))
const support = require('../support')
// step 2. Exchange
// -- exchange (signed) ephemeral keys. verify signatures.
module.exports = function exchange (session, cb) {
module.exports = function exchange (state, cb) {
log('2. exchange - start')
let genSharedKey
let exchangeOut
log('2. exchange - writing exchange')
support.write(state, crypto.createExchange(state))
support.read(state.shake, (err, msg) => {
if (err) {
return cb(err)
}
try {
const eResult = crypto.generateEphemeralKeyPair(session.local.curveT)
session.local.ephemeralPubKey = eResult.key
genSharedKey = eResult.genSharedKey
exchangeOut = makeExchange(session)
} catch (err) {
return cb(err)
}
session.insecureLp.write(exchangeOut)
session.insecureLp.once('data', (chunk) => {
const exchangeIn = pbm.Exchange.decode(chunk)
log('2. exchange - reading exchange')
try {
verify(session, exchangeIn)
keys(session, exchangeIn, genSharedKey)
macAndCipher(session)
crypto.verify(state, msg)
crypto.generateKeys(state)
} catch (err) {
return cb(err)
}
@ -46,63 +33,3 @@ module.exports = function exchange (session, cb) {
cb()
})
}
function makeExchange (session) {
// Gather corpus to sign.
const selectionOut = Buffer.concat([
session.proposal.out,
session.proposal.in,
session.local.ephemeralPubKey
])
const epubkey = session.local.ephemeralPubKey
const signature = new Buffer(session.localKey.sign(selectionOut), 'binary')
log('out', {epubkey, signature})
return pbm.Exchange.encode({epubkey, signature})
}
function verify (session, exchangeIn) {
log('2.1. verify', exchangeIn)
session.remote.ephemeralPubKey = exchangeIn.epubkey
const selectionIn = Buffer.concat([
session.proposal.in,
session.proposal.out,
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')
}
function keys (session, exchangeIn, genSharedKey) {
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 (session.proposal.order > 0) {
session.local.keys = keys.k1
session.remote.keys = keys.k2
} else if (session.proposal.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')
}
}
function macAndCipher (session) {
log('2.3. mac + cipher')
support.makeMacAndCipher(session.local)
support.makeMacAndCipher(session.remote)
}

View File

@ -1,35 +1,56 @@
'use strict'
const duplexify = require('duplexify')
const pull = require('pull-stream')
const handshake = require('pull-handshake')
const debug = require('debug')
const log = debug('libp2p:secio')
log.error = debug('libp2p:secio:error')
const read = require('async-buffered-reader')
const etm = require('../etm')
const crypto = require('./crypto')
// step 3. Finish
// -- send expected message to verify encryption works (send local nonce)
module.exports = function finish (session, cb) {
module.exports = function finish (state, cb) {
log('3. finish - start')
const w = etm.writer(session.insecure, session.local.cipher, session.local.mac)
const r = etm.reader(session.insecure, session.remote.cipher, session.remote.mac)
session.secure = duplexify(w, r)
session.secure.write(session.proposal.randIn)
const proto = state.protocols
const stream = state.shake.rest()
const shake = handshake({timeout: state.timeout})
// read our nonce back
read(session.secure, 16, (nonceOut2) => {
const nonceOut = session.proposal.nonceOut
if (!nonceOut.equals(nonceOut2)) {
const err = new Error(`Failed to read our encrypted nonce: ${nonceOut.toString('hex')} != ${nonceOut2.toString('hex')}`)
pull(
stream,
etm.createUnboxStream(proto.remote.cipher, proto.remote.mac),
shake,
etm.createBoxStream(proto.local.cipher, proto.local.mac),
stream
)
shake.handshake.write(state.proposal.in.rand)
shake.handshake.read(state.proposal.in.rand.length, (err, nonceBack) => {
const fail = (err) => {
log.error(err)
return cb(err)
state.secure.resolve({
source: pull.error(err),
sink (read) {
}
})
cb(err)
}
log('3. finish - finish', nonceOut.toString('hex'), nonceOut2.toString('hex'))
if (err) return fail(err)
try {
crypto.verifyNonce(state, nonceBack)
} catch (err) {
return fail(err)
}
log('3. finish - finish')
// Awesome that's all folks.
state.secure.resolve(shake.handshake.rest())
cb()
})
}

View File

@ -1,21 +1,25 @@
'use strict'
const debug = require('debug')
const series = require('run-series')
const log = debug('libp2p:secio')
log.error = debug('libp2p:secio:error')
const propose = require('./propose')
const exchange = require('./exchange')
const finish = require('./finish')
// Performs initial communication over insecure channel to share
// keys, IDs, and initiate communication, assigning all necessary params.
module.exports = function handshake (session, cb) {
module.exports = function handshake (state) {
series([
(cb) => propose(session, cb),
(cb) => exchange(session, cb),
(cb) => finish(session, cb)
], cb)
(cb) => propose(state, cb),
(cb) => exchange(state, cb),
(cb) => finish(state, cb)
], (err) => {
state.cleanSecrets()
if (err) {
state.shake.abort(err)
}
})
return state.stream
}

View File

@ -1,52 +1,30 @@
'use strict'
const forge = require('node-forge')
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 crypto = require('libp2p-crypto')
const support = require('../support')
const crypto = require('./crypto')
const log = debug('libp2p:secio')
log.error = debug('libp2p:secio:error')
// nonceSize is the size of our nonces (in bytes)
const nonceSize = 16
const pbm = protobuf(fs.readFileSync(path.join(__dirname, '../secio.proto')))
const support = require('../support')
// step 1. Propose
// -- propose cipher suite + send pubkeys + nonce
module.exports = function propose (session, cb) {
module.exports = function propose (state, cb) {
log('1. propose - start')
const nonceOut = new Buffer(forge.random.getBytesSync(nonceSize), 'binary')
const proposeOut = makeProposal(session, nonceOut)
session.proposal.out = proposeOut
session.proposal.nonceOut = nonceOut
log('1. propse - writing proposal')
session.insecureLp.write(proposeOut)
session.insecureLp.once('data', (chunk) => {
log('1. propse - reading proposal')
let proposeIn
try {
proposeIn = readProposal(chunk)
session.proposal.in = chunk
session.proposal.randIn = proposeIn.rand
identify(session, proposeIn)
} catch (err) {
log('1. propose - writing proposal')
support.write(state, crypto.createProposal(state))
support.read(state.shake, (err, msg) => {
if (err) {
return cb(err)
}
log('1. propose - reading proposal', msg)
try {
selection(session, nonceOut, proposeIn)
crypto.identify(state, msg)
crypto.selectProtocols(state)
} catch (err) {
return cb(err)
}
@ -56,91 +34,3 @@ module.exports = function propose (session, cb) {
cb()
})
}
// Generate and send Hello packet.
// Hello = (rand, PublicKey, Supported)
function makeProposal (session, nonceOut) {
session.local.permanentPubKey = session.localKey.public
const myPubKeyBytes = session.local.permanentPubKey.bytes
return pbm.Propose.encode({
rand: nonceOut,
pubkey: myPubKeyBytes,
exchanges: support.exchanges.join(','),
ciphers: support.ciphers.join(','),
hashes: support.hashes.join(',')
})
}
function readProposal (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(proposeIn.pubkey.toString('base64'))
log('1.1 identify - %s - identified remote peer as %s', session.localPeer.toB58String(), session.remotePeer.toB58String())
}
function selection (session, nonceOut, proposeIn) {
log('1.2 selection')
const local = {
pubKeyBytes: session.local.permanentPubKey.bytes,
exchanges: support.exchanges,
hashes: support.hashes,
ciphers: support.ciphers,
nonce: nonceOut
}
const remote = {
pubKeyBytes: proposeIn.pubkey,
exchanges: proposeIn.exchanges.split(','),
hashes: proposeIn.hashes.split(','),
ciphers: proposeIn.ciphers.split(','),
nonce: proposeIn.rand
}
let selected = selectBest(local, remote)
session.proposal.order = selected.order
session.local.curveT = selected.curveT
session.local.cipherT = selected.cipherT
session.local.hashT = selected.hashT
// 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
}
function selectBest (local, remote) {
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) {
throw new Error('you are trying to talk to yourself')
}
return {
curveT: support.theBest(order, local.exchanges, remote.exchanges),
cipherT: support.theBest(order, local.ciphers, remote.ciphers),
hashT: support.theBest(order, local.hashes, remote.hashes),
order
}
}
function digest (buf) {
return mh.digest(buf, 'sha2-256', buf.length)
}

View File

@ -1,119 +1,36 @@
'use strict'
const duplexify = require('duplexify')
const lpstream = require('length-prefixed-stream')
const PassThrough = require('readable-stream').PassThrough
const pull = require('pull-stream')
const Connection = require('interface-connection').Connection
const handshake = require('./handshake')
const State = require('./state')
exports.SecureSession = class SecureSession {
constructor (local, key, insecure) {
this.localKey = key
this.localPeer = local
this.sharedSecret = null
this.local = {}
this.remote = {}
this.proposal = {}
this.insecure = insecure
this.secure = null
const e = lpstream.encode()
const d = lpstream.decode()
this.insecureLp = duplexify(e, d)
e.pipe(this.insecure)
this.insecure.pipe(d)
if (!this.localPeer) {
if (!local) {
throw new Error('no local id provided')
}
if (!this.localKey) {
if (!key) {
throw new Error('no local private key provided')
}
// Enable when implemented in js-peer-id
// if (!this.localPeer.matchesPrivateKey(this.localKey)) {
// throw new Error('peer.ID does not match privateKey')
// }
if (!insecure) {
throw new Error('no insecure stream provided')
}
this.state = new State(local, key)
this.insecure = insecure
pull(
this.insecure,
handshake(this.state),
this.insecure
)
}
secureStream () {
let handshaked = false
const reader = new PassThrough()
const writer = new PassThrough()
const dp = duplexify(writer, reader)
const originalRead = reader.read.bind(reader)
const originalWrite = writer.write.bind(writer)
const doHandshake = () => {
if (handshaked) return
handshaked = true
// Restore methods to avoid overhead
reader.read = originalRead
writer.write = originalWrite
this.handshake((err) => {
if (err) {
dp.emit('error', err)
}
// Pipe things together
writer.pipe(this.secure)
this.secure.pipe(reader)
dp.uncork()
dp.resume()
})
}
// patch to detect first read
reader.read = (size) => {
doHandshake()
originalRead(size)
}
// patch to detect first write
writer.write = (chunk, encoding, callback) => {
doHandshake()
originalWrite(chunk, encoding, callback)
}
dp.cork()
dp.pause()
return dp
}
handshake (cb) {
// TODO: figure out how to best handle the handshake timeout
if (this._handshakeLock) {
return cb(new Error('handshake already in progress'))
}
this._handshakeLock = true
const finish = (err) => {
this._handshakeLock = false
cb(err)
}
if (this._handshakeDone) {
return finish()
}
handshake(this, (err) => {
if (err) {
return finish(err)
}
this._handshakeDone = true
finish()
})
get secure () {
return new Connection(this.state.secure, this.insecure)
}
}

67
src/state.js Normal file
View File

@ -0,0 +1,67 @@
'use strict'
const handshake = require('pull-handshake')
const deferred = require('pull-defer')
class State {
constructor (id, key, timeout, cb) {
this.setup()
this.id.local = id
this.key.local = key
this.timeout = timeout || 60 * 1000
cb = cb || (() => {})
this.secure = deferred.duplex()
this.stream = handshake({timeout: this.timeout}, cb)
this.shake = this.stream.handshake
delete this.stream.handshake
}
setup () {
this.id = {
local: null,
remote: null
}
this.key = {
local: null,
remote: null
}
this.shake = null
this.cleanSecrets()
}
// remove all data from the handshake that is not needed anymore
cleanSecrets () {
this.shared = {}
this.ephemeralKey = {
local: null,
remote: null
}
this.proposal = {
in: null,
out: null
}
this.proposalEncoded = {
in: null,
out: null
}
this.protocols = {
local: null,
remote: null
}
this.exchange = {
in: null,
out: null
}
}
}
module.exports = State

View File

@ -1,6 +1,9 @@
'use strict'
const mh = require('multihashing')
const forge = require('node-forge')
const lp = require('pull-length-prefixed')
const pull = require('pull-stream')
exports.exchanges = [
'P-256',
@ -86,3 +89,55 @@ function makeCipher (cipherType, iv, key) {
throw new Error(`unrecognized cipher type: ${cipherType}`)
}
exports.randomBytes = (nonceSize) => {
return new Buffer(forge.random.getBytesSync(nonceSize), 'binary')
}
exports.selectBest = (local, remote) => {
const oh1 = exports.digest(Buffer.concat([
remote.pubKeyBytes,
local.nonce
]))
const oh2 = exports.digest(Buffer.concat([
local.pubKeyBytes,
remote.nonce
]))
const order = Buffer.compare(oh1, oh2)
if (order === 0) {
throw new Error('you are trying to talk to yourself')
}
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
}
}
exports.digest = (buf) => {
return mh.digest(buf, 'sha2-256', buf.length)
}
exports.write = function write (state, msg, cb) {
cb = cb || (() => {})
pull(
pull.values([
msg
]),
lp.encode({fixed: true, bytes: 4}),
pull.collect((err, res) => {
if (err) {
return cb(err)
}
state.shake.write(res[0])
cb()
})
)
}
exports.read = function read (reader, cb) {
lp.decodeFromReader(reader, {fixed: true, bytes: 4}, cb)
}

View File

@ -1,92 +1,45 @@
/* eslint-env mocha */
'use strict'
const pair = require('pull-pair/duplex')
const expect = require('chai').expect
const through = require('through2')
const bl = require('bl')
const PeerId = require('peer-id')
const crypto = require('libp2p-crypto')
const streamPair = require('stream-pair')
const parallel = require('run-parallel')
const series = require('run-series')
const ms = require('multistream-select')
const pull = require('pull-stream')
const Listener = ms.Listener
const Dialer = ms.Dialer
const SecureSession = require('../src').SecureSession
describe('libp2p-secio', () => {
describe('insecure length prefixed stream', () => {
it('encodes', (done) => {
const id = PeerId.create({bits: 64})
const key = {}
const insecure = through()
const s = new SecureSession(id, key, insecure)
// encoded on raw
s.insecure.pipe(bl((err, res) => {
expect(err).to.not.exist
expect(res.toString()).to.be.eql('\u0005hello\u0005world')
done()
}))
s.insecureLp.write('hello')
s.insecureLp.write('world')
insecure.end()
})
it('decodes', (done) => {
const id = PeerId.create({bits: 64})
const key = {}
const insecure = through()
const s = new SecureSession(id, key, insecure)
// encoded on raw
s.insecureLp.pipe(bl((err, res) => {
expect(err).to.not.exist
expect(res.toString()).to.be.eql('helloworld')
done()
}))
s.insecure.write('\u0005hello')
s.insecure.write('\u0005world')
s.insecureLp.end()
})
it('all together now', (done) => {
const pair = streamPair.create()
const local = createSession(pair)
const remote = createSession(pair.other)
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()
const p = pair()
const local = createSession(pair)
const remote = createSession(pair.other)
const localSecure = local.session.secureStream()
localSecure.write('hello world')
const local = createSession(p[0])
const remote = createSession(p[1])
const localSecure = local.session.secure
const remoteSecure = remote.session.secureStream()
remoteSecure.once('data', (chunk) => {
expect(chunk.toString()).to.be.eql('hello world')
done()
})
pull(
pull.values(['hello world']),
localSecure
)
const remoteSecure = remote.session.secure
pull(
remoteSecure,
pull.collect((err, chunks) => {
expect(err).to.not.exist
expect(chunks).to.be.eql([new Buffer('hello world')])
done()
})
)
})
it('works over multistream', (done) => {
const pair = streamPair.create()
const p = pair()
const listener = new Listener()
const dialer = new Dialer()
@ -94,22 +47,29 @@ describe('libp2p-secio', () => {
let remote
series([
(cb) => parallel([
(cb) => listener.handle(pair, cb),
(cb) => dialer.handle(pair.other, cb)
(cb) => listener.handle(p[0], cb),
(cb) => dialer.handle(p[1], cb)
], cb),
(cb) => {
listener.addHandler('/banana/1.0.0', (conn) => {
local = createSession(conn).session.secureStream()
local.once('data', (res) => {
expect(res.toString()).to.be.eql('hello world')
done()
})
local = createSession(conn).session.secure
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).session.secureStream()
remote.write('hello world')
remote = createSession(conn).session.secure
pull(
pull.values(['hello world']),
remote
)
cb(err)
})
], (err) => {