From 1a70e8038e8bff6e15352e6305ab2e597c526dc4 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 7 Apr 2020 20:14:50 +1000 Subject: [PATCH 1/8] Revert test line --- ts/components/conversation/ResetSessionNotification.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/components/conversation/ResetSessionNotification.tsx b/ts/components/conversation/ResetSessionNotification.tsx index 92ab7c654..2d0394df3 100644 --- a/ts/components/conversation/ResetSessionNotification.tsx +++ b/ts/components/conversation/ResetSessionNotification.tsx @@ -14,7 +14,6 @@ export class ResetSessionNotification extends React.Component { return (
{i18n(sessionResetMessageKey)} - the fairies are coming home
); } From 8992234dd7ffba47ff3bde7e70260e1534f0da52 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 22 Apr 2020 16:10:53 +1000 Subject: [PATCH 2/8] lns mapping method rewrite --- js/modules/loki_snode_api.js | 146 ++++++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 46 deletions(-) diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index 7d120ff4e..98f078473 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -756,73 +756,127 @@ class LokiSnodeAPI { } } - async getLnsMapping(lnsName) { + async getLnsMapping(lnsName, timeout) { + // Returns { pubkey, error } + // pubkey is: + // null when there is confirmed to be no LNS mapping + // undefined when unconfirmee + // string when found + // timeout parameter optional (ms) + + // How many nodes to fetch data from? + const numRequests = 5; + + // How many nodes must have the same response value? + const numRequiredConfirms = 3; + + let ciphertextHex; + let pubkey; + let error; + const _ = window.Lodash; const input = Buffer.from(lnsName); - const output = await window.blake2b(input); - const nameHash = dcodeIO.ByteBuffer.wrap(output).toString('base64'); + // Return value of null represents a timeout + const timeoutResponse = { timedOut: true }; + const timeoutPromise = (cb, interval) => () => new Promise(resolve => setTimeout(() => cb(resolve), interval)); + const onTimeout = timeoutPromise(resolve => resolve(timeoutResponse), timeout || Number.MAX_SAFE_INTEGER); + // Get nodes capable of doing LNS - const lnsNodes = this.getNodesMinVersion('2.0.3'); - // randomPool should already be shuffled - // lnsNodes = _.shuffle(lnsNodes); + let lnsNodes = await this.getNodesMinVersion(window.CONSTANTS.LNS_CAPABLE_NODES_VERSION); + lnsNodes = _.shuffle(lnsNodes); - // Loop until 3 confirmations + // Enough nodes? + if (lnsNodes.length < numRequiredConfirms) { + error = window.i18n('lnsTooFewNodes'); + return {pubkey, error}; + } - // We don't trust any single node, so we accumulate - // answers here and select a dominating answer - const allResults = []; - let ciphertextHex = null; + const confirmedNodes = []; - 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); + let cipherResolve; + // eslint-disable-next-line no-unused-vars + const cipherPromise = () => new Promise((resolve, _reject) => { + cipherResolve = resolve; + }); - // eslint-disable-next-line no-await-in-loop - const results = await Promise.all( - nodes.map(node => this._requestLnsMapping(node, nameHash)) + const decryptHex = async cipherHex => { + const ciphertext = new Uint8Array( + StringView.hexToArrayBuffer(cipherHex) ); - 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 res = await window.decryptLnsEntry(lnsName, ciphertext); + const pubicKey = StringView.arrayBufferToHex(res); - const [winner, count] = _.maxBy( - _.entries(_.countBy(allResults)), - x => x[1] - ); + return pubicKey; + } + + const fetchFromNode = async node => { + const res = await this._requestLnsMapping(node, nameHash); + + // Do validation + if ( + res && + res.result && + res.result.status === 'OK' + ) { + const hasMapping = res.result.entries && res.result.entries.length > 0; + + const resValue = hasMapping + ? res.result.entries[0].encrypted_value + : null; + + confirmedNodes.push(resValue); + + if (confirmedNodes.length >= numRequiredConfirms) { + if (ciphertextHex){ + // result already found, dont worry + return; + } + + const [winner, count] = _.maxBy( + _.entries(_.countBy(confirmedNodes)), + x => x[1] + ); + + if (count >= numRequiredConfirms) { + ciphertextHex = winner === String(null) + ? null + : winner; - if (count >= 3) { - // eslint-disable-next-lint prefer-destructuring - ciphertextHex = winner; + // null represents no LNS mapping + if (ciphertextHex === null){ + error = window.i18n('lnsMappingNotFound'); + } + + cipherResolve({ciphertextHex}); + } + } } } - const ciphertext = new Uint8Array( - StringView.hexToArrayBuffer(ciphertextHex) - ); + const nodes = lnsNodes.splice(0, numRequests); + + // Start fetching from nodes + Promise.resolve(nodes.map(async node => fetchFromNode(node))); - const res = await window.decryptLnsEntry(lnsName, ciphertext); + // Timeouts (optional parameter) + // Wait for cipher to be found; race against timeout + const { timedOut } = await Promise.race([cipherPromise, onTimeout].map(f => f())); - const pubkey = StringView.arrayBufferToHex(res); + if (timedOut) { + error = window.i18n('lnsLookupTimeout'); + return { pubkey, error }; + } - return pubkey; + pubkey = ciphertextHex === null + ? null + : await decryptHex(ciphertextHex); + + return {pubkey, error}; } // get snodes for pubkey from random snode From d78a49d6896eec33ac58bf750f9ddc6f7eb2e677 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 22 Apr 2020 17:10:41 +1000 Subject: [PATCH 3/8] Constants --- js/modules/loki_snode_api.js | 66 +++++++++++++++++------------------- preload.js | 32 +++++++++++------ 2 files changed, 54 insertions(+), 44 deletions(-) diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index 98f078473..e4b739854 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -760,13 +760,13 @@ class LokiSnodeAPI { // Returns { pubkey, error } // pubkey is: // null when there is confirmed to be no LNS mapping - // undefined when unconfirmee + // undefined when unconfirmed // string when found // timeout parameter optional (ms) // How many nodes to fetch data from? const numRequests = 5; - + // How many nodes must have the same response value? const numRequiredConfirms = 3; @@ -782,47 +782,47 @@ class LokiSnodeAPI { // Return value of null represents a timeout const timeoutResponse = { timedOut: true }; - const timeoutPromise = (cb, interval) => () => new Promise(resolve => setTimeout(() => cb(resolve), interval)); - const onTimeout = timeoutPromise(resolve => resolve(timeoutResponse), timeout || Number.MAX_SAFE_INTEGER); + const timeoutPromise = (cb, interval) => () => + new Promise(resolve => setTimeout(() => cb(resolve), interval)); + const onTimeout = timeoutPromise( + resolve => resolve(timeoutResponse), + timeout || Number.MAX_SAFE_INTEGER + ); // Get nodes capable of doing LNS - let lnsNodes = await this.getNodesMinVersion(window.CONSTANTS.LNS_CAPABLE_NODES_VERSION); + let lnsNodes = await this.getNodesMinVersion( + window.CONSTANTS.LNS_CAPABLE_NODES_VERSION + ); lnsNodes = _.shuffle(lnsNodes); // Enough nodes? if (lnsNodes.length < numRequiredConfirms) { error = window.i18n('lnsTooFewNodes'); - return {pubkey, error}; + return { pubkey, error }; } const confirmedNodes = []; let cipherResolve; - // eslint-disable-next-line no-unused-vars - const cipherPromise = () => new Promise((resolve, _reject) => { - cipherResolve = resolve; - }); + const cipherPromise = () => + new Promise(resolve => { + cipherResolve = resolve; + }); const decryptHex = async cipherHex => { - const ciphertext = new Uint8Array( - StringView.hexToArrayBuffer(cipherHex) - ); + const ciphertext = new Uint8Array(StringView.hexToArrayBuffer(cipherHex)); const res = await window.decryptLnsEntry(lnsName, ciphertext); const pubicKey = StringView.arrayBufferToHex(res); return pubicKey; - } - + }; + const fetchFromNode = async node => { const res = await this._requestLnsMapping(node, nameHash); // Do validation - if ( - res && - res.result && - res.result.status === 'OK' - ) { + if (res && res.result && res.result.status === 'OK') { const hasMapping = res.result.entries && res.result.entries.length > 0; const resValue = hasMapping @@ -832,7 +832,7 @@ class LokiSnodeAPI { confirmedNodes.push(resValue); if (confirmedNodes.length >= numRequiredConfirms) { - if (ciphertextHex){ + if (ciphertextHex) { // result already found, dont worry return; } @@ -843,40 +843,38 @@ class LokiSnodeAPI { ); if (count >= numRequiredConfirms) { - ciphertextHex = winner === String(null) - ? null - : winner; + ciphertextHex = winner === String(null) ? null : winner; // null represents no LNS mapping - if (ciphertextHex === null){ + if (ciphertextHex === null) { error = window.i18n('lnsMappingNotFound'); } - cipherResolve({ciphertextHex}); + cipherResolve({ ciphertextHex }); } } } - } + }; const nodes = lnsNodes.splice(0, numRequests); - + // Start fetching from nodes Promise.resolve(nodes.map(async node => fetchFromNode(node))); // Timeouts (optional parameter) // Wait for cipher to be found; race against timeout - const { timedOut } = await Promise.race([cipherPromise, onTimeout].map(f => f())); + const { timedOut } = await Promise.race( + [cipherPromise, onTimeout].map(f => f()) + ); if (timedOut) { error = window.i18n('lnsLookupTimeout'); return { pubkey, error }; } - pubkey = ciphertextHex === null - ? null - : await decryptHex(ciphertextHex); - - return {pubkey, error}; + pubkey = ciphertextHex === null ? null : await decryptHex(ciphertextHex); + + return { pubkey, error }; } // get snodes for pubkey from random snode diff --git a/preload.js b/preload.js index 7178e1f69..8a84536ef 100644 --- a/preload.js +++ b/preload.js @@ -70,17 +70,29 @@ window.isBeforeVersion = (toCheck, baseVersion) => { } }; -window.CONSTANTS = { - MAX_LOGIN_TRIES: 3, - MAX_PASSWORD_LENGTH: 64, - MAX_USERNAME_LENGTH: 20, - MAX_GROUP_NAME_LENGTH: 64, - DEFAULT_PUBLIC_CHAT_URL: appConfig.get('defaultPublicChatServer'), - MAX_CONNECTION_DURATION: 5000, - MAX_MESSAGE_BODY_LENGTH: 64 * 1024, +// eslint-disable-next-line func-names +window.CONSTANTS = new function() { + this.MAX_LOGIN_TRIES = 3; + this.MAX_PASSWORD_LENGTH = 64; + this.MAX_USERNAME_LENGTH = 20; + this.MAX_GROUP_NAME_LENGTH = 64; + this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer'); + this.MAX_CONNECTION_DURATION = 5000; + this.MAX_MESSAGE_BODY_LENGTH = 64 * 1024; // Limited due to the proof-of-work requirement - SMALL_GROUP_SIZE_LIMIT: 10, - NOTIFICATION_ENABLE_TIMEOUT_SECONDS: 10, // number of seconds to turn on notifications after reconnect/start of app + this.SMALL_GROUP_SIZE_LIMIT = 10; + // Number of seconds to turn on notifications after reconnect/start of app + this.NOTIFICATION_ENABLE_TIMEOUT_SECONDS = 10; + this.SESSION_ID_LENGTH = 66; + + // Loki Name System (LNS) + this.LNS_DEFAULT_LOOKUP_TIMEOUT = 6000; + // Minimum nodes version for LNS lookup + this.LNS_CAPABLE_NODES_VERSION = '2.0.3'; + this.LNS_MAX_LENGTH = 64; + // Conforms to naming rules here + // https://loki.network/2020/03/25/loki-name-system-the-facts/ + this.LNS_REGEX = `^[a-zA-Z0-9_]([a-zA-Z0-9_-]{0,${this.LNS_MAX_LENGTH - 2}}[a-zA-Z0-9_]){0,1}$`; }; window.versionInfo = { From 2fde4d7e8019eb9f3967a47b03da738cac9ba617 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 22 Apr 2020 17:19:29 +1000 Subject: [PATCH 4/8] messages.json fixup --- _locales/en/messages.json | 21 ++++++++++++++++++--- preload.js | 5 +++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 94a1fede4..49e362294 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2521,14 +2521,29 @@ "message": "Remove" }, "invalidHexId": { - "message": "Invalid Hex ID", + "message": "Invalid Session ID or LNS Name", "description": - "Error string shown when user type an invalid pubkey hex string" + "Error string shown when user types an invalid pubkey hex string" + }, + "invalidLnsFormat": { + "message": "Invalid LNS Name", + "description": "Error string shown when user types an invalid LNS name" }, "invalidPubkeyFormat": { "message": "Invalid Pubkey Format", "description": "Error string shown when user types an invalid pubkey format" }, + "lnsMappingNotFound": { + "message": "There is no LNS mapping associated with this name", + "description": "Shown in toast if user enters an unknown LNS name" + }, + "lnsLookupTimeout": { + "message": "LNS lookup timed out", + "description": "Shown in toast if user enters an unknown LNS name" + }, + "lnsTooFewNodes": { + "message": "Not enough nodes currently active for LNS lookup" + }, "conversationsTab": { "message": "Conversations", "description": "conversation tab title" @@ -2711,7 +2726,7 @@ "message": "Enter Session ID" }, "pasteSessionIDRecipient": { - "message": "Enter a Session ID" + "message": "Enter a Session ID or LNS name" }, "usersCanShareTheir...": { "message": diff --git a/preload.js b/preload.js index 8a84536ef..6d3eb2977 100644 --- a/preload.js +++ b/preload.js @@ -92,8 +92,9 @@ window.CONSTANTS = new function() { this.LNS_MAX_LENGTH = 64; // Conforms to naming rules here // https://loki.network/2020/03/25/loki-name-system-the-facts/ - this.LNS_REGEX = `^[a-zA-Z0-9_]([a-zA-Z0-9_-]{0,${this.LNS_MAX_LENGTH - 2}}[a-zA-Z0-9_]){0,1}$`; -}; + this.LNS_REGEX = `^[a-zA-Z0-9_]([a-zA-Z0-9_-]{0,${this.LNS_MAX_LENGTH - + 2}}[a-zA-Z0-9_]){0,1}$`; +}(); window.versionInfo = { environment: window.getEnvironment(), From cd5a2f75dee97c7e94c85dfc2c4352491c399966 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 22 Apr 2020 18:02:30 +1000 Subject: [PATCH 5/8] Fix MAX_SAFE_INTEGER on timeout --- js/modules/loki_snode_api.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index e4b739854..4cd3f5a3f 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -780,20 +780,18 @@ class LokiSnodeAPI { const output = await window.blake2b(input); const nameHash = dcodeIO.ByteBuffer.wrap(output).toString('base64'); - // Return value of null represents a timeout const timeoutResponse = { timedOut: true }; + const maxTimeoutVal = 2**31 - 1; const timeoutPromise = (cb, interval) => () => - new Promise(resolve => setTimeout(() => cb(resolve), interval)); + new Promise(resolve => setTimeout(() => cb(resolve), interval || maxTimeoutVal)); const onTimeout = timeoutPromise( - resolve => resolve(timeoutResponse), - timeout || Number.MAX_SAFE_INTEGER + resolve => resolve(timeoutResponse), timeout ); // Get nodes capable of doing LNS - let lnsNodes = await this.getNodesMinVersion( + const lnsNodes = await this.getNodesMinVersion( window.CONSTANTS.LNS_CAPABLE_NODES_VERSION ); - lnsNodes = _.shuffle(lnsNodes); // Enough nodes? if (lnsNodes.length < numRequiredConfirms) { From 345c7b12d9de52c5b8740401c7d8843ec1aedbc1 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 22 Apr 2020 18:30:51 +1000 Subject: [PATCH 6/8] lint --- js/modules/loki_snode_api.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index 4cd3f5a3f..38470d4ab 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -781,11 +781,14 @@ class LokiSnodeAPI { const nameHash = dcodeIO.ByteBuffer.wrap(output).toString('base64'); const timeoutResponse = { timedOut: true }; - const maxTimeoutVal = 2**31 - 1; + const maxTimeoutVal = 2 ** 31 - 1; const timeoutPromise = (cb, interval) => () => - new Promise(resolve => setTimeout(() => cb(resolve), interval || maxTimeoutVal)); + new Promise(resolve => + setTimeout(() => cb(resolve), interval || maxTimeoutVal) + ); const onTimeout = timeoutPromise( - resolve => resolve(timeoutResponse), timeout + resolve => resolve(timeoutResponse), + timeout ); // Get nodes capable of doing LNS @@ -857,7 +860,7 @@ class LokiSnodeAPI { const nodes = lnsNodes.splice(0, numRequests); // Start fetching from nodes - Promise.resolve(nodes.map(async node => fetchFromNode(node))); + nodes.map(async node => fetchFromNode(node)); // Timeouts (optional parameter) // Wait for cipher to be found; race against timeout From 4033b70f5cf90b5a0daf7f9e3b89d24f57c2a5eb Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 22 Apr 2020 21:51:45 +1000 Subject: [PATCH 7/8] Refined error reporting --- js/modules/loki_snode_api.js | 44 ++++++++++++++++-------------------- ts/global.d.ts | 7 ++++++ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index 38470d4ab..27c13f257 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -758,9 +758,8 @@ class LokiSnodeAPI { async getLnsMapping(lnsName, timeout) { // Returns { pubkey, error } - // pubkey is: - // null when there is confirmed to be no LNS mapping - // undefined when unconfirmed + // pubkey is + // undefined when unconfirmed or no mapping found // string when found // timeout parameter optional (ms) @@ -780,16 +779,12 @@ class LokiSnodeAPI { const output = await window.blake2b(input); const nameHash = dcodeIO.ByteBuffer.wrap(output).toString('base64'); - const timeoutResponse = { timedOut: true }; + // Timeouts const maxTimeoutVal = 2 ** 31 - 1; - const timeoutPromise = (cb, interval) => () => - new Promise(resolve => - setTimeout(() => cb(resolve), interval || maxTimeoutVal) + const timeoutPromise = () => + new Promise((_resolve, reject) => + setTimeout(() => reject(), timeout || maxTimeoutVal) ); - const onTimeout = timeoutPromise( - resolve => resolve(timeoutResponse), - timeout - ); // Get nodes capable of doing LNS const lnsNodes = await this.getNodesMinVersion( @@ -798,12 +793,13 @@ class LokiSnodeAPI { // Enough nodes? if (lnsNodes.length < numRequiredConfirms) { - error = window.i18n('lnsTooFewNodes'); + error = { lnsTooFewNodes: window.i18n('lnsTooFewNodes') }; return { pubkey, error }; } const confirmedNodes = []; + // Promise is only resolved when a consensus is found let cipherResolve; const cipherPromise = () => new Promise(resolve => { @@ -834,7 +830,7 @@ class LokiSnodeAPI { if (confirmedNodes.length >= numRequiredConfirms) { if (ciphertextHex) { - // result already found, dont worry + // Result already found, dont worry return; } @@ -848,7 +844,7 @@ class LokiSnodeAPI { // null represents no LNS mapping if (ciphertextHex === null) { - error = window.i18n('lnsMappingNotFound'); + error = { lnsMappingNotFound: window.i18n('lnsMappingNotFound') }; } cipherResolve({ ciphertextHex }); @@ -864,16 +860,16 @@ class LokiSnodeAPI { // Timeouts (optional parameter) // Wait for cipher to be found; race against timeout - const { timedOut } = await Promise.race( - [cipherPromise, onTimeout].map(f => f()) - ); - - if (timedOut) { - error = window.i18n('lnsLookupTimeout'); - return { pubkey, error }; - } - - pubkey = ciphertextHex === null ? null : await decryptHex(ciphertextHex); + // eslint-disable-next-line more/no-then + await Promise.race([cipherPromise, timeoutPromise].map(f => f())) + .then(async () => { + if (ciphertextHex !== null) { + pubkey = await decryptHex(ciphertextHex); + } + }) + .catch(() => { + error = { lnsLookupTimeout: window.i18n('lnsLookupTimeout') }; + }); return { pubkey, error }; } diff --git a/ts/global.d.ts b/ts/global.d.ts index 34564325a..a718447d2 100644 --- a/ts/global.d.ts +++ b/ts/global.d.ts @@ -63,3 +63,10 @@ interface Window { interface Promise { ignore(): void; } + +// Types also correspond to messages.json keys +enum LnsLookupErrorType { + lnsTooFewNodes, + lnsLookupTimeout, + lnsMappingNotFound, +} From 47a2ecdf95ec5fcb6a75233d4821f182a0cb59f8 Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 25 May 2020 01:11:39 +1000 Subject: [PATCH 8/8] LNS mapping finalise --- js/modules/loki_snode_api.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index 27c13f257..b20114aba 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -810,9 +810,9 @@ class LokiSnodeAPI { const ciphertext = new Uint8Array(StringView.hexToArrayBuffer(cipherHex)); const res = await window.decryptLnsEntry(lnsName, ciphertext); - const pubicKey = StringView.arrayBufferToHex(res); + const publicKey = StringView.arrayBufferToHex(res); - return pubicKey; + return publicKey; }; const fetchFromNode = async node => { @@ -856,7 +856,7 @@ class LokiSnodeAPI { const nodes = lnsNodes.splice(0, numRequests); // Start fetching from nodes - nodes.map(async node => fetchFromNode(node)); + nodes.forEach(node => fetchFromNode(node)); // Timeouts (optional parameter) // Wait for cipher to be found; race against timeout