diff --git a/lib/handlers/peer-to-peer-message-handler.js b/lib/handlers/peer-to-peer-message-handler.js index 6bc0d1a7c..256b2ac4a 100644 --- a/lib/handlers/peer-to-peer-message-handler.js +++ b/lib/handlers/peer-to-peer-message-handler.js @@ -1,78 +1,91 @@ // @flow import type { IdentityServiceClient, DeviceOlmInboundKeys, } from '../types/identity-service-types.js'; import { peerToPeerMessageTypes, type PeerToPeerMessage, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import { getConfig } from '../utils/config.js'; +import { olmSessionErrors } from '../utils/olm-utils.js'; async function peerToPeerMessageHandler( message: PeerToPeerMessage, identityClient: IdentityServiceClient, ): Promise { const { olmAPI } = getConfig(); if (message.type === peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION) { + const { senderInfo, encryptedData, sessionVersion } = message; + const { userID: senderUserID, deviceID: senderDeviceID } = senderInfo; try { - const { senderInfo, encryptedData, sessionVersion } = message; - const { keys } = await identityClient.getInboundKeysForUser( - senderInfo.userID, - ); + const { keys } = await identityClient.getInboundKeysForUser(senderUserID); - const deviceKeys: ?DeviceOlmInboundKeys = keys[senderInfo.deviceID]; + const deviceKeys: ?DeviceOlmInboundKeys = keys[senderDeviceID]; if (!deviceKeys) { throw new Error( 'No keys for the device that requested creating a session, ' + - `deviceID: ${senderInfo.deviceID}`, + `deviceID: ${senderDeviceID}`, ); } await olmAPI.initializeCryptoAccount(); const result = await olmAPI.contentInboundSessionCreator( deviceKeys.identityKeysBlob.primaryIdentityPublicKeys, encryptedData, sessionVersion, ); console.log( 'Created inbound session with device ' + - `${message.senderInfo.deviceID}: ${result}, ` + + `${senderDeviceID}: ${result}, ` + `session version: ${sessionVersion}`, ); } catch (e) { - console.log( - 'Error creating inbound session with device ' + - `${message.senderInfo.deviceID}: ${e.message}`, - ); + 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)) { + console.log( + 'Race condition while creating session with ' + + `${senderDeviceID}, session version: ${sessionVersion}`, + ); + } 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.decrypt( message.encryptedData, message.senderInfo.deviceID, ); console.log( 'Decrypted message from device ' + `${message.senderInfo.deviceID}: ${decrypted}`, ); } catch (e) { 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}`); } } } export { peerToPeerMessageHandler }; diff --git a/lib/utils/olm-utils.js b/lib/utils/olm-utils.js index be238b5ee..18151edef 100644 --- a/lib/utils/olm-utils.js +++ b/lib/utils/olm-utils.js @@ -1,126 +1,136 @@ // @flow import type { Account as OlmAccount } from '@commapp/olm'; import { getOneTimeKeyValuesFromBlob, getPrekeyValueFromBlob, } from '../shared/crypto-utils.js'; import { ONE_TIME_KEYS_NUMBER } from '../types/identity-service-types.js'; const maxPublishedPrekeyAge = 30 * 24 * 60 * 60 * 1000; const maxOldPrekeyAge = 24 * 60 * 60 * 1000; type AccountKeysSet = { +identityKeys: string, +prekey: string, +prekeySignature: string, +oneTimeKeys: $ReadOnlyArray, }; type IdentityKeysAndPrekeys = { +identityKeys: string, +prekey: string, +prekeySignature: string, }; function validateAccountPrekey(account: OlmAccount) { if (shouldRotatePrekey(account)) { account.generate_prekey(); } if (shouldForgetPrekey(account)) { account.forget_old_prekey(); } } function shouldRotatePrekey(account: OlmAccount): boolean { // Our fork of Olm only remembers two prekeys at a time. // If the new one hasn't been published, then the old one is still active. // In that scenario, we need to avoid rotating the prekey because it will // result in the old active prekey being discarded. if (account.unpublished_prekey()) { return false; } const currentDate = new Date(); const lastPrekeyPublishDate = getLastPrekeyPublishTime(account); return ( currentDate.getTime() - lastPrekeyPublishDate.getTime() >= maxPublishedPrekeyAge ); } function shouldForgetPrekey(account: OlmAccount): boolean { // Our fork of Olm only remembers two prekeys at a time. // We have to hold onto the old one until the new one is published. if (account.unpublished_prekey()) { return false; } const currentDate = new Date(); const lastPrekeyPublishDate = getLastPrekeyPublishTime(account); return ( currentDate.getTime() - lastPrekeyPublishDate.getTime() >= maxOldPrekeyAge ); } function getLastPrekeyPublishTime(account: OlmAccount): Date { const olmLastPrekeyPublishTime = account.last_prekey_publish_time(); // Olm uses seconds, while the Date() constructor expects milliseconds. return new Date(olmLastPrekeyPublishTime * 1000); } function getAccountPrekeysSet(account: OlmAccount): { +prekey: string, +prekeySignature: ?string, } { const prekey = getPrekeyValueFromBlob(account.prekey()); const prekeySignature = account.prekey_signature(); return { prekey, prekeySignature }; } function getAccountOneTimeKeys( account: OlmAccount, numberOfKeys: number = ONE_TIME_KEYS_NUMBER, ): $ReadOnlyArray { let oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); if (oneTimeKeys.length < numberOfKeys) { account.generate_one_time_keys(numberOfKeys - oneTimeKeys.length); oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); } return oneTimeKeys; } function retrieveAccountKeysSet(account: OlmAccount): AccountKeysSet { const { identityKeys, prekey, prekeySignature } = retrieveIdentityKeysAndPrekeys(account); const oneTimeKeys = getAccountOneTimeKeys(account, ONE_TIME_KEYS_NUMBER); return { identityKeys, oneTimeKeys, prekey, prekeySignature }; } function retrieveIdentityKeysAndPrekeys( account: OlmAccount, ): IdentityKeysAndPrekeys { const identityKeys = account.identity_keys(); validateAccountPrekey(account); const { prekey, prekeySignature } = getAccountPrekeysSet(account); if (!prekeySignature || !prekey) { throw new Error('invalid_prekey'); } return { identityKeys, prekey, prekeySignature }; } +const olmSessionErrors = Object.freeze({ + // Two clients send the session request to each other at the same time, + // we choose which session to keep based on `deviceID`. + raceCondition: 'OLM_SESSION_CREATION_RACE_CONDITION', + // The client received a session request with a lower session version, + // this request can be ignored. + alreadyCreated: 'OLM_SESSION_ALREADY_CREATED', +}); + export { retrieveAccountKeysSet, getAccountPrekeysSet, shouldForgetPrekey, shouldRotatePrekey, getAccountOneTimeKeys, retrieveIdentityKeysAndPrekeys, + olmSessionErrors, };