diff --git a/lib/handlers/peer-to-peer-message-handler.js b/lib/handlers/peer-to-peer-message-handler.js index 9733fe7c6..fe77c1c48 100644 --- a/lib/handlers/peer-to-peer-message-handler.js +++ b/lib/handlers/peer-to-peer-message-handler.js @@ -1,209 +1,212 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import { verifyAndGetDeviceList, removeDeviceFromDeviceList, } from '../shared/device-list-utils.js'; import type { IdentityServiceClient, DeviceOlmInboundKeys, } from '../types/identity-service-types.js'; import { peerToPeerMessageTypes, type PeerToPeerMessage, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import { secondaryDeviceLogoutP2PMessageValidator } from '../types/tunnelbroker/user-actions-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'; async function peerToPeerMessageHandler( message: PeerToPeerMessage, identityClient: IdentityServiceClient, messageID: string, ): Promise { const { olmAPI, sqliteAPI } = getConfig(); if (message.type === peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION) { const { senderInfo, encryptedData, sessionVersion } = message; const { userID: senderUserID, deviceID: senderDeviceID } = senderInfo; let deviceKeys: ?DeviceOlmInboundKeys = null; try { const { keys } = await identityClient.getInboundKeysForUser(senderUserID); deviceKeys = keys[senderDeviceID]; } catch (e) { console.log(e.message); } if (!deviceKeys) { console.log( 'Error creating inbound session with device ' + `${senderDeviceID}: No keys for the device, ` + `session version: ${sessionVersion}`, ); return; } try { await olmAPI.initializeCryptoAccount(); const result = await olmAPI.contentInboundSessionCreator( deviceKeys.identityKeysBlob.primaryIdentityPublicKeys, encryptedData, sessionVersion, false, ); console.log( 'Created inbound session with device ' + `${senderDeviceID}: ${result}, ` + `session version: ${sessionVersion}`, ); } catch (e) { if (e.message?.includes(olmSessionErrors.alreadyCreated)) { console.log( 'Received session request with lower session version from ' + `${senderDeviceID}, session version: ${sessionVersion}`, ); } else if (e.message?.includes(olmSessionErrors.raceCondition)) { const currentDeviceID = await getContentSigningKey(); if (hasHigherDeviceID(currentDeviceID, senderDeviceID)) { console.log( 'Race condition while creating session with ' + `${senderDeviceID}, session version: ${sessionVersion}, ` + `this device has a higher deviceID and the session will be kept`, ); } else { const result = await olmAPI.contentInboundSessionCreator( deviceKeys.identityKeysBlob.primaryIdentityPublicKeys, encryptedData, sessionVersion, true, ); console.log( 'Overwrite session with device ' + `${senderDeviceID}: ${result}, ` + `session version: ${sessionVersion}`, ); // Resend all not-yet confirmed messages that were encrypted // with overwrite session. Tracked in ENG-6982. } } else { console.log( 'Error creating inbound session with device ' + `${senderDeviceID}: ${e.message}, ` + `session version: ${sessionVersion}`, ); } } } else if (message.type === peerToPeerMessageTypes.ENCRYPTED_MESSAGE) { try { await olmAPI.initializeCryptoAccount(); const decrypted = await olmAPI.decryptSequentialAndPersist( message.encryptedData, message.senderInfo.deviceID, messageID, ); console.log( '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, ); // TODO: broadcast device list update here } catch (e) { console.log(e); } } catch (e) { if (e.message?.includes(olmSessionErrors.messageAlreadyDecrypted)) { console.log( 'Received already decrypted message from device ' + `${message.senderInfo.deviceID}.`, ); } else if (e.message?.includes(olmSessionErrors.messageOutOfOrder)) { console.log( 'Received out-of-order message from device ' + `${message.senderInfo.deviceID}.`, ); } else { console.log( 'Error decrypting message from device ' + `${message.senderInfo.deviceID}: ${e.message}`, ); } } } else if (message.type === peerToPeerMessageTypes.REFRESH_KEY_REQUEST) { try { await olmAPI.initializeCryptoAccount(); const oneTimeKeys = await olmAPI.getOneTimeKeys(message.numberOfKeys); await identityClient.uploadOneTimeKeys(oneTimeKeys); } catch (e) { console.log(`Error uploading one-time keys: ${e.message}`); } } else if (message.type === peerToPeerMessageTypes.DEVICE_LIST_UPDATED) { try { const result = await verifyAndGetDeviceList( identityClient, message.userID, null, ); - if (result.valid) { + if (!result.valid) { console.log( - `Received valid device list update for user ${message.userID}`, + `Received invalid device list update for user ${message.userID}. Reason: ${result.reason}`, ); + return; + } + console.log( + `Received valid device list update for user ${message.userID}`, + ); + + if (message?.signedDeviceList?.rawDeviceList) { const receivedRawList = JSON.parse( message.signedDeviceList.rawDeviceList, ); // additional check for broadcasted and Identity device list equality const listsAreEqual = _isEqual(result.deviceList)(receivedRawList); console.log( `Identity and received device lists are ${ listsAreEqual ? '' : 'not' } equal.`, ); - } else { - console.log( - `Received invalid device list update for user ${message.userID}. Reason: ${result.reason}`, - ); } } catch (e) { console.log( `Error verifying device list for user ${message.userID}: ${e}`, ); } } else if (message.type === peerToPeerMessageTypes.MESSAGE_PROCESSED) { try { const { deviceID, messageID: tunnelbrokerMessageID } = message; const clientMessageID = getClientMessageIDFromTunnelbrokerMessageID( tunnelbrokerMessageID, ); await sqliteAPI.removeOutboundP2PMessagesOlderThan( clientMessageID, deviceID, ); } catch (e) { console.log( `Error removing message after processing: ${ getMessageForException(e) ?? 'unknown error' }`, ); } } } export { peerToPeerMessageHandler }; diff --git a/lib/types/tunnelbroker/peer-to-peer-message-types.js b/lib/types/tunnelbroker/peer-to-peer-message-types.js index bcd06de9a..4d2861575 100644 --- a/lib/types/tunnelbroker/peer-to-peer-message-types.js +++ b/lib/types/tunnelbroker/peer-to-peer-message-types.js @@ -1,118 +1,118 @@ // @flow import type { TInterface, TUnion } from 'tcomb'; import t from 'tcomb'; import { tShape, tString, tUserID } from '../../utils/validation-utils.js'; import { type EncryptedData, encryptedDataValidator } from '../crypto-types.js'; import { signedDeviceListValidator, type SignedDeviceList, } from '../identity-service-types.js'; export type SenderInfo = { +userID: string, +deviceID: string, }; const senderInfoValidator: TInterface = tShape({ userID: tUserID, deviceID: t.String, }); export const peerToPeerMessageTypes = Object.freeze({ OUTBOUND_SESSION_CREATION: 'OutboundSessionCreation', ENCRYPTED_MESSAGE: 'EncryptedMessage', REFRESH_KEY_REQUEST: 'RefreshKeyRequest', QR_CODE_AUTH_MESSAGE: 'QRCodeAuthMessage', DEVICE_LIST_UPDATED: 'DeviceListUpdated', MESSAGE_PROCESSED: 'MessageProcessed', }); export type OutboundSessionCreation = { +type: 'OutboundSessionCreation', +senderInfo: SenderInfo, +encryptedData: EncryptedData, +sessionVersion: number, }; export const outboundSessionCreationValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION), senderInfo: senderInfoValidator, encryptedData: encryptedDataValidator, sessionVersion: t.Number, }); export type EncryptedMessage = { +type: 'EncryptedMessage', +senderInfo: SenderInfo, +encryptedData: EncryptedData, }; export const encryptedMessageValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.ENCRYPTED_MESSAGE), senderInfo: senderInfoValidator, encryptedData: encryptedDataValidator, }); export type RefreshKeyRequest = { +type: 'RefreshKeyRequest', +deviceID: string, +numberOfKeys: number, }; export const refreshKeysRequestValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.REFRESH_KEY_REQUEST), deviceID: t.String, numberOfKeys: t.Number, }); export type QRCodeAuthMessage = { +type: 'QRCodeAuthMessage', +encryptedContent: string, }; export const qrCodeAuthMessageValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE), encryptedContent: t.String, }); export type DeviceListUpdated = { +type: 'DeviceListUpdated', +userID: string, - +signedDeviceList: SignedDeviceList, + +signedDeviceList?: SignedDeviceList, }; export const deviceListUpdatedValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.DEVICE_LIST_UPDATED), userID: tUserID, - signedDeviceList: signedDeviceListValidator, + signedDeviceList: t.maybe(signedDeviceListValidator), }); export type MessageProcessed = { +type: 'MessageProcessed', +messageID: string, +deviceID: string, }; export const messageProcessedValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.MESSAGE_PROCESSED), messageID: t.String, deviceID: t.String, }); export type PeerToPeerMessage = | OutboundSessionCreation | EncryptedMessage | RefreshKeyRequest | QRCodeAuthMessage | DeviceListUpdated | MessageProcessed; export const peerToPeerMessageValidator: TUnion = t.union([ outboundSessionCreationValidator, encryptedMessageValidator, refreshKeysRequestValidator, qrCodeAuthMessageValidator, deviceListUpdatedValidator, messageProcessedValidator, ]);