diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index e75341e82..c8e590321 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -1,5 +1,6 @@ const fetch = require('node-fetch'); const is = require('@sindresorhus/is'); +const { fork } = require('child_process'); module.exports = { initialize, @@ -19,8 +20,46 @@ function initialize({ url }) { sendMessage }; - async function sendMessage(pub_key, data, ttl) - { + function getPoWNonce(timestamp, ttl, pub_key, data) { + return new Promise((resolve, reject) => { + // Create forked node process to calculate PoW without blocking main process + const child = fork('./libloki/proof-of-work.js'); + + // Send data required for PoW to child process + child.send({ + timestamp, + ttl, + pub_key, + data + }); + + // Handle child process error (should never happen) + child.on('error', (err) => { + reject(err); + }); + + // Callback to receive PoW result + child.on('message', (msg) => { + if (msg.err) { + reject(msg.err); + } else { + child.kill(); + resolve(msg.nonce); + } + }); + + }); + }; + + async function sendMessage(pub_key, data, ttl) { + var timestamp = Math.floor(Date.now() / 1000); + // Nonce is returned as a base64 string to include in header + nonce = await getPoWNonce(timestamp, ttl, pub_key, data).catch((err) => { + // Something went horribly wrong + // TODO: Handle gracefully + console.log("Error computing PoW"); + }); + const options = { url: `${url}/send_message`, type: 'POST', @@ -30,12 +69,13 @@ function initialize({ url }) { log.info(options.type, options.url); - const fetchOptions = { method: options.type, body: data, headers: { - 'X-Loki-ttl': ttl, + 'X-Loki-pow-nonce': nonce, + 'X-Loki-timestamp': timestamp.toString(), + 'X-Loki-ttl': ttl.toString(), 'X-Loki-recipient': pub_key, 'Content-Length': data.byteLength, }, @@ -74,7 +114,7 @@ function initialize({ url }) { result ); } - } + }; } } diff --git a/libloki/proof-of-work.js b/libloki/proof-of-work.js new file mode 100644 index 000000000..df409fa28 --- /dev/null +++ b/libloki/proof-of-work.js @@ -0,0 +1,70 @@ +const hash = require('js-sha512'); +const bb = require('bytebuffer'); + +// Increment Uint8Array by 1 with carrying +function incrementNonce(nonce) { + idx = nonce.length - 1; + nonce[idx] += 1; + // Nonce will just reset to 0 if all values are 255 causing infinite loop, should never happen + while (nonce[idx] == 0 && idx >= 0) { + nonce[--idx] += 1; + } + return nonce; +} + +// Convert a Uint8Array to a base64 string. Copied from stackoverflow +function bufferToBase64(buf) { + var binstr = Array.prototype.map.call(buf, function (ch) { + return String.fromCharCode(ch); + }).join(''); + return bb.btoa(binstr); +} + +// Convert number to Uint8Array +function numberToUintArr(numberVal) { + // TODO: Make this not hardcoded for arrays of length 8? + var arr = new Uint8Array(8); + for (var idx = 7; idx >= 0; idx--) { + // Get the lowest 8 bits from the number + var byte = numberVal & 0xff; + arr[idx] = byte; + // Essentially bitshift + numberVal = (numberVal - byte) / 256 ; + } + return arr; +} + +// Return nonce that hashes together with payload lower than the target +function calcPoW(timestamp, ttl, pub_key, data) { + var leadingString = timestamp.toString() + ttl.toString() + pub_key; + var leadingArray = new Uint8Array(bb.wrap(leadingString, 'binary').toArrayBuffer()); + // Payload constructed from concatenating timestamp, ttl and pubkey strings, converting to Uint8Array + // and then appending to the message data array + var payload = leadingArray + data; + var nonceLen = 8; + // Modify this value for difficulty scaling + // TODO: Have more informed reason for setting this to 100 + var nonceTrialsPerByte = 100; + var nonce = new Uint8Array(nonceLen); + var trialValue = numberToUintArr(Number.MAX_SAFE_INTEGER); + // Target is converter to Uint8Array for simple comparison with trialValue + var target = numberToUintArr(Math.pow(2, 64) / ( + nonceTrialsPerByte * ( + payload.length + nonceLen + ( + (ttl * ( payload.length + nonceLen )) + / Math.pow(2, 16) + ) + ) + )); + initialHash = new Uint8Array(bb.wrap(hash(payload), 'hex').toArrayBuffer()); + while (target < trialValue) { + nonce = incrementNonce(nonce); + trialValue = (new Uint8Array(bb.wrap(hash(nonce + initialHash), 'hex').toArrayBuffer())).slice(0, 8); + } + return bufferToBase64(nonce); +} + +// Start calculation in child process when main process sends message data +process.on('message', (msg) => { + process.send({nonce: calcPoW(msg.timestamp, msg.ttl, msg.pub_key, msg.data)}); +}); \ No newline at end of file diff --git a/package.json b/package.json index 4d9720e8b..27e846a63 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "blueimp-canvas-to-blob": "^3.14.0", "blueimp-load-image": "^2.18.0", "bunyan": "^1.8.12", + "bytebuffer": "^5.0.1", "classnames": "^2.2.5", "config": "^1.28.1", "electron-editor-context-menu": "^1.1.1", @@ -68,6 +69,7 @@ "got": "^8.2.0", "intl-tel-input": "^12.1.15", "jquery": "^3.3.1", + "js-sha512": "^0.8.0", "linkify-it": "^2.0.3", "lodash": "^4.17.4", "mkdirp": "^0.5.1",