├── .gitignore ├── LICENSE ├── README.md ├── bin.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | sandbox.js 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 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 | # hyperbeam 2 | 3 | A 1-1 end-to-end encrypted internet pipe powered by [Hyperswarm](https://github.com/holepunchto/hyperswarm) and Noise 4 | 5 | ``` 6 | npm install hyperbeam 7 | ``` 8 | 9 | ## Usage 10 | 11 | ``` js 12 | const Hyperbeam = require('hyperbeam') 13 | 14 | // 'neznr3z3j44l7q7sgynbzpdrdlpausurbpcmqvwupmuoidolbopa' is 32-byte unique passphrase 15 | // to find the other side of your pipe. 16 | // once the other peer is discovered it is used to derive a noise keypair as well. 17 | const beam = new Hyperbeam('neznr3z3j44l7q7sgynbzpdrdlpausurbpcmqvwupmuoidolbopa') 18 | 19 | // to generate a passphrase, leave the constructor empty and hyperbeam will generate one for you 20 | // const beam = new Hyperbeam() 21 | // beam.key // <-- your passphrase 22 | 23 | // make a little chat app 24 | process.stdin.pipe(beam).pipe(process.stdout) 25 | ``` 26 | 27 | ## CLI 28 | 29 | Part of the [Hyperspace CLI, hyp](https://github.com/hypercore-protocol/cli) 30 | 31 | Provided here as a standalone CLI as well. 32 | 33 | First install it 34 | 35 | ```sh 36 | npm install -g hyperbeam 37 | ``` 38 | 39 | Then on one machine run 40 | 41 | ```sh 42 | echo 'hello world' | hyperbeam 43 | ``` 44 | 45 | This will generate a phrase, eg "neznr3z3j44l7q7sgynbzpdrdlpausurbpcmqvwupmuoidolbopa". Then on another machine run 46 | 47 | ```sh 48 | # will print "hello world" 49 | hyperbeam neznr3z3j44l7q7sgynbzpdrdlpausurbpcmqvwupmuoidolbopa 50 | ``` 51 | 52 | That's it! Happy piping. 53 | 54 | ## API 55 | 56 | #### `const stream = new Hyperbeam([key][, options])` 57 | 58 | Make a new Hyperbeam duplex stream. 59 | 60 | Will auto connect to another peer using the same key with an end to end encrypted tunnel. 61 | 62 | When the other peer writes it's emitted as `data` on this stream. 63 | 64 | Likewise when you write to this stream it's emitted as `data` on the other peers stream. 65 | 66 | If you do not pass a `key` into the constructor (the passphrase), one will be generated and put on `stream.key`. 67 | 68 | `options` include: 69 | 70 | - `dht`: A DHT instance. Defaults to a new instance. 71 | 72 | #### `stream.key` 73 | 74 | The passphrase used by the stream for connection. 75 | 76 | ## License 77 | 78 | MIT 79 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Hyperbeam = require('./') 4 | 5 | if (process.argv.includes('-h') || process.argv.includes('--help')) { 6 | console.error('Usage: hyperbeam [passphrase]') 7 | console.error('') 8 | console.error(' Creates a 1-1 end-to-end encrypted network pipe.') 9 | console.error(' If a passphrase is not supplied, will create a new phrase and begin listening.') 10 | process.exit(1) 11 | } 12 | 13 | let beam 14 | try { 15 | beam = new Hyperbeam(process.argv[2], process.argv.includes('-r')) 16 | } catch (e) { 17 | if (e.constructor.name === 'PassphraseError') { 18 | console.error(e.message) 19 | console.error('(If you are attempting to create a new pipe, do not provide a phrase and hyperbeam will generate one for you.)') 20 | process.exit(1) 21 | } else { 22 | throw e 23 | } 24 | } 25 | 26 | if (beam.announce) { 27 | console.error('[hyperbeam] Run hyperbeam ' + beam.key + ' to connect') 28 | console.error('[hyperbeam] To restart this side of the pipe with the same key add -r to the above') 29 | } else { 30 | console.error('[hyperbeam] Connecting pipe...') 31 | } 32 | 33 | beam.on('remote-address', function ({ host, port }) { 34 | if (!host) console.error('[hyperbeam] Could not detect remote address') 35 | else console.error('[hyperbeam] Joined the DHT - remote address is ' + host + ':' + port) 36 | }) 37 | 38 | beam.on('connected', function () { 39 | console.error('[hyperbeam] Success! Encrypted tunnel established to remote peer') 40 | }) 41 | 42 | beam.on('error', function (e) { 43 | console.error('[hyperbeam] Error:', e.message) 44 | closeASAP() 45 | }) 46 | 47 | beam.on('end', () => beam.end()) 48 | 49 | process.stdin.pipe(beam).pipe(process.stdout) 50 | if (typeof process.stdin.unref === 'function') process.stdin.unref() 51 | 52 | process.once('SIGINT', () => { 53 | if (!beam.connected) closeASAP() 54 | else beam.end() 55 | }) 56 | 57 | function closeASAP () { 58 | console.error('[hyperbeam] Shutting down beam...') 59 | 60 | const timeout = setTimeout(() => process.exit(1), 2000) 61 | beam.destroy() 62 | beam.on('close', function () { 63 | clearTimeout(timeout) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Duplex } = require('streamx') 2 | const sodium = require('sodium-universal') 3 | const b4a = require('b4a') 4 | const queueTick = require('queue-tick') 5 | const b32 = require('hi-base32') 6 | const DHT = require('hyperdht') 7 | 8 | module.exports = class Hyperbeam extends Duplex { 9 | constructor (key, options) { 10 | super() 11 | 12 | if (typeof key !== 'string') { 13 | options = key 14 | key = null 15 | } 16 | 17 | if (typeof options === 'boolean') { 18 | options = { announce: options } 19 | } else if (typeof options !== 'object') { 20 | options = {} 21 | } 22 | 23 | let announce = !!options.announce 24 | 25 | if (!key) { 26 | key = toBase32(randomBytes(32)) 27 | announce = true 28 | } 29 | 30 | this.key = key 31 | this.announce = announce 32 | this._node = options.dht || null 33 | this._server = null 34 | this._out = null 35 | this._inc = null 36 | this._now = Date.now() 37 | this._ondrain = null 38 | this._onopen = null 39 | this._onread = null 40 | } 41 | 42 | get connected () { 43 | return !!this._out 44 | } 45 | 46 | _ondrainDone (err) { 47 | if (this._ondrain) { 48 | const cb = this._ondrain 49 | this._ondrain = null 50 | cb(err) 51 | } 52 | } 53 | 54 | _onreadDone (err) { 55 | if (this._onread) { 56 | const cb = this._onread 57 | this._onread = null 58 | cb(err) 59 | } 60 | } 61 | 62 | _onopenDone (err) { 63 | if (this._onopen) { 64 | const cb = this._onopen 65 | this._onopen = null 66 | cb(err) 67 | } 68 | } 69 | 70 | async _open (cb) { 71 | const keyPair = DHT.keyPair(fromBase32(this.key)) 72 | 73 | this._onopen = cb 74 | 75 | if (!this._node) this._node = new DHT({ ephemeral: true }) 76 | 77 | const onConnection = s => { 78 | s.on('data', (data) => { 79 | if (!this._inc) { 80 | this._inc = s 81 | this._inc.on('error', (err) => this.destroy(err)) 82 | this._inc.on('end', () => this._push(null)) 83 | } 84 | 85 | if (s !== this._inc) return 86 | if (this._push(data) === false) s.pause() 87 | }) 88 | 89 | s.on('end', () => { 90 | if (this._inc) return 91 | this._push(null) 92 | }) 93 | 94 | if (!this._out) { 95 | this._out = s 96 | this._out.on('error', (err) => this.destroy(err)) 97 | this._out.on('drain', () => this._ondrain(null)) 98 | this.emit('connected') 99 | this._onopenDone(null) 100 | } 101 | } 102 | 103 | if (this.announce) { 104 | this._server = this._node.createServer({ 105 | firewall (remotePublicKey) { 106 | return !b4a.equals(remotePublicKey, keyPair.publicKey) 107 | } 108 | }) 109 | this._server.on('connection', onConnection) 110 | try { 111 | await this._server.listen(keyPair) 112 | } catch (err) { 113 | this._onopenDone(err) 114 | return 115 | } 116 | this.emit('remote-address', { host: this._node.host, port: this._node.port }) 117 | return 118 | } 119 | 120 | const connection = this._node.connect(keyPair.publicKey, { keyPair }) 121 | try { 122 | await new Promise((resolve, reject) => { 123 | connection.once('open', resolve) 124 | connection.once('close', reject) 125 | connection.once('error', reject) 126 | }) 127 | } catch (err) { 128 | this._onopenDone(err) 129 | return 130 | } 131 | this.emit('remote-address', { host: this._node.host, port: this._node.port }) 132 | onConnection(connection) 133 | } 134 | 135 | _read (cb) { 136 | this._onread = cb 137 | if (this._inc) this._inc.resume() 138 | } 139 | 140 | _push (data) { 141 | const res = this.push(data) 142 | queueTick(() => this._onreadDone(null)) 143 | return res 144 | } 145 | 146 | _write (data, cb) { 147 | if (this._out.write(data) !== false) return cb(null) 148 | this._ondrain = cb 149 | } 150 | 151 | _final (cb) { 152 | const done = () => { 153 | this._out.removeListener('finish', done) 154 | this._out.removeListener('error', done) 155 | cb(null) 156 | } 157 | 158 | this._out.end() 159 | this._out.on('finish', done) 160 | this._out.on('error', done) 161 | } 162 | 163 | _predestroy () { 164 | if (this._inc) this._inc.destroy() 165 | if (this._out) this._out.destroy() 166 | const err = new Error('Destroyed') 167 | this._onopenDone(err) 168 | this._onreadDone(err) 169 | this._ondrainDone(err) 170 | } 171 | 172 | async _destroy (cb) { 173 | if (!this._node) return cb(null) 174 | if (this._server) await this._server.close().catch(e => undefined) 175 | await this._node.destroy().catch(e => undefined) 176 | cb(null) 177 | } 178 | } 179 | 180 | function toBase32 (buf) { 181 | return b32.encode(buf).replace(/=/g, '').toLowerCase() 182 | } 183 | 184 | function fromBase32 (str) { 185 | return b4a.from(b32.decode.asBytes(str.toUpperCase())) 186 | } 187 | 188 | function randomBytes (length) { 189 | const buffer = b4a.alloc(length) 190 | sodium.randombytes_buf(buffer) 191 | return buffer 192 | } 193 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperbeam", 3 | "version": "3.1.0", 4 | "description": "A 1-1 end-to-end encrypted internet pipe powered by Hyperswarm", 5 | "main": "index.js", 6 | "bin": { 7 | "hyperbeam": "./bin.js" 8 | }, 9 | "scripts": { 10 | "test": "standard" 11 | }, 12 | "dependencies": { 13 | "b4a": "^1.6.7", 14 | "hi-base32": "^0.5.1", 15 | "hyperdht": "^6.20.5", 16 | "queue-tick": "^1.0.1", 17 | "sodium-universal": "^5.0.1", 18 | "streamx": "^2.22.0" 19 | }, 20 | "devDependencies": { 21 | "standard": "^17.1.2" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/mafintosh/hyperbeam.git" 26 | }, 27 | "author": "Mathias Buus (@mafintosh)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/mafintosh/hyperbeam/issues" 31 | }, 32 | "homepage": "https://github.com/mafintosh/hyperbeam" 33 | } 34 | --------------------------------------------------------------------------------