diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -3,7 +3,10 @@ import invariant from 'invariant'; import * as React from 'react'; -import { useBroadcastDeviceListUpdates } from '../hooks/peer-list-hooks.js'; +import { + useBroadcastDeviceListUpdates, + useBroadcastAccountDeletion, +} from '../hooks/peer-list-hooks.js'; import type { CallSingleKeyserverEndpoint, CallSingleKeyserverEndpointOptions, @@ -506,10 +509,17 @@ failed: 'DELETE_ACCOUNT_FAILED', }); +const accountDeletionBroadcastOptions = Object.freeze({ + broadcastToOwnDevices: true, +}); function useDeleteAccount(): (password: ?string) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; + const broadcastAccountDeletion = useBroadcastAccountDeletion( + accountDeletionBroadcastOptions, + ); + const preRequestUserState = usePreRequestUserState(); const callKeyserverDeleteAccount = useKeyserverCall(deleteKeyserverAccount); @@ -523,16 +533,16 @@ if (!identityClient) { throw new Error('Identity service client is not initialized'); } - if ( - !identityClient.deleteWalletUser || - !identityClient.deletePasswordUser - ) { + const { deleteWalletUser, deletePasswordUser } = identityClient; + if (!deleteWalletUser || !deletePasswordUser) { throw new Error('Delete user method unimplemented'); } + + await broadcastAccountDeletion(); if (password) { - await identityClient.deletePasswordUser(password); + await deletePasswordUser(password); } else { - await identityClient.deleteWalletUser(); + await deleteWalletUser(); } } try { @@ -565,6 +575,7 @@ }; }, [ + broadcastAccountDeletion, callKeyserverDeleteAccount, commServicesAccessToken, identityClient, diff --git a/lib/hooks/peer-list-hooks.js b/lib/hooks/peer-list-hooks.js --- a/lib/hooks/peer-list-hooks.js +++ b/lib/hooks/peer-list-hooks.js @@ -4,7 +4,10 @@ import * as React from 'react'; import { setPeerDeviceListsActionType } from '../actions/aux-user-actions.js'; -import { getAllPeerDevices } from '../selectors/user-selectors.js'; +import { + getAllPeerDevices, + getForeignPeerDevices, +} from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import type { @@ -15,9 +18,18 @@ } from '../types/identity-service-types.js'; import { type DeviceListUpdated, + type EncryptedMessage, peerToPeerMessageTypes, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; -import { getContentSigningKey } from '../utils/crypto-utils.js'; +import { + userActionsP2PMessageTypes, + type AccountDeletionP2PMessage, +} from '../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; +import { getConfig } from '../utils/config.js'; +import { + getContentSigningKey, + createOlmSessionWithPeer, +} from '../utils/crypto-utils.js'; import { convertSignedDeviceListsToRawDeviceLists } from '../utils/device-list-utils.js'; import { values } from '../utils/objects.js'; import { useDispatch, useSelector } from '../utils/redux-utils.js'; @@ -133,8 +145,86 @@ ); } +function useBroadcastAccountDeletion( + options: { broadcastToOwnDevices?: boolean } = {}, +): () => Promise { + const { broadcastToOwnDevices } = options; + + const identityContext = React.useContext(IdentityClientContext); + if (!identityContext) { + throw new Error('Identity service client is not initialized'); + } + const { sendMessageToDevice } = useTunnelbroker(); + + const devicesSelector = broadcastToOwnDevices + ? getAllPeerDevices + : getForeignPeerDevices; + const peerDevices = useSelector(devicesSelector); + + return React.useCallback(async () => { + const { identityClient, getAuthMetadata } = identityContext; + const authMetadata = await getAuthMetadata(); + const { userID, deviceID: thisDeviceID } = authMetadata; + if (!thisDeviceID || !userID) { + throw new Error('No auth metadata'); + } + // create and send Olm Tunnelbroker messages to peers + const { olmAPI } = getConfig(); + await olmAPI.initializeCryptoAccount(); + + const deletionMessage: AccountDeletionP2PMessage = { + type: userActionsP2PMessageTypes.ACCOUNT_DELETION, + }; + const rawPayload = JSON.stringify(deletionMessage); + + const recipientDeviceIDs = peerDevices.filter( + peerDeviceID => peerDeviceID !== thisDeviceID, + ); + for (const deviceID of recipientDeviceIDs) { + try { + const encryptedData = await olmAPI.encrypt(rawPayload, deviceID); + const encryptedMessage: EncryptedMessage = { + type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, + senderInfo: { deviceID: thisDeviceID, userID }, + encryptedData, + }; + await sendMessageToDevice({ + deviceID, + payload: JSON.stringify(encryptedMessage), + }); + } catch { + try { + await createOlmSessionWithPeer( + authMetadata, + identityClient, + sendMessageToDevice, + userID, + deviceID, + ); + const encryptedData = await olmAPI.encrypt(rawPayload, deviceID); + const encryptedMessage: EncryptedMessage = { + type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, + senderInfo: { deviceID: thisDeviceID, userID }, + encryptedData, + }; + await sendMessageToDevice({ + deviceID, + payload: JSON.stringify(encryptedMessage), + }); + } catch (err) { + console.warn( + `Error sending account deletion message to device ${deviceID}:`, + err, + ); + } + } + } + }, [identityContext, peerDevices, sendMessageToDevice]); +} + export { useGetDeviceListsForUsers, useBroadcastDeviceListUpdates, useGetAndUpdateDeviceListsForUsers, + useBroadcastAccountDeletion, };