mirror of
https://github.com/fluencelabs/js-libp2p-secio
synced 2025-03-16 02:10:52 +00:00
Async Crypto Endeavour (+benchmarks!) (#19)
* feat: add benchmarks and integrate with new libp2p-crypto * start removing node-forge * less forge * cleanup and fixes * benchmarked and tested * remove unused .aegir * Replace lib multihashing with multihashing-async * cleanup package.json * Using string instead of buffer for proto file * chore: fix benchmarks * docs(readme): update README URLs based on HTTP redirects * ready for next aegir * update aegir * ready * chore: update deps
This commit is contained in:
parent
4f1911f817
commit
a1167c0084
16
.aegir.js
16
.aegir.js
@ -1,16 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
webpack: {
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'node-forge': path.resolve(
|
|
||||||
path.dirname(require.resolve('libp2p-crypto')),
|
|
||||||
'../vendor/forge.bundle.js'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -31,5 +31,4 @@ build
|
|||||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
lib
|
|
||||||
dist
|
dist
|
||||||
|
20
.travis.yml
20
.travis.yml
@ -1,9 +1,15 @@
|
|||||||
sudo: false
|
sudo: false
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
matrix:
|
||||||
- 4
|
include:
|
||||||
- 5
|
- node_js: 4
|
||||||
- stable
|
env: CXX=g++-4.8
|
||||||
|
- node_js: 6
|
||||||
|
env:
|
||||||
|
- SAUCE=true
|
||||||
|
- CXX=g++-4.8
|
||||||
|
- node_js: stable
|
||||||
|
env: CXX=g++-4.8
|
||||||
|
|
||||||
# Make sure we have new NPM.
|
# Make sure we have new NPM.
|
||||||
before_install:
|
before_install:
|
||||||
@ -14,7 +20,6 @@ script:
|
|||||||
- npm test
|
- npm test
|
||||||
- npm run coverage
|
- npm run coverage
|
||||||
|
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- export DISPLAY=:99.0
|
- export DISPLAY=:99.0
|
||||||
- sh -e /etc/init.d/xvfb start
|
- sh -e /etc/init.d/xvfb start
|
||||||
@ -22,13 +27,10 @@ before_script:
|
|||||||
after_success:
|
after_success:
|
||||||
- npm run coverage-publish
|
- npm run coverage-publish
|
||||||
|
|
||||||
env:
|
|
||||||
- CXX=g++-4.8
|
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
firefox: 'latest'
|
firefox: 'latest'
|
||||||
apt:
|
apt:
|
||||||
sources:
|
sources:
|
||||||
- ubuntu-toolchain-r-test
|
- ubuntu-toolchain-r-test
|
||||||
packages:
|
packages:
|
||||||
- g++-4.8
|
- g++-4.8
|
@ -8,6 +8,10 @@
|
|||||||
[](https://travis-ci.org/libp2p/js-libp2p-secio)
|
[](https://travis-ci.org/libp2p/js-libp2p-secio)
|
||||||
[](https://circleci.com/gh/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)
|
[](https://david-dm.org/libp2p/js-libp2p-secio) [](https://github.com/feross/standard)
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
[](https://saucelabs.com/u/libp2p-js-secio)
|
||||||
|
|
||||||
> Secio implementation in JavaScript
|
> Secio implementation in JavaScript
|
||||||
|
|
||||||
|
87
benchmarks/send.js
Normal file
87
benchmarks/send.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const Benchmark = require('benchmark')
|
||||||
|
const pull = require('pull-stream')
|
||||||
|
const pair = require('pull-pair/duplex')
|
||||||
|
const PeerId = require('peer-id')
|
||||||
|
const crypto = require('libp2p-crypto')
|
||||||
|
|
||||||
|
const secio = require('../src')
|
||||||
|
|
||||||
|
const suite = new Benchmark.Suite('secio')
|
||||||
|
const ids = []
|
||||||
|
|
||||||
|
suite.add('createKey', function (d) {
|
||||||
|
crypto.generateKeyPair('RSA', 2048, (err, key) => {
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
PeerId.createFromPrivKey(key.bytes, (err, id) => {
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
ids.push(id)
|
||||||
|
d.resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
defer: true
|
||||||
|
})
|
||||||
|
.add('send', function (deferred) {
|
||||||
|
const p = pair()
|
||||||
|
|
||||||
|
createSession(p[0], (err, local) => {
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
createSession(p[1], (err, remote) => {
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
sendMessages(local, remote)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function sendMessages (local, remote) {
|
||||||
|
pull(
|
||||||
|
pull.infinite(),
|
||||||
|
pull.take(100),
|
||||||
|
pull.map((val) => Buffer(val.toString())),
|
||||||
|
local
|
||||||
|
)
|
||||||
|
|
||||||
|
pull(
|
||||||
|
remote,
|
||||||
|
pull.take(100),
|
||||||
|
pull.collect((err, chunks) => {
|
||||||
|
if (err) throw err
|
||||||
|
if (chunks.length !== 100) throw new Error('Did not receive enough chunks')
|
||||||
|
deferred.resolve()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
defer: true
|
||||||
|
})
|
||||||
|
.on('cycle', (event) => {
|
||||||
|
console.log(String(event.target))
|
||||||
|
})
|
||||||
|
// run async
|
||||||
|
.run({
|
||||||
|
async: true
|
||||||
|
})
|
||||||
|
|
||||||
|
function createSession (insecure, cb) {
|
||||||
|
crypto.generateKeyPair('RSA', 2048, (err, key) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
PeerId.createFromPrivKey(key.bytes, (err, id) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, secio.encrypt(id, key, insecure))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
33
package.json
33
package.json
@ -2,8 +2,7 @@
|
|||||||
"name": "libp2p-secio",
|
"name": "libp2p-secio",
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"description": "Secio implementation in JavaScript",
|
"description": "Secio implementation in JavaScript",
|
||||||
"main": "lib/index.js",
|
"main": "src/index.js",
|
||||||
"jsnext:main": "src/index.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "aegir-lint",
|
"lint": "aegir-lint",
|
||||||
"build": "aegir-build",
|
"build": "aegir-build",
|
||||||
@ -14,7 +13,8 @@
|
|||||||
"release-minor": "aegir-release --type minor",
|
"release-minor": "aegir-release --type minor",
|
||||||
"release-major": "aegir-release --type major",
|
"release-major": "aegir-release --type major",
|
||||||
"coverage": "aegir-coverage",
|
"coverage": "aegir-coverage",
|
||||||
"coverage-publish": "aegir-coverage publish"
|
"coverage-publish": "aegir-coverage publish",
|
||||||
|
"bench": "node benchmarks/send.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"IPFS",
|
"IPFS",
|
||||||
@ -22,37 +22,36 @@
|
|||||||
"crypto",
|
"crypto",
|
||||||
"rsa"
|
"rsa"
|
||||||
],
|
],
|
||||||
"author": "Friedel Ziegelmayer <dignifiedqurie@gmail.com>",
|
"author": "Friedel Ziegelmayer <dignifiedquire@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"async": "^2.1.2",
|
||||||
"debug": "^2.2.0",
|
"debug": "^2.2.0",
|
||||||
"interface-connection": "^0.2.1",
|
"interface-connection": "^0.3.0",
|
||||||
"libp2p-crypto": "^0.6.1",
|
"libp2p-crypto": "^0.7.0",
|
||||||
"multihashing": "^0.2.1",
|
"multihashing-async": "^0.2.0",
|
||||||
"node-forge": "^0.6.42",
|
"peer-id": "^0.8.0",
|
||||||
"peer-id": "^0.7.0",
|
|
||||||
"protocol-buffers": "^3.1.6",
|
"protocol-buffers": "^3.1.6",
|
||||||
"pull-defer": "^0.2.2",
|
"pull-defer": "^0.2.2",
|
||||||
"pull-handshake": "^1.1.4",
|
"pull-handshake": "^1.1.4",
|
||||||
"pull-length-prefixed": "^1.2.0",
|
"pull-length-prefixed": "^1.2.0",
|
||||||
"pull-stream": "^3.4.3",
|
"pull-stream": "^3.5.0"
|
||||||
"pull-through": "^1.0.18",
|
|
||||||
"run-series": "^1.1.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aegir": "^8.0.0",
|
"aegir": "^9.0.1",
|
||||||
|
"benchmark": "^2.1.2",
|
||||||
"chai": "^3.5.0",
|
"chai": "^3.5.0",
|
||||||
"multistream-select": "^0.11.0",
|
"multistream-select": "^0.11.1",
|
||||||
"pre-commit": "^1.1.3",
|
"pre-commit": "^1.1.3",
|
||||||
"pull-pair": "^1.1.0",
|
"pull-pair": "^1.1.0",
|
||||||
"run-parallel": "^1.1.6"
|
"pull-pushable": "^2.0.1"
|
||||||
},
|
},
|
||||||
"pre-commit": [
|
"pre-commit": [
|
||||||
"lint",
|
"lint",
|
||||||
"test"
|
"test"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^4.0.0"
|
"node": ">=4.0.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -68,4 +67,4 @@
|
|||||||
"Richard Littauer <richard.littauer@gmail.com>",
|
"Richard Littauer <richard.littauer@gmail.com>",
|
||||||
"greenkeeperio-bot <support@greenkeeper.io>"
|
"greenkeeperio-bot <support@greenkeeper.io>"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
98
src/etm.js
98
src/etm.js
@ -1,73 +1,67 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const through = require('pull-through')
|
|
||||||
const pull = require('pull-stream')
|
const pull = require('pull-stream')
|
||||||
const lp = require('pull-length-prefixed')
|
const lp = require('pull-length-prefixed')
|
||||||
|
|
||||||
const toForgeBuffer = require('./support').toForgeBuffer
|
|
||||||
|
|
||||||
const lpOpts = {
|
const lpOpts = {
|
||||||
fixed: true,
|
fixed: true,
|
||||||
bytes: 4
|
bytes: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createBoxStream = (cipher, mac) => {
|
exports.createBoxStream = (cipher, mac) => {
|
||||||
const pt = through(function (chunk) {
|
|
||||||
cipher.update(toForgeBuffer(chunk))
|
|
||||||
|
|
||||||
if (cipher.output.length() > 0) {
|
|
||||||
const data = new Buffer(cipher.output.getBytes(), 'binary')
|
|
||||||
mac.update(data.toString('binary'))
|
|
||||||
const macBuffer = new Buffer(mac.digest().getBytes(), 'binary')
|
|
||||||
|
|
||||||
this.queue(Buffer.concat([data, macBuffer]))
|
|
||||||
// reset hmac
|
|
||||||
mac.start(null, null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return pull(
|
return pull(
|
||||||
pt,
|
pull.asyncMap((chunk, cb) => {
|
||||||
|
cipher.encrypt(chunk, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mac.digest(data, (err, digest) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, Buffer.concat([data, digest]))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
lp.encode(lpOpts)
|
lp.encode(lpOpts)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createUnboxStream = (decipher, mac) => {
|
exports.createUnboxStream = (decipher, mac) => {
|
||||||
const pt = through(function (chunk) {
|
|
||||||
const l = chunk.length
|
|
||||||
const macSize = mac.getMac().length()
|
|
||||||
|
|
||||||
if (l < macSize) {
|
|
||||||
return this.emit('error', new Error(`buffer (${l}) shorter than MAC size (${macSize})`))
|
|
||||||
}
|
|
||||||
|
|
||||||
const mark = l - macSize
|
|
||||||
const data = chunk.slice(0, mark)
|
|
||||||
const macd = chunk.slice(mark)
|
|
||||||
|
|
||||||
// Clear out any previous data
|
|
||||||
mac.start(null, null)
|
|
||||||
|
|
||||||
mac.update(data.toString('binary'))
|
|
||||||
const expected = new Buffer(mac.getMac().getBytes(), 'binary')
|
|
||||||
|
|
||||||
// reset hmac
|
|
||||||
mac.start(null, null)
|
|
||||||
if (!macd.equals(expected)) {
|
|
||||||
return this.emit('error', new Error(`MAC Invalid: ${macd.toString('hex')} != ${expected.toString('hex')}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
// all good, decrypt
|
|
||||||
decipher.update(toForgeBuffer(data))
|
|
||||||
|
|
||||||
if (decipher.output.length() > 0) {
|
|
||||||
const data = new Buffer(decipher.output.getBytes(), 'binary')
|
|
||||||
this.queue(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return pull(
|
return pull(
|
||||||
lp.decode(lpOpts),
|
lp.decode(lpOpts),
|
||||||
pt
|
pull.asyncMap((chunk, cb) => {
|
||||||
|
const l = chunk.length
|
||||||
|
const macSize = mac.length
|
||||||
|
|
||||||
|
if (l < macSize) {
|
||||||
|
return cb(new Error(`buffer (${l}) shorter than MAC size (${macSize})`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const mark = l - macSize
|
||||||
|
const data = chunk.slice(0, mark)
|
||||||
|
const macd = chunk.slice(mark)
|
||||||
|
|
||||||
|
mac.digest(data, (err, expected) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!macd.equals(expected)) {
|
||||||
|
return cb(new Error(`MAC Invalid: ${macd.toString('hex')} != ${expected.toString('hex')}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
// all good, decrypt
|
||||||
|
decipher.decrypt(data, (err, decrypted) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, decrypted)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const protobuf = require('protocol-buffers')
|
const protobuf = require('protocol-buffers')
|
||||||
const path = require('path')
|
|
||||||
const fs = require('fs')
|
|
||||||
const PeerId = require('peer-id')
|
const PeerId = require('peer-id')
|
||||||
const crypto = require('libp2p-crypto')
|
const crypto = require('libp2p-crypto')
|
||||||
|
const parallel = require('async/parallel')
|
||||||
|
const waterfall = require('async/waterfall')
|
||||||
const debug = require('debug')
|
const debug = require('debug')
|
||||||
const log = debug('libp2p:secio')
|
const log = debug('libp2p:secio')
|
||||||
log.error = debug('libp2p:secio:error')
|
log.error = debug('libp2p:secio:error')
|
||||||
|
|
||||||
const pbm = protobuf(fs.readFileSync(path.join(__dirname, 'secio.proto')))
|
const pbm = protobuf(require('./secio.proto'))
|
||||||
|
|
||||||
const support = require('../support')
|
const support = require('../support')
|
||||||
|
|
||||||
@ -29,27 +29,38 @@ exports.createProposal = (state) => {
|
|||||||
return state.proposalEncoded.out
|
return state.proposalEncoded.out
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createExchange = (state) => {
|
exports.createExchange = (state, callback) => {
|
||||||
const res = crypto.generateEphemeralKeyPair(state.protocols.local.curveT)
|
crypto.generateEphemeralKeyPair(state.protocols.local.curveT, (err, res) => {
|
||||||
state.ephemeralKey.local = res.key
|
if (err) {
|
||||||
state.shared.generate = res.genSharedKey
|
return callback(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Gather corpus to sign.
|
state.ephemeralKey.local = res.key
|
||||||
const selectionOut = Buffer.concat([
|
state.shared.generate = res.genSharedKey
|
||||||
state.proposalEncoded.out,
|
|
||||||
state.proposalEncoded.in,
|
|
||||||
state.ephemeralKey.local
|
|
||||||
])
|
|
||||||
|
|
||||||
state.exchange.out = {
|
// Gather corpus to sign.
|
||||||
epubkey: state.ephemeralKey.local,
|
const selectionOut = Buffer.concat([
|
||||||
signature: new Buffer(state.key.local.sign(selectionOut), 'binary')
|
state.proposalEncoded.out,
|
||||||
}
|
state.proposalEncoded.in,
|
||||||
|
state.ephemeralKey.local
|
||||||
|
])
|
||||||
|
|
||||||
return pbm.Exchange.encode(state.exchange.out)
|
state.key.local.sign(selectionOut, (err, sig) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.exchange.out = {
|
||||||
|
epubkey: state.ephemeralKey.local,
|
||||||
|
signature: sig
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, pbm.Exchange.encode(state.exchange.out))
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.identify = (state, msg) => {
|
exports.identify = (state, msg, callback) => {
|
||||||
log('1.1 identify')
|
log('1.1 identify')
|
||||||
|
|
||||||
state.proposalEncoded.in = msg
|
state.proposalEncoded.in = msg
|
||||||
@ -57,12 +68,19 @@ exports.identify = (state, msg) => {
|
|||||||
const pubkey = state.proposal.in.pubkey
|
const pubkey = state.proposal.in.pubkey
|
||||||
|
|
||||||
state.key.remote = crypto.unmarshalPublicKey(pubkey)
|
state.key.remote = crypto.unmarshalPublicKey(pubkey)
|
||||||
state.id.remote = PeerId.createFromPubKey(pubkey.toString('base64'))
|
PeerId.createFromPubKey(pubkey.toString('base64'), (err, remoteId) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
|
||||||
log('1.1 identify - %s - identified remote peer as %s', state.id.local.toB58String(), state.id.remote.toB58String())
|
state.id.remote = remoteId
|
||||||
|
|
||||||
|
log('1.1 identify - %s - identified remote peer as %s', state.id.local.toB58String(), state.id.remote.toB58String())
|
||||||
|
callback()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.selectProtocols = (state) => {
|
exports.selectProtocols = (state, callback) => {
|
||||||
log('1.2 selection')
|
log('1.2 selection')
|
||||||
|
|
||||||
const local = {
|
const local = {
|
||||||
@ -81,25 +99,30 @@ exports.selectProtocols = (state) => {
|
|||||||
nonce: state.proposal.in.rand
|
nonce: state.proposal.in.rand
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected = support.selectBest(local, remote)
|
support.selectBest(local, remote, (err, selected) => {
|
||||||
// we use the same params for both directions (must choose same curve)
|
if (err) {
|
||||||
// WARNING: if they dont SelectBest the same way, this won't work...
|
return callback(err)
|
||||||
state.protocols.remote = {
|
}
|
||||||
order: selected.order,
|
// we use the same params for both directions (must choose same curve)
|
||||||
curveT: selected.curveT,
|
// WARNING: if they dont SelectBest the same way, this won't work...
|
||||||
cipherT: selected.cipherT,
|
state.protocols.remote = {
|
||||||
hashT: selected.hashT
|
order: selected.order,
|
||||||
}
|
curveT: selected.curveT,
|
||||||
|
cipherT: selected.cipherT,
|
||||||
|
hashT: selected.hashT
|
||||||
|
}
|
||||||
|
|
||||||
state.protocols.local = {
|
state.protocols.local = {
|
||||||
order: selected.order,
|
order: selected.order,
|
||||||
curveT: selected.curveT,
|
curveT: selected.curveT,
|
||||||
cipherT: selected.cipherT,
|
cipherT: selected.cipherT,
|
||||||
hashT: selected.hashT
|
hashT: selected.hashT
|
||||||
}
|
}
|
||||||
|
callback()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.verify = (state, msg) => {
|
exports.verify = (state, msg, callback) => {
|
||||||
log('2.1. verify')
|
log('2.1. verify')
|
||||||
|
|
||||||
state.exchange.in = pbm.Exchange.decode(msg)
|
state.exchange.in = pbm.Exchange.decode(msg)
|
||||||
@ -111,43 +134,57 @@ exports.verify = (state, msg) => {
|
|||||||
state.ephemeralKey.remote
|
state.ephemeralKey.remote
|
||||||
])
|
])
|
||||||
|
|
||||||
const sigOk = state.key.remote.verify(selectionIn, state.exchange.in.signature)
|
state.key.remote.verify(selectionIn, state.exchange.in.signature, (err, sigOk) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
|
||||||
if (!sigOk) {
|
if (!sigOk) {
|
||||||
throw new Error('Bad signature')
|
return callback(new Error('Bad signature'))
|
||||||
}
|
}
|
||||||
|
|
||||||
log('2.1. verify - signature verified')
|
log('2.1. verify - signature verified')
|
||||||
|
callback()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.generateKeys = (state) => {
|
exports.generateKeys = (state, callback) => {
|
||||||
log('2.2. keys')
|
log('2.2. keys')
|
||||||
|
|
||||||
state.shared.secret = state.shared.generate(state.exchange.in.epubkey)
|
waterfall([
|
||||||
|
(cb) => state.shared.generate(state.exchange.in.epubkey, cb),
|
||||||
|
(secret, cb) => {
|
||||||
|
state.shared.secret = secret
|
||||||
|
|
||||||
const keys = crypto.keyStretcher(
|
crypto.keyStretcher(
|
||||||
state.protocols.local.cipherT,
|
state.protocols.local.cipherT,
|
||||||
state.protocols.local.hashT,
|
state.protocols.local.hashT,
|
||||||
state.shared.secret
|
state.shared.secret,
|
||||||
)
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
(keys, cb) => {
|
||||||
|
// use random nonces to decide order.
|
||||||
|
if (state.protocols.local.order > 0) {
|
||||||
|
state.protocols.local.keys = keys.k1
|
||||||
|
state.protocols.remote.keys = keys.k2
|
||||||
|
} else if (state.protocols.local.order < 0) {
|
||||||
|
// swap
|
||||||
|
state.protocols.local.keys = keys.k2
|
||||||
|
state.protocols.remote.keys = keys.k1
|
||||||
|
} else {
|
||||||
|
// we should've bailed before state. but if not, bail here.
|
||||||
|
return cb(new Error('you are trying to talk to yourself'))
|
||||||
|
}
|
||||||
|
|
||||||
// use random nonces to decide order.
|
log('2.3. mac + cipher')
|
||||||
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')
|
parallel([
|
||||||
|
(cb) => support.makeMacAndCipher(state.protocols.local, cb),
|
||||||
support.makeMacAndCipher(state.protocols.local)
|
(cb) => support.makeMacAndCipher(state.protocols.remote, cb)
|
||||||
support.makeMacAndCipher(state.protocols.remote)
|
], cb)
|
||||||
|
}
|
||||||
|
], callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.verifyNonce = (state, n2) => {
|
exports.verifyNonce = (state, n2) => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const debug = require('debug')
|
const debug = require('debug')
|
||||||
|
const waterfall = require('async/waterfall')
|
||||||
|
|
||||||
const support = require('../support')
|
const support = require('../support')
|
||||||
const crypto = require('./crypto')
|
const crypto = require('./crypto')
|
||||||
@ -14,21 +15,22 @@ module.exports = function exchange (state, cb) {
|
|||||||
log('2. exchange - start')
|
log('2. exchange - start')
|
||||||
|
|
||||||
log('2. exchange - writing exchange')
|
log('2. exchange - writing exchange')
|
||||||
support.write(state, crypto.createExchange(state))
|
waterfall([
|
||||||
support.read(state.shake, (err, msg) => {
|
(cb) => crypto.createExchange(state, cb),
|
||||||
|
(ex, cb) => {
|
||||||
|
support.write(state, ex)
|
||||||
|
support.read(state.shake, cb)
|
||||||
|
},
|
||||||
|
(msg, cb) => {
|
||||||
|
log('2. exchange - reading exchange')
|
||||||
|
crypto.verify(state, msg, cb)
|
||||||
|
},
|
||||||
|
(cb) => crypto.generateKeys(state, cb)
|
||||||
|
], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err)
|
return cb(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log('2. exchange - reading exchange')
|
|
||||||
|
|
||||||
try {
|
|
||||||
crypto.verify(state, msg)
|
|
||||||
crypto.generateKeys(state)
|
|
||||||
} catch (err) {
|
|
||||||
return cb(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log('2. exchange - finish')
|
log('2. exchange - finish')
|
||||||
cb()
|
cb()
|
||||||
})
|
})
|
||||||
|
@ -17,7 +17,11 @@ module.exports = function finish (state, cb) {
|
|||||||
|
|
||||||
const proto = state.protocols
|
const proto = state.protocols
|
||||||
const stream = state.shake.rest()
|
const stream = state.shake.rest()
|
||||||
const shake = handshake({timeout: state.timeout})
|
const shake = handshake({timeout: state.timeout}, (err) => {
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
pull(
|
pull(
|
||||||
stream,
|
stream,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const series = require('run-series')
|
const series = require('async/series')
|
||||||
|
|
||||||
const propose = require('./propose')
|
const propose = require('./propose')
|
||||||
const exchange = require('./exchange')
|
const exchange = require('./exchange')
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const debug = require('debug')
|
const debug = require('debug')
|
||||||
|
const waterfall = require('async/waterfall')
|
||||||
|
|
||||||
const support = require('../support')
|
const support = require('../support')
|
||||||
const crypto = require('./crypto')
|
const crypto = require('./crypto')
|
||||||
@ -15,22 +16,20 @@ module.exports = function propose (state, cb) {
|
|||||||
|
|
||||||
log('1. propose - writing proposal')
|
log('1. propose - writing proposal')
|
||||||
support.write(state, crypto.createProposal(state))
|
support.write(state, crypto.createProposal(state))
|
||||||
support.read(state.shake, (err, msg) => {
|
|
||||||
|
waterfall([
|
||||||
|
(cb) => support.read(state.shake, cb),
|
||||||
|
(msg, cb) => {
|
||||||
|
log('1. propose - reading proposal', msg)
|
||||||
|
crypto.identify(state, msg, cb)
|
||||||
|
},
|
||||||
|
(cb) => crypto.selectProtocols(state, cb)
|
||||||
|
], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err)
|
return cb(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log('1. propose - reading proposal', msg)
|
|
||||||
|
|
||||||
try {
|
|
||||||
crypto.identify(state, msg)
|
|
||||||
crypto.selectProtocols(state)
|
|
||||||
} catch (err) {
|
|
||||||
return cb(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log('1. propose - finish')
|
log('1. propose - finish')
|
||||||
|
|
||||||
cb()
|
cb()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
message Propose {
|
|
||||||
optional bytes rand = 1;
|
|
||||||
optional bytes pubkey = 2;
|
|
||||||
optional string exchanges = 3;
|
|
||||||
optional string ciphers = 4;
|
|
||||||
optional string hashes = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Exchange {
|
|
||||||
optional bytes epubkey = 1;
|
|
||||||
optional bytes signature = 2;
|
|
||||||
}
|
|
14
src/handshake/secio.proto.js
Normal file
14
src/handshake/secio.proto.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
module.exports = `message Propose {
|
||||||
|
optional bytes rand = 1;
|
||||||
|
optional bytes pubkey = 2;
|
||||||
|
optional string exchanges = 3;
|
||||||
|
optional string ciphers = 4;
|
||||||
|
optional string hashes = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Exchange {
|
||||||
|
optional bytes epubkey = 1;
|
||||||
|
optional bytes signature = 2;
|
||||||
|
}`
|
10
src/index.js
10
src/index.js
@ -8,7 +8,7 @@ const State = require('./state')
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
tag: '/secio/1.0.0',
|
tag: '/secio/1.0.0',
|
||||||
encrypt (local, key, insecure) {
|
encrypt (local, key, insecure, callback) {
|
||||||
if (!local) {
|
if (!local) {
|
||||||
throw new Error('no local id provided')
|
throw new Error('no local id provided')
|
||||||
}
|
}
|
||||||
@ -21,7 +21,13 @@ module.exports = {
|
|||||||
throw new Error('no insecure stream provided')
|
throw new Error('no insecure stream provided')
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = new State(local, key)
|
if (!callback) {
|
||||||
|
callback = (err) => {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = new State(local, key, callback)
|
||||||
|
|
||||||
pull(
|
pull(
|
||||||
insecure,
|
insecure,
|
||||||
|
@ -5,6 +5,11 @@ const deferred = require('pull-defer')
|
|||||||
|
|
||||||
class State {
|
class State {
|
||||||
constructor (id, key, timeout, cb) {
|
constructor (id, key, timeout, cb) {
|
||||||
|
if (typeof timeout === 'function') {
|
||||||
|
cb = timeout
|
||||||
|
timeout = undefined
|
||||||
|
}
|
||||||
|
|
||||||
this.setup()
|
this.setup()
|
||||||
this.id.local = id
|
this.id.local = id
|
||||||
this.key.local = key
|
this.key.local = key
|
||||||
|
108
src/support.js
108
src/support.js
@ -1,9 +1,10 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const mh = require('multihashing')
|
const mh = require('multihashing-async')
|
||||||
const forge = require('node-forge')
|
|
||||||
const lp = require('pull-length-prefixed')
|
const lp = require('pull-length-prefixed')
|
||||||
const pull = require('pull-stream')
|
const pull = require('pull-stream')
|
||||||
|
const crypto = require('libp2p-crypto')
|
||||||
|
const parallel = require('async/parallel')
|
||||||
|
|
||||||
exports.exchanges = [
|
exports.exchanges = [
|
||||||
'P-256',
|
'P-256',
|
||||||
@ -47,78 +48,73 @@ exports.theBest = (order, p1, p2) => {
|
|||||||
throw new Error('No algorithms in common!')
|
throw new Error('No algorithms in common!')
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.makeMacAndCipher = (target) => {
|
exports.makeMacAndCipher = (target, callback) => {
|
||||||
target.mac = makeMac(target.hashT, target.keys.macKey)
|
parallel([
|
||||||
target.cipher = makeCipher(target.cipherT, target.keys.iv, target.keys.cipherKey)
|
(cb) => makeMac(target.hashT, target.keys.macKey, cb),
|
||||||
|
(cb) => makeCipher(target.cipherT, target.keys.iv, target.keys.cipherKey, cb)
|
||||||
|
], (err, macAndCipher) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target.mac = macAndCipher[0]
|
||||||
|
target.cipher = macAndCipher[1]
|
||||||
|
callback()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashMap = {
|
function makeMac (hash, key, callback) {
|
||||||
SHA1: 'sha1',
|
crypto.hmac.create(hash, key, callback)
|
||||||
SHA256: 'sha256',
|
|
||||||
// workaround for https://github.com/digitalbazaar/forge/issues/401
|
|
||||||
SHA512: forge.md.sha512.create()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toForgeBuffer = exports.toForgeBuffer = (buf) => (
|
function makeCipher (cipherType, iv, key, callback) {
|
||||||
forge.util.createBuffer(buf.toString('binary'))
|
|
||||||
)
|
|
||||||
|
|
||||||
function makeMac (hashType, key) {
|
|
||||||
const hash = hashMap[hashType]
|
|
||||||
|
|
||||||
if (!hash) {
|
|
||||||
throw new Error(`unsupported hash type: ${hashType}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mac = forge.hmac.create()
|
|
||||||
mac.start(hash, toForgeBuffer(key))
|
|
||||||
return mac
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCipher (cipherType, iv, key) {
|
|
||||||
if (cipherType === 'AES-128' || cipherType === 'AES-256') {
|
if (cipherType === 'AES-128' || cipherType === 'AES-256') {
|
||||||
// aes in counter (CTR) mode because that is what
|
return crypto.aes.create(key, iv, callback)
|
||||||
// is used in go (cipher.NewCTR)
|
|
||||||
const cipher = forge.cipher.createCipher('AES-CTR', toForgeBuffer(key))
|
|
||||||
cipher.start({iv: toForgeBuffer(iv)})
|
|
||||||
return cipher
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Blowfish is not supported in node-forge, figure out if
|
// TODO: figure out if Blowfish is needed and if so find a library for it.
|
||||||
// it's needed and if so find a library for it.
|
callback(new Error(`unrecognized cipher type: ${cipherType}`))
|
||||||
|
|
||||||
throw new Error(`unrecognized cipher type: ${cipherType}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.randomBytes = (nonceSize) => {
|
exports.randomBytes = (nonceSize) => {
|
||||||
return new Buffer(forge.random.getBytesSync(nonceSize), 'binary')
|
return Buffer.from(crypto.webcrypto.getRandomValues(new Uint8Array(nonceSize)))
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.selectBest = (local, remote) => {
|
exports.selectBest = (local, remote, cb) => {
|
||||||
const oh1 = exports.digest(Buffer.concat([
|
exports.digest(Buffer.concat([
|
||||||
remote.pubKeyBytes,
|
remote.pubKeyBytes,
|
||||||
local.nonce
|
local.nonce
|
||||||
]))
|
]), (err, oh1) => {
|
||||||
const oh2 = exports.digest(Buffer.concat([
|
if (err) {
|
||||||
local.pubKeyBytes,
|
return cb(err)
|
||||||
remote.nonce
|
}
|
||||||
]))
|
|
||||||
const order = Buffer.compare(oh1, oh2)
|
|
||||||
|
|
||||||
if (order === 0) {
|
exports.digest(Buffer.concat([
|
||||||
throw new Error('you are trying to talk to yourself')
|
local.pubKeyBytes,
|
||||||
}
|
remote.nonce
|
||||||
|
]), (err, oh2) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
const order = Buffer.compare(oh1, oh2)
|
||||||
curveT: exports.theBest(order, local.exchanges, remote.exchanges),
|
|
||||||
cipherT: exports.theBest(order, local.ciphers, remote.ciphers),
|
if (order === 0) {
|
||||||
hashT: exports.theBest(order, local.hashes, remote.hashes),
|
cb(new Error('you are trying to talk to yourself'))
|
||||||
order
|
}
|
||||||
}
|
|
||||||
|
cb(null, {
|
||||||
|
curveT: exports.theBest(order, local.exchanges, remote.exchanges),
|
||||||
|
cipherT: exports.theBest(order, local.ciphers, remote.ciphers),
|
||||||
|
hashT: exports.theBest(order, local.hashes, remote.hashes),
|
||||||
|
order
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.digest = (buf) => {
|
exports.digest = (buf, cb) => {
|
||||||
return mh.digest(buf, 'sha2-256', buf.length)
|
mh.digest(buf, 'sha2-256', buf.length, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.write = function write (state, msg, cb) {
|
exports.write = function write (state, msg, cb) {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint max-nested-callbacks: ["error", 8] */
|
||||||
/* eslint-env mocha */
|
/* eslint-env mocha */
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
@ -5,8 +6,8 @@ const pair = require('pull-pair/duplex')
|
|||||||
const expect = require('chai').expect
|
const expect = require('chai').expect
|
||||||
const PeerId = require('peer-id')
|
const PeerId = require('peer-id')
|
||||||
const crypto = require('libp2p-crypto')
|
const crypto = require('libp2p-crypto')
|
||||||
const parallel = require('run-parallel')
|
const parallel = require('async/parallel')
|
||||||
const series = require('run-series')
|
const series = require('async/series')
|
||||||
const ms = require('multistream-select')
|
const ms = require('multistream-select')
|
||||||
const pull = require('pull-stream')
|
const pull = require('pull-stream')
|
||||||
const Listener = ms.Listener
|
const Listener = ms.Listener
|
||||||
@ -21,23 +22,31 @@ describe('libp2p-secio', () => {
|
|||||||
|
|
||||||
it('upgrades a connection', (done) => {
|
it('upgrades a connection', (done) => {
|
||||||
const p = pair()
|
const p = pair()
|
||||||
|
createSession(p[0], (err, local) => {
|
||||||
|
if (err) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
|
||||||
const local = createSession(p[0])
|
createSession(p[1], (err, remote) => {
|
||||||
const remote = createSession(p[1])
|
if (err) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
|
||||||
pull(
|
pull(
|
||||||
pull.values(['hello world']),
|
pull.values([new Buffer('hello world')]),
|
||||||
local
|
local
|
||||||
)
|
)
|
||||||
|
|
||||||
pull(
|
pull(
|
||||||
remote,
|
remote,
|
||||||
pull.collect((err, chunks) => {
|
pull.collect((err, chunks) => {
|
||||||
expect(err).to.not.exist
|
expect(err).to.not.exist
|
||||||
expect(chunks).to.be.eql([new Buffer('hello world')])
|
expect(chunks).to.be.eql([new Buffer('hello world')])
|
||||||
done()
|
done()
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('works over multistream', (done) => {
|
it('works over multistream', (done) => {
|
||||||
@ -45,8 +54,7 @@ describe('libp2p-secio', () => {
|
|||||||
|
|
||||||
const listener = new Listener()
|
const listener = new Listener()
|
||||||
const dialer = new Dialer()
|
const dialer = new Dialer()
|
||||||
let local
|
|
||||||
let remote
|
|
||||||
series([
|
series([
|
||||||
(cb) => parallel([
|
(cb) => parallel([
|
||||||
(cb) => listener.handle(p[0], cb),
|
(cb) => listener.handle(p[0], cb),
|
||||||
@ -54,35 +62,58 @@ describe('libp2p-secio', () => {
|
|||||||
], cb),
|
], cb),
|
||||||
(cb) => {
|
(cb) => {
|
||||||
listener.addHandler('/banana/1.0.0', (conn) => {
|
listener.addHandler('/banana/1.0.0', (conn) => {
|
||||||
local = createSession(conn)
|
createSession(conn, (err, local) => {
|
||||||
pull(
|
if (err) {
|
||||||
local,
|
return done(err)
|
||||||
pull.collect((err, chunks) => {
|
}
|
||||||
expect(err).to.not.exist
|
pull(
|
||||||
expect(chunks).to.be.eql([new Buffer('hello world')])
|
local,
|
||||||
done()
|
pull.collect((err, chunks) => {
|
||||||
})
|
expect(err).to.not.exist
|
||||||
)
|
expect(chunks).to.be.eql([new Buffer('hello world')])
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
cb()
|
cb()
|
||||||
},
|
},
|
||||||
(cb) => dialer.select('/banana/1.0.0', (err, conn) => {
|
(cb) => dialer.select('/banana/1.0.0', (err, conn) => {
|
||||||
remote = createSession(conn)
|
if (err) {
|
||||||
pull(
|
return cb(err)
|
||||||
pull.values(['hello world']),
|
}
|
||||||
remote
|
|
||||||
)
|
createSession(conn, (err, remote) => {
|
||||||
cb(err)
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
pull(
|
||||||
|
pull.values([new Buffer('hello world')]),
|
||||||
|
remote
|
||||||
|
)
|
||||||
|
cb()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
], (err) => {
|
], (err) => {
|
||||||
if (err) throw err
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function createSession (insecure) {
|
function createSession (insecure, cb) {
|
||||||
const key = crypto.generateKeyPair('RSA', 2048)
|
crypto.generateKeyPair('RSA', 2048, (err, key) => {
|
||||||
const id = PeerId.createFromPrivKey(key.bytes)
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
return secio.encrypt(id, key, insecure)
|
key.public.hash((err, digest) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, secio.encrypt(new PeerId(digest, key), key, insecure))
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user