Skip to content

Commit c4b4431

Browse files
jacobheunvasco-santos
authored andcommitted
refactor: crypto and pnet (#469)
* feat: add initial plaintext 2 module * refactor: initial refactor of pnet * chore: fix lint * fix: update plaintext api usage * test: use plaintext for test crypto * chore: update deps test: update dialer suite scope * feat: add connection protection to the upgrader * refactor: cleanup and lint fix * chore: remove unncessary transforms * chore: temporarily disable bundlesize * chore: add missing dep * fix: use it-handshake to prevent overreading * chore(fix): PR feedback updates * chore: apply suggestions from code review Co-Authored-By: Vasco Santos <[email protected]>
1 parent 584413d commit c4b4431

File tree

19 files changed

+577
-309
lines changed

19 files changed

+577
-309
lines changed

.aegir.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
11
'use strict'
22

3-
const TransportManager = require('./src/transport-manager')
4-
const mockUpgrader = require('./test/utils/mockUpgrader')
3+
const Libp2p = require('./src')
54
const { MULTIADDRS_WEBSOCKETS } = require('./test/fixtures/browser')
6-
let tm
7-
5+
const Peers = require('./test/fixtures/peers')
6+
const PeerId = require('peer-id')
7+
const PeerInfo = require('peer-info')
88
const WebSockets = require('libp2p-websockets')
9+
const Muxer = require('libp2p-mplex')
10+
const Crypto = require('./src/insecure/plaintext')
11+
const pipe = require('it-pipe')
12+
let libp2p
913

1014
const before = async () => {
11-
tm = new TransportManager({
12-
upgrader: mockUpgrader,
13-
onConnection: () => {}
15+
// Use the last peer
16+
const peerId = await PeerId.createFromJSON(Peers[Peers.length - 1])
17+
const peerInfo = new PeerInfo(peerId)
18+
peerInfo.multiaddrs.add(MULTIADDRS_WEBSOCKETS[0])
19+
20+
libp2p = new Libp2p({
21+
peerInfo,
22+
modules: {
23+
transport: [WebSockets],
24+
streamMuxer: [Muxer],
25+
connEncryption: [Crypto]
26+
}
1427
})
15-
tm.add(WebSockets.prototype[Symbol.toStringTag], WebSockets)
16-
await tm.listen(MULTIADDRS_WEBSOCKETS)
28+
// Add the echo protocol
29+
libp2p.handle('/echo/1.0.0', ({ stream }) => pipe(stream, stream))
30+
31+
await libp2p.start()
1732
}
1833

1934
const after = async () => {
20-
await tm.close()
35+
await libp2p.stop()
2136
}
2237

2338
module.exports = {

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
include:
2121
- stage: check
2222
script:
23-
- npx aegir build --bundlesize
23+
# - npx aegir build --bundlesize
2424
- npx aegir dep-check -- -i wrtc -i electron-webrtc
2525
- npm run lint
2626

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@
4949
"err-code": "^1.1.2",
5050
"fsm-event": "^2.1.0",
5151
"hashlru": "^2.3.0",
52+
"it-handshake": "^1.0.1",
53+
"it-length-prefixed": "jacobheun/pull-length-prefixed#feat/fromReader",
5254
"it-pipe": "^1.0.1",
5355
"latency-monitor": "~0.2.1",
5456
"libp2p-crypto": "^0.16.2",
55-
"libp2p-interfaces": "^0.1.1",
57+
"libp2p-interfaces": "^0.1.3",
5658
"mafmt": "^7.0.0",
5759
"merge-options": "^1.0.1",
5860
"moving-average": "^1.0.0",
@@ -99,7 +101,7 @@
99101
"libp2p-secio": "^0.11.1",
100102
"libp2p-spdy": "^0.13.2",
101103
"libp2p-tcp": "^0.14.1",
102-
"libp2p-websockets": "^0.13.0",
104+
"libp2p-websockets": "^0.13.1",
103105
"lodash.times": "^4.3.2",
104106
"nock": "^10.0.6",
105107
"p-defer": "^3.0.0",

src/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class Libp2p extends EventEmitter {
9090
if (this._modules.connEncryption) {
9191
const cryptos = this._modules.connEncryption
9292
cryptos.forEach((crypto) => {
93-
this.upgrader.cryptos.set(crypto.tag, crypto)
93+
this.upgrader.cryptos.set(crypto.protocol, crypto)
9494
})
9595
}
9696

@@ -108,7 +108,7 @@ class Libp2p extends EventEmitter {
108108

109109
// Attach private network protector
110110
if (this._modules.connProtector) {
111-
this._switch.protector = this._modules.connProtector
111+
this.upgrader.protector = this._modules.connProtector
112112
} else if (process.env.LIBP2P_FORCE_PNET) {
113113
throw new Error('Private network is enforced, but no protector was provided')
114114
}
@@ -229,6 +229,7 @@ class Libp2p extends EventEmitter {
229229

230230
try {
231231
await this.transportManager.close()
232+
await this._switch.stop()
232233
} catch (err) {
233234
if (err) {
234235
log.error(err)

src/insecure/plaintext.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict'
2+
3+
const handshake = require('it-handshake')
4+
const lp = require('it-length-prefixed')
5+
const PeerId = require('peer-id')
6+
const debug = require('debug')
7+
const log = debug('libp2p:plaintext')
8+
log.error = debug('libp2p:plaintext:error')
9+
const { UnexpectedPeerError, InvalidCryptoExchangeError } = require('libp2p-interfaces/src/crypto/errors')
10+
11+
const { Exchange, KeyType } = require('./proto')
12+
const protocol = '/plaintext/2.0.0'
13+
14+
function lpEncodeExchange (exchange) {
15+
const pb = Exchange.encode(exchange)
16+
return lp.encode.single(pb)
17+
}
18+
19+
async function encrypt (localId, conn, remoteId) {
20+
const shake = handshake(conn)
21+
22+
// Encode the public key and write it to the remote peer
23+
shake.write(lpEncodeExchange({
24+
id: localId.toBytes(),
25+
pubkey: {
26+
Type: KeyType.RSA, // TODO: dont hard code
27+
Data: localId.marshalPubKey()
28+
}
29+
}))
30+
31+
log('write pubkey exchange to peer %j', remoteId)
32+
33+
// Get the Exchange message
34+
const response = (await lp.decodeFromReader(shake.reader).next()).value
35+
const id = Exchange.decode(response.slice())
36+
log('read pubkey exchange from peer %j', remoteId)
37+
38+
let peerId
39+
try {
40+
peerId = await PeerId.createFromPubKey(id.pubkey.Data)
41+
} catch (err) {
42+
log.error(err)
43+
throw new InvalidCryptoExchangeError('Remote did not provide its public key')
44+
}
45+
46+
if (remoteId && !peerId.isEqual(remoteId)) {
47+
throw new UnexpectedPeerError()
48+
}
49+
50+
log('plaintext key exchange completed successfully with peer %j', peerId)
51+
52+
shake.rest()
53+
return {
54+
conn: shake.stream,
55+
remotePeer: peerId
56+
}
57+
}
58+
59+
module.exports = {
60+
protocol,
61+
secureInbound: (localId, conn, remoteId) => {
62+
return encrypt(localId, conn, remoteId)
63+
},
64+
secureOutbound: (localId, conn, remoteId) => {
65+
return encrypt(localId, conn, remoteId)
66+
}
67+
}

src/insecure/proto.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict'
2+
3+
const protobuf = require('protons')
4+
5+
module.exports = protobuf(`
6+
message Exchange {
7+
optional bytes id = 1;
8+
optional PublicKey pubkey = 2;
9+
}
10+
11+
enum KeyType {
12+
RSA = 0;
13+
Ed25519 = 1;
14+
Secp256k1 = 2;
15+
ECDSA = 3;
16+
}
17+
18+
message PublicKey {
19+
required KeyType Type = 1;
20+
required bytes Data = 2;
21+
}
22+
`)

src/pnet/crypto.js

Lines changed: 21 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,46 @@
11
'use strict'
22

3-
const pull = require('pull-stream')
43
const debug = require('debug')
54
const Errors = require('./errors')
65
const xsalsa20 = require('xsalsa20')
76
const KEY_LENGTH = require('./key-generator').KEY_LENGTH
87

98
const log = debug('libp2p:pnet')
109
log.trace = debug('libp2p:pnet:trace')
11-
log.err = debug('libp2p:pnet:err')
10+
log.error = debug('libp2p:pnet:err')
1211

1312
/**
14-
* Creates a pull stream to encrypt messages in a private network
13+
* Creates a stream iterable to encrypt messages in a private network
1514
*
1615
* @param {Buffer} nonce The nonce to use in encryption
1716
* @param {Buffer} psk The private shared key to use in encryption
18-
* @returns {PullStream} a through stream
17+
* @returns {*} a through iterable
1918
*/
2019
module.exports.createBoxStream = (nonce, psk) => {
2120
const xor = xsalsa20(nonce, psk)
22-
return pull(
23-
ensureBuffer(),
24-
pull.map((chunk) => {
25-
return xor.update(chunk, chunk)
26-
})
27-
)
21+
return (source) => (async function * () {
22+
for await (const chunk of source) {
23+
yield Buffer.from(xor.update(chunk.slice()))
24+
}
25+
})()
2826
}
2927

3028
/**
31-
* Creates a pull stream to decrypt messages in a private network
29+
* Creates a stream iterable to decrypt messages in a private network
3230
*
33-
* @param {Object} remote Holds the nonce of the peer
31+
* @param {Buffer} nonce The nonce of the remote peer
3432
* @param {Buffer} psk The private shared key to use in decryption
35-
* @returns {PullStream} a through stream
33+
* @returns {*} a through iterable
3634
*/
37-
module.exports.createUnboxStream = (remote, psk) => {
38-
let xor
39-
return pull(
40-
ensureBuffer(),
41-
pull.map((chunk) => {
42-
if (!xor) {
43-
xor = xsalsa20(remote.nonce, psk)
44-
log.trace('Decryption enabled')
45-
}
35+
module.exports.createUnboxStream = (nonce, psk) => {
36+
return (source) => (async function * () {
37+
const xor = xsalsa20(nonce, psk)
38+
log.trace('Decryption enabled')
4639

47-
return xor.update(chunk, chunk)
48-
})
49-
)
40+
for await (const chunk of source) {
41+
yield Buffer.from(xor.update(chunk.slice()))
42+
}
43+
})()
5044
}
5145

5246
/**
@@ -61,7 +55,7 @@ module.exports.decodeV1PSK = (pskBuffer) => {
6155
// This should pull from multibase/multicodec to allow for
6256
// more encoding flexibility. Ideally we'd consume the codecs
6357
// from the buffer line by line to evaluate the next line
64-
// programatically instead of making assumptions about the
58+
// programmatically instead of making assumptions about the
6559
// encodings of each line.
6660
const metadata = pskBuffer.toString().split(/(?:\r\n|\r|\n)/g)
6761
const pskTag = metadata.shift()
@@ -78,21 +72,7 @@ module.exports.decodeV1PSK = (pskBuffer) => {
7872
psk: psk
7973
}
8074
} catch (err) {
75+
log.error(err)
8176
throw new Error(Errors.INVALID_PSK)
8277
}
8378
}
84-
85-
/**
86-
* Returns a through pull-stream that ensures the passed chunks
87-
* are buffers instead of strings
88-
* @returns {PullStream} a through stream
89-
*/
90-
function ensureBuffer () {
91-
return pull.map((chunk) => {
92-
if (typeof chunk === 'string') {
93-
return Buffer.from(chunk, 'utf-8')
94-
}
95-
96-
return chunk
97-
})
98-
}

src/pnet/index.js

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
'use strict'
22

3-
const pull = require('pull-stream')
4-
const { Connection } = require('libp2p-interfaces/src/connection')
3+
const pipe = require('it-pipe')
54
const assert = require('assert')
6-
5+
const duplexPair = require('it-pair/duplex')
6+
const crypto = require('libp2p-crypto')
77
const Errors = require('./errors')
8-
const State = require('./state')
9-
const decodeV1PSK = require('./crypto').decodeV1PSK
8+
const {
9+
createBoxStream,
10+
createUnboxStream,
11+
decodeV1PSK
12+
} = require('./crypto')
13+
const handshake = require('it-handshake')
14+
const { NONCE_LENGTH } = require('./key-generator')
1015
const debug = require('debug')
1116
const log = debug('libp2p:pnet')
1217
log.err = debug('libp2p:pnet:err')
@@ -27,41 +32,41 @@ class Protector {
2732
}
2833

2934
/**
30-
* Takes a given Connection and creates a privaste encryption stream
35+
* Takes a given Connection and creates a private encryption stream
3136
* between its two peers from the PSK the Protector instance was
3237
* created with.
3338
*
3439
* @param {Connection} connection The connection to protect
35-
* @param {function(Error)} callback
36-
* @returns {Connection} The protected connection
40+
* @returns {*} A protected duplex iterable
3741
*/
38-
protect (connection, callback) {
42+
async protect (connection) {
3943
assert(connection, Errors.NO_HANDSHAKE_CONNECTION)
4044

41-
const protectedConnection = new Connection(undefined, connection)
42-
const state = new State(this.psk)
43-
45+
// Exchange nonces
4446
log('protecting the connection')
47+
const localNonce = crypto.randomBytes(NONCE_LENGTH)
48+
49+
const shake = handshake(connection)
50+
shake.write(localNonce)
4551

46-
// Run the connection through an encryptor
47-
pull(
48-
connection,
49-
state.encrypt((err, encryptedOuterStream) => {
50-
if (err) {
51-
log.err('There was an error attempting to protect the connection', err)
52-
return callback(err)
53-
}
52+
const result = await shake.reader.next(NONCE_LENGTH)
53+
const remoteNonce = result.value.slice()
54+
shake.rest()
5455

55-
connection.getPeerInfo(() => {
56-
protectedConnection.setInnerConn(new Connection(encryptedOuterStream, connection))
57-
log('the connection has been successfully wrapped by the protector')
58-
callback()
59-
})
60-
}),
61-
connection
56+
// Create the boxing/unboxing pipe
57+
log('exchanged nonces')
58+
const [internal, external] = duplexPair()
59+
pipe(
60+
external,
61+
// Encrypt all outbound traffic
62+
createBoxStream(localNonce, this.psk),
63+
shake.stream,
64+
// Decrypt all inbound traffic
65+
createUnboxStream(remoteNonce, this.psk),
66+
external
6267
)
6368

64-
return protectedConnection
69+
return internal
6570
}
6671
}
6772

0 commit comments

Comments
 (0)