/* eslint-disable no-prototype-builtins */ /* eslint-disable no-restricted-syntax */ import { compact, sample } from 'lodash'; import pRetry from 'p-retry'; import { Snode } from '../../../data/data'; import { getSodiumRenderer } from '../../crypto'; import { StringUtils, UserUtils } from '../../utils'; import { ed25519Str, fromBase64ToArray, fromHexToArray } from '../../utils/String'; import { doSnodeBatchRequest } from './batchRequest'; import { getSwarmFor } from './snodePool'; import { SnodeSignature } from './snodeSignatures'; export const ERROR_CODE_NO_CONNECT = 'ENETUNREACH: No network connection.'; // TODOLATER we should merge those two functions together as they are almost exactly the same const forceNetworkDeletion = async (): Promise | null> => { const sodium = await getSodiumRenderer(); const userX25519PublicKey = UserUtils.getOurPubKeyStrFromCache(); const userED25519KeyPair = await UserUtils.getUserED25519KeyPair(); if (!userED25519KeyPair) { window?.log?.warn('Cannot forceNetworkDeletion, did not find user ed25519 key.'); return null; } const method = 'delete_all' as const; const namespace = 'all' as const; try { const maliciousSnodes = await pRetry( async () => { const userSwarm = await getSwarmFor(userX25519PublicKey); const snodeToMakeRequestTo: Snode | undefined = sample(userSwarm); if (!snodeToMakeRequestTo) { window?.log?.warn('Cannot forceNetworkDeletion, without a valid swarm node.'); return null; } return pRetry( async () => { const signOpts = await SnodeSignature.getSnodeSignatureParams({ method, namespace, pubkey: userX25519PublicKey, }); const ret = await doSnodeBatchRequest( [{ method, params: { ...signOpts, namespace } }], snodeToMakeRequestTo, 10000, userX25519PublicKey ); if (!ret || !ret?.[0].body || ret[0].code !== 200) { throw new Error( `Empty response got for ${method} on snode ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )}` ); } try { const firstResultParsedBody = ret[0].body; const { swarm } = firstResultParsedBody; if (!swarm) { throw new Error( `Invalid JSON swarm response got for ${method} on snode ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )}, ${firstResultParsedBody}` ); } const swarmAsArray = Object.entries(swarm) as Array>; if (!swarmAsArray.length) { throw new Error( `Invalid JSON swarmAsArray response got for ${method} on snode ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )}, ${firstResultParsedBody}` ); } // results will only contains the snode pubkeys which returned invalid/empty results const results: Array = compact( swarmAsArray.map(snode => { const snodePubkey = snode[0]; const snodeJson = snode[1]; const isFailed = snodeJson.failed || false; if (isFailed) { const reason = snodeJson.reason; const statusCode = snodeJson.code; if (reason && statusCode) { window?.log?.warn( `Could not ${method} from ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )} due to error: ${reason}: ${statusCode}` ); // if we tried to make the delete on a snode not in our swarm, just trigger a pRetry error so the outer block here finds new snodes to make the request to. if (statusCode === 421) { throw new pRetry.AbortError( `421 error on network ${method}. Retrying with a new snode` ); } } else { window?.log?.warn( `Could not ${method} from ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )}` ); } return snodePubkey; } const deletedObj = snodeJson.deleted as Record>; const hashes: Array = []; for (const key in deletedObj) { if (deletedObj.hasOwnProperty(key)) { hashes.push(...deletedObj[key]); } } const sortedHashes = hashes.sort(); const signatureSnode = snodeJson.signature as string; // The signature format is (with sortedHashes accross all namespaces) ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ) const dataToVerify = `${userX25519PublicKey}${ signOpts.timestamp }${sortedHashes.join('')}`; const dataToVerifyUtf8 = StringUtils.encode(dataToVerify, 'utf8'); const isValid = sodium.crypto_sign_verify_detached( fromBase64ToArray(signatureSnode), new Uint8Array(dataToVerifyUtf8), fromHexToArray(snodePubkey) ); if (!isValid) { return snodePubkey; } return null; }) ); return results; } catch (e) { throw new Error( `Invalid JSON response got for ${method} on snode ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )}, ${ret}` ); } }, { retries: 3, minTimeout: SnodeAPI.TEST_getMinTimeout(), onFailedAttempt: e => { window?.log?.warn( `${method} INNER request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...` ); }, } ); }, { retries: 3, minTimeout: SnodeAPI.TEST_getMinTimeout(), onFailedAttempt: e => { window?.log?.warn( `${method} OUTER request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left... ${e.message}` ); }, } ); return maliciousSnodes; } catch (e) { window?.log?.warn(`failed to ${method} everything on network:`, e); return null; } }; const TEST_getMinTimeout = () => 500; /** * Locally deletes message and deletes message on the network (all nodes that contain the message) */ const networkDeleteMessages = async (hashes: Array): Promise | null> => { const sodium = await getSodiumRenderer(); const userX25519PublicKey = UserUtils.getOurPubKeyStrFromCache(); const userED25519KeyPair = await UserUtils.getUserED25519KeyPair(); if (!userED25519KeyPair) { window?.log?.warn('Cannot networkDeleteMessages, did not find user ed25519 key.'); return null; } const method = 'delete' as const; try { const maliciousSnodes = await pRetry( async () => { const userSwarm = await getSwarmFor(userX25519PublicKey); const snodeToMakeRequestTo: Snode | undefined = sample(userSwarm); if (!snodeToMakeRequestTo) { window?.log?.warn('Cannot networkDeleteMessages, without a valid swarm node.'); return null; } return pRetry( async () => { const signOpts = await SnodeSignature.getSnodeSignatureByHashesParams({ messages: hashes, method, pubkey: userX25519PublicKey, }); const ret = await doSnodeBatchRequest( [{ method, params: signOpts }], snodeToMakeRequestTo, 10000, userX25519PublicKey ); if (!ret || !ret?.[0].body || ret[0].code !== 200) { throw new Error( `Empty response got for ${method} on snode ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )}` ); } try { const firstResultParsedBody = ret[0].body; const { swarm } = firstResultParsedBody; if (!swarm) { throw new Error( `Invalid JSON swarm response got for ${method} on snode ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )}, ${firstResultParsedBody}` ); } const swarmAsArray = Object.entries(swarm) as Array>; if (!swarmAsArray.length) { throw new Error( `Invalid JSON swarmAsArray response got for ${method} on snode ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )}, ${firstResultParsedBody}` ); } // results will only contains the snode pubkeys which returned invalid/empty results const results: Array = compact( swarmAsArray.map(snode => { const snodePubkey = snode[0]; const snodeJson = snode[1]; const isFailed = snodeJson.failed || false; if (isFailed) { const reason = snodeJson.reason; const statusCode = snodeJson.code; if (reason && statusCode) { window?.log?.warn( `Could not ${method} from ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )} due to error: ${reason}: ${statusCode}` ); // if we tried to make the delete on a snode not in our swarm, just trigger a pRetry error so the outer block here finds new snodes to make the request to. if (statusCode === 421) { throw new pRetry.AbortError( `421 error on network ${method}. Retrying with a new snode` ); } } else { window?.log?.warn( `Could not ${method} from ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )}` ); } return snodePubkey; } const responseHashes = snodeJson.deleted as Array; const signatureSnode = snodeJson.signature as string; // The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] ) const dataToVerify = `${userX25519PublicKey}${hashes.join( '' )}${responseHashes.join('')}`; const dataToVerifyUtf8 = StringUtils.encode(dataToVerify, 'utf8'); const isValid = sodium.crypto_sign_verify_detached( fromBase64ToArray(signatureSnode), new Uint8Array(dataToVerifyUtf8), fromHexToArray(snodePubkey) ); if (!isValid) { return snodePubkey; } return null; }) ); return results; } catch (e) { throw new Error( `Invalid JSON response got for ${method} on snode ${ed25519Str( snodeToMakeRequestTo.pubkey_ed25519 )}, ${ret}` ); } }, { retries: 3, minTimeout: SnodeAPI.TEST_getMinTimeout(), onFailedAttempt: e => { window?.log?.warn( `${method} INNER request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...` ); }, } ); }, { retries: 3, minTimeout: SnodeAPI.TEST_getMinTimeout(), onFailedAttempt: e => { window?.log?.warn( `${method} OUTER request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left... ${e.message}` ); }, } ); return maliciousSnodes; } catch (e) { window?.log?.warn(`failed to ${method} message on network:`, e); return null; } }; export const SnodeAPI = { TEST_getMinTimeout, networkDeleteMessages, forceNetworkDeletion, };