diff --git a/ts/components/dialog/DeleteAccountModal.tsx b/ts/components/dialog/DeleteAccountModal.tsx index c396e3df8..83a6aa3ce 100644 --- a/ts/components/dialog/DeleteAccountModal.tsx +++ b/ts/components/dialog/DeleteAccountModal.tsx @@ -1,9 +1,7 @@ import React, { useCallback, useState } from 'react'; import { ed25519Str } from '../../session/onions/onionPath'; -import { - forceNetworkDeletion, - forceSyncConfigurationNowIfNeeded, -} from '../../session/utils/syncUtils'; +import { forceNetworkDeletion } from '../../session/snode_api/SNodeAPI'; +import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils'; import { updateConfirmModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog'; import { SpacerLG } from '../basic/Text'; import { SessionButton, SessionButtonColor } from '../session/SessionButton'; diff --git a/ts/session/snode_api/SNodeAPI.ts b/ts/session/snode_api/SNodeAPI.ts index 492d35db3..e6f538fb6 100644 --- a/ts/session/snode_api/SNodeAPI.ts +++ b/ts/session/snode_api/SNodeAPI.ts @@ -11,12 +11,18 @@ const { remote } = Electron; import { snodeRpc } from './lokiRpc'; -import { getRandomSnode, getRandomSnodePool, requiredSnodesForAgreement } from './snodePool'; +import { + getRandomSnode, + getRandomSnodePool, + getSwarmFor, + requiredSnodesForAgreement, +} from './snodePool'; import { Constants } from '..'; import { getSodium, sha256 } from '../crypto'; import _, { range } from 'lodash'; import pRetry from 'p-retry'; import { + fromBase64ToArray, fromHex, fromHexToArray, fromUInt8ArrayToBase64, @@ -25,6 +31,8 @@ import { } from '../utils/String'; import { Snode } from '../../data/data'; import { updateIsOnline } from '../../state/ducks/onion'; +import { ed25519Str } from '../onions/onionPath'; +import { UserUtils, StringUtils } from '../utils'; // ONS name can have [a-zA-Z0-9_-] except that - is not allowed as start or end // do not define a regex but rather create it on the fly to avoid https://stackoverflow.com/questions/3891641/regex-test-only-works-every-other-time @@ -593,3 +601,163 @@ export async function retrieveNextMessages( return []; } } + +/** + * Makes a post to a node to receive the timestamp info. If non-existant, returns -1 + * @param snode Snode to send request to + * @returns timestamp of the response from snode + */ +const getNetworkTime = async (snode: Snode): Promise => { + 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; +}; + +// tslint:disable-next-line: max-func-body-length +export const forceNetworkDeletion = async (): Promise | null> => { + const sodium = await getSodium(); + 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 edKeyPriv = userED25519KeyPair.privKey; + + try { + const maliciousSnodes = await pRetry(async () => { + const userSwarm = await getSwarmFor(userX25519PublicKey); + const snodeToMakeRequestTo: Snode | undefined = _.sample(userSwarm); + const edKeyPrivBytes = fromHexToArray(edKeyPriv); + + if (!snodeToMakeRequestTo) { + window.log.warn('Cannot forceNetworkDeletion, without a valid swarm node.'); + return null; + } + + 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, + pubkey_ed25519: userED25519KeyPair.pubKey.toUpperCase(), + timestamp, + signature: signatureBase64, + }; + const ret = await snodeRpc( + 'delete_all', + deleteMessageParams, + snodeToMakeRequestTo, + userX25519PublicKey + ); + + if (!ret) { + throw new Error( + `Empty response got for delete_all on snode ${ed25519Str( + snodeToMakeRequestTo.pubkey_ed25519 + )}` + ); + } + + try { + const parsedResponse = JSON.parse(ret.body); + const { swarm } = parsedResponse; + + 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}` + ); + } + // 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 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; + } + + 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}${timestamp}${hashes.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 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 maliciousSnodes; + } catch (e) { + window.log.warn('failed to delete everything on network:', e); + return null; + } +}; diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index 208ae63ba..70609cc0c 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -120,166 +120,6 @@ export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = fal }); }); -/** - * Makes a post to a node to receive the timestamp info. If non-existant, returns -1 - * @param snode Snode to send request to - * @returns timestamp of the response from snode - */ -const getNetworkTime = async (snode: Snode): Promise => { - 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; -}; - -// tslint:disable-next-line: max-func-body-length -export const forceNetworkDeletion = async (): Promise | null> => { - const sodium = await getSodium(); - 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 edKeyPriv = userED25519KeyPair.privKey; - - try { - const maliciousSnodes = await pRetry(async () => { - const userSwarm = await getSwarmFor(userX25519PublicKey); - const snodeToMakeRequestTo: Snode | undefined = _.sample(userSwarm); - const edKeyPrivBytes = fromHexToArray(edKeyPriv); - - if (!snodeToMakeRequestTo) { - window.log.warn('Cannot forceNetworkDeletion, without a valid swarm node.'); - return null; - } - - 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, - pubkey_ed25519: userED25519KeyPair.pubKey.toUpperCase(), - timestamp, - signature: signatureBase64, - }; - const ret = await snodeRpc( - 'delete_all', - deleteMessageParams, - snodeToMakeRequestTo, - userX25519PublicKey - ); - - if (!ret) { - throw new Error( - `Empty response got for delete_all on snode ${ed25519Str( - snodeToMakeRequestTo.pubkey_ed25519 - )}` - ); - } - - try { - const parsedResponse = JSON.parse(ret.body); - const { swarm } = parsedResponse; - - 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}` - ); - } - // 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 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; - } - - 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}${timestamp}${hashes.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 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 maliciousSnodes; - } catch (e) { - window.log.warn('failed to delete everything on network:', e); - return null; - } -}; - const getActiveOpenGroupV2CompleteUrls = async ( convos: Array ): Promise> => { diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index e22bba7ca..a7d779b50 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -4,18 +4,11 @@ import { UserUtils } from '../session/utils'; import { fromArrayBufferToBase64, fromHex, toHex } from '../session/utils/String'; import { getOurPubKeyStrFromCache } from '../session/utils/User'; import { trigger } from '../shims/events'; -import { - forceNetworkDeletion, - forceSyncConfigurationNowIfNeeded, -} from '../session/utils/syncUtils'; + import { actions as userActions } from '../state/ducks/user'; import { mn_decode, mn_encode } from '../session/crypto/mnemonic'; import { ConversationTypeEnum } from '../models/conversation'; import _ from 'underscore'; -import { persistStore } from 'redux-persist'; -import { ed25519Str } from '../session/onions/onionPath'; -import { SessionButtonColor } from '../components/session/SessionButton'; -import { updateConfirmModal } from '../state/ducks/modalDialog'; /** * Might throw