diff --git a/lib/handlers/peer-to-peer-message-handler.js b/lib/handlers/peer-to-peer-message-handler.js --- a/lib/handlers/peer-to-peer-message-handler.js +++ b/lib/handlers/peer-to-peer-message-handler.js @@ -9,13 +9,16 @@ } from '../types/identity-service-types.js'; import { peerToPeerMessageTypes, + secondaryDeviceLogoutP2PMessageValidator, type PeerToPeerMessage, + type SecondaryDeviceLogoutP2PMessage, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import { getConfig } from '../utils/config.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; import { getMessageForException } from '../utils/errors.js'; import { hasHigherDeviceID, olmSessionErrors } from '../utils/olm-utils.js'; import { getClientMessageIDFromTunnelbrokerMessageID } from '../utils/peer-to-peer-communication-utils.js'; +import { removeDeviceFromDeviceList } from '../shared/device-list-utils.js'; async function peerToPeerMessageHandler( message: PeerToPeerMessage, @@ -106,6 +109,24 @@ 'Decrypted message from device ' + `${message.senderInfo.deviceID}: ${decrypted}`, ); + + try { + const parsedMessageToDevice = JSON.parse(decrypted); + if ( + !secondaryDeviceLogoutP2PMessageValidator.is(parsedMessageToDevice) + ) { + return; + } + const { userID, deviceID: deviceIDToLogOut } = message.senderInfo; + await removeDeviceFromDeviceList( + identityClient, + userID, + deviceIDToLogOut, + ); + // broadcast device list update here + } catch (e) { + console.log(e); + } } catch (e) { if (e.message?.includes(olmSessionErrors.messageAlreadyDecrypted)) { console.log( diff --git a/lib/shared/device-list-utils.js b/lib/shared/device-list-utils.js --- a/lib/shared/device-list-utils.js +++ b/lib/shared/device-list-utils.js @@ -6,6 +6,7 @@ SignedDeviceList, } from '../types/identity-service-types.js'; import { getConfig } from '../utils/config.js'; +import { getContentSigningKey } from '../utils/crypto-utils'; import { composeRawDeviceList, rawDeviceListFromSignedList, @@ -134,4 +135,57 @@ }; } -export { verifyAndGetDeviceList, createAndSignInitialDeviceList }; +async function signDeviceListUpdate( + deviceListPayload: RawDeviceList, +): Promise { + const deviceID = await getContentSigningKey(); + const rawDeviceList = JSON.stringify(deviceListPayload); + + // don't sign device list if current device is not a primary one + if (deviceListPayload.devices[0] !== deviceID) { + return { + rawDeviceList, + }; + } + + const { olmAPI } = getConfig(); + const curPrimarySignature = await olmAPI.signMessage(rawDeviceList); + return { + rawDeviceList, + curPrimarySignature, + }; +} + +async function removeDeviceFromDeviceList( + identityClient: IdentityServiceClient, + userID: string, + deviceIDToRemove: string, +): Promise { + const { getDeviceListHistoryForUser, updateDeviceList } = identityClient; + if (!updateDeviceList) { + throw new Error('calling device list update on non-native device'); + } + + const deviceLists = await getDeviceListHistoryForUser(userID); + if (deviceLists.length < 1) { + throw new Error('received empty device list history'); + } + + const lastSignedDeviceList = deviceLists[deviceLists.length - 1]; + const { devices } = rawDeviceListFromSignedList(lastSignedDeviceList); + if (devices.includes(deviceIDToRemove)) { + return; + } + const newDevices = devices.filter(it => it !== deviceIDToRemove); + + const newDeviceList = composeRawDeviceList(newDevices); + const signedDeviceList = await signDeviceListUpdate(newDeviceList); + await updateDeviceList(signedDeviceList); +} + +export { + verifyAndGetDeviceList, + createAndSignInitialDeviceList, + removeDeviceFromDeviceList, + signDeviceListUpdate, +}; diff --git a/native/profile/secondary-device-qr-code-scanner.react.js b/native/profile/secondary-device-qr-code-scanner.react.js --- a/native/profile/secondary-device-qr-code-scanner.react.js +++ b/native/profile/secondary-device-qr-code-scanner.react.js @@ -7,6 +7,7 @@ import { View } from 'react-native'; import { parseDataFromDeepLink } from 'lib/facts/links.js'; +import { signDeviceListUpdate } from 'lib/shared/device-list-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import { @@ -36,7 +37,6 @@ import { composeTunnelbrokerQRAuthMessage, parseTunnelbrokerQRAuthMessage, - signDeviceListUpdate, } from '../qr-code/qr-code-utils.js'; import { useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; diff --git a/native/qr-code/qr-code-utils.js b/native/qr-code/qr-code-utils.js --- a/native/qr-code/qr-code-utils.js +++ b/native/qr-code/qr-code-utils.js @@ -58,29 +58,4 @@ return Promise.resolve(payload); } -async function signDeviceListUpdate( - deviceListPayload: RawDeviceList, -): Promise { - const deviceID = await getContentSigningKey(); - const rawDeviceList = JSON.stringify(deviceListPayload); - - // don't sign device list if current device is not a primary one - if (deviceListPayload.devices[0] !== deviceID) { - return { - rawDeviceList, - }; - } - - const { olmAPI } = getConfig(); - const curPrimarySignature = await olmAPI.signMessage(rawDeviceList); - return { - rawDeviceList, - curPrimarySignature, - }; -} - -export { - composeTunnelbrokerQRAuthMessage, - parseTunnelbrokerQRAuthMessage, - signDeviceListUpdate, -}; +export { composeTunnelbrokerQRAuthMessage, parseTunnelbrokerQRAuthMessage };