diff --git a/lib/handlers/peer-to-peer-message-handler.js b/lib/handlers/peer-to-peer-message-handler.js index 1ee06bc7a..2a6864bf8 100644 --- a/lib/handlers/peer-to-peer-message-handler.js +++ b/lib/handlers/peer-to-peer-message-handler.js @@ -1,92 +1,119 @@ // @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'; +import { getContentSigningKey } from '../utils/crypto-utils.js'; +import { hasHigherDeviceID, 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; + + let deviceKeys: ?DeviceOlmInboundKeys = null; try { const { keys } = await identityClient.getInboundKeysForUser(senderUserID); + deviceKeys = keys[senderDeviceID]; + } catch (e) { + console.log(e.message); + } - const deviceKeys: ?DeviceOlmInboundKeys = keys[senderDeviceID]; - if (!deviceKeys) { - throw new Error( - 'No keys for the device that requested creating a session, ' + - `deviceID: ${senderDeviceID}`, - ); - } + 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)) { - console.log( - 'Race condition while creating session with ' + - `${senderDeviceID}, session version: ${sessionVersion}`, - ); + 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.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 18151edef..9854b8e28 100644 --- a/lib/utils/olm-utils.js +++ b/lib/utils/olm-utils.js @@ -1,136 +1,148 @@ // @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', }); +function hasHigherDeviceID( + currenDeviceID: string, + otherDeviceID: string, +): boolean { + const compareResult = currenDeviceID.localeCompare(otherDeviceID); + if (compareResult === 0) { + throw new Error('Comparing the same deviceIDs'); + } + return compareResult === 1; +} + export { retrieveAccountKeysSet, getAccountPrekeysSet, shouldForgetPrekey, shouldRotatePrekey, getAccountOneTimeKeys, retrieveIdentityKeysAndPrekeys, + hasHigherDeviceID, olmSessionErrors, };