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>
This commit is contained in:
Jacob Heun 2019-10-21 14:44:17 +02:00 committed by GitHub
parent f7239faefc
commit 5a5c44a770
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 304 additions and 1 deletions

View File

@ -51,6 +51,7 @@
},
"devDependencies": {
"aegir": "^20.4.1",
"it-handshake": "^1.0.0",
"it-pair": "^1.0.0",
"it-pipe": "^1.0.1",
"peer-info": "^0.17.0"

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

@ -0,0 +1,96 @@
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 {
UnexpectedPeerError
} = require('libp2p-interfaces/src/crypto/errors')
const error = new UnexpectedPeerError('a custom error message')
console.log(error.code === UnexpectedPeerError.code) // true
```
### Error Types
- `UnexpectedPeerError` - Should be thrown when the expected peer id does not match the peer id determined via the crypto exchange

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

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

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 { Connection } = require('../../src/connection')
const peers = require('../utils/peers')
const peers = require('../../src/utils/peers')
const PeerId = require('peer-id')
const multiaddr = require('multiaddr')
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())
}
}
}