Compare commits

...

10 Commits

Author SHA1 Message Date
b960f29757 chore: release version v0.1.3 2019-10-30 17:10:37 +01:00
5206862504 chore: update contributors 2019-10-30 17:10:37 +01:00
749a8d035d fix: localAddr should be optional (#6)
The local address of a connection will not always be known, such as a browser client, so it should not be required.
2019-10-30 16:56:28 +01:00
763187beb1 chore: release version v0.1.2 2019-10-29 12:15:58 +01:00
44e08c9007 chore: update contributors 2019-10-29 12:15:57 +01:00
d2fe2d1b36 feat: crypto errors (#4)
* chore: ignore docs folder

* feat: add invalid crypto exchange error
2019-10-29 12:12:09 +01:00
bcb52ae709 docs: add crypto to readme (#5) 2019-10-29 11:24:28 +01:00
c52c3dc8a3 chore: release version v0.1.1 2019-10-21 14:45:40 +02:00
92f13f8106 chore: update contributors 2019-10-21 14:45:39 +02:00
5a5c44a770 feat: crypto interface (#2)
* docs: initial crypto readme

* feat: add basic crypto interface test suite

* feat: add optional remotepeer for inbound

feat: add errors export

* docs(fix): update src/crypto/README.md

Co-Authored-By: Vasco Santos <vasco.santos@moxy.studio>
2019-10-21 14:44:17 +02:00
13 changed files with 354 additions and 5 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
build build
dist dist
docs
# Dependency directory # Dependency directory
# 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

View File

@ -1,3 +1,33 @@
<a name="0.1.3"></a>
## [0.1.3](https://github.com/libp2p/js-interfaces/compare/v0.1.2...v0.1.3) (2019-10-30)
### Bug Fixes
* localAddr should be optional ([#6](https://github.com/libp2p/js-interfaces/issues/6)) ([749a8d0](https://github.com/libp2p/js-interfaces/commit/749a8d0))
<a name="0.1.2"></a>
## [0.1.2](https://github.com/libp2p/js-interfaces/compare/v0.1.1...v0.1.2) (2019-10-29)
### Features
* crypto errors ([#4](https://github.com/libp2p/js-interfaces/issues/4)) ([d2fe2d1](https://github.com/libp2p/js-interfaces/commit/d2fe2d1))
<a name="0.1.1"></a>
## [0.1.1](https://github.com/libp2p/js-interfaces/compare/v0.1.0...v0.1.1) (2019-10-21)
### Features
* crypto interface ([#2](https://github.com/libp2p/js-interfaces/issues/2)) ([5a5c44a](https://github.com/libp2p/js-interfaces/commit/5a5c44a))
<a name="0.1.0"></a> <a name="0.1.0"></a>
# 0.1.0 (2019-10-20) # 0.1.0 (2019-10-20)

View File

@ -15,6 +15,7 @@
- [Connection](./src/connection) - [Connection](./src/connection)
- [Content Routing](./src/content-routing) - [Content Routing](./src/content-routing)
- [Crypto](./src/crypto)
- [Peer Discovery](./src/peer-discovery) - [Peer Discovery](./src/peer-discovery)
- [Peer Routing](./src/peer-routing) - [Peer Routing](./src/peer-routing)
- [Stream Muxer](./src/stream-muxer) - [Stream Muxer](./src/stream-muxer)

View File

@ -1,6 +1,6 @@
{ {
"name": "libp2p-interfaces", "name": "libp2p-interfaces",
"version": "0.1.0", "version": "0.1.3",
"description": "Interfaces for JS Libp2p", "description": "Interfaces for JS Libp2p",
"main": "src/index.js", "main": "src/index.js",
"files": [ "files": [
@ -51,6 +51,7 @@
}, },
"devDependencies": { "devDependencies": {
"aegir": "^20.4.1", "aegir": "^20.4.1",
"it-handshake": "^1.0.0",
"it-pair": "^1.0.0", "it-pair": "^1.0.0",
"it-pipe": "^1.0.1", "it-pipe": "^1.0.1",
"peer-info": "^0.17.0" "peer-info": "^0.17.0"

View File

@ -109,7 +109,7 @@ const conn = new Connection({
Creates a new Connection instance. Creates a new Connection instance.
`localAddr` is the [multiaddr](https://github.com/multiformats/multiaddr) address used by the local peer to reach the remote. `localAddr` is the optional [multiaddr](https://github.com/multiformats/multiaddr) address used by the local peer to reach the remote.
`remoteAddr` is the [multiaddr](https://github.com/multiformats/multiaddr) address used to communicate with the remote peer. `remoteAddr` is the [multiaddr](https://github.com/multiformats/multiaddr) address used to communicate with the remote peer.
`localPeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the local peer. `localPeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the local peer.
`remotePeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the remote peer. `remotePeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the remote peer.

View File

@ -16,7 +16,7 @@ class Connection {
/** /**
* Creates an instance of Connection. * Creates an instance of Connection.
* @param {object} properties properties of the connection. * @param {object} properties properties of the connection.
* @param {multiaddr} properties.localAddr local multiaddr of the connection. * @param {multiaddr} [properties.localAddr] local multiaddr of the connection if known.
* @param {multiaddr} properties.remoteAddr remote multiaddr of the connection. * @param {multiaddr} properties.remoteAddr remote multiaddr of the connection.
* @param {PeerId} properties.localPeer local peer-id. * @param {PeerId} properties.localPeer local peer-id.
* @param {PeerId} properties.remotePeer remote peer-id. * @param {PeerId} properties.remotePeer remote peer-id.
@ -32,7 +32,7 @@ class Connection {
* @param {string} [properties.stat.encryption] connection encryption method identifier. * @param {string} [properties.stat.encryption] connection encryption method identifier.
*/ */
constructor ({ localAddr, remoteAddr, localPeer, remotePeer, newStream, close, getStreams, stat }) { constructor ({ localAddr, remoteAddr, localPeer, remotePeer, newStream, close, getStreams, stat }) {
assert(multiaddr.isMultiaddr(localAddr), 'localAddr must be an instance of multiaddr') localAddr && assert(multiaddr.isMultiaddr(localAddr), 'localAddr must be an instance of multiaddr')
assert(multiaddr.isMultiaddr(remoteAddr), 'remoteAddr must be an instance of multiaddr') assert(multiaddr.isMultiaddr(remoteAddr), 'remoteAddr must be an instance of multiaddr')
assert(PeerId.isPeerId(localPeer), 'localPeer must be an instance of peer-id') assert(PeerId.isPeerId(localPeer), 'localPeer must be an instance of peer-id')
assert(PeerId.isPeerId(remotePeer), 'remotePeer must be an instance of peer-id') assert(PeerId.isPeerId(remotePeer), 'remotePeer must be an instance of peer-id')

98
src/crypto/README.md Normal file
View File

@ -0,0 +1,98 @@
interface-crypto
==================
> A test suite you can use to implement a libp2p crypto module. A libp2p crypto module is used to ensure all exchanged data between two peers is encrypted.
**Modules that implement the interface**
- [js-libp2p-secio](https://github.com/libp2p/js-libp2p-secio)
## Table of Contents
- [interface-crypto](#interface-crypto)
- [Table of Contents](#table-of-contents)
- [Using the Test Suite](#using-the-test-suite)
- [API](#api)
- [Secure Inbound](#secure-inbound)
- [Secure Outbound](#secure-outbound)
- [Crypto Errors](#crypto-errors)
- [Error Types](#error-types)
## Using the Test Suite
You can also check out the [internal test suite](../../test/crypto/compliance.spec.js) to see the setup in action.
```js
const tests = require('libp2p-interfaces/src/crypto/tests')
const yourCrypto = require('./your-crypto')
tests({
setup () {
// Set up your crypto if needed, then return it
return yourCrypto
},
teardown () {
// Clean up your crypto if needed
}
})
```
## API
- `Crypto`
- `protocol<string>`: The protocol id of the crypto module.
- `secureInbound<function(PeerId, duplex)>`: Secures inbound connections.
- `secureOutbound<function(PeerId, duplex, PeerId)>`: Secures outbound connections.
### Secure Inbound
- `const { conn, remotePeer } = await crypto.secureInbound(localPeer, duplex, [remotePeer])`
Secures an inbound [streaming iterable duplex][iterable-duplex] connection. It returns an encrypted [streaming iterable duplex][iterable-duplex], as well as the [PeerId][peer-id] of the remote peer.
**Parameters**
- `localPeer` is the [PeerId][peer-id] of the receiving peer.
- `duplex` is the [streaming iterable duplex][iterable-duplex] that will be encryption.
- `remotePeer` is the optional [PeerId][peer-id] of the initiating peer, if known. This may only exist during transport upgrades.
**Return Value**
- `<object>`
- `conn<duplex>`: An encrypted [streaming iterable duplex][iterable-duplex].
- `remotePeer<PeerId>`: The [PeerId][peer-id] of the remote peer.
### Secure Outbound
- `const { conn, remotePeer } = await crypto.secureOutbound(localPeer, duplex, remotePeer)`
Secures an outbound [streaming iterable duplex][iterable-duplex] connection. It returns an encrypted [streaming iterable duplex][iterable-duplex], as well as the [PeerId][peer-id] of the remote peer.
**Parameters**
- `localPeer` is the [PeerId][peer-id] of the receiving peer.
- `duplex` is the [streaming iterable duplex][iterable-duplex] that will be encrypted.
- `remotePeer` is the [PeerId][peer-id] of the remote peer. If provided, implementations **should** use this to validate the integrity of the remote peer.
**Return Value**
- `<object>`
- `conn<duplex>`: An encrypted [streaming iterable duplex][iterable-duplex].
- `remotePeer<PeerId>`: The [PeerId][peer-id] of the remote peer. This **should** match the `remotePeer` parameter, and implementations should enforce this.
[peer-id]: https://github.com/libp2p/js-peer-id
[iterable-duplex]: https://gist.github.com/alanshaw/591dc7dd54e4f99338a347ef568d6ee9#duplex-it
## Crypto Errors
Common crypto errors come with the interface, and can be imported directly. All Errors take an optional message.
```js
const {
InvalidCryptoExchangeError,
UnexpectedPeerError
} = require('libp2p-interfaces/src/crypto/errors')
const error = new UnexpectedPeerError('a custom error message')
console.log(error.code === UnexpectedPeerError.code) // true
```
### Error Types
- `InvalidCryptoExchangeError` - Should be thrown when a peer provides data that is insufficient to finish the crypto exchange.
- `UnexpectedPeerError` - Should be thrown when the expected peer id does not match the peer id determined via the crypto exchange.

28
src/crypto/errors.js Normal file
View File

@ -0,0 +1,28 @@
'use strict'
class UnexpectedPeerError extends Error {
constructor (message = 'Unexpected Peer') {
super(message)
this.code = UnexpectedPeerError.code
}
static get code () {
return 'ERR_UNEXPECTED_PEER'
}
}
class InvalidCryptoExchangeError extends Error {
constructor (message = 'Invalid crypto exchange') {
super(message)
this.code = InvalidCryptoExchangeError.code
}
static get code () {
return 'ERR_INVALID_CRYPTO_EXCHANGE'
}
}
module.exports = {
UnexpectedPeerError,
InvalidCryptoExchangeError
}

102
src/crypto/tests/index.js Normal file
View File

@ -0,0 +1,102 @@
/* eslint-env mocha */
'use strict'
const duplexPair = require('it-pair/duplex')
const pipe = require('it-pipe')
const peers = require('../../utils/peers')
const { UnexpectedPeerError } = require('../errors')
const PeerId = require('peer-id')
const { collect } = require('streaming-iterables')
const chai = require('chai')
const expect = chai.expect
chai.use(require('dirty-chai'))
module.exports = (common) => {
describe('interface-crypto', () => {
let crypto
let localPeer
let remotePeer
let mitmPeer
before(async () => {
[
crypto,
localPeer,
remotePeer,
mitmPeer
] = await Promise.all([
common.setup(),
PeerId.createFromJSON(peers[0]),
PeerId.createFromJSON(peers[1]),
PeerId.createFromJSON(peers[2])
])
})
after(() => common.teardown && common.teardown())
it('has a protocol string', () => {
expect(crypto.protocol).to.exist()
expect(crypto.protocol).to.be.a('string')
})
it('it wraps the provided duplex connection', async () => {
const [localConn, remoteConn] = duplexPair()
const [
inboundResult,
outboundResult
] = await Promise.all([
crypto.secureInbound(remotePeer, localConn),
crypto.secureOutbound(localPeer, remoteConn, remotePeer)
])
// Echo server
pipe(inboundResult.conn, inboundResult.conn)
// Send some data and collect the result
const input = Buffer.from('data to encrypt')
const result = await pipe(
[input],
outboundResult.conn,
// Convert BufferList to Buffer via slice
(source) => (async function * toBuffer () {
for await (const chunk of source) {
yield chunk.slice()
}
})(),
collect
)
expect(result).to.eql([input])
})
it('should return the remote peer id', async () => {
const [localConn, remoteConn] = duplexPair()
const [
inboundResult,
outboundResult
] = await Promise.all([
crypto.secureInbound(remotePeer, localConn),
crypto.secureOutbound(localPeer, remoteConn, remotePeer)
])
// Inbound should return the initiator (local) peer
expect(inboundResult.remotePeer.id).to.eql(localPeer.id)
// Outbound should return the receiver (remote) peer
expect(outboundResult.remotePeer.id).to.eql(remotePeer.id)
})
it('inbound connections should verify peer integrity if known', async () => {
const [localConn, remoteConn] = duplexPair()
await Promise.all([
crypto.secureInbound(remotePeer, localConn, mitmPeer),
crypto.secureOutbound(localPeer, remoteConn, remotePeer)
]).then(expect.fail, (err) => {
expect(err).to.exist()
expect(err).to.have.property('code', UnexpectedPeerError.code)
})
})
})
}

View File

@ -3,7 +3,7 @@
const tests = require('../../src/connection/tests') const tests = require('../../src/connection/tests')
const { Connection } = require('../../src/connection') const { Connection } = require('../../src/connection')
const peers = require('../utils/peers') const peers = require('../../src/utils/peers')
const PeerId = require('peer-id') const PeerId = require('peer-id')
const multiaddr = require('multiaddr') const multiaddr = require('multiaddr')
const pair = require('it-pair') const pair = require('it-pair')

View File

@ -0,0 +1,13 @@
/* eslint-env mocha */
'use strict'
const tests = require('../../src/crypto/tests')
const mockCrypto = require('./mock-crypto')
describe('compliance tests', () => {
tests({
setup () {
return mockCrypto
}
})
})

View File

@ -0,0 +1,75 @@
'use strict'
const PeerId = require('peer-id')
const handshake = require('it-handshake')
const duplexPair = require('it-pair/duplex')
const pipe = require('it-pipe')
const { UnexpectedPeerError } = require('../../src/crypto/errors')
// A basic transform that does nothing to the data
const transform = () => {
return (source) => (async function * () {
for await (const chunk of source) {
yield chunk
}
})()
}
module.exports = {
protocol: 'insecure',
secureInbound: async (localPeer, duplex, expectedPeer) => {
// 1. Perform a basic handshake.
const shake = handshake(duplex)
shake.write(localPeer.id)
const remoteId = await shake.read()
const remotePeer = new PeerId(remoteId.slice())
shake.rest()
if (expectedPeer && expectedPeer.id !== remotePeer.id) {
throw new UnexpectedPeerError()
}
// 2. Create your encryption box/unbox wrapper
const wrapper = duplexPair()
const encrypt = transform() // Use transform iterables to modify data
const decrypt = transform()
pipe(
wrapper[0], // We write to wrapper
encrypt, // The data is encrypted
shake.stream, // It goes to the remote peer
decrypt, // Decrypt the incoming data
wrapper[0] // Pipe to the wrapper
)
return {
conn: wrapper[1],
remotePeer
}
},
secureOutbound: async (localPeer, duplex, remotePeer) => {
// 1. Perform a basic handshake.
const shake = handshake(duplex)
shake.write(localPeer.id)
const remoteId = await shake.read()
shake.rest()
// 2. Create your encryption box/unbox wrapper
const wrapper = duplexPair()
const encrypt = transform()
const decrypt = transform()
pipe(
wrapper[0], // We write to wrapper
encrypt, // The data is encrypted
shake.stream, // It goes to the remote peer
decrypt, // Decrypt the incoming data
wrapper[0] // Pipe to the wrapper
)
return {
conn: wrapper[1],
remotePeer: new PeerId(remoteId.slice())
}
}
}