diff --git a/js/expire.js b/js/expire.js
index 228508caf..0d4750c3b 100644
--- a/js/expire.js
+++ b/js/expire.js
@@ -1,4 +1,4 @@
-/* global LokiAppDotNetServerAPI, LokiFileServerAPI, semver, log */
+/* global LokiAppDotNetServerAPI, semver, log */
// eslint-disable-next-line func-names
(function() {
'use strict';
@@ -12,9 +12,8 @@
);
// use the anonymous access token
window.tokenlessFileServerAdnAPI.token = 'loki';
- window.tokenlessFileServerAdnAPI.pubKey = window.Signal.Crypto.base64ToArrayBuffer(
- LokiFileServerAPI.secureRpcPubKey
- );
+ // configure for file server comms
+ window.tokenlessFileServerAdnAPI.getPubKeyForUrl();
let nextWaitSeconds = 5;
const checkForUpgrades = async () => {
diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js
index bddd36c73..e6645cf69 100644
--- a/js/modules/loki_app_dot_net_api.js
+++ b/js/modules/loki_app_dot_net_api.js
@@ -7,6 +7,8 @@ const FormData = require('form-data');
const https = require('https');
const path = require('path');
+const lokiRpcUtils = require('./loki_rpc');
+
// Can't be less than 1200 if we have unauth'd requests
const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s
const PUBLICCHAT_CHAN_POLL_EVERY = 20 * 1000; // 20s
@@ -14,6 +16,7 @@ const PUBLICCHAT_DELETION_POLL_EVERY = 5 * 1000; // 5s
const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s
const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
+// FIXME: replace with something on urlPubkeyMap...
const FILESERVER_HOSTS = [
'file-dev.lokinet.org',
'file.lokinet.org',
@@ -21,6 +24,17 @@ const FILESERVER_HOSTS = [
'file.getsession.org',
];
+const LOKIFOUNDATION_DEVFILESERVER_PUBKEY =
+ 'BSZiMVxOco/b3sYfaeyiMWv/JnqokxGXkHoclEx8TmZ6';
+const LOKIFOUNDATION_FILESERVER_PUBKEY =
+ 'BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc';
+const urlPubkeyMap = {
+ 'https://file-dev.getsession.org': LOKIFOUNDATION_DEVFILESERVER_PUBKEY,
+ 'https://file-dev.lokinet.org': LOKIFOUNDATION_DEVFILESERVER_PUBKEY,
+ 'https://file.getsession.org': LOKIFOUNDATION_FILESERVER_PUBKEY,
+ 'https://file.lokinet.org': LOKIFOUNDATION_FILESERVER_PUBKEY,
+};
+
const HOMESERVER_USER_ANNOTATION_TYPE = 'network.loki.messenger.homeserver';
const AVATAR_USER_ANNOTATION_TYPE = 'network.loki.messenger.avatar';
const SETTINGS_CHANNEL_ANNOTATION_TYPE = 'net.patter-app.settings';
@@ -34,30 +48,141 @@ const snodeHttpsAgent = new https.Agent({
const timeoutDelay = ms => new Promise(resolve => setTimeout(resolve, ms));
-const sendToProxy = async (
- srvPubKey,
- endpoint,
- pFetchOptions,
- options = {}
-) => {
+const sendViaOnion = async (srvPubKey, url, fetchOptions, options = {}) => {
if (!srvPubKey) {
log.error(
- 'loki_app_dot_net:::sendToProxy - called without a server public key'
+ 'loki_app_dot_net:::sendViaOnion - called without a server public key'
);
return {};
}
- const fetchOptions = pFetchOptions; // make lint happy
- // safety issue with file server, just safer to have this
- if (fetchOptions.headers === undefined) {
- fetchOptions.headers = {};
+ // set retry count
+ if (options.retry === undefined) {
+ // eslint-disable-next-line no-param-reassign
+ options.retry = 0;
+ // eslint-disable-next-line no-param-reassign
+ options.requestNumber = window.lokiSnodeAPI.assignOnionRequestNumber();
+ }
+
+ const payloadObj = {
+ method: fetchOptions.method,
+ body: fetchOptions.body,
+ // safety issue with file server, just safer to have this
+ headers: fetchOptions.headers || {},
+ // no initial /
+ endpoint: url.pathname.replace(/^\//, ''),
+ };
+ if (url.search) {
+ payloadObj.endpoint += `?${url.search}`;
+ }
+
+ // from https://github.com/sindresorhus/is-stream/blob/master/index.js
+ if (
+ payloadObj.body &&
+ typeof payloadObj.body === 'object' &&
+ typeof payloadObj.body.pipe === 'function'
+ ) {
+ const fData = payloadObj.body.getBuffer();
+ const fHeaders = payloadObj.body.getHeaders();
+ // update headers for boundary
+ payloadObj.headers = { ...payloadObj.headers, ...fHeaders };
+ // update body with base64 chunk
+ payloadObj.body = {
+ fileUpload: fData.toString('base64'),
+ };
+ }
+
+ let pathNodes = [];
+ try {
+ pathNodes = await lokiSnodeAPI.getOnionPath();
+ } catch (e) {
+ log.error(
+ `loki_app_dot_net:::sendViaOnion #${
+ options.requestNumber
+ } - getOnionPath Error ${e.code} ${e.message}`
+ );
+ }
+ if (!pathNodes || !pathNodes.length) {
+ log.warn(
+ `loki_app_dot_net:::sendViaOnion #${
+ options.requestNumber
+ } - failing, no path available`
+ );
+ // should we retry?
+ return {};
+ }
+
+ // do the request
+ let result;
+ try {
+ result = await lokiRpcUtils.sendOnionRequestLsrpcDest(
+ 0,
+ pathNodes,
+ srvPubKey,
+ url.host,
+ payloadObj,
+ options.requestNumber
+ );
+ } catch (e) {
+ log.error(
+ 'loki_app_dot_net:::sendViaOnion - lokiRpcUtils error',
+ e.code,
+ e.message
+ );
+ return {};
+ }
+
+ // handle error/retries
+ if (!result.status) {
+ log.error(
+ `loki_app_dot_net:::sendViaOnion #${options.requestNumber} - Retry #${
+ options.retry
+ } Couldnt handle onion request, retrying`,
+ payloadObj
+ );
+ return sendViaOnion(srvPubKey, url, fetchOptions, {
+ ...options,
+ retry: options.retry + 1,
+ counter: options.requestNumber,
+ });
+ }
+
+ // get the return variables we need
+ let response = {};
+ let txtResponse = '';
+ let body = '';
+ try {
+ body = JSON.parse(result.body);
+ } catch (e) {
+ log.error(
+ `loki_app_dot_net:::sendViaOnion #${
+ options.requestNumber
+ } - Cant decode JSON body`,
+ result.body
+ );
+ }
+ // result.status has the http response code
+ txtResponse = JSON.stringify(body);
+ response = body;
+ response.headers = result.headers;
+
+ return { result, txtResponse, response };
+};
+
+const sendToProxy = async (srvPubKey, endpoint, fetchOptions, options = {}) => {
+ if (!srvPubKey) {
+ log.error(
+ 'loki_app_dot_net:::sendToProxy - called without a server public key'
+ );
+ return {};
}
const payloadObj = {
body: fetchOptions.body, // might need to b64 if binary...
endpoint,
method: fetchOptions.method,
- headers: fetchOptions.headers,
+ // safety issue with file server, just safer to have this
+ headers: fetchOptions.headers || {},
};
// from https://github.com/sindresorhus/is-stream/blob/master/index.js
@@ -87,7 +212,7 @@ const sendToProxy = async (
log.warn('proxy random snode pool is not ready, retrying 10s', endpoint);
// no nodes in the pool yet, give it some time and retry
await timeoutDelay(1000);
- return sendToProxy(srvPubKey, endpoint, pFetchOptions, options);
+ return sendToProxy(srvPubKey, endpoint, fetchOptions, options);
}
const url = `https://${randSnode.ip}:${randSnode.port}/file_proxy`;
@@ -98,7 +223,10 @@ const sendToProxy = async (
payloadObj.body = false; // free memory
// make temporary key for this request/response
- const ephemeralKey = await libsignal.Curve.async.generateKeyPair();
+ // async maybe preferable to avoid cpu spikes
+ // tho I think sync might be more apt in certain cases here...
+ // like sending
+ const ephemeralKey = await libloki.crypto.generateEphemeralKeyPair();
// mix server pub key with our priv key
const symKey = await libsignal.Curve.async.calculateAgreement(
@@ -257,6 +385,21 @@ const serverRequest = async (endpoint, options = {}) => {
const host = url.host.toLowerCase();
// log.info('host', host, FILESERVER_HOSTS);
if (
+ window.lokiFeatureFlags.useFileOnionRequests &&
+ FILESERVER_HOSTS.includes(host)
+ ) {
+ mode = 'sendViaOnion';
+ // url.search automatically includes the ? part
+ // const search = url.search || '';
+ // strip first slash
+ // const endpointWithQS = `${url.pathname}${search}`.replace(/^\//, '');
+ ({ response, txtResponse, result } = await sendViaOnion(
+ srvPubKey,
+ url,
+ fetchOptions,
+ options
+ ));
+ } else if (
window.lokiFeatureFlags.useSnodeProxy &&
FILESERVER_HOSTS.includes(host)
) {
@@ -317,6 +460,14 @@ const serverRequest = async (endpoint, options = {}) => {
err: e,
};
}
+
+ if (!result) {
+ return {
+ err: 'noResult',
+ response,
+ };
+ }
+
// if it's a response style with a meta
if (result.status !== 200) {
if (!forceFreshToken && (!response.meta || response.meta.code === 401)) {
@@ -418,6 +569,66 @@ class LokiAppDotNetServerAPI {
this.channels.splice(i, 1);
}
+ // set up pubKey & pubKeyHex properties
+ // optionally called for mainly file server comms
+ getPubKeyForUrl() {
+ // Hard coded
+ let pubKeyAB;
+ if (urlPubkeyMap) {
+ pubKeyAB = window.Signal.Crypto.base64ToArrayBuffer(
+ urlPubkeyMap[this.baseServerUrl]
+ );
+ }
+ // else will fail validation later
+
+ // if in proxy mode, don't allow "file-dev."...
+ // it only supports "file."... host.
+ if (
+ window.lokiFeatureFlags.useSnodeProxy &&
+ !window.lokiFeatureFlags.useOnionRequests
+ ) {
+ pubKeyAB = window.Signal.Crypto.base64ToArrayBuffer(
+ LOKIFOUNDATION_FILESERVER_PUBKEY
+ );
+ }
+
+ // do we have their pubkey locally?
+ // FIXME: this._server won't be set yet...
+ // can't really do this for the file server because we'll need the key
+ // before we can communicate with lsrpc
+ /*
+ // get remote pubKey
+ this._server.serverRequest('loki/v1/public_key').then(keyRes => {
+ // we don't need to delay to protect identity because the token request
+ // should only be done over lokinet-lite
+ this.delayToken = true;
+ if (keyRes.err || !keyRes.response || !keyRes.response.data) {
+ if (keyRes.err) {
+ log.error(`Error ${keyRes.err}`);
+ }
+ } else {
+ // store it
+ this.pubKey = dcodeIO.ByteBuffer.wrap(
+ keyRes.response.data,
+ 'base64'
+ ).toArrayBuffer();
+ // write it to a file
+ }
+ });
+ */
+
+ // now that key is loaded, lets verify
+ if (pubKeyAB && pubKeyAB.byteLength && pubKeyAB.byteLength !== 33) {
+ log.error('FILESERVER PUBKEY is invalid, length:', pubKeyAB.byteLength);
+ process.exit(1);
+ }
+
+ this.pubKey = pubKeyAB;
+ this.pubKeyHex = StringView.arrayBufferToHex(pubKeyAB);
+
+ return pubKeyAB;
+ }
+
async setProfileName(profileName) {
// when we add an annotation, may need this
/*
diff --git a/js/modules/loki_file_server_api.js b/js/modules/loki_file_server_api.js
index 13ca54800..241741cb7 100644
--- a/js/modules/loki_file_server_api.js
+++ b/js/modules/loki_file_server_api.js
@@ -8,49 +8,11 @@ const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
const DEVICE_MAPPING_USER_ANNOTATION_TYPE =
'network.loki.messenger.devicemapping';
-// const LOKIFOUNDATION_DEVFILESERVER_PUBKEY =
-// 'BSZiMVxOco/b3sYfaeyiMWv/JnqokxGXkHoclEx8TmZ6';
-const LOKIFOUNDATION_FILESERVER_PUBKEY =
- 'BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc';
-
// can have multiple of these instances as each user can have a
// different home server
class LokiFileServerInstance {
constructor(ourKey) {
this.ourKey = ourKey;
-
- // do we have their pubkey locally?
- /*
- // get remote pubKey
- this._server.serverRequest('loki/v1/public_key').then(keyRes => {
- // we don't need to delay to protect identity because the token request
- // should only be done over lokinet-lite
- this.delayToken = true;
- if (keyRes.err || !keyRes.response || !keyRes.response.data) {
- if (keyRes.err) {
- log.error(`Error ${keyRes.err}`);
- }
- } else {
- // store it
- this.pubKey = dcodeIO.ByteBuffer.wrap(
- keyRes.response.data,
- 'base64'
- ).toArrayBuffer();
- // write it to a file
- }
- });
- */
- // Hard coded
- this.pubKey = window.Signal.Crypto.base64ToArrayBuffer(
- LOKIFOUNDATION_FILESERVER_PUBKEY
- );
- if (this.pubKey.byteLength && this.pubKey.byteLength !== 33) {
- log.error(
- 'FILESERVER PUBKEY is invalid, length:',
- this.pubKey.byteLength
- );
- process.exit(1);
- }
}
// FIXME: this is not file-server specific
@@ -65,9 +27,8 @@ class LokiFileServerInstance {
} else {
this._server = new LokiAppDotNetAPI(this.ourKey, serverUrl);
}
-
- // configure proxy
- this._server.pubKey = this.pubKey;
+ // make sure pubKey & pubKeyHex are set in _server
+ this.pubKey = this._server.getPubKeyForUrl();
if (options !== undefined && options.skipToken) {
return;
@@ -80,6 +41,7 @@ class LokiFileServerInstance {
log.error('You are blacklisted form this home server');
}
}
+
async getUserDeviceMapping(pubKey) {
const annotations = await this._server.getUserAnnotations(pubKey);
const deviceMapping = annotations.find(
@@ -333,7 +295,5 @@ class LokiFileServerFactoryAPI {
return thisServer;
}
}
-// smuggle some data out of this joint (for expire.js/version upgrade check)
-LokiFileServerFactoryAPI.secureRpcPubKey = LOKIFOUNDATION_FILESERVER_PUBKEY;
module.exports = LokiFileServerFactoryAPI;
diff --git a/js/modules/loki_rpc.js b/js/modules/loki_rpc.js
index f1cabf18f..dfbce16ae 100644
--- a/js/modules/loki_rpc.js
+++ b/js/modules/loki_rpc.js
@@ -11,88 +11,257 @@ const snodeHttpsAgent = new https.Agent({
const endpointBase = '/storage_rpc/v1';
-// Request index for debugging
-let onionReqIdx = 0;
-
-const encryptForNode = async (node, payloadStr) => {
- const textEncoder = new TextEncoder();
- const plaintext = textEncoder.encode(payloadStr);
-
- return libloki.crypto.encryptForPubkey(node.pubkey_x25519, plaintext);
-};
-
// Returns the actual ciphertext, symmetric key that will be used
// for decryption, and an ephemeral_key to send to the next hop
-const encryptForDestination = async (node, payload) => {
- // Do we still need "headers"?
- const reqStr = JSON.stringify({ body: payload, headers: '' });
+const encryptForPubKey = async (pubKeyX25519hex, reqObj) => {
+ const reqStr = JSON.stringify(reqObj);
+
+ const textEncoder = new TextEncoder();
+ const plaintext = textEncoder.encode(reqStr);
- return encryptForNode(node, reqStr);
+ return libloki.crypto.encryptForPubkey(pubKeyX25519hex, plaintext);
};
// `ctx` holds info used by `node` to relay further
-const encryptForRelay = async (node, nextNode, ctx) => {
+const encryptForRelay = async (relayX25519hex, destination, ctx) => {
+ // ctx contains: ciphertext, symmetricKey, ephemeralKey
const payload = ctx.ciphertext;
- const reqJson = {
+ if (!destination.host && !destination.destination) {
+ log.warn(`loki_rpc::encryptForRelay - no destination`, destination);
+ }
+
+ const reqObj = {
+ ...destination,
ciphertext: dcodeIO.ByteBuffer.wrap(payload).toString('base64'),
ephemeral_key: StringView.arrayBufferToHex(ctx.ephemeralKey),
- destination: nextNode.pubkey_ed25519,
};
- const reqStr = JSON.stringify(reqJson);
-
- return encryptForNode(node, reqStr);
+ return encryptForPubKey(relayX25519hex, reqObj);
};
-const BAD_PATH = 'bad_path';
+const makeGuardPayload = guardCtx => {
+ const ciphertextBase64 = dcodeIO.ByteBuffer.wrap(
+ guardCtx.ciphertext
+ ).toString('base64');
-// May return false BAD_PATH, indicating that we should try a new
-const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => {
- const ctxes = [await encryptForDestination(targetNode, plaintext)];
+ const guardPayloadObj = {
+ ciphertext: ciphertextBase64,
+ ephemeral_key: StringView.arrayBufferToHex(guardCtx.ephemeralKey),
+ };
+ return guardPayloadObj;
+};
+
+// we just need the targetNode.pubkey_ed25519 for the encryption
+// targetPubKey is ed25519 if snode is the target
+const makeOnionRequest = async (
+ nodePath,
+ destCtx,
+ targetED25519Hex,
+ finalRelayOptions = false,
+ id = ''
+) => {
+ const ctxes = [destCtx];
// from (3) 2 to 0
const firstPos = nodePath.length - 1;
for (let i = firstPos; i > -1; i -= 1) {
- // this nodePath points to the previous (i + 1) context
- ctxes.push(
+ let dest;
+ const relayingToFinalDestination = i === 0; // if last position
+
+ if (relayingToFinalDestination && finalRelayOptions) {
+ dest = {
+ host: finalRelayOptions.host,
+ target: '/loki/v1/lsrpc',
+ method: 'POST',
+ };
+ } else {
+ // set x25519 if destination snode
+ let pubkeyHex = targetED25519Hex; // relayingToFinalDestination
+ // or ed25519 snode destination
+ if (!relayingToFinalDestination) {
+ pubkeyHex = nodePath[i + 1].pubkey_ed25519;
+ if (!pubkeyHex) {
+ log.error(
+ `loki_rpc:::makeOnionRequest ${id} - no ed25519 for`,
+ nodePath[i + 1],
+ 'path node',
+ i + 1
+ );
+ }
+ }
+ // destination takes a hex key
+ dest = {
+ destination: pubkeyHex,
+ };
+ }
+ try {
// eslint-disable-next-line no-await-in-loop
- await encryptForRelay(
- nodePath[i],
- i === firstPos ? targetNode : nodePath[i + 1],
+ const ctx = await encryptForRelay(
+ nodePath[i].pubkey_x25519,
+ dest,
ctxes[ctxes.length - 1]
- )
- );
+ );
+ ctxes.push(ctx);
+ } catch (e) {
+ log.error(
+ `loki_rpc:::makeOnionRequest ${id} - encryptForRelay failure`,
+ e.code,
+ e.message
+ );
+ throw e;
+ }
}
const guardCtx = ctxes[ctxes.length - 1]; // last ctx
- const ciphertextBase64 = dcodeIO.ByteBuffer.wrap(
- guardCtx.ciphertext
- ).toString('base64');
+ const payloadObj = makeGuardPayload(guardCtx);
- const payload = {
- ciphertext: ciphertextBase64,
- ephemeral_key: StringView.arrayBufferToHex(guardCtx.ephemeralKey),
- };
+ // all these requests should use AesGcm
+ return payloadObj;
+};
- const fetchOptions = {
+// finalDestOptions is an object
+// FIXME: internally track reqIdx, not externally
+const sendOnionRequest = async (
+ reqIdx,
+ nodePath,
+ destX25519Any,
+ finalDestOptions,
+ finalRelayOptions = false,
+ lsrpcIdx
+) => {
+ if (!destX25519Any) {
+ log.error('loki_rpc::sendOnionRequest - no destX25519Any given');
+ return {};
+ }
+
+ // loki-storage may need this to function correctly
+ // but ADN calls will not always have a body
+ /*
+ if (!finalDestOptions.body) {
+ finalDestOptions.body = '';
+ }
+ */
+
+ let id = '';
+ if (lsrpcIdx !== undefined) {
+ id += `${lsrpcIdx}=>`;
+ }
+ if (reqIdx !== undefined) {
+ id += `${reqIdx}`;
+ }
+
+ // get destination pubkey in array buffer format
+ let destX25519hex = destX25519Any;
+ if (typeof destX25519hex !== 'string') {
+ // convert AB to hex
+ destX25519hex = StringView.arrayBufferToHex(destX25519Any);
+ }
+
+ // safely build destination
+ let targetEd25519hex;
+ if (finalDestOptions) {
+ if (finalDestOptions.destination_ed25519_hex) {
+ // snode destination
+ targetEd25519hex = finalDestOptions.destination_ed25519_hex;
+ // eslint-disable-next-line no-param-reassign
+ delete finalDestOptions.destination_ed25519_hex;
+ }
+ // else it's lsrpc...
+ } else {
+ // eslint-disable-next-line no-param-reassign
+ finalDestOptions = {};
+ log.warn(`loki_rpc::sendOnionRequest ${id} - no finalDestOptions`);
+ return {};
+ }
+
+ const options = finalDestOptions; // lint
+ // do we need this?
+ if (options.headers === undefined) {
+ options.headers = '';
+ }
+
+ let destCtx;
+ try {
+ destCtx = await encryptForPubKey(destX25519hex, options);
+ } catch (e) {
+ log.error(
+ `loki_rpc::sendOnionRequest ${id} - encryptForPubKey failure [`,
+ e.code,
+ e.message,
+ '] destination X25519',
+ destX25519hex.substr(0, 32),
+ '...',
+ destX25519hex.substr(32),
+ 'options',
+ options
+ );
+ throw e;
+ }
+
+ const payloadObj = await makeOnionRequest(
+ nodePath,
+ destCtx,
+ targetEd25519hex,
+ finalRelayOptions,
+ id
+ );
+
+ const guardFetchOptions = {
method: 'POST',
- body: JSON.stringify(payload),
+ body: JSON.stringify(payloadObj),
+ // we are talking to a snode...
+ agent: snodeHttpsAgent,
};
- const url = `https://${nodePath[0].ip}:${nodePath[0].port}/onion_req`;
-
- // we only proxy to snodes...
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
- const response = await nodeFetch(url, fetchOptions);
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
+ const guardUrl = `https://${nodePath[0].ip}:${nodePath[0].port}/onion_req`;
+ const response = await nodeFetch(guardUrl, guardFetchOptions);
- return processOnionResponse(reqIdx, response, ctxes[0].symmetricKey, true);
+ return processOnionResponse(reqIdx, response, destCtx.symmetricKey, true);
};
+const sendOnionRequestSnodeDest = async (
+ reqIdx,
+ nodePath,
+ targetNode,
+ plaintext
+) =>
+ sendOnionRequest(reqIdx, nodePath, targetNode.pubkey_x25519, {
+ destination_ed25519_hex: targetNode.pubkey_ed25519,
+ body: plaintext,
+ });
+
+// need relay node's pubkey_x25519_hex
+// always the same target: /loki/v1/lsrpc
+const sendOnionRequestLsrpcDest = async (
+ reqIdx,
+ nodePath,
+ destX25519Any,
+ host,
+ payloadObj,
+ lsrpcIdx = 0
+) =>
+ sendOnionRequest(
+ reqIdx,
+ nodePath,
+ destX25519Any,
+ payloadObj,
+ { host },
+ lsrpcIdx
+ );
+
+const BAD_PATH = 'bad_path';
+
// 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) => {
+// May return false BAD_PATH, indicating that we should try a new path.
+const processOnionResponse = async (
+ reqIdx,
+ response,
+ sharedKey,
+ useAesGcm,
+ debug
+) => {
// FIXME: 401/500 handling?
// detect SNode is not ready (not in swarm; not done syncing)
@@ -115,16 +284,26 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
if (response.status !== 200) {
log.warn(
- `(${reqIdx}) [path] fetch unhandled error code: ${response.status}`
+ `(${reqIdx}) [path] lokiRpc::processOnionResponse - fetch unhandled error code: ${
+ response.status
+ }`
);
return false;
}
const ciphertext = await response.text();
if (!ciphertext) {
- log.warn(`(${reqIdx}) [path]: Target node return empty ciphertext`);
+ log.warn(
+ `(${reqIdx}) [path] lokiRpc::processOnionResponse - Target node return empty ciphertext`
+ );
return false;
}
+ if (debug) {
+ log.debug(
+ `(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertext`,
+ ciphertext
+ );
+ }
let plaintext;
let ciphertextBuffer;
@@ -134,22 +313,52 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
'base64'
).toArrayBuffer();
+ if (debug) {
+ log.debug(
+ `(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`,
+ StringView.arrayBufferToHex(ciphertextBuffer),
+ 'useAesGcm',
+ useAesGcm
+ );
+ }
+
const decryptFn = useAesGcm
- ? window.libloki.crypto.DecryptGCM
- : window.libloki.crypto.DHDecrypt;
+ ? libloki.crypto.DecryptGCM
+ : libloki.crypto.DHDecrypt;
- const plaintextBuffer = await decryptFn(sharedKey, ciphertextBuffer);
+ const plaintextBuffer = await decryptFn(sharedKey, ciphertextBuffer, debug);
+ if (debug) {
+ log.debug(
+ 'lokiRpc::processOnionResponse - plaintextBuffer',
+ plaintextBuffer.toString()
+ );
+ }
const textDecoder = new TextDecoder();
plaintext = textDecoder.decode(plaintextBuffer);
} catch (e) {
- log.error(`(${reqIdx}) [path] decode error`);
+ log.error(
+ `(${reqIdx}) [path] lokiRpc::processOnionResponse - decode error`,
+ e.code,
+ e.message
+ );
+ log.error(
+ `(${reqIdx}) [path] lokiRpc::processOnionResponse - symKey`,
+ StringView.arrayBufferToHex(sharedKey)
+ );
if (ciphertextBuffer) {
- log.error(`(${reqIdx}) [path] ciphertextBuffer`, ciphertextBuffer);
+ log.error(
+ `(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`,
+ StringView.arrayBufferToHex(ciphertextBuffer)
+ );
}
return false;
}
+ if (debug) {
+ log.debug('lokiRpc::processOnionResponse - plaintext', plaintext);
+ }
+
try {
const jsonRes = JSON.parse(plaintext);
// emulate nodeFetch response...
@@ -158,13 +367,22 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
const res = JSON.parse(jsonRes.body);
return res;
} catch (e) {
- log.error(`(${reqIdx}) [path] parse error json: `, jsonRes.body);
+ log.error(
+ `(${reqIdx}) [path] lokiRpc::processOnionResponse - parse error inner json: `,
+ jsonRes.body
+ );
}
return false;
};
return jsonRes;
} catch (e) {
- log.error('[path] parse error', e.code, e.message, `json:`, plaintext);
+ log.error(
+ `(${reqIdx}) [path] lokiRpc::processOnionResponse - parse error outer json`,
+ e.code,
+ e.message,
+ `json:`,
+ plaintext
+ );
return false;
}
};
@@ -206,7 +424,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
const snPubkeyHex = StringView.hexToArrayBuffer(targetNode.pubkey_x25519);
- const myKeys = await window.libloki.crypto.generateEphemeralKeyPair();
+ const myKeys = await libloki.crypto.generateEphemeralKeyPair();
const symmetricKey = await libsignal.Curve.async.calculateAgreement(
snPubkeyHex,
@@ -217,7 +435,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
const body = JSON.stringify(options);
const plainText = textEncoder.encode(body);
- const ivAndCiphertext = await window.libloki.crypto.DHEncrypt(
+ const ivAndCiphertext = await libloki.crypto.DHEncrypt(
symmetricKey,
plainText
);
@@ -279,6 +497,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
// grab a fresh random one
return sendToProxy(options, targetNode, pRetryNumber);
}
+ // 502 is "Next node not found"
// detect SNode is not ready (not in swarm; not done syncing)
// 503 can be proxy target or destination in pre 2.0.3
@@ -364,7 +583,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
'base64'
).toArrayBuffer();
- const plaintextBuffer = await window.libloki.crypto.DHDecrypt(
+ const plaintextBuffer = await libloki.crypto.DHDecrypt(
symmetricKey,
ciphertextBuffer
);
@@ -460,6 +679,7 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
// Wrong PoW difficulty
if (response.status === 432) {
const result = await response.json();
+ log.error(`lokirpc:::lokiFetch ${type} - WRONG POW`, result);
throw new textsecure.WrongDifficultyError(result.difficulty);
}
@@ -480,11 +700,10 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
// Get a path excluding `targetNode`:
// eslint-disable-next-line no-await-in-loop
const path = await lokiSnodeAPI.getOnionPath(targetNode);
- const thisIdx = onionReqIdx;
- onionReqIdx += 1;
+ const thisIdx = window.lokiSnodeAPI.assignOnionRequestNumber();
// eslint-disable-next-line no-await-in-loop
- const result = await sendOnionRequest(
+ const result = await sendOnionRequestSnodeDest(
thisIdx,
path,
targetNode,
@@ -640,4 +859,5 @@ const lokiRpc = (
module.exports = {
lokiRpc,
+ sendOnionRequestLsrpcDest,
};
diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js
index 0ad0cd8f5..d6d030493 100644
--- a/js/modules/loki_snode_api.js
+++ b/js/modules/loki_snode_api.js
@@ -23,8 +23,17 @@ const compareSnodes = (current, search) =>
// just get the filtered list
async function tryGetSnodeListFromLokidSeednode(
- seedNodes = [...window.seedNodeList]
+ seedNodes = window.seedNodeList
) {
+ if (!seedNodes.length) {
+ log.error(
+ `loki_snodes:::tryGetSnodeListFromLokidSeednode - no seedNodes given`,
+ seedNodes,
+ 'window',
+ window.seedNodeList
+ );
+ return [];
+ }
// Removed limit until there is a way to get snode info
// for individual nodes (needed for guard nodes); this way
// we get all active nodes
@@ -42,6 +51,13 @@ async function tryGetSnodeListFromLokidSeednode(
Math.floor(Math.random() * seedNodes.length),
1
)[0];
+ if (!seedNode) {
+ log.error(
+ `loki_snodes:::tryGetSnodeListFromLokidSeednode - seedNode selection failure - seedNodes`,
+ seedNodes
+ );
+ return [];
+ }
let snodes = [];
try {
const getSnodesFromSeedUrl = async urlObj => {
@@ -53,6 +69,30 @@ async function tryGetSnodeListFromLokidSeednode(
{}, // Options
'/json_rpc' // Seed request endpoint
);
+ if (!response) {
+ log.error(
+ `loki_snodes:::tryGetSnodeListFromLokidSeednode - invalid response from seed ${urlObj.toString()}:`,
+ response
+ );
+ return [];
+ }
+
+ // should we try to JSON.parse this?
+ if (typeof response === 'string') {
+ log.error(
+ `loki_snodes:::tryGetSnodeListFromLokidSeednode - invalid string response from seed ${urlObj.toString()}:`,
+ response
+ );
+ return [];
+ }
+
+ if (!response.result) {
+ log.error(
+ `loki_snodes:::tryGetSnodeListFromLokidSeednode - invalid result from seed ${urlObj.toString()}:`,
+ response
+ );
+ return [];
+ }
// Filter 0.0.0.0 nodes which haven't submitted uptime proofs
return response.result.service_node_states.filter(
snode => snode.public_ip !== '0.0.0.0'
@@ -72,6 +112,13 @@ async function tryGetSnodeListFromLokidSeednode(
);
}
}
+ if (snodes.length) {
+ log.info(
+ `loki_snodes:::tryGetSnodeListFromLokidSeednode - got ${
+ snodes.length
+ } service nodes from seed`
+ );
+ }
return snodes;
} catch (e) {
log.warn(
@@ -87,9 +134,18 @@ async function tryGetSnodeListFromLokidSeednode(
}
async function getSnodeListFromLokidSeednode(
- seedNodes = [...window.seedNodeList],
+ seedNodes = window.seedNodeList,
retries = 0
) {
+ if (!seedNodes.length) {
+ log.error(
+ `loki_snodes:::getSnodeListFromLokidSeednode - no seedNodes given`,
+ seedNodes,
+ 'window',
+ window.seedNodeList
+ );
+ return [];
+ }
let snodes = [];
try {
snodes = await tryGetSnodeListFromLokidSeednode(seedNodes);
@@ -129,6 +185,12 @@ class LokiSnodeAPI {
this.onionPaths = [];
this.guardNodes = [];
+ this.onionRequestCounter = 0; // Request index for debugging
+ }
+
+ assignOnionRequestNumber() {
+ this.onionRequestCounter += 1;
+ return this.onionRequestCounter;
}
async getRandomSnodePool() {
@@ -202,7 +264,7 @@ class LokiSnodeAPI {
// FIXME: handle rejections
let nodePool = await this.getRandomSnodePool();
if (nodePool.length === 0) {
- log.error(`Could not select guarn nodes: node pool is empty`);
+ log.error(`Could not select guard nodes: node pool is empty`);
return [];
}
@@ -213,7 +275,7 @@ class LokiSnodeAPI {
const DESIRED_GUARD_COUNT = 3;
if (shuffled.length < DESIRED_GUARD_COUNT) {
log.error(
- `Could not select guarn nodes: node pool is not big enough, pool size ${
+ `Could not select guard nodes: node pool is not big enough, pool size ${
shuffled.length
}, need ${DESIRED_GUARD_COUNT}, attempting to refresh randomPool`
);
@@ -222,7 +284,7 @@ class LokiSnodeAPI {
shuffled = _.shuffle(nodePool);
if (shuffled.length < DESIRED_GUARD_COUNT) {
log.error(
- `Could not select guarn nodes: node pool is not big enough, pool size ${
+ `Could not select guard nodes: node pool is not big enough, pool size ${
shuffled.length
}, need ${DESIRED_GUARD_COUNT}, failing...`
);
@@ -278,12 +340,15 @@ class LokiSnodeAPI {
`Must have at least 2 good onion paths, actual: ${goodPaths.length}`
);
await this.buildNewOnionPaths();
+ // should we add a delay? buildNewOnionPaths should act as one
+ // reload goodPaths now
+ return this.getOnionPath(toExclude);
}
const paths = _.shuffle(goodPaths);
if (!toExclude) {
- return paths[0];
+ return paths[0].path;
}
// Select a path that doesn't contain `toExclude`
@@ -294,6 +359,19 @@ class LokiSnodeAPI {
if (otherPaths.length === 0) {
// This should never happen!
+ // well it did happen, should we
+ // await this.buildNewOnionPaths();
+ // and restart call?
+ log.error(
+ `loki_snode_api::getOnionPath - no paths without`,
+ toExclude.pubkey_ed25519,
+ 'path count',
+ paths.length,
+ 'goodPath count',
+ goodPaths.length,
+ 'paths',
+ paths
+ );
throw new Error('No onion paths available after filtering');
}
@@ -569,7 +647,17 @@ class LokiSnodeAPI {
);
}
- async refreshRandomPool(seedNodes = [...window.seedNodeList]) {
+ async refreshRandomPool(seedNodes = window.seedNodeList) {
+ if (!seedNodes.length) {
+ if (!window.seedNodeList || !window.seedNodeList.length) {
+ log.error(
+ `loki_snodes:::refreshRandomPool - seedNodeList has not been loaded yet`
+ );
+ return [];
+ }
+ // eslint-disable-next-line no-param-reassign
+ seedNodes = window.seedNodeList;
+ }
return primitives.allowOnlyOneAtATime('refreshRandomPool', async () => {
// are we running any _getAllVerionsForRandomSnodePool
if (this.stopGetAllVersionPromiseControl !== false) {
diff --git a/preload.js b/preload.js
index 2b57298d6..301537a5d 100644
--- a/preload.js
+++ b/preload.js
@@ -416,6 +416,7 @@ window.lokiFeatureFlags = {
privateGroupChats: true,
useSnodeProxy: !process.env.USE_STUBBED_NETWORK,
useOnionRequests: true,
+ useFileOnionRequests: false,
onionRequestHops: 1,
};
diff --git a/test/index.html b/test/index.html
index b503f8b23..a5ffa28a3 100644
--- a/test/index.html
+++ b/test/index.html
@@ -492,13 +492,14 @@
-
+
+