From ee57c698d77e0aa31bfeab558ceba4a3ac6b98f8 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Mon, 8 Oct 2018 13:38:51 +1100 Subject: [PATCH 1/2] Client side proof of work attached to messages Skeleton calcPoW function that prepares payload for hashingFunction to increment Uint8Array nonce Util function for converting a number to its Uint8Array representation Actually perform calculate PoW nonce for every message Adding bytebuffer as dependency and updated package-lock Move PoW to new file and execute it in child process TODO: Move send message logic to callback after child process has finished calculating the PoW Refactored nonce calculation to function to be awaited More readable timestamp grab plus converted to seconds Nonce is now returned as a base64 string and included in request header Also converted timestamp and ttl to string when including them in the header Added a couple comments and tidied up some nonce logic --- js/modules/loki_message_api.js | 50 +++++++++++++++++++++--- libloki/proof-of-work.js | 70 ++++++++++++++++++++++++++++++++++ package.json | 2 + 3 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 libloki/proof-of-work.js 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", From dfa8b59bc37a56409875dc33c52c9597aa324b45 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Thu, 18 Oct 2018 17:30:50 +1100 Subject: [PATCH 2/2] Using let and const properly, updated number to uint function, general cleaning --- js/modules/loki_message_api.js | 9 ++++--- libloki/proof-of-work.js | 45 ++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index c8e590321..e608f8b4d 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -52,13 +52,16 @@ function initialize({ url }) { }; async function sendMessage(pub_key, data, ttl) { - var timestamp = Math.floor(Date.now() / 1000); + const 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) => { + 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`, diff --git a/libloki/proof-of-work.js b/libloki/proof-of-work.js index df409fa28..e423efce1 100644 --- a/libloki/proof-of-work.js +++ b/libloki/proof-of-work.js @@ -1,9 +1,9 @@ const hash = require('js-sha512'); const bb = require('bytebuffer'); -// Increment Uint8Array by 1 with carrying +// Increment Uint8Array nonce by 1 with carrying function incrementNonce(nonce) { - idx = nonce.length - 1; + 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) { @@ -12,52 +12,49 @@ function incrementNonce(nonce) { return nonce; } -// Convert a Uint8Array to a base64 string. Copied from stackoverflow +// Convert a Uint8Array to a base64 string function bufferToBase64(buf) { - var binstr = Array.prototype.map.call(buf, function (ch) { + let binstr = Array.prototype.map.call(buf, function (ch) { return String.fromCharCode(ch); }).join(''); return bb.btoa(binstr); } -// Convert number to Uint8Array +// Convert javascript number to Uint8Array of length 8 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 ; + 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) { - var leadingString = timestamp.toString() + ttl.toString() + pub_key; - var leadingArray = new Uint8Array(bb.wrap(leadingString, 'binary').toArrayBuffer()); + 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 - var payload = leadingArray + data; - var nonceLen = 8; + const payload = leadingArray + data; + const 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); + 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 - var target = numberToUintArr(Math.pow(2, 64) / ( + const target = numberToUintArr(Math.floor(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) { + ))); + 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); }