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 @@ -22,6 +22,7 @@ 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, @@ -59,6 +60,11 @@ SubscriptionUpdateResult, } from '../types/subscription-types.js'; import type { RawThreadInfos } from '../types/thread-types'; +import type { + EncryptedMessage, + SecondaryDeviceLogoutP2PMessage, +} from '../types/tunnelbroker/peer-to-peer-message-types'; +import { peerToPeerMessageTypes } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import type { CurrentUserInfo, UserInfo, @@ -67,6 +73,7 @@ } from '../types/user-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { getConfig } from '../utils/config.js'; +import { rawDeviceListFromSignedList } from '../utils/device-list-utils.js'; import { useSelector } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; import sleep from '../utils/sleep.js'; @@ -218,10 +225,62 @@ } function useSecondaryDeviceLogOut(): () => Promise { + const identityContext = React.useContext(IdentityClientContext); + if (!identityContext) { + throw new Error('Identity service client is not initialized'); + } + const { identityClient, getAuthMetadata } = identityContext; + + const { sendMessage } = useTunnelbroker(); const logOut = useLogOut({ logOutType: 'secondary_device', }); - return logOut; + + const performLogOut: () => Promise = + React.useCallback(async () => { + const { userID, deviceID } = await getAuthMetadata(); + if (!deviceID || !userID) { + console.log('no auth metadata'); + throw new Error('No auth metadata'); + } + + // get current device list and primary device ID + const deviceLists = + await identityClient.getDeviceListHistoryForUser(userID); + invariant(deviceLists.length > 0, 'received empty device list history'); + + const lastSignedDeviceList = deviceLists[deviceLists.length - 1]; + const { devices } = rawDeviceListFromSignedList(lastSignedDeviceList); + const primaryDeviceID = devices[0]; + if (deviceID === primaryDeviceID) { + throw new Error('Used secondary device logout on primary device'); + } + + // create and send Olm Tunnelbroker message + const { olmAPI } = getConfig(); + await olmAPI.initializeCryptoAccount(); + const messageContents: SecondaryDeviceLogoutP2PMessage = { + type: 'SECONDARY_DEVICE_LOGOUT', + }; + 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), + }); + + // log out of identity service, keyserver and visually + return logOut(); + }, [identityClient, getAuthMetadata, sendMessage, logOut]); + + return performLogOut; } const claimUsernameActionTypes = Object.freeze({ diff --git a/lib/types/tunnelbroker/peer-to-peer-message-types.js b/lib/types/tunnelbroker/peer-to-peer-message-types.js --- a/lib/types/tunnelbroker/peer-to-peer-message-types.js +++ b/lib/types/tunnelbroker/peer-to-peer-message-types.js @@ -100,6 +100,16 @@ deviceID: t.String, }); +// Olm-encrypted message types +export type SecondaryDeviceLogoutP2PMessage = { + +type: 'SECONDARY_DEVICE_LOGOUT', + // we have senderID so we shouldnt have to put it here, right? +}; +export const secondaryDeviceLogoutP2PMessageValidator: TInterface = + tShape({ + type: tString('SECONDARY_DEVICE_LOGOUT'), + }); + export type PeerToPeerMessage = | OutboundSessionCreation | EncryptedMessage