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 @@ -19,9 +19,11 @@ getOneTimeKeyValuesFromBlob, getPrekeyValueFromBlob, } from '../shared/crypto-utils.js'; +import { fetchLatestDeviceList } from '../shared/device-list-utils.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import threadWatcher from '../shared/thread-watcher.js'; import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; +import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import type { LegacyLogInInfo, LegacyLogInResult, @@ -62,7 +64,15 @@ SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; -import type { RawThreadInfos } from '../types/thread-types'; +import type { RawThreadInfos } from '../types/thread-types.js'; +import { + peerToPeerMessageTypes, + type EncryptedMessage, +} from '../types/tunnelbroker/peer-to-peer-message-types.js'; +import { + userActionsP2PMessageTypes, + type SecondaryDeviceLogoutP2PMessage, +} from '../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; import type { CurrentUserInfo, UserInfo, @@ -71,6 +81,7 @@ } from '../types/user-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { getConfig } from '../utils/config.js'; +import { createOlmSessionWithPeer } from '../utils/crypto-utils.js'; import { getMessageForException } from '../utils/errors.js'; import { useSelector } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; @@ -226,7 +237,79 @@ }); function useSecondaryDeviceLogOut(): () => Promise { - return useLogOut(secondaryDeviceLogOutOptions); + const { sendMessage } = useTunnelbroker(); + const logOut = useLogOut(secondaryDeviceLogOutOptions); + + const identityContext = React.useContext(IdentityClientContext); + if (!identityContext) { + throw new Error('Identity service client is not initialized'); + } + + return React.useCallback(async () => { + const { identityClient, getAuthMetadata } = identityContext; + const authMetadata = await getAuthMetadata(); + const { userID, deviceID } = authMetadata; + if (!deviceID || !userID) { + throw new Error('No auth metadata'); + } + + // get current device list and primary device ID + const { devices } = await fetchLatestDeviceList(identityClient, userID); + const primaryDeviceID = devices[0]; + if (deviceID === primaryDeviceID) { + throw new Error('Used secondary device logout on primary device'); + } + + // create and send Olm Tunnelbroker message to primary device + const { olmAPI } = getConfig(); + await olmAPI.initializeCryptoAccount(); + const messageContents: SecondaryDeviceLogoutP2PMessage = { + type: userActionsP2PMessageTypes.LOG_OUT_SECONDARY_DEVICE, + }; + try { + const encryptedData = await olmAPI.encrypt( + JSON.stringify(messageContents), + primaryDeviceID, + ); + const encryptedMessage: EncryptedMessage = { + type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, + senderInfo: { deviceID, userID }, + encryptedData, + }; + await sendMessage({ + deviceID: primaryDeviceID, + payload: JSON.stringify(encryptedMessage), + }); + } catch { + try { + await createOlmSessionWithPeer( + authMetadata, + identityClient, + sendMessage, + userID, + primaryDeviceID, + ); + const encryptedData = await olmAPI.encrypt( + JSON.stringify(messageContents), + primaryDeviceID, + ); + const encryptedMessage: EncryptedMessage = { + type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, + senderInfo: { deviceID, userID }, + encryptedData, + }; + await sendMessage({ + deviceID: primaryDeviceID, + payload: JSON.stringify(encryptedMessage), + }); + } catch (err) { + console.warn('Error sending secondary device logout message:', err); + } + } + + // log out of identity service, keyserver and visually + return logOut(); + }, [identityContext, sendMessage, logOut]); } const claimUsernameActionTypes = Object.freeze({ diff --git a/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js b/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js new file mode 100644 --- /dev/null +++ b/lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js @@ -0,0 +1,23 @@ +// @flow + +import type { TInterface } from 'tcomb'; + +import { tShape, tString } from '../../utils/validation-utils.js'; + +export const userActionsP2PMessageTypes = Object.freeze({ + LOG_OUT_SECONDARY_DEVICE: 'LOG_OUT_SECONDARY_DEVICE', +}); + +export type SecondaryDeviceLogoutP2PMessage = { + +type: 'LOG_OUT_SECONDARY_DEVICE', + // there is `senderID` so we don't have to add deviceID here +}; +export const secondaryDeviceLogoutP2PMessageValidator: TInterface = + tShape({ + type: tString(userActionsP2PMessageTypes.LOG_OUT_SECONDARY_DEVICE), + }); + +export type UserActionP2PMessage = SecondaryDeviceLogoutP2PMessage; + +export const userActionP2PMessageValidator: TInterface = + secondaryDeviceLogoutP2PMessageValidator;