From 67c2124a3be24a882a7567b7a4ba7a77c6c82e27 Mon Sep 17 00:00:00 2001 From: audric Date: Thu, 5 Aug 2021 13:29:56 +1000 Subject: [PATCH] do not try to fetch seed node data with ip as cert will not be valid --- ts/session/snode_api/lokiRpc.ts | 2 +- ts/session/snode_api/onions.ts | 19 ++- ts/session/snode_api/snodePool.ts | 20 +-- ts/session/utils/syncUtils.ts | 246 ++++++++++++++++-------------- 4 files changed, 150 insertions(+), 137 deletions(-) diff --git a/ts/session/snode_api/lokiRpc.ts b/ts/session/snode_api/lokiRpc.ts index 668b45aaa..1a7809de8 100644 --- a/ts/session/snode_api/lokiRpc.ts +++ b/ts/session/snode_api/lokiRpc.ts @@ -82,7 +82,7 @@ export async function snodeRpc( method: string, params: any, targetNode: Snode, - associatedWith?: string //the user pubkey this call is for. if the onion request fails, this is used to handle the error for this user swarm for isntance + associatedWith?: string //the user pubkey this call is for. if the onion request fails, this is used to handle the error for this user swarm for instance ): Promise { const url = `https://${targetNode.ip}:${targetNode.port}/storage_rpc/v1`; diff --git a/ts/session/snode_api/onions.ts b/ts/session/snode_api/onions.ts index 2dffc2332..1152fa55b 100644 --- a/ts/session/snode_api/onions.ts +++ b/ts/session/snode_api/onions.ts @@ -387,7 +387,11 @@ const debug = false; /** * Only exported for testing purpose */ -export async function decodeOnionResult(symmetricKey: ArrayBuffer, ciphertext: string, test?: string) { +export async function decodeOnionResult( + symmetricKey: ArrayBuffer, + ciphertext: string, + test?: string +) { let parsedCiphertext = ciphertext; try { const jsonRes = JSON.parse(ciphertext); @@ -417,7 +421,7 @@ export async function processOnionResponse({ abortSignal, associatedWith, lsrpcEd25519Key, - test + test, }: { response?: { text: () => Promise; status: number }; symmetricKey?: ArrayBuffer; @@ -663,7 +667,7 @@ const sendOnionRequestHandlingSnodeEject = async ({ abortSignal, associatedWith, finalRelayOptions, - test + test, }: { nodePath: Array; destX25519Any: string; @@ -689,7 +693,7 @@ const sendOnionRequestHandlingSnodeEject = async ({ finalDestOptions, finalRelayOptions, abortSignal, - test + test, }); response = result.response; @@ -706,7 +710,7 @@ const sendOnionRequestHandlingSnodeEject = async ({ lsrpcEd25519Key: finalDestOptions?.destination_ed25519_hex, abortSignal, associatedWith, - test + test, }); return processed; @@ -729,7 +733,7 @@ const sendOnionRequest = async ({ finalDestOptions, finalRelayOptions, abortSignal, - test + test, }: { nodePath: Array; destX25519Any: string; @@ -827,7 +831,6 @@ const sendOnionRequest = async ({ // no logs for that one insecureNodeFetch as we do need to call insecureNodeFetch to our guardNode // window?.log?.info('insecureNodeFetch => plaintext for sendOnionRequest'); - const response = await insecureNodeFetch(guardUrl, guardFetchOptions); return { response, decodingSymmetricKey: destCtx.symmetricKey }; }; @@ -847,7 +850,7 @@ async function sendOnionRequestSnodeDest( body: plaintext, }, associatedWith, - test + test, }); } diff --git a/ts/session/snode_api/snodePool.ts b/ts/session/snode_api/snodePool.ts index b9c1c6ce5..13f01dbe6 100644 --- a/ts/session/snode_api/snodePool.ts +++ b/ts/session/snode_api/snodePool.ts @@ -66,24 +66,12 @@ async function tryGetSnodeListFromLokidSeednode( // throw before clearing the lock, so the retries can kick in if (snodes.length === 0) { window?.log?.warn( - `loki_snode_api::tryGetSnodeListFromLokidSeednode - ${seedNode.url} did not return any snodes, falling back to IP`, - seedNode.ip_url - ); - // fall back on ip_url - const tryIpUrl = new URL(seedNode.ip_url); - snodes = await getSnodesFromSeedUrl(tryIpUrl); - if (snodes.length === 0) { - window?.log?.warn( - `loki_snode_api::tryGetSnodeListFromLokidSeednode - ${seedNode.ip_url} did not return any snodes` - ); - // does this error message need to be exactly this? - throw new window.textsecure.SeedNodeError('Failed to contact seed node'); - } - } else { - window?.log?.info( - `loki_snode_api::tryGetSnodeListFromLokidSeednode - ${seedNode.url} returned ${snodes.length} snodes` + `loki_snode_api::tryGetSnodeListFromLokidSeednode - ${seedNode.ip_url} did not return any snodes` ); + // does this error message need to be exactly this? + throw new window.textsecure.SeedNodeError('Failed to contact seed node'); } + return snodes; } catch (e) { window?.log?.warn( diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index d9a550546..b2191d4d7 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -53,6 +53,8 @@ import { import { KeyPair } from '../../../libtextsecure/libsignal-protocol'; import { getIdentityKeyPair } from './User'; import { PubKey } from '../types'; +import pRetry from 'p-retry'; +import { ed25519Str } from '../onions/onionPath'; const ITEM_ID_LAST_SYNC_TIMESTAMP = 'lastSyncedTimestamp'; @@ -124,132 +126,152 @@ export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = fal * @returns timestamp of the response from snode */ const getNetworkTime = async (snode: Snode): Promise => { - // let response: any = await insecureNodeFetch(url, fetchOptions) - try { - const response: any = await snodeRpc('info', {}, snode); - const body = JSON.parse(response.body); - const timestamp = body.timestamp; - - return timestamp ? timestamp : -1; - } catch (e) { - return -1; + const response: any = await snodeRpc('info', {}, snode); + const body = JSON.parse(response.body); + const timestamp = body.timestamp; + if (!timestamp) { + throw new Error(`getNetworkTime returned invalid timestamp: ${timestamp}`); } + return timestamp; }; -export const forceNetworkDeletion = async () => { +export const forceNetworkDeletion = async (): Promise | null> => { const sodium = await getSodium(); - const userPubKey = UserUtils.getOurPubKeyFromCache(); - - const edKey = await UserUtils.getUserED25519KeyPair(); - const edKeyPriv = edKey?.privKey || ''; - - console.warn({ edKey }); - console.warn({ edKeyPriv }); - - const snode: Snode | undefined = _.shuffle(await getSwarmFor(userPubKey.key))[0]; - const timestamp = await getNetworkTime(snode); - - const text = `delete_all${timestamp.toString()}`; - - const toSign = StringUtils.encode(text, 'utf8'); - const toSignBytes = new Uint8Array(toSign); - console.warn({ toSign }); - console.warn({ toSignBytes }); - - const edKeyBytes = fromHexToArray(edKeyPriv); - - // using uint or string for message input makes no difference here. - // let sig = sodium.crypto_sign_detached(toSignBytes, edKeyBytes); - const sig = sodium.crypto_sign_detached(toSignBytes, edKeyBytes); // NO - - const kp = await UserUtils.getIdentityKeyPair(); - // let sig = await window.libsignal.Curve.async.calculateSignature(kp?.privKey, toSign) - // console.log({isVerified: sodium.crypto_sign_verify_detached(sig, text, fromHexToArray(edKey?.pubKey || ''))}) - // try: encode toSign to base64 before signing then decode to bytes afterwards - // todo: - // try the exact example with fillter values off the documentation. - - const sig64 = fromUInt8ArrayToBase64(sig); - - const sig64a = to_base64(sig); - console.warn({ sig64a }); - - console.warn({ sig }); - console.warn({ sig64: sig64 }); - console.warn({ sigLength: sig64.length }); - - // pubkey - hex - from xSK.public_key - // timestamp - ms - // signature - ? Base64.encodeBytes(signature) - - const deleteMessageParams = { - pubkey: userPubKey.key, // pubkey is doing alright - pubkeyED25519: edKey?.pubKey.toUpperCase(), // ed pubkey is right - timestamp, - signature: sig64, - }; - - // let lokiRpcRes = await snodeRpc('delete_all', deleteMessageParams, snode, userPubKey.key); - // let lokiRpcRes = sendOnionRequest() + const userX25519PublicKey = UserUtils.getOurPubKeyFromCache(); - await send(deleteMessageParams, snode, userPubKey.key); -}; - -const send = async (params: any, snode: Snode, userPubKey: string) => { - // window?.log?.info(`Testing a candidate guard node ${ed25519Str(snode.pubkey_ed25519)}`); - - // Send a post request and make sure it is OK - const endpoint = '/storage_rpc/v1'; + const userED25519KeyPair = await UserUtils.getUserED25519KeyPair(); - const url = `https://${snode.ip}:${snode.port}${endpoint}`; - - const ourPK = UserUtils.getOurPubKeyStrFromCache(); - const pubKey = window.getStoragePubKey(ourPK); // truncate if testnet + if (!userED25519KeyPair) { + window.log.warn('Cannot forceNetworkDeletion, did not find user ed25519 key.'); + return null; + } + const edKeyPriv = userED25519KeyPair.privKey; - // testt - const method = 'delete_all'; - params.pubkey = pubKey; // trying with getStoragePubkey + console.warn({ userED25519KeyPair }); + console.warn({ edKeyPriv }); - const body = { - jsonrpc: '2.0', - id: '0', - method, - params, - }; + return pRetry( + async () => { + const userSwarm = await getSwarmFor(userX25519PublicKey.key); + const snodeToMakeRequestTo: Snode | undefined = _.sample(userSwarm); + const edKeyPrivBytes = fromHexToArray(edKeyPriv); - const fetchOptions = { - method: 'POST', - body: JSON.stringify(body), - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'WhatsApp', - 'Accept-Language': 'en-us', - }, - timeout: 10000, // 10s, we want a smaller timeout for testing - agent: snodeHttpsAgent, - }; + if (!snodeToMakeRequestTo) { + window.log.warn('Cannot forceNetworkDeletion, without a valid swarm node.'); + return null; + } - let response; + // FIXME audric pretry getNetworkTime separately too + return pRetry( + async () => { + const timestamp = await getNetworkTime(snodeToMakeRequestTo); + + const verificationData = StringUtils.encode(`delete_all${timestamp}`, 'utf8'); + const message = new Uint8Array(verificationData); + const signature = sodium.crypto_sign_detached(message, edKeyPrivBytes); + const signatureBase64 = fromUInt8ArrayToBase64(signature); + + const deleteMessageParams = { + pubkey: userX25519PublicKey.key, + pubkey_ed25519: userED25519KeyPair.pubKey.toUpperCase(), + timestamp, + signature: signatureBase64, + }; + + const ret = await snodeRpc( + 'delete_all', + deleteMessageParams, + snodeToMakeRequestTo, + userX25519PublicKey.key + ); + + if (!ret) { + throw new Error( + `Empty response got for delete_all on snode ${ed25519Str( + snodeToMakeRequestTo.pubkey_ed25519 + )}` + ); + } - try { - // Log this line for testing - // curl -k -X POST -H 'Content-Type: application/json' -d '"+fetchOptions.body.replace(/"/g, "\\'")+"'", url - window?.log?.info('Sending delete all'); + try { + const parsedResponse = JSON.parse(ret.body); + const { swarm } = parsedResponse; - response = await insecureNodeFetch(url, fetchOptions); - } catch (e) { - if (e.type === 'request-timeout') { - window?.log?.warn('test timeout for node,', snode); + if (!swarm) { + throw new Error( + `Invalid JSON swarm response got for delete_all on snode ${ed25519Str( + snodeToMakeRequestTo.pubkey_ed25519 + )}, ${ret?.body}` + ); + } + const swarmAsArray = Object.entries(swarm) as Array>; + if (!swarmAsArray.length) { + throw new Error( + `Invalid JSON swarmAsArray response got for delete_all on snode ${ed25519Str( + snodeToMakeRequestTo.pubkey_ed25519 + )}, ${ret?.body}` + ); + } + const results: Map = new Map( + swarmAsArray.map(snode => { + const snodePubkey = snode[0]; + const snodeJson = snode[1]; + console.warn({ snodePubkey, snodeJson }); + + const isFailed = snodeJson.failed || false; + + if (isFailed) { + const reason = snodeJson.reason; + const statusCode = snodeJson.code; + if (reason && statusCode) { + window.log.warn( + `Could not delete data from ${ed25519Str( + snodeToMakeRequestTo.pubkey_ed25519 + )} due to error: ${reason}: ${statusCode}` + ); + } else { + window.log.warn( + `Could not delete data from ${ed25519Str(snodeToMakeRequestTo.pubkey_ed25519)}` + ); + } + return [snodePubkey, false]; + } + + const hashes = snodeJson.deleted as Array; + const signatureSnode = snodeJson.signature as string; + // The signature format is ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ) + const dataToVerify = `${userX25519PublicKey.key}${timestamp}${hashes.join('')}`; + const dataToVerifyUtf8 = StringUtils.encode(dataToVerify, 'utf8'); + const isValid = sodium.crypto_sign_verify_detached( + fromBase64ToArray(signatureSnode), + new Uint8Array(dataToVerifyUtf8), + fromHexToArray(snodePubkey) + ); + return [snodePubkey, isValid]; + }) + ); + + return results; + } catch (e) { + throw new Error( + `Invalid JSON response got for delete_all on snode ${ed25519Str( + snodeToMakeRequestTo.pubkey_ed25519 + )}, ${ret?.body}` + ); + } + }, + { + retries: 3, + minTimeout: 500, + onFailedAttempt: e => { + window?.log?.warn( + `delete_all request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...` + ); + }, + }, } - return false; - } - - if (!response.ok) { - const tg = await response.text(); - window?.log?.info('Node failed the guard test:', snode); - } + ); - return; }; const getActiveOpenGroupV2CompleteUrls = async (