From 932ea23cebafd877d86b9ffd7dafc09c9b904ee9 Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Thu, 2 Apr 2020 13:35:31 +1100 Subject: [PATCH] API implementation for LNS --- js/modules/loki_rpc.js | 6 +- js/modules/loki_snode_api.js | 104 ++++++++++++++++++++++++++++++++++- main.js | 53 ++++++++++++++++++ package.json | 1 + preload.js | 20 +++++++ yarn.lock | 12 ++++ 6 files changed, 192 insertions(+), 4 deletions(-) diff --git a/js/modules/loki_rpc.js b/js/modules/loki_rpc.js index 78255f506..b33872810 100644 --- a/js/modules/loki_rpc.js +++ b/js/modules/loki_rpc.js @@ -79,7 +79,7 @@ const BAD_PATH = 'bad_path'; // May return false BAD_PATH, indicating that we should try a new const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => { - log.info('Sending an onion request'); + log.debug('Sending an onion request'); const ctx1 = await encryptForDestination(targetNode, plaintext); const ctx2 = await encryptForRelay(nodePath[2], targetNode, ctx1); @@ -113,7 +113,7 @@ const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => { // Process a response as it arrives from `nodeFetch`, handling // http errors and attempting to decrypt the body with `sharedKey` const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => { - log.info(`(${reqIdx}) [path] processing onion response`); + log.debug(`(${reqIdx}) [path] processing onion response`); // detect SNode is not ready (not in swarm; not done syncing) if (response.status === 503) { @@ -427,7 +427,7 @@ const lokiFetch = async (url, options = {}, targetNode = null) => { const thisIdx = onionReqIdx; onionReqIdx += 1; - log.info( + log.debug( `(${thisIdx}) using path ${path[0].ip}:${path[0].port} -> ${ path[1].ip }:${path[1].port} -> ${path[2].ip}:${path[2].port} => ${ diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index 9642d64e0..9919cd94f 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -1,5 +1,5 @@ /* eslint-disable class-methods-use-this */ -/* global window, textsecure, ConversationController, _, log, clearTimeout, process */ +/* global window, textsecure, ConversationController, _, log, clearTimeout, process, Buffer, StringView, dcodeIO */ const is = require('@sindresorhus/is'); const { lokiRpc } = require('./loki_rpc'); @@ -270,6 +270,16 @@ class LokiSnodeAPI { ]; } + async getNodesMinVersion(minVersion) { + const _ = window.Lodash; + + return _.flatten( + _.entries(this.versionPools) + .filter(v => semver.gte(v[0], minVersion)) + .map(v => v[1]) + ); + } + // use nodes that support more than 1mb async getRandomProxySnodeAddress() { /* resolve random snode */ @@ -624,6 +634,98 @@ class LokiSnodeAPI { return newSwarmNodes; } + // helper function + async _requestLnsMapping(node, nameHash) { + log.debug('[lns] lns requests to {}:{}', node.ip, node.port); + + try { + // Hm, in case of proxy/onion routing we + // are not even using ip/port... + return lokiRpc( + `https://${node.ip}`, + node.port, + 'get_lns_mapping', + { + name_hash: nameHash, + }, + {}, + '/storage_rpc/v1', + node + ); + } catch (e) { + log.warn('exception caught making lns requests to a node', node, e); + return false; + } + } + + async getLnsMapping(lnsName) { + const _ = window.Lodash; + + const input = Buffer.from(lnsName); + + const output = await window.blake2b(input); + + const nameHash = dcodeIO.ByteBuffer.wrap(output).toString('base64'); + + // Get nodes capable of doing LNS + let lnsNodes = await this.getNodesMinVersion('2.0.3'); + lnsNodes = _.shuffle(lnsNodes); + + // Loop until 3 confirmations + + // We don't trust any single node, so we accumulate + // answers here and select a dominating answer + const allResults = []; + let ciphertextHex = null; + + while (!ciphertextHex) { + if (lnsNodes.length < 3) { + log.error('Not enough nodes for lns lookup'); + return false; + } + + // extract 3 and make requests in parallel + const nodes = lnsNodes.splice(0, 3); + + // eslint-disable-next-line no-await-in-loop + const results = await Promise.all( + nodes.map(node => this._requestLnsMapping(node, nameHash)) + ); + + results.forEach(res => { + if ( + res && + res.result && + res.result.status === 'OK' && + res.result.entries && + res.result.entries.length > 0 + ) { + allResults.push(results[0].result.entries[0].encrypted_value); + } + }); + + const [winner, count] = _.maxBy( + _.entries(_.countBy(allResults)), + x => x[1] + ); + + if (count >= 3) { + // eslint-disable-next-lint prefer-destructuring + ciphertextHex = winner; + } + } + + const ciphertext = new Uint8Array( + StringView.hexToArrayBuffer(ciphertextHex) + ); + + const res = await window.decryptLnsEntry(lnsName, ciphertext); + + const pubkey = StringView.arrayBufferToHex(res); + + return pubkey; + } + async getSnodesForPubkey(snode, pubKey) { try { const result = await lokiRpc( diff --git a/main.js b/main.js index ca2a1558a..5330295e0 100644 --- a/main.js +++ b/main.js @@ -82,6 +82,13 @@ const { } = require('./app/protocol_filter'); const { installPermissionsHandler } = require('./app/permissions'); +const _sodium = require('libsodium-wrappers'); + +async function getSodium() { + await _sodium.ready; + return _sodium; +} + let appStartInitialSpellcheckSetting = true; async function getSpellCheckSetting() { @@ -1119,6 +1126,52 @@ ipc.on('get-auto-update-setting', event => { event.returnValue = typeof configValue !== 'boolean' ? true : configValue; }); +async function decryptLns(event, lnsName, ciphertext) { + const sodium = await getSodium(); + + const salt = new Uint8Array(sodium.crypto_pwhash_SALTBYTES); + + try { + const key = sodium.crypto_pwhash( + sodium.crypto_secretbox_KEYBYTES, + lnsName, + salt, + sodium.crypto_pwhash_OPSLIMIT_MODERATE, + sodium.crypto_pwhash_MEMLIMIT_MODERATE, + sodium.crypto_pwhash_ALG_ARGON2ID13 + ); + + const nonce = new Uint8Array(sodium.crypto_secretbox_NONCEBYTES); + + const res = sodium.crypto_secretbox_open_easy(ciphertext, nonce, key); + + // null as first parameter to indivate no error + event.reply('decrypt-lns-response', null, res); + } catch (err) { + event.reply('decrypt-lns-response', err); + } +} + +async function blake2bDigest(event, input) { + const sodium = await getSodium(); + + try { + const res = sodium.crypto_generichash(32, input); + + event.reply('blake2b-digest-response', null, res); + } catch (err) { + event.reply('blake2b-digest-response', err); + } +} + +ipc.on('blake2b-digest', (event, input) => { + blake2bDigest(event, input); +}); + +ipc.on('decrypt-lns-entry', (event, lnsName, ciphertext) => { + decryptLns(event, lnsName, ciphertext); +}); + ipc.on('set-auto-update-setting', (event, enabled) => { userConfig.set('autoUpdate', !!enabled); diff --git a/package.json b/package.json index ee6822cbc..3112faa23 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "js-sha512": "0.8.0", "js-yaml": "3.13.0", "jsbn": "1.1.0", + "libsodium-wrappers": "^0.7.6", "linkify-it": "2.0.3", "lodash": "4.17.11", "mkdirp": "0.5.1", diff --git a/preload.js b/preload.js index 08c73ed67..df65e856f 100644 --- a/preload.js +++ b/preload.js @@ -95,6 +95,26 @@ window.wrapDeferred = deferredToPromise; const ipc = electron.ipcRenderer; const localeMessages = ipc.sendSync('locale-data'); +window.blake2b = input => + new Promise((resolve, reject) => { + ipc.once('blake2b-digest-response', (event, error, res) => { + // eslint-disable-next-line no-unused-expressions + error ? reject(error) : resolve(res); + }); + + ipc.send('blake2b-digest', input); + }); + +window.decryptLnsEntry = (key, value) => + new Promise((resolve, reject) => { + ipc.once('decrypt-lns-response', (event, error, res) => { + // eslint-disable-next-line no-unused-expressions + error ? reject(error) : resolve(res); + }); + + ipc.send('decrypt-lns-entry', key, value); + }); + window.updateZoomFactor = () => { const zoomFactor = window.getSettingValue('zoom-factor-setting') || 100; window.setZoomFactor(zoomFactor / 100); diff --git a/yarn.lock b/yarn.lock index ff06052c3..7a4aa95c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5972,6 +5972,18 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libsodium-wrappers@^0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.6.tgz#baed4c16d4bf9610104875ad8a8e164d259d48fb" + integrity sha512-OUO2CWW5bHdLr6hkKLHIKI4raEkZrf3QHkhXsJ1yCh6MZ3JDA7jFD3kCATNquuGSG6MjjPHQIQms0y0gBDzjQg== + dependencies: + libsodium "0.7.6" + +libsodium@0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.6.tgz#018b80c5728054817845fbffa554274441bda277" + integrity sha512-hPb/04sEuLcTRdWDtd+xH3RXBihpmbPCsKW/Jtf4PsvdyKh+D6z2D2gvp/5BfoxseP+0FCOg66kE+0oGUE/loQ== + lie@*: version "3.3.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"