mirror of
https://github.com/fluencelabs/js-libp2p-secio
synced 2025-05-09 13:12:16 +00:00
feat(secio): implement with pull-streams, ensure interop with go
This commit is contained in:
parent
b948ad62dd
commit
10a4cf0337
52
README.md
52
README.md
@ -4,14 +4,14 @@
|
||||
[](http://ipfs.io/)
|
||||
[](http://webchat.freenode.net/?channels=%23ipfs)
|
||||
[](https://github.com/RichardLitt/standard-readme)
|
||||
[](https://coveralls.io/github/ipfs/js-libp2p-secio?branch=master)
|
||||
[](https://travis-ci.org/ipfs/js-libp2p-secio)
|
||||
[](https://circleci.com/gh/ipfs/js-libp2p-secio)
|
||||
[](https://david-dm.org/ipfs/js-libp2p-secio) [](https://github.com/feross/standard)
|
||||
[](https://coveralls.io/github/libp2p/js-libp2p-secio?branch=master)
|
||||
[](https://travis-ci.org/libp2p/js-libp2p-secio)
|
||||
[](https://circleci.com/gh/libp2p/js-libp2p-secio)
|
||||
[](https://david-dm.org/libp2p/js-libp2p-secio) [](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)!
|
||||
|
26
package.json
26
package.json
@ -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>"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
53
src/etm.js
53
src/etm.js
@ -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
163
src/handshake/crypto.js
Normal 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')}`
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
115
src/index.js
115
src/index.js
@ -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
67
src/state.js
Normal 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
|
@ -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)
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user