/* eslint-disable no-prototype-builtins */ /* eslint-disable no-restricted-syntax */ import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs'; import { compact, isEmpty } from 'lodash'; import pRetry from 'p-retry'; import AbortController from 'abort-controller'; import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface'; import { getSodiumRenderer } from '../../crypto'; import { PubKey } from '../../types'; import { StringUtils, UserUtils } from '../../utils'; import { ed25519Str, fromBase64ToArray, fromHexToArray } from '../../utils/String'; import { DeleteAllFromUserNodeSubRequest } from './SnodeRequestTypes'; import { BatchRequests } from './batchRequest'; import { DeleteGroupHashesFactory } from './factories/DeleteGroupHashesRequestFactory'; import { DeleteUserHashesFactory } from './factories/DeleteUserHashesRequestFactory'; import { SnodePool } from './snodePool'; import { DURATION } from '../../constants'; import { timeoutWithAbort } from '../../utils/Promise'; 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 usPk = UserUtils.getOurPubKeyStrFromCache(); const request = new DeleteAllFromUserNodeSubRequest(); try { const maliciousSnodes = await pRetry( async () => { if (!window.isOnline) { window?.log?.warn('forceNetworkDeletion: we are offline.'); return null; } const targetNode = await SnodePool.getNodeFromSwarmOrThrow(usPk); const builtRequest = await request.build(); const ret = await BatchRequests.doSnodeBatchRequestNoRetries({ subRequests: [builtRequest], targetNode, timeoutMs: 10 * DURATION.SECONDS, associatedWith: usPk, allow401s: false, method: 'batch', }); if (!ret || !ret?.[0].body || ret[0].code !== 200) { throw new Error( `Empty response got for ${request.method} on snode ${ed25519Str( targetNode.pubkey_ed25519 )}` ); } try { const firstResultParsedBody = ret[0].body; const { swarm } = firstResultParsedBody; if (!swarm) { throw new Error( `Invalid JSON swarm response got for ${request.method} on snode ${ed25519Str( targetNode.pubkey_ed25519 )}, ${firstResultParsedBody}` ); } const swarmAsArray = Object.entries(swarm) as Array>; if (!swarmAsArray.length) { throw new Error( `Invalid JSON swarmAsArray response got for ${request.method} on snode ${ed25519Str( targetNode.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 ${request.method} from ${ed25519Str( targetNode.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 ${request.method}. Retrying with a new snode` ); } } else { window?.log?.warn( `Could not ${request.method} from ${ed25519Str(targetNode.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 across all namespaces) ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ) const dataToVerify = `${usPk}${builtRequest.params.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 ${request.method} on snode ${ed25519Str( targetNode.pubkey_ed25519 )}, ${ret}` ); } }, { retries: 5, minTimeout: SnodeAPI.TEST_getMinTimeout(), onFailedAttempt: e => { window?.log?.warn( `${request.method} OUTER request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left... ${e.message}` ); }, } ); return maliciousSnodes; } catch (e) { window?.log?.warn(`failed to ${request.method} everything on network:`, e); return null; } }; const TEST_getMinTimeout = () => 500; /** * Delete the specified message hashes from our own swarm only. * Note: legacy groups does not support this */ const networkDeleteMessageOurSwarm = async ( messagesHashes: Set, pubkey: PubkeyType ): Promise => { const sodium = await getSodiumRenderer(); if (!PubKey.is05Pubkey(pubkey) || pubkey !== UserUtils.getOurPubKeyStrFromCache()) { throw new Error('networkDeleteMessageOurSwarm with 05 pk can only for our own swarm'); } if (isEmpty(messagesHashes)) { window.log.info('networkDeleteMessageOurSwarm: messageHashes is empty'); return true; } const messageHashesArr = [...messagesHashes]; const request = DeleteUserHashesFactory.makeUserHashesToDeleteSubRequest({ messagesHashes }); if (!request) { throw new Error('makeUserHashesToDeleteSubRequest returned invalid sub request'); } try { const success = await pRetry( async () => { const targetNode = await SnodePool.getNodeFromSwarmOrThrow(request.destination); const controller = new AbortController(); const ret = await timeoutWithAbort( BatchRequests.doUnsignedSnodeBatchRequestNoRetries({ unsignedSubRequests: [request], targetNode, timeoutMs: 10 * DURATION.SECONDS, associatedWith: request.destination, allow401s: false, method: 'batch', abortSignal: controller.signal, }), 30 * DURATION.SECONDS, controller ); if (!ret || !ret?.[0].body || ret[0].code !== 200) { throw new Error( `networkDeleteMessageOurSwarm: Empty response got for ${request.method} on snode ${ed25519Str( targetNode.pubkey_ed25519 )} about pk: ${ed25519Str(request.destination)}` ); } try { const firstResultParsedBody = ret[0].body; const { swarm } = firstResultParsedBody; if (!swarm) { throw new Error( `networkDeleteMessageOurSwarm: Invalid JSON swarm response got for ${request.method} on snode ${ed25519Str( targetNode.pubkey_ed25519 )}, ${firstResultParsedBody}` ); } const swarmAsArray = Object.entries(swarm) as Array>; if (!swarmAsArray.length) { throw new Error( `networkDeleteMessageOurSwarm: Invalid JSON swarmAsArray response got for ${request.method} on snode ${ed25519Str( targetNode.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( `networkDeleteMessageOurSwarm: Could not ${request.method} from ${ed25519Str( targetNode.pubkey_ed25519 )} due to error: ${reason}: ${statusCode}` ); } else { window?.log?.warn( `networkDeleteMessageOurSwarm: Could not ${request.method} from ${ed25519Str( targetNode.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 = `${request.destination}${messageHashesArr.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 isEmpty(results); } catch (e) { throw new Error( `networkDeleteMessageOurSwarm: Invalid JSON response got for ${request.method} on snode ${ed25519Str( targetNode.pubkey_ed25519 )}, ${ret}` ); } }, { retries: 5, minTimeout: SnodeAPI.TEST_getMinTimeout(), onFailedAttempt: e => { window?.log?.warn( `networkDeleteMessageOurSwarm: ${request.method} request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left... ${e.message}` ); }, } ); return success; } catch (e) { window?.log?.warn( `networkDeleteMessageOurSwarm: failed to ${request.method} message on network:`, e ); return false; } }; /** * Delete the specified message hashes from the 03-group's swarm. * Returns true when the hashes have been removed successfully. * Returns false when * - we don't have the secretKey * - if one of the hashes was already not found in the swarm, * - if the request failed too many times */ const networkDeleteMessagesForGroup = async ( messagesHashes: Set, groupPk: GroupPubkeyType ): Promise => { if (!PubKey.is03Pubkey(groupPk)) { throw new Error('networkDeleteMessagesForGroup with 05 pk can only delete for ourself'); } const group = await UserGroupsWrapperActions.getGroup(groupPk); if (!group || !group.secretKey || isEmpty(group.secretKey)) { window.log.warn( `networkDeleteMessagesForGroup: not deleting from swarm of 03-group ${messagesHashes.size} hashes as we do not the adminKey` ); return false; } try { const request = DeleteGroupHashesFactory.makeGroupHashesToDeleteSubRequest({ messagesHashes, group, }); if (!request) { throw new Error( 'DeleteGroupHashesFactory.makeGroupHashesToDeleteSubRequest failed to build a request ' ); } await pRetry( async () => { const targetNode = await SnodePool.getNodeFromSwarmOrThrow(request.destination); const controller = new AbortController(); const ret = await timeoutWithAbort( BatchRequests.doUnsignedSnodeBatchRequestNoRetries({ unsignedSubRequests: [request], targetNode, timeoutMs: 10 * DURATION.SECONDS, associatedWith: request.destination, allow401s: false, method: 'batch', abortSignal: controller.signal, }), 30 * DURATION.SECONDS, controller ); if (!ret || !ret?.[0].body || ret[0].code !== 200) { throw new Error( `networkDeleteMessagesForGroup: Empty response got for ${request.method} on snode ${ed25519Str( targetNode.pubkey_ed25519 )} about pk: ${ed25519Str(request.destination)}` ); } }, { retries: 5, minTimeout: SnodeAPI.TEST_getMinTimeout(), onFailedAttempt: e => { window?.log?.warn( `networkDeleteMessagesForGroup: ${request.method} request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left... ${e.message}` ); }, } ); return true; } catch (e) { window?.log?.warn(`networkDeleteMessagesForGroup: failed to delete messages on network:`, e); return false; } }; export const SnodeAPI = { TEST_getMinTimeout, networkDeleteMessagesForGroup, networkDeleteMessageOurSwarm, forceNetworkDeletion, };