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/expire.ts

240 lines
7.0 KiB
TypeScript

/* eslint-disable no-restricted-syntax */
import { isEmpty, sample } from 'lodash';
import { Snode } from '../../../data/data';
import { getSodiumRenderer } from '../../crypto';
import { StringUtils, UserUtils } from '../../utils';
import { fromBase64ToArray, fromHexToArray } from '../../utils/String';
import { EmptySwarmError } from '../../utils/errors';
import { UpdateExpireNodeUserParams } from './SnodeRequestTypes';
import { doSnodeBatchRequest } from './batchRequest';
import { GetNetworkTime } from './getNetworkTime';
import { SnodeSignature } from './signature/snodeSignatures';
import { getSwarmFor } from './snodePool';
async function verifySignature({
pubkey,
snodePubkey,
expiryApplied,
signature,
messageHashes,
updatedHashes,
unchangedHashes,
}: {
pubkey: string;
snodePubkey: any;
expiryApplied: number;
signature: string;
messageHashes: Array<string>;
updatedHashes: Array<string>;
// only used when shorten or extend is in the request
unchangedHashes?: Record<string, string>;
}): Promise<boolean> {
if (!expiryApplied || isEmpty(messageHashes) || isEmpty(signature)) {
window.log.warn('verifySignature missing argument');
return false;
}
const edKeyPrivBytes = fromHexToArray(snodePubkey);
/* PUBKEY_HEX || EXPIRY || RMSGs... || UMSGs... || CMSG_EXPs...
where RMSGs are the requested expiry hashes,
UMSGs are the actual updated hashes, and
CMSG_EXPs are (HASH || EXPIRY) values, ascii-sorted by hash, for the unchanged message hashes included in the "unchanged" field.
*/
const hashes = [...messageHashes, ...updatedHashes];
if (unchangedHashes && Object.keys(unchangedHashes).length > 0) {
hashes.push(
...Object.entries(unchangedHashes)
.map(([key, value]: [string, string]) => {
return `${key}${value}`;
})
.sort()
);
}
const verificationString = `${pubkey}${expiryApplied}${hashes.join('')}`;
const verificationData = StringUtils.encode(verificationString, 'utf8');
window.log.debug('verifySignature verificationString', verificationString);
const sodium = await getSodiumRenderer();
try {
const isValid = sodium.crypto_sign_verify_detached(
fromBase64ToArray(signature),
new Uint8Array(verificationData),
edKeyPrivBytes
);
return isValid;
} catch (e) {
window.log.warn('verifySignature failed with: ', e.message);
return false;
}
}
async function processExpirationResults(
pubkey: string,
targetNode: Snode,
swarm: Record<string, any>,
messageHashes: Array<string>
) {
if (isEmpty(swarm)) {
throw Error(`expireOnNodes failed! ${messageHashes}`);
}
// TODO need proper typing for swarm and results
const results: Record<string, { hashes: Array<string>; expiry: number }> = {};
// window.log.debug(`processExpirationResults start`, swarm, messageHashes);
for (const nodeKey of Object.keys(swarm)) {
if (!isEmpty(swarm[nodeKey].failed)) {
const reason = 'Unknown';
const statusCode = '404';
window?.log?.warn(
`loki_message:::expireMessage - Couldn't delete data from: ${targetNode.pubkey_ed25519}${
reason && statusCode && ` due to an error ${reason} (${statusCode})`
}`
);
// TODO This might be a redundant step
results[nodeKey] = { hashes: [], expiry: 0 };
}
const updatedHashes = swarm[nodeKey].updated;
const unchangedHashes = swarm[nodeKey].unchanged;
const expiryApplied = swarm[nodeKey].expiry;
const signature = swarm[nodeKey].signature;
// eslint-disable-next-line no-await-in-loop
const isValid = await verifySignature({
pubkey,
snodePubkey: nodeKey,
expiryApplied,
signature,
messageHashes,
updatedHashes,
unchangedHashes,
});
if (!isValid) {
window.log.warn(
'loki_message:::expireMessage - Signature verification failed!',
messageHashes
);
}
results[nodeKey] = { hashes: updatedHashes, expiry: expiryApplied };
}
return results;
}
async function expireOnNodes(targetNode: Snode, params: UpdateExpireNodeUserParams) {
try {
const result = await doSnodeBatchRequest(
[
{
method: 'expire',
params,
},
],
targetNode,
4000,
params.pubkey,
'batch'
);
if (!result || result.length !== 1 || result[0]?.code !== 200 || !result[0]?.body) {
return false;
}
try {
// TODOLATER make sure that this code still works once disappearing messages is merged
const parsed = result[0].body;
const expirationResults = await processExpirationResults(
params.pubkey,
targetNode,
parsed.swarm,
params.messages
);
window.log.debug('expireOnNodes attempt complete. Here are the results', expirationResults);
return true;
} catch (e) {
window?.log?.warn('expireOnNodes Failed to parse "swarm" result: ', e.msg);
}
return false;
} catch (e) {
window?.log?.warn('expire - send error:', e, `destination ${targetNode.ip}:${targetNode.port}`);
throw e;
}
}
type ExpireMessageOnSnodeProps = {
messageHash: string;
expireTimer: number;
extend?: boolean;
shorten?: boolean;
};
// TODO make this retry in case of updated swarm
export async function expireMessageOnSnode(props: ExpireMessageOnSnodeProps) {
const { messageHash, expireTimer, extend, shorten } = props;
if (extend && shorten) {
window.log.error(
'[expireMessageOnSnode] We cannot extend and shorten a message at the same time',
messageHash
);
return;
}
const shortenOrExtend = shorten ? 'shorten' : extend ? 'extend' : ('' as const);
const ourPubKey = UserUtils.getOurPubKeyStrFromCache();
if (!ourPubKey) {
window.log.eror('[expireMessageOnSnode] No pubkey found', messageHash);
return;
}
const swarm = await getSwarmFor(ourPubKey);
const expiry = GetNetworkTime.getNowWithNetworkOffset() + expireTimer;
const signResult = await SnodeSignature.generateUpdateExpiryOurSignature({
shortenOrExtend,
timestamp: expiry,
messagesHashes: [messageHash],
});
if (!signResult) {
window.log.error('[expireMessageOnSnode] Signing message expiry on swarm failed', messageHash);
return;
}
const params: UpdateExpireNodeUserParams = {
pubkey: ourPubKey,
pubkey_ed25519: signResult.pubkey.toUpperCase(),
// TODO better testing for failed case
messages: [messageHash],
expiry,
extend: extend || undefined,
shorten: shorten || undefined,
signature: signResult?.signature,
};
const snode = sample(swarm);
if (!snode) {
throw new EmptySwarmError(ourPubKey, 'Ran out of swarm nodes to query');
}
try {
// TODO make this whole function `expireMessageOnSnode` retry
await expireOnNodes(snode, params);
} catch (e) {
const snodeStr = snode ? `${snode.ip}:${snode.port}` : 'null';
window?.log?.warn(
`loki_message:::expireMessage - ${e.code ? `${e.code} ` : ''}${
e.message
} by ${ourPubKey} for ${messageHash} via snode:${snodeStr}`
);
throw e;
}
}