├── .gitignore ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package.json └── tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sandbox.js 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # noise-network 2 | 3 | Authenticated network P2P backed by [Hyperswarm](https://github.com/hyperswarm) and [Noise](https://github.com/emilbayes/noise-peer) 4 | 5 | ## Usage 6 | 7 | First spin up a server 8 | 9 | ```js 10 | const noise = require('noise-network') 11 | 12 | const server = noise.createServer() 13 | 14 | server.on('connection', function (encryptedStream) { 15 | console.log('new encrypted stream!') 16 | 17 | // encryptedStream is a noise-peer stream instance 18 | encryptedStream.on('data', function (data) { 19 | console.log('client wrote:', data) 20 | }) 21 | }) 22 | 23 | const keyPair = noise.keygen() 24 | 25 | // Announce ourself to the HyperSwarm DHT on the following keyPair's publicKey 26 | server.listen(keyPair, function () { 27 | console.log('Server is listening on:', server.publicKey.toString('hex')) 28 | }) 29 | ``` 30 | 31 | Then connect to the server by connecting to the public key 32 | 33 | ```js 34 | // noise guarantees that we connect to the server in a E2E encrypted stream 35 | const client = noise.connect('{public key from above}') 36 | 37 | // client is a noise-peer stream instance 38 | client.write('hello server') 39 | ``` 40 | 41 | ## API 42 | 43 | #### `const server = noise.createServer([options])` 44 | 45 | Create a new Noise server. 46 | 47 | Options include: 48 | 49 | ```js 50 | { 51 | // validate the remote client's public key before allowing them to connect 52 | validate (remoteKey, done) { ... }, 53 | // you can add the onconnection handler here also 54 | onconnection (connection) { ... } 55 | } 56 | ``` 57 | 58 | #### `const client = noise.connect(serverPublicKey, [keyPair])` 59 | 60 | Connect to a server. Does UDP hole punching if necessary. 61 | `serverPublicKey` must be of type Buffer or hex. 62 | 63 | ## License 64 | 65 | MIT 66 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const noise = require('./') 2 | 3 | const server = noise.createServer() 4 | 5 | server.on('connection', function (connection) { 6 | console.log('someone joined wup wup') 7 | connection.on('data', console.log) 8 | }) 9 | 10 | server.listen(noise.keygen(), function () { 11 | const client = noise.connect(server.publicKey) 12 | client.write('hello world') 13 | }) 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const utp = require('utp-native') 2 | const net = require('net') 3 | const sodium = require('sodium-native') 4 | const noise = require('noise-peer') 5 | const Nanoresource = require('nanoresource') 6 | const Duplexify = require('duplexify') 7 | const discovery = require('@hyperswarm/discovery') 8 | const { EventEmitter } = require('events') 9 | 10 | exports.globalAgent = null 11 | exports.agent = () => new NoiseAgent() 12 | exports.keygen = noise.keygen 13 | exports.seedKeygen = noise.seedKeygen 14 | exports.createServer = (opts) => new NoiseServer(opts) 15 | exports.connect = connect 16 | 17 | function connect (publicKey) { 18 | publicKey = maybeConvertKey(publicKey) 19 | 20 | if (exports.globalAgent && !(exports.globalAgent.closed || exports.globalAgent.closing)) { 21 | return exports.globalAgent.connect(publicKey) 22 | } 23 | 24 | exports.globalAgent = new NoiseAgent() 25 | const stream = exports.globalAgent.connect(publicKey) 26 | exports.globalAgent.close(true) // allow more actives 27 | return stream 28 | } 29 | 30 | class NoiseServer extends EventEmitter { 31 | constructor (opts) { 32 | if (!opts) opts = {} 33 | if (typeof opts === 'function') opts = { onconnection: opts } 34 | 35 | super() 36 | 37 | this.connections = new Set() 38 | this.server = new ServerResource(this._onrawstream.bind(this)) 39 | this.keyPair = null 40 | this.discoveryKey = null 41 | this.topic = null 42 | this.validate = opts.validate || null 43 | 44 | if (opts.onconnection) this.on('connection', opts.onconnection) 45 | } 46 | 47 | _onrawstream (rawStream) { 48 | const self = this 49 | 50 | const encryptedStream = noise(rawStream, false, { 51 | pattern: 'XK', 52 | staticKeyPair: this.keyPair, 53 | onstatickey: function (remoteKey, done) { 54 | if (self.validate) return self.validate(remoteKey, done) 55 | done(null) 56 | } 57 | }) 58 | 59 | encryptedStream.on('handshake', function () { 60 | if (self.server.closed) return encryptedStream.destroy() 61 | encryptedStream.on('close', self.connections.delete.bind(self.connections, encryptedStream)) 62 | self.connections.add(encryptedStream) 63 | self.emit('connection', encryptedStream) 64 | }) 65 | 66 | encryptedStream.on('error', function (err) { 67 | self.emit('clientError', err) 68 | }) 69 | } 70 | 71 | get publicKey () { 72 | return this.keyPair && this.keyPair.publicKey 73 | } 74 | 75 | address () { 76 | return this.server.tcp && this.server.tcp.address() 77 | } 78 | 79 | listen (keyPairs, cb) { 80 | if (!cb) cb = noop 81 | 82 | var keyPair = maybeConvertKeyPair(keyPairs) 83 | 84 | const self = this 85 | 86 | this.server.open(function (err) { 87 | if (err) return cb(err) 88 | if (self.keyPair) return cb(new Error('Already listening')) 89 | 90 | const localPort = self.server.tcp.address().port 91 | 92 | self.discoveryKey = discoveryKey(keyPair.publicKey) 93 | self.keyPair = keyPair 94 | self.topic = self.server.discovery.announce(self.discoveryKey, { localPort, port: 0 }) 95 | self.topic.on('update', () => self.emit('announce')) 96 | self.emit('listening') 97 | 98 | cb(null) 99 | }) 100 | } 101 | 102 | close (cb) { 103 | if (!cb) cb = noop 104 | 105 | const self = this 106 | 107 | this.server.close(function (err) { 108 | if (err) return cb(err) 109 | self.topic = null 110 | self.discoveryKey = null 111 | self.keyPair = null 112 | self.emit('close') 113 | cb(null) 114 | }) 115 | } 116 | } 117 | 118 | class RawStream extends Duplexify { 119 | constructor (agent, publicKey, timeout) { 120 | super() 121 | 122 | this.agent = agent 123 | this.publicKey = publicKey 124 | 125 | const topic = agent.discovery.lookup(discoveryKey(publicKey)) 126 | 127 | this.topic = topic 128 | this.tried = new Set() 129 | this.connected = false 130 | 131 | topic.on('peer', this._onpeer.bind(this)) 132 | 133 | this._timeout = timeout 134 | ? setTimeout(this.destroy.bind(this, new Error('ETIMEDOUT')), timeout) 135 | : null 136 | 137 | this.on('close', this._onclose) 138 | } 139 | 140 | _onclose () { 141 | if (this._timeout) clearTimeout(this._timeout) 142 | this._timeout = null 143 | this.agent.inactive() 144 | } 145 | 146 | _onpeer (peer) { 147 | if (this.destroyed || this.connected) return 148 | 149 | const id = peer.host + ':' + peer.port 150 | 151 | if (this.tried.has(id)) return 152 | this.tried.add(id) 153 | 154 | this._connect(peer) 155 | } 156 | 157 | _connect (peer) { 158 | const self = this 159 | const tcp = net.connect({ 160 | port: peer.port, 161 | host: peer.host, 162 | allowHalfOpen: true 163 | }) 164 | 165 | tcp.on('error', tcp.destroy) 166 | tcp.on('connect', onconnect) 167 | 168 | if (!peer.referrer) return 169 | 170 | this.agent.discovery.holepunch(peer, function (err) { 171 | if (err || self.connected || self.destroyed) return 172 | 173 | const utp = self.agent.utp.connect(peer.port, peer.host, { allowHalfOpen: true }) 174 | 175 | utp.on('error', utp.destroy) 176 | utp.on('connect', onconnect) 177 | }) 178 | 179 | function onconnect () { 180 | if (self.destroyed || self.connected) return this.destroy() 181 | clearTimeout(self._timeout) 182 | self._timeout = null 183 | 184 | self.connected = true 185 | self.setReadable(this) 186 | self.setWritable(this) 187 | self.emit('connect') 188 | 189 | const destroy = self.destroy.bind(self) 190 | 191 | this.on('error', destroy) 192 | this.on('close', destroy) 193 | } 194 | } 195 | } 196 | 197 | class NoiseAgent extends Nanoresource { 198 | constructor () { 199 | super() 200 | 201 | this.utp = null 202 | this.discovery = null 203 | } 204 | 205 | connect (publicKey, keyPair) { 206 | this.open() 207 | if (!this.active()) throw new Error('Agent is closed') 208 | 209 | publicKey = maybeConvertKey(publicKey) 210 | const rawStream = new RawStream(this, publicKey) 211 | 212 | return noise(rawStream, true, { 213 | pattern: 'XK', 214 | staticKeyPair: keyPair || noise.keygen(), 215 | remoteStaticKey: publicKey 216 | }) 217 | } 218 | 219 | _open (cb) { 220 | this.utp = utp() 221 | this.discovery = discovery({ socket: this.utp }) 222 | cb(null) 223 | } 224 | 225 | _close (cb) { 226 | this.discovery.destroy() 227 | this.discovery.once('close', cb) 228 | } 229 | } 230 | 231 | class ServerResource extends Nanoresource { 232 | constructor (onconnection) { 233 | super() 234 | 235 | this.onconnection = onconnection 236 | this.discovery = null 237 | this.utp = null 238 | this.tcp = null 239 | } 240 | 241 | _open (cb) { 242 | const self = this 243 | 244 | this.utp = utp({ allowHalfOpen: true }) 245 | this.tcp = net.createServer({ allowHalfOpen: true }) 246 | 247 | listenBoth(this.tcp, this.utp, function (err) { 248 | if (err) return cb(err) 249 | 250 | self.discovery = discovery({ socket: self.utp }) 251 | self.utp.on('connection', self.onconnection) 252 | self.tcp.on('connection', self.onconnection) 253 | 254 | cb(null) 255 | }) 256 | } 257 | 258 | _close (cb) { 259 | const self = this 260 | 261 | this.discovery.destroy() 262 | this.discovery.once('close', function () { 263 | self.tcp.close() 264 | self.tcp = null 265 | self.utp = null 266 | self.discovery = null 267 | cb(null) 268 | }) 269 | } 270 | } 271 | 272 | function listenBoth (tcp, utp, cb) { 273 | tcp.on('listening', onlistening) 274 | utp.on('listening', done) 275 | utp.on('error', retry) 276 | 277 | tcp.listen(0) 278 | 279 | function retry (err) { 280 | if (err.code !== 'EADDRINUSE') { 281 | tcp.once('close', () => cb(err)) 282 | tcp.close() 283 | return 284 | } 285 | 286 | tcp.once('close', () => tcp.listen(0)) 287 | tcp.close() 288 | } 289 | 290 | function done () { 291 | utp.removeListener('done', done) 292 | tcp.removeListener('listening', onlistening) 293 | utp.removeListener('error', retry) 294 | cb() 295 | } 296 | 297 | function onlistening () { 298 | utp.listen(tcp.address().port) 299 | } 300 | } 301 | 302 | function noop () {} 303 | 304 | function discoveryKey (publicKey) { 305 | const buf = Buffer.alloc(32) 306 | const str = Buffer.from('noise-network') 307 | sodium.crypto_generichash(buf, str, publicKey) 308 | return buf 309 | } 310 | 311 | function maybeConvertKey (key) { 312 | return typeof key === 'string' ? Buffer.from(key, 'hex') : key 313 | } 314 | 315 | function maybeConvertKeyPair (keys) { 316 | return { 317 | publicKey: maybeConvertKey(keys.publicKey), 318 | secretKey: maybeConvertKey(keys.secretKey) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noise-network", 3 | "version": "1.1.2", 4 | "description": "Authenticated P2P network backed by HyperSwarm and NOISE", 5 | "main": "index.js", 6 | "dependencies": { 7 | "@hyperswarm/discovery": "^1.2.0", 8 | "duplexify": "^3.6.1", 9 | "nanoresource": "^1.2.0", 10 | "noise-peer": "^1.0.0", 11 | "sodium-native": "^2.2.3", 12 | "utp-native": "^2.1.3" 13 | }, 14 | "devDependencies": { 15 | "standard": "^12.0.1", 16 | "tape": "^4.9.1" 17 | }, 18 | "scripts": { 19 | "test": "standard && node tests.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/mafintosh/noise-network.git" 24 | }, 25 | "keywords": [ 26 | "noise", 27 | "noise-peer", 28 | "authenticated", 29 | "p2p", 30 | "hyperswarm" 31 | ], 32 | "author": "Mathias Buus (@mafintosh)", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/mafintosh/noise-network/issues" 36 | }, 37 | "homepage": "https://github.com/mafintosh/noise-network#readme" 38 | } 39 | -------------------------------------------------------------------------------- /tests.js: -------------------------------------------------------------------------------- 1 | var network = require('.') 2 | var test = require('tape') 3 | 4 | test('destroy', function (assert) { 5 | assert.plan(2) 6 | var server = network.createServer(function (s) { 7 | assert.pass('Connected') 8 | s.pipe(s) 9 | s.on('error', assert.error) 10 | }) 11 | 12 | var serverKeys = network.keygen() 13 | server.listen(serverKeys, function () { 14 | var client = network.connect(serverKeys.publicKey) 15 | 16 | client.on('handshake', () => { 17 | server.close() 18 | }) 19 | 20 | client.end(assert.pass) 21 | }) 22 | }) 23 | 24 | test('accepts keys as hex', { timeout: 1000 }, function (assert) { 25 | assert.plan(4) 26 | var server = network.createServer() 27 | var serverKeys = network.keygen() 28 | 29 | var client 30 | 31 | server.on('connection', function (encryptedStream) { 32 | assert.pass('Connected') 33 | encryptedStream.pipe(encryptedStream) 34 | encryptedStream.on('error', assert.error) 35 | 36 | encryptedStream.on('data', function (data) { 37 | assert.pass('received data') 38 | assert.same( 39 | keys.publicKey, 40 | serverKeys.publicKey.toString('hex'), 41 | 'reference is not touched' 42 | ) 43 | 44 | server.close() 45 | client.end(assert.pass) 46 | }) 47 | }) 48 | 49 | var keys = { 50 | publicKey: serverKeys.publicKey.toString('hex'), 51 | secretKey: serverKeys.secretKey.toString('hex') 52 | } 53 | 54 | server.listen(keys, function connectClient () { 55 | client = network.connect(keys.publicKey) 56 | client.write('hello') 57 | }) 58 | }) 59 | 60 | test('connect', { timeout: 1000 }, function (assert) { 61 | assert.plan(3) 62 | var server = network.createServer() 63 | var client 64 | 65 | server.on('connection', function (encryptedStream) { 66 | assert.pass('Connected') 67 | 68 | encryptedStream.pipe(encryptedStream) 69 | encryptedStream.on('error', assert.error) 70 | 71 | encryptedStream.on('data', function (data) { 72 | assert.pass('received data') 73 | 74 | server.close() 75 | client.end(assert.pass) 76 | }) 77 | }) 78 | 79 | var serverKeys = network.keygen() 80 | server.listen(serverKeys, connectClient) 81 | 82 | function connectClient () { 83 | client = network.connect(serverKeys.publicKey) 84 | client.write('hello') 85 | } 86 | }) 87 | --------------------------------------------------------------------------------