API implementation for LNS

This commit is contained in:
Maxim Shishmarev 2020-04-02 13:35:31 +11:00
parent 24114bab8d
commit 932ea23ceb
6 changed files with 192 additions and 4 deletions

View File

@ -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} => ${

View File

@ -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(

53
main.js
View File

@ -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);

View File

@ -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",

View File

@ -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);

View File

@ -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"