diff --git a/package-lock.json b/package-lock.json index fc449b1..e1526cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "applause-cli": "^1.7.0", "log": "^6.2.0", "make-cert": "^1.2.1", + "nexline": "^1.2.2", + "systeminformation": "^5.9.4", "tweetnacl": "^1.0.3" } }, @@ -89,6 +91,43 @@ "type": "^2.5.0" } }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + }, + "node_modules/iconv-lite": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/log": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/log/-/log-6.2.0.tgz", @@ -113,6 +152,18 @@ "make-cert": "make-cert.js" } }, + "node_modules/nexline": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/nexline/-/nexline-1.2.2.tgz", + "integrity": "sha512-YLX5uoqNP7XVsXk889i8ZQcuMkukA4My4JD9wqTRLT+4dFo6QEEn+hU26J5H89m+mzW9BfhDgriGdbMEP06eeQ==", + "dependencies": { + "fs-extra": "^8.1.0", + "iconv-lite": "^0.5.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -126,6 +177,11 @@ "node": ">= 6.0.0" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/sprintf-kit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/sprintf-kit/-/sprintf-kit-2.0.1.tgz", @@ -134,6 +190,30 @@ "es5-ext": "^0.10.53" } }, + "node_modules/systeminformation": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.9.4.tgz", + "integrity": "sha512-FOsiTn0CyJZoj9kIhla11ndsMzbbwwuriul81wpqIBt9IpbxHZ6P/oZCphIFgJrwqjTnme0Qp1HDzIkUD9Xr/g==", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=4.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -143,6 +223,14 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } } }, "dependencies": { @@ -222,6 +310,37 @@ "type": "^2.5.0" } }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + }, + "iconv-lite": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, "log": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/log/-/log-6.2.0.tgz", @@ -243,6 +362,15 @@ "node-forge": "^0.10.0" } }, + "nexline": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/nexline/-/nexline-1.2.2.tgz", + "integrity": "sha512-YLX5uoqNP7XVsXk889i8ZQcuMkukA4My4JD9wqTRLT+4dFo6QEEn+hU26J5H89m+mzW9BfhDgriGdbMEP06eeQ==", + "requires": { + "fs-extra": "^8.1.0", + "iconv-lite": "^0.5.0" + } + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -253,6 +381,11 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "sprintf-kit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/sprintf-kit/-/sprintf-kit-2.0.1.tgz", @@ -261,6 +394,11 @@ "es5-ext": "^0.10.53" } }, + "systeminformation": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.9.4.tgz", + "integrity": "sha512-FOsiTn0CyJZoj9kIhla11ndsMzbbwwuriul81wpqIBt9IpbxHZ6P/oZCphIFgJrwqjTnme0Qp1HDzIkUD9Xr/g==" + }, "tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -270,6 +408,11 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" } } } diff --git a/package.json b/package.json index 0230d48..8dde60f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "applause-cli": "^1.7.0", "log": "^6.2.0", "make-cert": "^1.2.1", + "nexline": "^1.2.2", + "systeminformation": "^5.9.4", "tweetnacl": "^1.0.3" } } diff --git a/src/lib/agent/Agent.mjs b/src/lib/agent/Agent.mjs new file mode 100644 index 0000000..8e67a41 --- /dev/null +++ b/src/lib/agent/Agent.mjs @@ -0,0 +1,30 @@ +"use strict"; + +import os from 'os'; + +import make_cert from 'make-cert'; +import systeminfo from 'systeminformation'; + +import hash from '../crypto/hash.mjs'; + +class Agent { + constructor() { + + } + + async init(secret_join) { + this.secret_join = secret_join; + + /** Our peer id - calculated automatically from the system's uuid */ + this.peer_id = hash("sha256", "base64", await systeminfo.system().serial) + .replace(/[+/=]/g, ""); + this.peer_name = os.hostname(); + + + + // Properties: key, cert + this.cert = make_cert(`${our_id}.systemquery-peer.localhost`); + } +} + +export default Agent; diff --git a/src/lib/agent/PeerServer.mjs b/src/lib/agent/PeerServer.mjs new file mode 100644 index 0000000..0ff1de3 --- /dev/null +++ b/src/lib/agent/PeerServer.mjs @@ -0,0 +1,9 @@ +"use strict"; + +class PeerServer { + constructor() { + + } +} + +export default PeerServer; diff --git a/src/lib/crypto/hash.mjs b/src/lib/crypto/hash.mjs new file mode 100644 index 0000000..b36b998 --- /dev/null +++ b/src/lib/crypto/hash.mjs @@ -0,0 +1,9 @@ +"use strict"; + +import crypto from 'crypto'; + +export default function hash(algorithm, encoding, data) { + let hasher = crypto.createHash(algorithm); + hasher.update(data); + return hasher.digest(encoding); +} diff --git a/src/lib/crypto/secretbox.mjs b/src/lib/crypto/secretbox.mjs new file mode 100644 index 0000000..e1e5c9f --- /dev/null +++ b/src/lib/crypto/secretbox.mjs @@ -0,0 +1,56 @@ +"use strict"; + +import { randomBytes, secretbox } from 'tweetnacl'; + +/** + * Creates a new key ready for encryption. + * @return {string} A new base64-encoded key. + */ +function make_key() { + return randomBytes(secretbox.keyLength).toString("base64"); +} + +/** + * Encrypts the given data with the given key. + * @param {string} key The base64-encoded key to use to encrypt the data. + * @param {string} data The data to encrypt. + * @return {string} The encrypted data, base64 encoded. + */ +function encrypt(key, data) { + const key_bytes = Buffer.from(key, "base64"); + const nonce = randomBytes(secretbox.nonceLength); + const data_bytes = Buffer.from(data, "utf-8"); + + const cipher_bytes = secretbox(data_bytes, nonce, key_bytes); + + const concat_bytes = Buffer.concat([nonce, cipher_bytes]); + + key_bytes.fill(0); + nonce.fill(0); + + return concat_bytes.toString("base64"); +} + +/** + * Decrypts the given data with the given key. + * @param {string} key The base64-encoded keyto use to decrypt the data. + * @param {string} cipher_text The base64-encoded ciphertext to decrypt. + * @return {string} The decoded data, utf-8 encoded. + */ +function decrypt(key, cipher_text) { + const concat_bytes = Buffer.from(cipher_text, "base64"); + const key_bytes = Buffer.from(key, "basse64"); + + const nonce = concat_bytes.slice(0, secretbox.nonceLength); + const cipher_bytes = concat_bytes.slice(secretbox.nonceLength); + + const data_bytes = secretbox.open(cipher_bytes, nonce, key_bytes); + // Failed to decrypt message. Could be because the nonce, key, or ciphertext is invalid + // Ref https://github.com/dchest/tweetnacl-js/blob/master/test/04-secretbox.quick.js + // Ref https://github.com/dchest/tweetnacl-js/wiki/Examples#secretbox + if(!data_bytes) return null; + + return data_bytes.toString("utf-8"); +} + +export { make_key, encrypt, decrypt }; diff --git a/src/lib/io/StreamHelpers.mjs b/src/lib/io/StreamHelpers.mjs new file mode 100644 index 0000000..dda88b5 --- /dev/null +++ b/src/lib/io/StreamHelpers.mjs @@ -0,0 +1,73 @@ +"use strict"; +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/* + * A pair of functions to make writing to streams in Node.js less painful. + * I've used these in a number of different projects so far, and they both + * *appear* to be stable. + * @licence MPL-2.0 + * + * Changelog + ********************************* + * 22nd March 2021 + * We should have brought this into our code snippet library *ages* ago lol :P + */ + + +/** + * Writes data to a stream, automatically waiting for the drain event if asked. + * @param {stream.Writable} stream_out The writable stream to write to. + * @param {string|Buffer|Uint8Array} data The data to write. + * @return {Promise} A promise that resolves when writing is complete. + */ +function write_safe(stream_out, data) { + return new Promise(function (resolve, reject) { + // console.log(`Beginning write`); + // Handle errors + let handler_error = (error) => { + stream_out.off("error", handler_error); + // console.log(`Error received, handler detached, rejecting`); + reject(error); + }; + stream_out.on("error", handler_error); + + let returnval = typeof data == "string" ? stream_out.write(data, "utf-8") : stream_out.write(data); + // console.log(`Write returned`, returnval); + if(returnval) { + // We're good to go + stream_out.off("error", handler_error); + // console.log("We're good to go, handler detached, resolving"); + resolve(); + } + else { + // We need to wait for the drain event before continuing + // console.log(`Waiting for drain event`); + stream_out.once("drain", () => { + stream_out.off("error", handler_error); + // console.log(`Drain event received, handler detached, resolving`); + resolve(); + }); + } + }); +} + +/** + * Waits for the given stream to end and finish writing data. + * @param {stream.Writable} stream The stream to end. + * @param {Buffer|string} [chunk=undefined] Optional. A chunk to write when calling .end(). + * @return {Promise} A Promise that resolves when writing is complete. + */ +function end_safe(stream) { + return new Promise((resolve, _reject) => { + // Handle streams that have already been closed + if(stream.writableFinished) resolve(); + stream.once("finish", resolve); + stream.end(); + }); +} + +export { + write_safe, end_safe +}; diff --git a/src/lib/transport/Connection.mjs b/src/lib/transport/Connection.mjs new file mode 100644 index 0000000..fffeb24 --- /dev/null +++ b/src/lib/transport/Connection.mjs @@ -0,0 +1,70 @@ +"use strict"; + +import net from 'net'; +import { EventEmitter, once } from 'events'; + +import l from 'log'; +import nexline from 'nexline'; + +import settings from '../../settings.mjs'; + +/** + * Represents a connection to a single endpoint. + */ +class Connection extends EventEmitter { + constructor() { + super(); + } + + /** + * Connects to a peer and initialises a secure TCP connection thereto. + * @param {string} address The address to connect to. + * @param {string} port The TCP port to connect to. + * @return {net.Socket} A socket setup for secure communication. + */ + async connect(address, port) { + this.address = address; this.port = port; + this.socket = new new.Socket(); + this.socket.connect({ + address, port + }); + await once(this.socket, "connect"); + this.socket.setKeepAlive(true); + + + + + + // this.reader = nexline({ + // input: this.socket + // }); + // + // this.read_task = read_loop(); + } + + destroy() { + this.socket.destroy(); + this.emit("destroy"); + } + + async read_loop() { + try { + for await (let line of this.reader) { + handle_message(line); + } + } + catch(error) { + l.warn(`Warning: Killing connection to ${this.address}:${this.port} after error: ${settings.cli.verbose ? error : error.message}`); + } + finally { + this.destroy(); + } + } + + handle_message(text) { + let message = JSON.parse(text); + + } +} + +export default Connection; diff --git a/src/lib/transport/starttls.mjs b/src/lib/transport/starttls.mjs new file mode 100644 index 0000000..a18515f --- /dev/null +++ b/src/lib/transport/starttls.mjs @@ -0,0 +1,34 @@ +"use strict"; + +import { once } from 'events'; +import tls from 'tls'; + +import { write_safe } from '../io/StreamHelpers.mjs'; +import { encrypt, decrypt } from '../crypto/secretbox.mjs'; + + +export default async function starttls(socket, secret_join, { cert: cert_ours, key: key_ours }, is_server = true) { + // 1: Encrypt our self-signed cert and send it to the peer + const cert_encrypted = encrypt(secret_join, cert_ours); + await write_safe(socket, Buffer.from(cert_encrypted, "base64")); + + // 2: Receive our peer's certificate and decrypt it + let data_bytes = Buffer.from(await once(socket, "data")[0], "base64"); + + const cert_theirs = decrypt(secret_join, data_bytes.toString("base64")); + if(cert_theirs === null) { + socket.destroy(); + return null; + } + + // 3: Upgrade to proper TLS + const tls_socket = new tls.TLSSocket(socket, { + isServer: is_server, + requestCert: true, + ca: cert_theirs, + cert: cert_ours, + key: key_ours + }); + + return tls_socket; +}