You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/session/apis/snode_api/SNodeAPI.ts

389 lines
14 KiB
TypeScript

/* 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<Array<string> | 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<Array<any>>;
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<string> = 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<number, Array<string>>;
const hashes: Array<string> = [];
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<string>,
pubkey: PubkeyType
): Promise<boolean> => {
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<Array<any>>;
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<string> = 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<string>;
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<string>,
groupPk: GroupPubkeyType
): Promise<boolean> => {
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,
};