Merge pull request #1091 from vincentbavitz/lns-map

LNS Mapping Rewrite
pull/1156/head
Vince 5 years ago committed by GitHub
commit 23c57a8a11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2534,14 +2534,29 @@
"message": "Remove" "message": "Remove"
}, },
"invalidHexId": { "invalidHexId": {
"message": "Invalid Hex ID", "message": "Invalid Session ID or LNS Name",
"description": "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": { "invalidPubkeyFormat": {
"message": "Invalid Pubkey Format", "message": "Invalid Pubkey Format",
"description": "Error string shown when user types an 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": { "conversationsTab": {
"message": "Conversations", "message": "Conversations",
"description": "conversation tab title" "description": "conversation tab title"
@ -2724,7 +2739,7 @@
"message": "Enter Session ID" "message": "Enter Session ID"
}, },
"pasteSessionIDRecipient": { "pasteSessionIDRecipient": {
"message": "Enter a Session ID" "message": "Enter a Session ID or LNS name"
}, },
"usersCanShareTheir...": { "usersCanShareTheir...": {
"message": "message":

@ -858,73 +858,122 @@ class LokiSnodeAPI {
} }
} }
async getLnsMapping(lnsName) { async getLnsMapping(lnsName, timeout) {
// Returns { pubkey, error }
// pubkey is
// undefined when unconfirmed or no mapping found
// 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 _ = window.Lodash;
const input = Buffer.from(lnsName); const input = Buffer.from(lnsName);
const output = await window.blake2b(input); const output = await window.blake2b(input);
const nameHash = dcodeIO.ByteBuffer.wrap(output).toString('base64'); const nameHash = dcodeIO.ByteBuffer.wrap(output).toString('base64');
// Timeouts
const maxTimeoutVal = 2 ** 31 - 1;
const timeoutPromise = () =>
new Promise((_resolve, reject) =>
setTimeout(() => reject(), timeout || maxTimeoutVal)
);
// Get nodes capable of doing LNS // Get nodes capable of doing LNS
const lnsNodes = this.getNodesMinVersion('2.0.3'); const lnsNodes = await this.getNodesMinVersion(
// randomPool should already be shuffled window.CONSTANTS.LNS_CAPABLE_NODES_VERSION
// lnsNodes = _.shuffle(lnsNodes); );
// Loop until 3 confirmations // Enough nodes?
if (lnsNodes.length < numRequiredConfirms) {
error = { lnsTooFewNodes: window.i18n('lnsTooFewNodes') };
return { pubkey, error };
}
// We don't trust any single node, so we accumulate const confirmedNodes = [];
// answers here and select a dominating answer
const allResults = [];
let ciphertextHex = null;
while (!ciphertextHex) { // Promise is only resolved when a consensus is found
if (lnsNodes.length < 3) { let cipherResolve;
log.error('Not enough nodes for lns lookup'); const cipherPromise = () =>
return false; new Promise(resolve => {
} cipherResolve = resolve;
});
// extract 3 and make requests in parallel const decryptHex = async cipherHex => {
const nodes = lnsNodes.splice(0, 3); const ciphertext = new Uint8Array(StringView.hexToArrayBuffer(cipherHex));
// eslint-disable-next-line no-await-in-loop const res = await window.decryptLnsEntry(lnsName, ciphertext);
const results = await Promise.all( const publicKey = StringView.arrayBufferToHex(res);
nodes.map(node => this._requestLnsMapping(node, nameHash))
);
results.forEach(res => { return publicKey;
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( const fetchFromNode = async node => {
_.entries(_.countBy(allResults)), const res = await this._requestLnsMapping(node, nameHash);
x => x[1]
);
if (count >= 3) { // Do validation
// eslint-disable-next-lint prefer-destructuring if (res && res.result && res.result.status === 'OK') {
ciphertextHex = winner; 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;
// null represents no LNS mapping
if (ciphertextHex === null) {
error = { lnsMappingNotFound: window.i18n('lnsMappingNotFound') };
}
cipherResolve({ ciphertextHex });
}
}
} }
} };
const ciphertext = new Uint8Array( const nodes = lnsNodes.splice(0, numRequests);
StringView.hexToArrayBuffer(ciphertextHex)
);
const res = await window.decryptLnsEntry(lnsName, ciphertext); // Start fetching from nodes
nodes.forEach(node => fetchFromNode(node));
const pubkey = StringView.arrayBufferToHex(res); // Timeouts (optional parameter)
// Wait for cipher to be found; race against timeout
// 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; return { pubkey, error };
} }
// get snodes for pubkey from random snode // get snodes for pubkey from random snode

@ -70,18 +70,31 @@ window.isBeforeVersion = (toCheck, baseVersion) => {
} }
}; };
window.CONSTANTS = { // eslint-disable-next-line func-names
MAX_LOGIN_TRIES: 3, window.CONSTANTS = new function() {
MAX_PASSWORD_LENGTH: 64, this.MAX_LOGIN_TRIES = 3;
MAX_USERNAME_LENGTH: 20, this.MAX_PASSWORD_LENGTH = 64;
MAX_GROUP_NAME_LENGTH: 64, this.MAX_USERNAME_LENGTH = 20;
DEFAULT_PUBLIC_CHAT_URL: appConfig.get('defaultPublicChatServer'), this.MAX_GROUP_NAME_LENGTH = 64;
MAX_CONNECTION_DURATION: 5000, this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer');
MAX_MESSAGE_BODY_LENGTH: 64 * 1024, this.MAX_CONNECTION_DURATION = 5000;
this.MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
// Limited due to the proof-of-work requirement // Limited due to the proof-of-work requirement
SMALL_GROUP_SIZE_LIMIT: 10, this.SMALL_GROUP_SIZE_LIMIT = 10;
NOTIFICATION_ENABLE_TIMEOUT_SECONDS: 10, // number of seconds to turn on notifications after reconnect/start of app // 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 = { window.versionInfo = {
environment: window.getEnvironment(), environment: window.getEnvironment(),

7
ts/global.d.ts vendored

@ -63,3 +63,10 @@ interface Window {
interface Promise<T> { interface Promise<T> {
ignore(): void; ignore(): void;
} }
// Types also correspond to messages.json keys
enum LnsLookupErrorType {
lnsTooFewNodes,
lnsLookupTimeout,
lnsMappingNotFound,
}

Loading…
Cancel
Save