diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index e75341e82..e608f8b4d 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,49 @@ 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) { + const timestamp = Math.floor(Date.now() / 1000); + // Nonce is returned as a base64 string to include in header + let nonce; + try { + 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 +72,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 +117,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..e423efce1 --- /dev/null +++ b/libloki/proof-of-work.js @@ -0,0 +1,67 @@ +const hash = require('js-sha512'); +const bb = require('bytebuffer'); + +// Increment Uint8Array nonce by 1 with carrying +function incrementNonce(nonce) { + let 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 +function bufferToBase64(buf) { + let binstr = Array.prototype.map.call(buf, function (ch) { + return String.fromCharCode(ch); + }).join(''); + return bb.btoa(binstr); +} + +// Convert javascript number to Uint8Array of length 8 +function numberToUintArr(numberVal) { + let arr = new Uint8Array(8); + for (let idx = 7; idx >= 0; idx--) { + let n = 8 - (idx + 1); + // 256 ** n is the value of one bit in arr[idx], modulus to carry over + arr[idx] = (numberVal / 256**n) % 256; + } + return arr; +} + +// Return nonce that hashes together with payload lower than the target +function calcPoW(timestamp, ttl, pub_key, data) { + const leadingString = timestamp.toString() + ttl.toString() + pub_key; + const 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 + const payload = leadingArray + data; + const nonceLen = 8; + // Modify this value for difficulty scaling + // TODO: Have more informed reason for setting this to 100 + const nonceTrialsPerByte = 1000; + let nonce = new Uint8Array(nonceLen); + let trialValue = numberToUintArr(Number.MAX_SAFE_INTEGER); + // Target is converter to Uint8Array for simple comparison with trialValue + const target = numberToUintArr(Math.floor(Math.pow(2, 64) / ( + nonceTrialsPerByte * ( + payload.length + nonceLen + ( + (ttl * ( payload.length + nonceLen )) + / Math.pow(2, 16) + ) + ) + ))); + const initialHash = new Uint8Array(bb.wrap(hash(payload), 'hex').toArrayBuffer()); + while (trialValue > target) { + 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",