diff --git a/config/default.json b/config/default.json index e642583f7..c2b0f0e2f 100644 --- a/config/default.json +++ b/config/default.json @@ -5,6 +5,7 @@ "contentProxyUrl": "random.snode", "localServerPort": "8081", "snodeServerPort": "8080", + "defaultPoWDifficulty": "100", "disableAutoUpdate": false, "updatesUrl": "https://updates2.signal.org/desktop", "updatesPublicKey": diff --git a/js/background.js b/js/background.js index 76ae88606..6bdb08222 100644 --- a/js/background.js +++ b/js/background.js @@ -233,6 +233,11 @@ window.libloki.api.sendOnlineBroadcastMessage(pubKey, isPing); }); + const currentPoWDifficulty = storage.get('PoWDifficulty', null); + if (!currentPoWDifficulty) { + storage.put('PoWDifficulty', window.getDefaultPoWDifficulty()); + } + // These make key operations available to IPC handlers created in preload.js window.Events = { getDeviceName: () => textsecure.storage.user.getDeviceName(), diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index f24afb171..75de95d4b 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -31,10 +31,10 @@ const filterIncomingMessages = async messages => { }; const calcNonce = (messageEventData, pubKey, data64, timestamp, ttl) => { + const difficulty = window.storage.get('PoWDifficulty', null); // Nonce is returned as a base64 string to include in header window.Whisper.events.trigger('calculatingPoW', messageEventData); - const development = window.getEnvironment() !== 'production'; - return callWorker('calcPoW', timestamp, ttl, pubKey, data64, development); + return callWorker('calcPoW', timestamp, ttl, pubKey, data64, difficulty); }; const trySendP2p = async (pubKey, data64, isPing, messageEventData) => { @@ -124,7 +124,17 @@ class LokiMessageAPI { promises.push(this.openSendConnection(params)); } - const results = await Promise.all(promises); + let results; + try { + results = await Promise.all(promises); + } catch (e) { + if (e instanceof textsecure.WrongDifficultyError) { + // Force nonce recalculation + this.sendMessage(pubKey, data, messageTimeStamp, ttl, options); + return; + } + throw e; + } delete this.sendingSwarmNodes[timestamp]; if (results.every(value => value === false)) { throw new window.textsecure.EmptySwarmError( @@ -155,7 +165,19 @@ class LokiMessageAPI { while (successiveFailures < 3) { await sleepFor(successiveFailures * 500); try { - await rpc(`https://${url}`, this.snodeServerPort, 'store', params); + const result = await rpc( + `https://${url}`, + this.snodeServerPort, + 'store', + params + ); + + // Make sure we aren't doing too much PoW + const currentDifficulty = window.storage.get('PoWDifficulty', null); + const newDifficulty = result.difficulty; + if (newDifficulty != null && newDifficulty !== currentDifficulty) { + window.storage.put('PoWDifficulty', newDifficulty); + } return true; } catch (e) { log.warn('Loki send message:', e); @@ -164,6 +186,12 @@ class LokiMessageAPI { await lokiSnodeAPI.updateSwarmNodes(params.pubKey, newSwarm); this.sendingSwarmNodes[params.timestamp] = newSwarm; return false; + } else if (e instanceof textsecure.WrongDifficultyError) { + const { newDifficulty } = e; + if (!Number.isNaN(newDifficulty)) { + window.storage.put('PoWDifficulty', newDifficulty); + } + throw e; } else if (e instanceof textsecure.NotFoundError) { // TODO: Handle resolution error successiveFailures += 1; diff --git a/js/modules/loki_rpc.js b/js/modules/loki_rpc.js index a61422506..1e0f52975 100644 --- a/js/modules/loki_rpc.js +++ b/js/modules/loki_rpc.js @@ -6,6 +6,21 @@ const { parse } = require('url'); const LOKI_EPHEMKEY_HEADER = 'X-Loki-EphemKey'; const endpointBase = '/v1/storage_rpc'; +const decryptResponse = async (response, address) => { + try { + const ciphertext = await response.text(); + const plaintext = await libloki.crypto.snodeCipher.decrypt( + address, + ciphertext + ); + const result = plaintext === '' ? {} : JSON.parse(plaintext); + return result; + } catch (e) { + log.warn(`Could not decrypt response from ${address}`, e); + } + return {}; +}; + // A small wrapper around node-fetch which deserializes response const fetch = async (url, options = {}) => { const timeout = options.timeout || 10000; @@ -39,49 +54,41 @@ const fetch = async (url, options = {}) => { method, }); + let result; + // Wrong swarm if (response.status === 421) { - let newSwarm = await response.text(); if (doEncryptChannel) { - try { - newSwarm = await libloki.crypto.snodeCipher.decrypt( - address, - newSwarm - ); - } catch (e) { - log.warn(`Could not decrypt response from ${address}`, e); - } - try { - newSwarm = newSwarm === '' ? {} : JSON.parse(newSwarm); - } catch (e) { - log.warn(`Could not parse string to json ${newSwarm}`, e); - } + result = decryptResponse(response, address); + } else { + result = await response.json(); } + const newSwarm = result.snodes ? result.snodes : []; throw new textsecure.WrongSwarmError(newSwarm); } + // Wrong PoW difficulty + if (response.status === 432) { + if (doEncryptChannel) { + result = decryptResponse(response, address); + } else { + result = await response.json(); + } + const { difficulty } = result; + throw new textsecure.WrongDifficultyError(difficulty); + } + if (!response.ok) { throw new textsecure.HTTPError('Loki_rpc error', response); } - let result; if (response.headers.get('Content-Type') === 'application/json') { result = await response.json(); } else if (options.responseType === 'arraybuffer') { result = await response.buffer(); + } else if (doEncryptChannel) { + result = decryptResponse(response, address); } else { result = await response.text(); - if (doEncryptChannel) { - try { - result = await libloki.crypto.snodeCipher.decrypt(address, result); - } catch (e) { - log.warn(`Could not decrypt response from ${address}`, e); - } - try { - result = result === '' ? {} : JSON.parse(result); - } catch (e) { - log.warn(`Could not parse string to json ${result}`, e); - } - } } return result; diff --git a/js/util_worker_tasks.js b/js/util_worker_tasks.js index 446ee7da8..b084e7b1a 100644 --- a/js/util_worker_tasks.js +++ b/js/util_worker_tasks.js @@ -47,7 +47,7 @@ function calcPoW( pubKey, data, development, - nonceTrials = undefined, + difficulty = undefined, increment = 1, startNonce = 0 ) { @@ -57,7 +57,7 @@ function calcPoW( pubKey, data, development, - nonceTrials, + difficulty, increment, startNonce ); diff --git a/libloki/proof-of-work.js b/libloki/proof-of-work.js index 039e2395d..b1359d4e5 100644 --- a/libloki/proof-of-work.js +++ b/libloki/proof-of-work.js @@ -1,8 +1,7 @@ /* global dcodeIO, crypto, JSBI */ const NONCE_LEN = 8; // Modify this value for difficulty scaling -const DEV_NONCE_TRIALS = 10; -const PROD_NONCE_TRIALS = 100; +const FALLBACK_DIFFICULTY = 10; const pow = { // Increment Uint8Array nonce by '_increment' with carrying @@ -62,8 +61,7 @@ const pow = { ttl, pubKey, data, - development = false, - _nonceTrials = null, + _difficulty = null, increment = 1, startNonce = 0 ) { @@ -74,9 +72,8 @@ const pow = { ).toArrayBuffer() ); - const nonceTrials = - _nonceTrials || (development ? DEV_NONCE_TRIALS : PROD_NONCE_TRIALS); - const target = pow.calcTarget(ttl, payload.length, nonceTrials); + const difficulty = _difficulty || FALLBACK_DIFFICULTY; + const target = pow.calcTarget(ttl, payload.length, difficulty); let nonce = new Uint8Array(NONCE_LEN); nonce = pow.incrementNonce(nonce, startNonce); // initial value @@ -103,7 +100,7 @@ const pow = { return pow.bufferToBase64(nonce); }, - calcTarget(ttl, payloadLen, nonceTrials = PROD_NONCE_TRIALS) { + calcTarget(ttl, payloadLen, difficulty = FALLBACK_DIFFICULTY) { // payloadLength + NONCE_LEN const totalLen = JSBI.add(JSBI.BigInt(payloadLen), JSBI.BigInt(NONCE_LEN)); // ttl converted to seconds @@ -119,9 +116,9 @@ const pow = { const innerFrac = JSBI.divide(ttlMult, two16); // totalLen + innerFrac const lenPlusInnerFrac = JSBI.add(totalLen, innerFrac); - // nonceTrials * lenPlusInnerFrac + // difficulty * lenPlusInnerFrac const denominator = JSBI.multiply( - JSBI.BigInt(nonceTrials), + JSBI.BigInt(difficulty), lenPlusInnerFrac ); // 2^64 - 1 diff --git a/libloki/test/metrics.js b/libloki/test/metrics.js index cdd49059d..222177c52 100644 --- a/libloki/test/metrics.js +++ b/libloki/test/metrics.js @@ -3,7 +3,7 @@ let jobId = 0; let currentTrace = 0; let plotlyDiv; const workers = []; -async function run(messageLength, numWorkers = 1, nonceTrials = 100, ttl = 72) { +async function run(messageLength, numWorkers = 1, difficulty = 100, ttl = 72) { const timestamp = Math.floor(Date.now() / 1000); const pubKey = '05ec8635a07a13743516c7c9b3412f3e8252efb7fcaf67eb1615ffba62bebc6802'; @@ -29,7 +29,7 @@ async function run(messageLength, numWorkers = 1, nonceTrials = 100, ttl = 72) { pubKey, data, false, - nonceTrials, + difficulty, increment, index, ]); @@ -50,12 +50,12 @@ async function run(messageLength, numWorkers = 1, nonceTrials = 100, ttl = 72) { async function runPoW({ iteration, - nonceTrials, + difficulty, numWorkers, messageLength = 50, ttl = 72, }) { - const name = `W:${numWorkers} - NT: ${nonceTrials} - L:${messageLength} - TTL:${ttl}`; + const name = `W:${numWorkers} - NT: ${difficulty} - L:${messageLength} - TTL:${ttl}`; Plotly.addTraces(plotlyDiv, { y: [], type: 'box', @@ -64,7 +64,7 @@ async function runPoW({ }); for (let i = 0; i < iteration; i += 1) { // eslint-disable-next-line no-await-in-loop - await run(messageLength, numWorkers, nonceTrials, ttl); + await run(messageLength, numWorkers, difficulty, ttl); } currentTrace += 1; @@ -86,9 +86,7 @@ function addPoint(duration) { } async function startMessageLengthRun() { const iteration0 = parseFloat(document.getElementById('iteration0').value); - const nonceTrials0 = parseFloat( - document.getElementById('nonceTrials0').value - ); + const difficulty0 = parseFloat(document.getElementById('difficulty0').value); const numWorkers0 = parseFloat(document.getElementById('numWorkers0').value); const messageLengthStart0 = parseFloat( document.getElementById('messageLengthStart0').value @@ -108,7 +106,7 @@ async function startMessageLengthRun() { // eslint-disable-next-line no-await-in-loop await runPoW({ iteration: iteration0, - nonceTrials: nonceTrials0, + difficulty: difficulty0, numWorkers: numWorkers0, messageLength: l, ttl: TTL0, @@ -117,9 +115,7 @@ async function startMessageLengthRun() { } async function startNumWorkerRun() { const iteration1 = parseFloat(document.getElementById('iteration1').value); - const nonceTrials1 = parseFloat( - document.getElementById('nonceTrials1').value - ); + const difficulty1 = parseFloat(document.getElementById('difficulty1').value); const numWorkersStart1 = parseFloat( document.getElementById('numWorkersStart1').value ); @@ -138,34 +134,34 @@ async function startNumWorkerRun() { // eslint-disable-next-line no-await-in-loop await runPoW({ iteration: iteration1, - nonceTrials: nonceTrials1, + difficulty: difficulty1, numWorkers, messageLength: messageLength1, ttl: TTL1, }); } } -async function startNonceTrialsRun() { +async function startDifficultyRun() { const iteration2 = parseFloat(document.getElementById('iteration2').value); const messageLength2 = parseFloat( document.getElementById('messageLength2').value ); const numWorkers2 = parseFloat(document.getElementById('numWorkers2').value); - const nonceTrialsStart2 = parseFloat( - document.getElementById('nonceTrialsStart2').value + const difficultyStart2 = parseFloat( + document.getElementById('difficultyStart2').value ); - const nonceTrialsStop2 = parseFloat( - document.getElementById('nonceTrialsStop2').value + const difficultyStop2 = parseFloat( + document.getElementById('difficultyStop2').value ); - const nonceTrialsStep2 = parseFloat( - document.getElementById('nonceTrialsStep2').value + const difficultyStep2 = parseFloat( + document.getElementById('difficultyStep2').value ); const TTL2 = parseFloat(document.getElementById('TTL2').value); - for (let n = nonceTrialsStart2; n < nonceTrialsStop2; n += nonceTrialsStep2) { + for (let n = difficultyStart2; n < difficultyStop2; n += difficultyStep2) { // eslint-disable-next-line no-await-in-loop await runPoW({ iteration: iteration2, - nonceTrials: n, + difficulty: n, numWorkers: numWorkers2, messageLength: messageLength2, ttl: TTL2, @@ -174,9 +170,7 @@ async function startNonceTrialsRun() { } async function starTTLRun() { const iteration3 = parseFloat(document.getElementById('iteration3').value); - const nonceTrials3 = parseFloat( - document.getElementById('nonceTrials3').value - ); + const difficulty3 = parseFloat(document.getElementById('difficulty3').value); const messageLength3 = parseFloat( document.getElementById('messageLength3').value ); @@ -188,7 +182,7 @@ async function starTTLRun() { // eslint-disable-next-line no-await-in-loop await runPoW({ iteration: iteration3, - nonceTrials: nonceTrials3, + difficulty: difficulty3, numWorkers: numWorkers3, messageLength: messageLength3, ttl, @@ -216,7 +210,7 @@ async function start(index) { await startNumWorkerRun(); break; case 2: - await startNonceTrialsRun(); + await startDifficultyRun(); break; case 3: await starTTLRun(); diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js index 78f9fcfb1..0ad331080 100644 --- a/libtextsecure/errors.js +++ b/libtextsecure/errors.js @@ -222,6 +222,19 @@ } } + function WrongDifficultyError(newDifficulty) { + this.name = 'WrongDifficultyError'; + this.newDifficulty = newDifficulty; + + Error.call(this, this.name); + + // Maintains proper stack trace, where our error was thrown (only available on V8) + // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } + } + window.textsecure.UnregisteredUserError = UnregisteredUserError; window.textsecure.SendMessageNetworkError = SendMessageNetworkError; window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError; @@ -237,4 +250,5 @@ window.textsecure.HTTPError = HTTPError; window.textsecure.NotFoundError = NotFoundError; window.textsecure.WrongSwarmError = WrongSwarmError; + window.textsecure.WrongDifficultyError = WrongDifficultyError; })(); diff --git a/main.js b/main.js index 9cf854a7f..eaad6252c 100644 --- a/main.js +++ b/main.js @@ -156,6 +156,7 @@ function prepareURL(pathSegments, moreKeys) { cdnUrl: config.get('cdnUrl'), snodeServerPort: config.get('snodeServerPort'), localServerPort: config.get('localServerPort'), + defaultPoWDifficulty: config.get('defaultPoWDifficulty'), certificateAuthority: config.get('certificateAuthority'), environment: config.environment, node_version: process.versions.node, diff --git a/preload.js b/preload.js index 90c7f4885..5cad3915d 100644 --- a/preload.js +++ b/preload.js @@ -22,6 +22,7 @@ if (config.appInstance) { } window.platform = process.platform; +window.getDefaultPoWDifficulty = () => config.defaultPoWDifficulty; window.getTitle = () => title; window.getEnvironment = () => config.environment; window.getAppInstance = () => config.appInstance;