diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad6a69e --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +yarn.lock +package-lock.json +**/node_modules/ +**/*.log +test/repo-tests* + +# Logs +logs +*.log + +coverage +.nyc_output/ + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +build + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +lib +dist +docs \ No newline at end of file diff --git a/README.md b/README.md index f664931..2a63c64 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,79 @@ -# js-libp2p-pnet -A module providing swarm protection for libp2p +js-libp2p-pnet +================== + +Connection protection management for libp2p leveraging PSK encryption via XSalsa20. + +## Lead Maintainer + +[Jacob Heun](https://github.com/jacobheun) + +## Table of Contents + +- [Usage](#usage) + - [Private Shared Keys (PSK)](#private-shared-keys) + - [PSK Generation](#psk-generation) +- [Contribute](#contribute) +- [License](#license) + +## Usage + +```js +const Protector = require('libp2p-pnet') +const protector = new Protector(swarmKeyBuffer) +const privateConnection = protector.protect(myPublicConnection, (err) => { }) +``` + +### Private Shared Keys + +Private Shared Keys are expected to be in the following format: + +``` +/key/swarm/psk/1.0.0/ +/base16/ +dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d +``` + +### PSK Generation + +A utility method has been created to generate a key for your private network. You can +use one of the methods below to generate your key. + +#### From libp2p-pnet + +If you have libp2p-pnet locally, you can run the following from the projects root. + +```sh +node src/key-generator.js > swarm.key +``` + +#### From a module using libp2p + +If you have a module locally that depends on libp2p-pnet, you can run the following from +that project, assuming the node_modules are installed. + +```sh +node -e "require('libp2p-pnet').generate(process.stdout)" > swarm.key +``` + +#### Programmatically + +```js +const writeKey = require('libp2p-pnet').generate +const swarmKey = Buffer.alloc(95) +writeKey(swarmKey) +fs.writeFileSync('swarm.key', swarmKey) +``` + +## Contribute + +There are some ways you can make this module better: + +- Consult our [open issues](https://github.com/libp2p/js-libp2p-pnet/issues) and take on one of them + +This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md) + +## License + +[MIT](LICENSE) diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile new file mode 100644 index 0000000..a7da2e5 --- /dev/null +++ b/ci/Jenkinsfile @@ -0,0 +1,2 @@ +// Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. +javascript() diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..2673d06 --- /dev/null +++ b/circle.yml @@ -0,0 +1,21 @@ +# Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. +machine: + node: + version: stable + +test: + pre: + - npm run lint + post: + - npm run coverage -- --upload --providers coveralls + +dependencies: + pre: + - google-chrome --version + - curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb + - sudo dpkg -i google-chrome.deb || true + - sudo apt-get update + - sudo apt-get install -f + - sudo apt-get install --only-upgrade lsb-base + - sudo dpkg -i google-chrome.deb + - google-chrome --version diff --git a/package.json b/package.json new file mode 100644 index 0000000..5e69680 --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "libp2p-pnet", + "version": "1.0.0", + "description": "Private Network protection implementation", + "leadMaintainer": "Jacob Heun ", + "main": "src/index.js", + "scripts": { + "lint": "aegir lint", + "build": "aegir build", + "test": "aegir test", + "test:node": "aegir test -t node", + "test:browser": "aegir test -t browser", + "release": "aegir release -t node browser", + "release-minor": "aegir release --type minor -t node browser", + "release-major": "aegir release --type major -t node browser", + "coverage": "aegir coverage", + "coverage-publish": "aegir coverage publish", + "benchmark": "node benchmarks/send.js" + }, + "keywords": [ + "IPFS", + "libp2p", + "crypto", + "psk" + ], + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "interface-connection": "^0.3.2", + "pull-cat": "^1.1.11", + "pull-defer": "^0.2.2", + "pull-handshake": "^1.1.4", + "pull-reader": "^1.2.9", + "pull-stream": "^3.6.7", + "xsalsa20": "^1.0.2" + }, + "devDependencies": { + "aegir": "^13.1.0", + "async": "^2.6.1", + "chai": "^4.1.2", + "dirty-chai": "^2.0.1", + "peer-id": "^0.10.7", + "pre-commit": "^1.2.2", + "pull-pair": "^1.1.0" + }, + "pre-commit": [ + "lint", + "test" + ], + "engines": { + "node": ">=6.0.0", + "npm": ">=3.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/ipfs/js-libp2p-pnet.git" + }, + "bugs": { + "url": "https://github.com/ipfs/js-libp2p-pnet/issues" + }, + "homepage": "https://github.com/ipfs/js-libp2p-pnet", + "contributors": [ + "Jacob Heun " + ] +} diff --git a/src/crypto.js b/src/crypto.js new file mode 100644 index 0000000..9c61f26 --- /dev/null +++ b/src/crypto.js @@ -0,0 +1,98 @@ +'use strict' + +const pull = require('pull-stream') +const debug = require('debug') +const Errors = require('./errors') +const xsalsa20 = require('xsalsa20') +const KEY_LENGTH = require('./key-generator').KEY_LENGTH + +const log = debug('libp2p:pnet') +log.trace = debug('libp2p:pnet:trace') +log.err = debug('libp2p:pnet:err') + +/** + * Creates a pull stream to encrypt messages in a private network + * + * @param {Buffer} nonce The nonce to use in encryption + * @param {Buffer} psk The private shared key to use in encryption + * @returns {PullStream} a through stream + */ +module.exports.createBoxStream = (nonce, psk) => { + const xor = xsalsa20(nonce, psk) + return pull( + ensureBuffer(), + pull.map((chunk) => { + return xor.update(chunk, chunk) + }) + ) +} + +/** + * Creates a pull stream to decrypt messages in a private network + * + * @param {Object} remote Holds the nonce of the peer + * @param {Buffer} psk The private shared key to use in decryption + * @returns {PullStream} a through stream + */ +module.exports.createUnboxStream = (remote, psk) => { + let xor + return pull( + ensureBuffer(), + pull.map((chunk) => { + if (!xor) { + xor = xsalsa20(remote.nonce, psk) + log.trace('Decryption enabled') + } + + return xor.update(chunk, chunk) + }) + ) +} + +/** + * Decode the version 1 psk from the given Buffer + * + * @param {Buffer} pskBuffer + * @throws {INVALID_PSK} + * @returns {Object} The PSK metadata (tag, codecName, psk) + */ +module.exports.decodeV1PSK = (pskBuffer) => { + try { + // This should pull from multibase/multicodec to allow for + // more encoding flexibility. Ideally we'd consume the codecs + // from the buffer line by line to evaluate the next line + // programatically instead of making assumptions about the + // encodings of each line. + const metadata = pskBuffer.toString().split(/(?:\r\n|\r|\n)/g) + const pskTag = metadata.shift() + const codec = metadata.shift() + const psk = Buffer.from(metadata.shift(), 'hex') + + if (psk.byteLength !== KEY_LENGTH) { + throw new Error(Errors.INVALID_PSK) + } + + return { + tag: pskTag, + codecName: codec, + psk: psk + } + } catch (err) { + throw new Error(Errors.INVALID_PSK) + } +} + +/** + * Returns a through pull-stream that ensures the passed chunks + * are buffers instead of strings + * @returns {PullStream} a through stream + */ +function ensureBuffer () { + return pull.map((chunk) => { + if (typeof chunk === 'string') { + return Buffer.from(chunk, 'utf-8') + } + + return chunk + }) +} diff --git a/src/errors.js b/src/errors.js new file mode 100644 index 0000000..57c4422 --- /dev/null +++ b/src/errors.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports.INVALID_PEER = 'Not a valid peer connection' +module.exports.INVALID_PSK = 'Your private shared key is invalid' +module.exports.NO_LOCAL_ID = 'No local private key provided' +module.exports.NO_HANDSHAKE_CONNECTION = 'No connection for the handshake provided' +module.exports.STREAM_ENDED = 'Stream ended prematurely' diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..a6480dc --- /dev/null +++ b/src/index.js @@ -0,0 +1,70 @@ +'use strict' + +const pull = require('pull-stream') +const Connection = require('interface-connection').Connection +const assert = require('assert') + +const Errors = require('./errors') +const State = require('./state') +const decodeV1PSK = require('./crypto').decodeV1PSK +const debug = require('debug') +const log = debug('libp2p:pnet') +log.err = debug('libp2p:pnet:err') + +/** + * Takes a Private Shared Key (psk) and provides a `protect` method + * for wrapping existing connections in a private encryption stream + */ +class Protector { + /** + * @param {Buffer} keyBuffer The private shared key buffer + * @constructor + */ + constructor (keyBuffer) { + const decodedPSK = decodeV1PSK(keyBuffer) + this.psk = decodedPSK.psk + this.tag = decodedPSK.tag + } + + /** + * Takes a given Connection and creates a privaste encryption stream + * between its two peers from the PSK the Protector instance was + * created with. + * + * @param {Connection} connection The connection to protect + * @param {function(Error)} callback + * @returns {Connection} The protected connection + */ + protect (connection, callback) { + assert(connection, Errors.NO_HANDSHAKE_CONNECTION) + + const protectedConnection = new Connection(undefined, connection) + const state = new State(this.psk) + + log('protecting the connection') + + // Run the connection through an encryptor + pull( + connection, + state.encrypt((err, encryptedOuterStream) => { + if (err) { + log.err('There was an error attempting to protect the connection', err) + return callback(err) + } + + connection.getPeerInfo(() => { + protectedConnection.setInnerConn(new Connection(encryptedOuterStream, connection)) + log('the connection has been successfully wrapped by the protector') + callback() + }) + }), + connection + ) + + return protectedConnection + } +} + +module.exports = Protector +module.exports.errors = Errors +module.exports.generate = require('./key-generator') diff --git a/src/key-generator.js b/src/key-generator.js new file mode 100644 index 0000000..d50cc64 --- /dev/null +++ b/src/key-generator.js @@ -0,0 +1,22 @@ +'use strict' + +const crypto = require('crypto') +const KEY_LENGTH = 32 + +/** + * Generates a PSK that can be used in a libp2p-pnet private network + * @param {Writer} writer An object containing a `write` method + * @returns {void} + */ +function generate (writer) { + const psk = crypto.randomBytes(KEY_LENGTH).toString('hex') + writer.write('/key/swarm/psk/1.0.0/\n/base16/\n' + psk) +} + +module.exports = generate +module.exports.NONCE_LENGTH = 24 +module.exports.KEY_LENGTH = KEY_LENGTH + +if (require.main === module) { + generate(process.stdout) +} diff --git a/src/state.js b/src/state.js new file mode 100644 index 0000000..b084e26 --- /dev/null +++ b/src/state.js @@ -0,0 +1,110 @@ +'use strict' + +const crypto = require('crypto') +const debug = require('debug') +const pair = require('pull-pair') +const Reader = require('pull-reader') +const cat = require('pull-cat') +const pull = require('pull-stream') +const deferred = require('pull-defer') + +const cryptoStreams = require('./crypto') +const NONCE_LENGTH = require('./key-generator').NONCE_LENGTH + +const log = debug('libp2p:pnet') +log.err = debug('libp2p:pnet:err') +log.trace = debug('libp2p:pnet:trace') + +/** + * Keeps track of the state of a given connection, such as the local psk + * and local and remote nonces for encryption/decryption + */ +class State { + /** + * @param {Buffer} psk The key buffer used for encryption + * @constructor + */ + constructor (psk) { + this.local = { + nonce: Buffer.from( + crypto.randomBytes(NONCE_LENGTH) + ), + psk: psk + } + this.remote = { nonce: null } + + this.rawReader = Reader(60e3) + this.encryptedReader = Reader(60e3) + + this.rawPairStream = pair() + this.encryptedPairStream = pair() + + // The raw, pair stream + this.innerRawStream = null + this.outerRawStream = { + sink: this.rawReader, + source: cat([ + pull.values([ + this.local.nonce + ]), + this.rawPairStream.source + ]) + } + + // The encrypted, pair stream + this.innerEncryptedStream = { + sink: this.encryptedReader, + source: this.encryptedPairStream.source + } + this.outerEncryptedStream = null + } + + /** + * Creates encryption streams for the given state + * + * @param {function(Error, Connection)} callback + * @returns {void} + */ + encrypt (callback) { + // The outer stream needs to be returned before we setup the + // rest of the streams, so we're delaying the execution + setTimeout(() => { + // Read the nonce first, once we have it resolve the + // deferred source, so we keep reading + const deferredSource = deferred.source() + this.rawReader.read(NONCE_LENGTH, (err, data) => { + if (err) { + log.err('There was an error attempting to read the nonce', err) + } + log.trace('remote nonce received') + this.remote.nonce = data + deferredSource.resolve(this.rawReader.read()) + }) + + this.innerRawStream = { + sink: this.rawPairStream.sink, + source: deferredSource + } + + // Create the pull exchange between the two inner streams + pull( + this.innerRawStream, + cryptoStreams.createUnboxStream(this.remote, this.local.psk), + this.innerEncryptedStream, + cryptoStreams.createBoxStream(this.local.nonce, this.local.psk), + this.innerRawStream + ) + + this.outerEncryptedStream = { + sink: this.encryptedPairStream.sink, + source: this.encryptedReader.read() + } + + callback(null, this.outerEncryptedStream) + }, 0) + + return this.outerRawStream + } +} + +module.exports = State diff --git a/test/fixtures/peer-a.json b/test/fixtures/peer-a.json new file mode 100644 index 0000000..107c894 --- /dev/null +++ b/test/fixtures/peer-a.json @@ -0,0 +1,5 @@ +{ + "id": "QmeS1ou3mrjCFGoFtRx3MwrGDzqKD6xbuYJU1CKtMrtFFu", + "privKey": "CAASqAkwggSkAgEAAoIBAQChwzYwCNIyUkzEK3sILqq9ChAKZ9eU+ribY+B/xwAwDKPfvuqHq0hjauJBMcjiQyKAWz9xEBR3WupOM7h9M8oU+/e0xJUTt/CDOrtJ0PCgUXeBLkqsApbBoXW3yomHEDHxYttKzrtoTimiP1bhrxurcpVNC4CUYD+q8gw3sRZlsrqpeYYAfU04kS0BM75W/sUT90znnHvOxFXrEdMMdenEFhZOsDyEK9ENzwhkKgOGb18MBY4kN5DoW4bVd4ItfZnNwdkQtpP/X99tMWJxO4yqpngbywZGnkfirLeuRwt/xRGFVbLOigjBpTVpcbBqe1t2Flhuf/bfWYX4FbyElA5FAgMBAAECggEAJnDTcbrG6LpyD7QdeqZMYLwBb9eZfYfPUu37LaJGwyRd1Q/zf+YOP8HonoGMMWiuzD3i56Vgl7R9NbRIxUgHX9E43jZRDuyJNUZBt5r1c8OoWIR9rj63QLBz3wc8g2Iv3CMX5cEW/ASHFE1lAiCwvJ9wJ2zyU1BEEQWQLbPhlKzw7SLhr4fee45/7pnrKZMllt5vwC9pM6lrpIkICO5gUu0OWu5wfzzlTvfmCgfTb11VqKESEPbDBMUtpJibRqegE4xvipLklJ8VV8jz7NFs9bhgCpNM74Ngt5vGHcddeqtj//86UsClEw5YgWAdRe29ZjMApWvKIkginLjZEO8eiQKBgQDoDWii0rmlgBl1/8fENUSWxYvknGmWO7eWjVqMjDvA+waWUVDpTE+eHT1QAaPofM+nFz5PG+SpB55o4rXdxDesq+DqnaRAI9WtSHdgRtjgETyqoBAiahQ0zGWmSEYHGDB+xGctTMr8GxdhZxqZjjfyptp6oXXqZkmxgcogrx+WTwKBgQCydNDmCDpeH0kSvhAPxaNx5c9WkFEFSA0OCZOx57Y+Mt0MVamRILFrUrcMz095w8BQZkjlHjSHfsRgKa/b2eOd+3BhoMLZVtxRqBdpdqq1KTAcRRG4yA2KA39rttpVzaTV5SPfdDf3tsVlBtV784W63gVpN9gNfajyyrpeffiBKwKBgDnDrLprbl8uZigjhdznza0ie9JqxTXqo6bMhS/bcLx3QIqGr3eD0YXwjWSvI9gpyZ80gAQ9U0xoYxyE4vTTdXB8UL7Wgx6cTQKXuW+z8yTD5bArrBiFA4apItyjvRrjAJ9t0KlMJnNfYxCSE+MJrg+vTU+dhbbVw552SpScQ2atAoGBAKMu3rb6XyUiRpe05MsHVuYX1vi5Dt1dfVKQv1W3JJbLvAZDbsMeuh4BjRFRoMMflQPwBEg+zpn3+WpVtFG9dL5J5gHgF0zWeLDSnFX8BS2TdELlhccKaBcEC8hbdFtxqIFO/vaeN2902hv/m8e0b1zpGNmWDyKG/a7GYpV1a3/xAoGBAJtgGANDVk6qqcWGEVk56FH1ZksvgF3SPXWaXpzbZ5KLCcV5ooRyhowylKUZBBPowMeZ46tem2xwJbraB5kDg6WiSjBsXcbN95ivb8AuoRa6gDqAszjokQUSdpY7FTgMaL046AuihrKsQSly1jrQqbQu8JBgmnnBzus3s77inL/j", + "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChwzYwCNIyUkzEK3sILqq9ChAKZ9eU+ribY+B/xwAwDKPfvuqHq0hjauJBMcjiQyKAWz9xEBR3WupOM7h9M8oU+/e0xJUTt/CDOrtJ0PCgUXeBLkqsApbBoXW3yomHEDHxYttKzrtoTimiP1bhrxurcpVNC4CUYD+q8gw3sRZlsrqpeYYAfU04kS0BM75W/sUT90znnHvOxFXrEdMMdenEFhZOsDyEK9ENzwhkKgOGb18MBY4kN5DoW4bVd4ItfZnNwdkQtpP/X99tMWJxO4yqpngbywZGnkfirLeuRwt/xRGFVbLOigjBpTVpcbBqe1t2Flhuf/bfWYX4FbyElA5FAgMBAAE=" +} diff --git a/test/fixtures/peer-b.json b/test/fixtures/peer-b.json new file mode 100644 index 0000000..4c52c6e --- /dev/null +++ b/test/fixtures/peer-b.json @@ -0,0 +1,5 @@ +{ + "id": "QmYWHGZ9y1Bzx59bBzn85JsJxwmpBy5bpXDWDfwMfsHsxz", + "privKey": "CAASqQkwggSlAgEAAoIBAQDLVaPqWFA8WgK6ixuPvhTHeQfBblmEFLEmraLlIDSWbMUPva6aJ1V/hi2I5QLXNeeiig5sco+nF+RKhGnzQ9NpgHRVZ7Ze+LWq3Q4YxONdzFeNUjTvJrDSKgkubA5EKC/LI6pU33WZbjyKkomGo+Gzuqvlj4Rx1dLVXRIOjxUYcIQw3vpLQgwPpiz52eWCeoCpzn06DcsF6aNPjhlp9uJRZCRxZ4yeiwh/A0xxiQtnB4fdZuUPmia1r62+oaxrDl4hUwR7kzHYl0YGfXxAW9GT17KGtjES2yO4kAUgquelNh0hgBKZRvny9imwsObG7ntw5ZG7H62sP7UySIUJqoNRAgMBAAECggEBAKLVU25BCQg7wQGokwra2wMfPoG+IDuw4mkqFlBNKS/prSo86c2TgFmel2qQk2TLS1OUIZbha38RmAXA4qQohe5wKzmV06tcmwdY/YgCbF5aXSbUVYXLQ0Ea3r1pVUdps1SHnElZpnCXoi4Kyc2kAgSPkkdFVnhfFvc9EE/Ob8NgMkdFhlosE5WVNqm4BKQ+mqONddSz4JDbDOApPs/rRpgYm7pJKc3vkrYwniPjyQGYb5EoSbSWuu31RzIcn3Bhte3wKtfMMlpn8MMpPiYo2WJ2eVG6hlUOxhHgS93Y6czCfAgsDtD3C2JpteewuBjg8N0d6WRArKxny83J34q0qy0CgYEA6YSo5UDEq1TF8sbtSVYg6MKSX92NO5MQI/8fTjU4tEwxn/yxpGsnqUu0WGYIc2qVaZuxtcnk2CQxEilxQTbWSIxKuTt7qofEcpSjLLQ4f4chk4DpPsba+S8zSUdWdjthPHZT9IYzobylGBLfbPxyXXiYn1VuqAJfFy8iV9XqmdcCgYEA3ukROQQZCJcgsNTc5uFAKUeQvzv1iae3fGawgJmIJW3Bl8+4dSm1diqG3ZXP1WU31no2aX50PqOZjoIpbl1ggT76cnBDuu3pItR3dNJFQyMEpQOWOjO+NBWF7sRswCvlqbyjofWkzsdd0BioL7vWMjPftiusyyAFA55HRoeStxcCgYEA0tP7rKdSKKFr6inhl+GT6rGod7bOSSgYXXd7qx9v55AXCauaMqiv8TAxTdIo9RMYfHWd91OlMeNTDmOuJcO9qVhIKn5iw266VPyPac/4ZmL5VHQBobTlhC4yLomirTIlMvJeEBmNygtIPrjjUUGGe49itA/szPD/Ky5Z4lV27pcCgYAWU3mqIELxnVFk5K0LYtwuRkC1Jqg9FVNHXnGnL7l3JjsRnXh4I6lNII1JfEvIr86b6LmybzvtWi1zHI5Rw4B68XfcJmpiOpnzJxyf0r+lLci1Tlqpka0nQlCbzYim5r6l9YLeIeBT5Zv7z7xoq4OUm6V4dX9lCNv3tM6mvcVwGQKBgQC9hhjD64/VKXL8wYKZyTAOVO5xYCcqylrpI39qdzl+sS8oqmLUbXnKsGY4If9U61XdULld41BJCRlv6CsKreynm6ZN41j9YRuWWLu8STJcniV9Ef9uVl1M1zo8kfnCHMCym9LkTfJY+Ow/kYhqPukJJL6ve1CVmIuA4rnZlshjbg==", + "pubKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLVaPqWFA8WgK6ixuPvhTHeQfBblmEFLEmraLlIDSWbMUPva6aJ1V/hi2I5QLXNeeiig5sco+nF+RKhGnzQ9NpgHRVZ7Ze+LWq3Q4YxONdzFeNUjTvJrDSKgkubA5EKC/LI6pU33WZbjyKkomGo+Gzuqvlj4Rx1dLVXRIOjxUYcIQw3vpLQgwPpiz52eWCeoCpzn06DcsF6aNPjhlp9uJRZCRxZ4yeiwh/A0xxiQtnB4fdZuUPmia1r62+oaxrDl4hUwR7kzHYl0YGfXxAW9GT17KGtjES2yO4kAUgquelNh0hgBKZRvny9imwsObG7ntw5ZG7H62sP7UySIUJqoNRAgMBAAE=" +} diff --git a/test/pnet.spec.js b/test/pnet.spec.js new file mode 100644 index 0000000..df320a7 --- /dev/null +++ b/test/pnet.spec.js @@ -0,0 +1,105 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +chai.use(dirtyChai) +const expect = chai.expect +const parallel = require('async/parallel') +const PeerId = require('peer-id') +const Connection = require('interface-connection').Connection +const pair = require('pull-pair/duplex') +const pull = require('pull-stream') + +const Protector = require('../src') +const Errors = require('../src').errors +const generate = require('../src/key-generator') + +const swarmKeyBuffer = Buffer.alloc(95) +const wrongSwarmKeyBuffer = Buffer.alloc(95) + +// Write new psk files to the buffers +generate(swarmKeyBuffer) +generate(wrongSwarmKeyBuffer) + +describe('private network', () => { + before((done) => { + parallel([ + (cb) => PeerId.createFromJSON(require('./fixtures/peer-a'), cb), + (cb) => PeerId.createFromJSON(require('./fixtures/peer-b'), cb) + ], (err) => { + expect(err).to.not.exist() + done() + }) + }) + + it('should accept a valid psk buffer', () => { + const protector = new Protector(swarmKeyBuffer) + + expect(protector.tag).to.equal('/key/swarm/psk/1.0.0/') + expect(protector.psk.byteLength).to.equal(32) + }) + + it('should protect a simple connection', (done) => { + const p = pair() + const protector = new Protector(swarmKeyBuffer) + + const aToB = protector.protect(new Connection(p[0]), (err) => { + expect(err).to.not.exist() + }) + const bToA = protector.protect(new Connection(p[1]), (err) => { + expect(err).to.not.exist() + }) + + pull( + pull.values([Buffer.from('hello world'), Buffer.from('doo dah')]), + aToB + ) + + pull( + bToA, + pull.collect((err, chunks) => { + expect(err).to.not.exist() + expect(chunks).to.eql([Buffer.from('hello world'), Buffer.from('doo dah')]) + done() + }) + ) + }) + + it('should not connect to a peer with a different key', (done) => { + const p = pair() + const protector = new Protector(swarmKeyBuffer) + const protectorB = new Protector(wrongSwarmKeyBuffer) + + const aToB = protector.protect(new Connection(p[0]), () => { }) + const bToA = protectorB.protect(new Connection(p[1]), () => { }) + + pull( + pull.values([Buffer.from('hello world'), Buffer.from('doo dah')]), + aToB + ) + + pull( + bToA, + pull.collect((values) => { + expect(values).to.equal(null) + done() + }) + ) + }) + + describe('invalid psks', () => { + it('should not accept a bad psk', () => { + expect(() => { + return new Protector(Buffer.from('not-a-key')) + }).to.throw(Errors.INVALID_PSK) + }) + + it('should not accept a psk of incorrect length', () => { + expect(() => { + return new Protector(Buffer.from('/key/swarm/psk/1.0.0/\n/base16/\ndffb7e')) + }).to.throw(Errors.INVALID_PSK) + }) + }) +})