mirror of
https://github.com/fluencelabs/js-libp2p-secio
synced 2025-05-16 16:11:18 +00:00
Merge pull request #2 from ipfs/baseline-test
Add base line test and fix issues with handshaking
This commit is contained in:
commit
77709c3320
@ -28,20 +28,21 @@
|
||||
"debug": "^2.2.0",
|
||||
"duplexify": "^3.4.3",
|
||||
"length-prefixed-stream": "^1.5.0",
|
||||
"libp2p-crypto": "^0.3.1",
|
||||
"libp2p-crypto": "^0.4.0",
|
||||
"multihashing": "^0.2.1",
|
||||
"node-forge": "^0.6.39",
|
||||
"peer-id": "^0.6.6",
|
||||
"protocol-buffers": "^3.1.6",
|
||||
"readable-stream": "1.1.13",
|
||||
"run-series": "^1.1.4"
|
||||
"run-series": "^1.1.4",
|
||||
"through2": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aegir": "^3.0.4",
|
||||
"bl": "^1.1.2",
|
||||
"chai": "^3.5.0",
|
||||
"pre-commit": "^1.1.3",
|
||||
"through2": "^2.0.1"
|
||||
"stream-pair": "^1.0.3"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint",
|
||||
|
38
src/etm.js
38
src/etm.js
@ -2,38 +2,36 @@
|
||||
|
||||
const through = require('through2')
|
||||
const lpm = require('length-prefixed-stream')
|
||||
const forge = require('node-forge')
|
||||
|
||||
const createBuffer = forge.util.createBuffer
|
||||
const toForgeBuffer = require('./support').toForgeBuffer
|
||||
|
||||
exports.writer = function etmWriter (insecure, cipher, mac) {
|
||||
const encode = lpm.encode()
|
||||
const pt = through(function (chunk, enc, cb) {
|
||||
cipher.update(createBuffer(chunk.toString('binary')))
|
||||
cipher.update(toForgeBuffer(chunk))
|
||||
|
||||
if (cipher.output.length() > 0) {
|
||||
const data = new Buffer(cipher.output.getBytes(), 'binary')
|
||||
mac.update(data)
|
||||
this.push(Buffer.concat([
|
||||
data,
|
||||
new Buffer(mac.digest(), 'binary')
|
||||
]))
|
||||
const macBuffer = new Buffer(mac.getMac().getBytes(), 'binary')
|
||||
|
||||
this.push(Buffer.concat([data, macBuffer]))
|
||||
// reset hmac
|
||||
mac.start()
|
||||
mac.start(null, null)
|
||||
}
|
||||
|
||||
cb()
|
||||
})
|
||||
|
||||
return insecure.pipe(pt).pipe(encode)
|
||||
pt.pipe(encode).pipe(insecure)
|
||||
|
||||
return pt
|
||||
}
|
||||
|
||||
exports.reader = function etmReader (insecure, decipher, mac) {
|
||||
const decode = lpm.decode()
|
||||
const pt = through(function (chunk, enc, cb) {
|
||||
const l = chunk.length
|
||||
|
||||
// TODO: check that this mac.getMac().length() is correct
|
||||
const macSize = mac.getMac().length()
|
||||
|
||||
if (l < macSize) {
|
||||
@ -44,17 +42,19 @@ exports.reader = function etmReader (insecure, decipher, mac) {
|
||||
const data = chunk.slice(0, mark)
|
||||
const macd = chunk.slice(mark)
|
||||
|
||||
mac.update(data)
|
||||
const expected = new Buffer(mac.digest(), 'binary')
|
||||
// reset hmac
|
||||
mac.start()
|
||||
// Clear out any previous data
|
||||
mac.start(null, null)
|
||||
|
||||
mac.update(data)
|
||||
const expected = new Buffer(mac.getMac().getBytes(), 'binary')
|
||||
// reset hmac
|
||||
mac.start(null, null)
|
||||
if (!macd.equals(expected)) {
|
||||
return cb(new Error(`MAC Invalid: ${macd} != ${expected}`))
|
||||
return cb(new Error(`MAC Invalid: ${macd.toString('hex')} != ${expected.toString('hex')}`))
|
||||
}
|
||||
|
||||
// all good, decrypt
|
||||
decipher.update(data)
|
||||
decipher.update(toForgeBuffer(data))
|
||||
|
||||
if (decipher.output.length() > 0) {
|
||||
const data = new Buffer(decipher.output.getBytes(), 'binary')
|
||||
@ -64,5 +64,7 @@ exports.reader = function etmReader (insecure, decipher, mac) {
|
||||
cb()
|
||||
})
|
||||
|
||||
return insecure.pipe(decode).pipe(pt)
|
||||
insecure.pipe(decode).pipe(pt)
|
||||
|
||||
return pt
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
const protobuf = require('protocol-buffers')
|
||||
|
||||
const log = debug('libp2p:secio:handshake')
|
||||
log.error = debug('libp2p:secio:handshake:error')
|
||||
const log = debug('libp2p:secio')
|
||||
log.error = debug('libp2p:secio:error')
|
||||
|
||||
const pbm = protobuf(fs.readFileSync(path.join(__dirname, '../secio.proto')))
|
||||
|
||||
@ -18,17 +18,18 @@ const support = require('../support')
|
||||
module.exports = function exchange (session, cb) {
|
||||
log('2. exchange - start')
|
||||
|
||||
let eResult
|
||||
let genSharedKey
|
||||
let exchangeOut
|
||||
|
||||
try {
|
||||
eResult = crypto.generateEphemeralKeyPair(session.local.curveT)
|
||||
const eResult = crypto.generateEphemeralKeyPair(session.local.curveT)
|
||||
session.local.ephemeralPubKey = eResult.key
|
||||
genSharedKey = eResult.genSharedKey
|
||||
exchangeOut = makeExchange(session)
|
||||
} catch (err) {
|
||||
return cb(err)
|
||||
}
|
||||
|
||||
session.local.ephemeralPubKey = eResult.key
|
||||
const genSharedKey = eResult.genSharedKey
|
||||
const exchangeOut = makeExchange(session)
|
||||
|
||||
session.insecureLp.write(exchangeOut)
|
||||
session.insecureLp.once('data', (chunk) => {
|
||||
const exchangeIn = pbm.Exchange.decode(chunk)
|
||||
@ -53,17 +54,16 @@ function makeExchange (session) {
|
||||
session.proposal.in,
|
||||
session.local.ephemeralPubKey
|
||||
])
|
||||
return pbm.Exchange({
|
||||
epubkey: session.local.ephemeralPubKey,
|
||||
signature: session.localKey.sign(selectionOut)
|
||||
})
|
||||
const epubkey = session.local.ephemeralPubKey
|
||||
const signature = session.localKey.sign(selectionOut)
|
||||
|
||||
return pbm.Exchange.encode({epubkey, signature})
|
||||
}
|
||||
|
||||
function verify (session, exchangeIn) {
|
||||
log('2.1. verify')
|
||||
|
||||
session.remote.ephemeralPubKey = exchangeIn.epubkey
|
||||
|
||||
const selectionIn = Buffer.concat([
|
||||
session.proposal.in,
|
||||
session.proposal.out,
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
const duplexify = require('duplexify')
|
||||
const debug = require('debug')
|
||||
const log = debug('libp2p:secio:handshake')
|
||||
log.error = debug('libp2p:secio:handshake:error')
|
||||
const log = debug('libp2p:secio')
|
||||
log.error = debug('libp2p:secio:error')
|
||||
|
||||
const etm = require('../etm')
|
||||
|
||||
@ -16,7 +16,7 @@ module.exports = function finish (session, cb) {
|
||||
const r = etm.reader(session.insecure, session.remote.cipher, session.remote.mac)
|
||||
session.secure = duplexify(w, r)
|
||||
|
||||
session.secure.write(session.proposal.in.rand)
|
||||
session.secure.write(session.proposal.randIn)
|
||||
|
||||
// read our nonce back
|
||||
session.secure.once('data', (nonceOut2) => {
|
||||
|
@ -3,8 +3,8 @@
|
||||
const debug = require('debug')
|
||||
const series = require('run-series')
|
||||
|
||||
const log = debug('libp2p:secio:handshake')
|
||||
log.error = debug('libp2p:secio:handshake:error')
|
||||
const log = debug('libp2p:secio')
|
||||
log.error = debug('libp2p:secio:error')
|
||||
|
||||
const propose = require('./propose')
|
||||
const exchange = require('./exchange')
|
||||
|
@ -9,8 +9,8 @@ const PeerId = require('peer-id')
|
||||
const mh = require('multihashing')
|
||||
const crypto = require('libp2p-crypto')
|
||||
|
||||
const log = debug('libp2p:secio:handshake')
|
||||
log.error = debug('libp2p:secio:handshake:error')
|
||||
const log = debug('libp2p:secio')
|
||||
log.error = debug('libp2p:secio:error')
|
||||
|
||||
// nonceSize is the size of our nonces (in bytes)
|
||||
const nonceSize = 16
|
||||
@ -23,17 +23,20 @@ const support = require('../support')
|
||||
module.exports = function propose (session, cb) {
|
||||
log('1. propose - start')
|
||||
|
||||
const nonceOut = forge.random.getBytesSync(nonceSize)
|
||||
const nonceOut = new Buffer(forge.random.getBytesSync(nonceSize), 'binary')
|
||||
const proposeOut = makeProposal(session, nonceOut)
|
||||
|
||||
session.proposal.out = proposeOut
|
||||
session.proposal.nonceOut = nonceOut
|
||||
|
||||
session.insecureLp.write(proposeOut)
|
||||
session.insecureLp.once('data', (chunk) => {
|
||||
const proposeIn = readProposal(chunk)
|
||||
session.proposal.in = proposeIn
|
||||
let proposeIn
|
||||
|
||||
try {
|
||||
proposeIn = readProposal(chunk)
|
||||
session.proposal.in = chunk
|
||||
session.proposal.randIn = proposeIn.rand
|
||||
identify(session, proposeIn)
|
||||
} catch (err) {
|
||||
return cb(err)
|
||||
@ -54,12 +57,12 @@ module.exports = function propose (session, cb) {
|
||||
// Generate and send Hello packet.
|
||||
// Hello = (rand, PublicKey, Supported)
|
||||
function makeProposal (session, nonceOut) {
|
||||
session.local.permanentPubKey = session.localKey.getPublic()
|
||||
session.local.permanentPubKey = session.localKey.public
|
||||
const myPubKeyBytes = session.local.permanentPubKey.bytes
|
||||
|
||||
return pbm.Propose({
|
||||
return pbm.Propose.encode({
|
||||
rand: nonceOut,
|
||||
pubKey: myPubKeyBytes,
|
||||
pubkey: myPubKeyBytes,
|
||||
exchanges: support.exchanges.join(','),
|
||||
ciphers: support.ciphers.join(','),
|
||||
hashes: support.hashes.join(',')
|
||||
@ -67,14 +70,14 @@ function makeProposal (session, nonceOut) {
|
||||
}
|
||||
|
||||
function readProposal (bytes) {
|
||||
return pbm.Proposal.decode(bytes)
|
||||
return pbm.Propose.decode(bytes)
|
||||
}
|
||||
|
||||
function identify (session, proposeIn) {
|
||||
log('1.1 identify')
|
||||
|
||||
session.remote.permanentPubKey = crypto.unmarshalPublicKey(proposeIn.pubKey)
|
||||
session.remotePeer = PeerId.createFromPubKey(session.remote.permanentPubKey)
|
||||
session.remote.permanentPubKey = crypto.unmarshalPublicKey(proposeIn.pubkey)
|
||||
session.remotePeer = PeerId.createFromPubKey(proposeIn.pubkey.toString('base64'))
|
||||
|
||||
log('1.1 identify - %s - identified remote peer as %s', session.localPeer.toB58String(), session.remotePeer.toB58String())
|
||||
}
|
||||
@ -91,7 +94,7 @@ function selection (session, nonceOut, proposeIn) {
|
||||
}
|
||||
|
||||
const remote = {
|
||||
pubKeyBytes: proposeIn.pubKey,
|
||||
pubKeyBytes: proposeIn.pubkey,
|
||||
exchanges: proposeIn.exchanges.split(','),
|
||||
hashes: proposeIn.hashes.split(','),
|
||||
ciphers: proposeIn.ciphers.split(','),
|
||||
@ -113,8 +116,14 @@ function selection (session, nonceOut, proposeIn) {
|
||||
}
|
||||
|
||||
function selectBest (local, remote) {
|
||||
const oh1 = digest(Buffer.concat(remote.pubKeyBytes, local.nonce))
|
||||
const oh2 = digest(Buffer.concat(local.pubKeyBytes, remote.nonce))
|
||||
const oh1 = digest(Buffer.concat([
|
||||
remote.pubKeyBytes,
|
||||
local.nonce
|
||||
]))
|
||||
const oh2 = digest(Buffer.concat([
|
||||
local.pubKeyBytes,
|
||||
remote.nonce
|
||||
]))
|
||||
const order = Buffer.compare(oh1, oh2)
|
||||
|
||||
if (order === 0) {
|
||||
|
10
src/index.js
10
src/index.js
@ -14,6 +14,7 @@ exports.SecureSession = class SecureSession {
|
||||
this.remote = {}
|
||||
this.proposal = {}
|
||||
this.insecure = insecure
|
||||
this.secure = null
|
||||
const e = lpstream.encode()
|
||||
const d = lpstream.decode()
|
||||
this.insecureLp = duplexify(e, d)
|
||||
@ -39,9 +40,16 @@ exports.SecureSession = class SecureSession {
|
||||
}
|
||||
}
|
||||
|
||||
secureStream (cb) {
|
||||
this.handshake((err) => {
|
||||
if (err) return cb(err)
|
||||
|
||||
cb(null, this.secure)
|
||||
})
|
||||
}
|
||||
|
||||
handshake (cb) {
|
||||
// TODO: figure out how to best handle the handshake timeout
|
||||
// TODO: better locking
|
||||
if (this._handshakeLock) {
|
||||
return cb(new Error('handshake already in progress'))
|
||||
}
|
||||
|
@ -10,8 +10,7 @@ exports.exchanges = [
|
||||
|
||||
exports.ciphers = [
|
||||
'AES-256',
|
||||
'AES-128',
|
||||
'Blowfish'
|
||||
'AES-128'
|
||||
]
|
||||
|
||||
exports.hashes = [
|
||||
@ -57,6 +56,10 @@ const hashMap = {
|
||||
SHA512: forge.md.sha512.create()
|
||||
}
|
||||
|
||||
const toForgeBuffer = exports.toForgeBuffer = (buf) => (
|
||||
forge.util.createBuffer(buf.toString('binary'))
|
||||
)
|
||||
|
||||
function makeMac (hashType, key) {
|
||||
const hash = hashMap[hashType]
|
||||
|
||||
@ -65,7 +68,7 @@ function makeMac (hashType, key) {
|
||||
}
|
||||
|
||||
const mac = forge.hmac.create()
|
||||
mac.start(hash, key)
|
||||
mac.start(hash, toForgeBuffer(key))
|
||||
return mac
|
||||
}
|
||||
|
||||
@ -73,8 +76,8 @@ function makeCipher (cipherType, iv, key) {
|
||||
if (cipherType === 'AES-128' || cipherType === 'AES-256') {
|
||||
// aes in counter (CTR) mode because that is what
|
||||
// is used in go (cipher.NewCTR)
|
||||
const cipher = forge.cipher.createCipher('AES-CTR', key)
|
||||
cipher.start({iv})
|
||||
const cipher = forge.cipher.createCipher('AES-CTR', toForgeBuffer(key))
|
||||
cipher.start({iv: toForgeBuffer(iv)})
|
||||
return cipher
|
||||
}
|
||||
|
||||
|
@ -5,22 +5,18 @@ const expect = require('chai').expect
|
||||
const through = require('through2')
|
||||
const bl = require('bl')
|
||||
const PeerId = require('peer-id')
|
||||
const duplexify = require('duplexify')
|
||||
const crypto = require('libp2p-crypto')
|
||||
const streamPair = require('stream-pair')
|
||||
|
||||
const secio = require('../src')
|
||||
const SecureSession = require('../src').SecureSession
|
||||
|
||||
describe('libp2p-secio', () => {
|
||||
it('exists', () => {
|
||||
expect(secio).to.exist
|
||||
})
|
||||
|
||||
describe('insecure length prefixed stream', () => {
|
||||
it('encodes', (done) => {
|
||||
const id = PeerId.create({bits: 64})
|
||||
const key = {}
|
||||
const pt = through()
|
||||
const insecure = duplexify(pt, pt)
|
||||
const s = new secio.SecureSession(id, key, insecure)
|
||||
const insecure = through()
|
||||
const s = new SecureSession(id, key, insecure)
|
||||
|
||||
// encoded on raw
|
||||
s.insecure.pipe(bl((err, res) => {
|
||||
@ -37,9 +33,8 @@ describe('libp2p-secio', () => {
|
||||
it('decodes', (done) => {
|
||||
const id = PeerId.create({bits: 64})
|
||||
const key = {}
|
||||
const pt = through()
|
||||
const insecure = duplexify(pt, pt)
|
||||
const s = new secio.SecureSession(id, key, insecure)
|
||||
const insecure = through()
|
||||
const s = new SecureSession(id, key, insecure)
|
||||
|
||||
// encoded on raw
|
||||
s.insecureLp.pipe(bl((err, res) => {
|
||||
@ -52,5 +47,64 @@ describe('libp2p-secio', () => {
|
||||
s.insecure.write('\u0005world')
|
||||
s.insecureLp.end()
|
||||
})
|
||||
|
||||
it('all together now', (done) => {
|
||||
const pair = streamPair.create()
|
||||
|
||||
createSession(pair, (err, local) => {
|
||||
if (err) throw err
|
||||
createSession(pair.other, (err, remote) => {
|
||||
if (err) throw err
|
||||
|
||||
remote.session.insecureLp.pipe(bl((err, res) => {
|
||||
if (err) throw err
|
||||
expect(res.toString()).to.be.eql('hello world')
|
||||
done()
|
||||
}))
|
||||
|
||||
local.session.insecureLp.write('hello ')
|
||||
local.session.insecureLp.write('world')
|
||||
pair.end()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('upgrades a connection', (done) => {
|
||||
const pair = streamPair.create()
|
||||
|
||||
createSession(pair, (err, local) => {
|
||||
if (err) throw err
|
||||
createSession(pair.other, (err, remote) => {
|
||||
if (err) throw err
|
||||
|
||||
local.session.secureStream((err, localSecure) => {
|
||||
if (err) throw err
|
||||
|
||||
localSecure.write('hello world')
|
||||
})
|
||||
|
||||
remote.session.secureStream((err, remoteSecure) => {
|
||||
if (err) throw err
|
||||
remoteSecure.once('data', (chunk) => {
|
||||
expect(chunk.toString()).to.be.eql('hello world')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function createSession (insecure, cb) {
|
||||
crypto.generateKeyPair('RSA', 2048, (err, key) => {
|
||||
if (err) return cb(err)
|
||||
const id = PeerId.createFromPrivKey(key.bytes)
|
||||
cb(null, {
|
||||
id,
|
||||
key,
|
||||
insecure,
|
||||
session: new SecureSession(id, key, insecure)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user