diff --git a/lib/components/peer-olm-session-creator-provider.react.js b/lib/components/peer-olm-session-creator-provider.react.js index 8bdc0181a..891ab9ad4 100644 --- a/lib/components/peer-olm-session-creator-provider.react.js +++ b/lib/components/peer-olm-session-creator-provider.react.js @@ -1,96 +1,95 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import { - createOlmSessionWithPeer, + createOlmSessionsWithUser, type SessionCreationOptions, } from '../utils/crypto-utils.js'; export type PeerOlmSessionCreatorContextType = { +createOlmSessionsWithPeer: ( userID: string, deviceID: string, sessionCreationOptions?: SessionCreationOptions, ) => Promise, }; const PeerOlmSessionCreatorContext: React.Context = React.createContext(); type Props = { +children: React.Node, }; function PeerOlmSessionCreatorProvider(props: Props): React.Node { const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); const { identityClient, getAuthMetadata } = identityContext; const { sendMessageToDevice } = useTunnelbroker(); const runningPromises = React.useRef<{ [userID: string]: { [deviceID: string]: ?Promise }, }>({}); const createOlmSessionsWithPeer = React.useCallback( ( userID: string, deviceID: string, sessionCreationOptions?: SessionCreationOptions, ) => { if ( runningPromises.current[userID] && runningPromises.current[userID][deviceID] ) { return runningPromises.current[userID][deviceID]; } const promise = (async () => { const authMetadata = await getAuthMetadata(); - await createOlmSessionWithPeer( + await createOlmSessionsWithUser( authMetadata, identityClient, sendMessageToDevice, userID, - deviceID, - sessionCreationOptions, + [{ deviceID, sessionCreationOptions }], ); runningPromises.current[userID][deviceID] = null; })(); if (!runningPromises.current[userID]) { runningPromises.current[userID] = {}; } runningPromises.current[userID][deviceID] = promise; return promise; }, [identityClient, sendMessageToDevice, getAuthMetadata], ); const peerOlmSessionCreatorContextValue: PeerOlmSessionCreatorContextType = React.useMemo( () => ({ createOlmSessionsWithPeer }), [createOlmSessionsWithPeer], ); return ( {props.children} ); } function usePeerOlmSessionsCreatorContext(): PeerOlmSessionCreatorContextType { const context = React.useContext(PeerOlmSessionCreatorContext); invariant(context, 'PeerOlmSessionsCreatorContext should be set'); return context; } export { PeerOlmSessionCreatorProvider, usePeerOlmSessionsCreatorContext }; diff --git a/lib/utils/crypto-utils.js b/lib/utils/crypto-utils.js index b45aad685..2f8b58c73 100644 --- a/lib/utils/crypto-utils.js +++ b/lib/utils/crypto-utils.js @@ -1,221 +1,263 @@ // @flow import t from 'tcomb'; import { type TInterface } from 'tcomb'; import { getConfig } from './config.js'; +import { getMessageForException } from './errors.js'; import { primaryIdentityPublicKeyRegex } from './siwe-utils.js'; import { tRegex, tShape } from './validation-utils.js'; import type { AuthMetadata } from '../shared/identity-client-context.js'; import type { TunnelbrokerClientMessageToDevice } from '../tunnelbroker/tunnelbroker-context.js'; import type { IdentityKeysBlob, OLMIdentityKeys, OutboundSessionCreationResult, SignedIdentityKeysBlob, } from '../types/crypto-types.js'; import type { IdentityServiceClient } from '../types/identity-service-types'; import { type OutboundSessionCreation, peerToPeerMessageTypes, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; const signedIdentityKeysBlobValidator: TInterface = tShape({ payload: t.String, signature: t.String, }); const olmIdentityKeysValidator: TInterface = tShape({ ed25519: tRegex(primaryIdentityPublicKeyRegex), curve25519: tRegex(primaryIdentityPublicKeyRegex), }); const identityKeysBlobValidator: TInterface = tShape({ primaryIdentityPublicKeys: olmIdentityKeysValidator, notificationIdentityPublicKeys: olmIdentityKeysValidator, }); async function getContentSigningKey(): Promise { const { olmAPI } = getConfig(); await olmAPI.initializeCryptoAccount(); const { primaryIdentityPublicKeys: { ed25519 }, } = await olmAPI.getUserPublicKey(); return ed25519; } async function createOlmSessionsWithOwnDevices( authMetadata: AuthMetadata, identityClient: IdentityServiceClient, sendMessage: (message: TunnelbrokerClientMessageToDevice) => Promise, ): Promise { const { olmAPI } = getConfig(); const { userID, deviceID, accessToken } = authMetadata; await olmAPI.initializeCryptoAccount(); if (!userID || !deviceID || !accessToken) { throw new Error('CommServicesAuthMetadata is missing'); } const keysResponse = await identityClient.getOutboundKeysForUser(userID); for (const deviceKeys of keysResponse) { const { keys } = deviceKeys; if (!keys) { console.log(`Keys missing for device ${deviceKeys.deviceID}`); continue; } const { primaryIdentityPublicKeys } = keys.identityKeysBlob; if (primaryIdentityPublicKeys.ed25519 === deviceID) { continue; } const recipientDeviceID = primaryIdentityPublicKeys.ed25519; try { const { sessionVersion, encryptedData } = await olmAPI.contentOutboundSessionCreator( primaryIdentityPublicKeys, keys.contentInitializationInfo, ); const sessionCreationMessage: OutboundSessionCreation = { type: peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION, senderInfo: { userID, deviceID, }, encryptedData, sessionVersion, }; await sendMessage({ deviceID: recipientDeviceID, payload: JSON.stringify(sessionCreationMessage), }); console.log( `Request to create a session with device ${recipientDeviceID} sent.`, ); } catch (e) { console.log( 'Error creating outbound session with ' + `device ${recipientDeviceID}: ${e.message}`, ); } } } export type SessionCreationOptions = { +overwriteNotifSession?: boolean, +overwriteContentSession?: boolean, }; -async function createOlmSessionWithPeer( +export type DeviceSessionCreationRequest = { + +deviceID: string, + +sessionCreationOptions?: SessionCreationOptions, +}; +async function createOlmSessionsWithUser( authMetadata: AuthMetadata, identityClient: IdentityServiceClient, sendMessage: (message: TunnelbrokerClientMessageToDevice) => Promise, userID: string, - deviceID: string, - options?: SessionCreationOptions, + devices: $ReadOnlyArray, ): Promise { const { olmAPI } = getConfig(); await olmAPI.initializeCryptoAccount(); - const [hasContentSession, hasNotifsSession] = await Promise.all([ - (async () => { - if (options?.overwriteContentSession) { - return false; - } - return await olmAPI.isContentSessionInitialized(deviceID); - })(), - (async () => { - if (options?.overwriteNotifSession) { - return false; - } - return await olmAPI.isDeviceNotificationsSessionInitialized(deviceID); - })(), - ]); - - if (hasContentSession && hasNotifsSession) { - return; - } const { userID: authUserID, deviceID: authDeviceID, accessToken, } = authMetadata; if (!authUserID || !authDeviceID || !accessToken) { throw new Error('CommServicesAuthMetadata is missing'); } - const keysResponse = await identityClient.getOutboundKeysForUser(userID); - const deviceKeysResponse = keysResponse.find( - keys => keys.deviceID === deviceID, + const filteredDevices: $ReadOnlyArray<{ + +deviceID: string, + +hasContentSession: boolean, + +hasNotifsSession: boolean, + }> = await Promise.all( + devices.map(request => + (async () => { + const [hasContentSession, hasNotifsSession] = await Promise.all([ + (async () => { + if (request.sessionCreationOptions?.overwriteContentSession) { + return false; + } + return await olmAPI.isContentSessionInitialized(request.deviceID); + })(), + (async () => { + if (request.sessionCreationOptions?.overwriteNotifSession) { + return false; + } + return await olmAPI.isDeviceNotificationsSessionInitialized( + request.deviceID, + ); + })(), + ]); + return { + deviceID: request.deviceID, + hasContentSession, + hasNotifsSession, + }; + })(), + ), ); - if (!deviceKeysResponse || !deviceKeysResponse.keys) { - throw new Error(`Keys missing for device ${deviceID}`); - } - const { keys } = deviceKeysResponse; - - const { primaryIdentityPublicKeys, notificationIdentityPublicKeys } = - keys.identityKeysBlob; - const recipientDeviceID = primaryIdentityPublicKeys.ed25519; - - if (hasContentSession) { - await olmAPI.notificationsOutboundSessionCreator( - recipientDeviceID, - notificationIdentityPublicKeys, - keys.notifInitializationInfo, - ); + + const sessionCreationNeeded = filteredDevices.some( + device => !device.hasContentSession || !device.hasNotifsSession, + ); + if (!sessionCreationNeeded) { return; } - let outboundSessionCreationResult: OutboundSessionCreationResult; - if (hasNotifsSession) { - outboundSessionCreationResult = await olmAPI.contentOutboundSessionCreator( - primaryIdentityPublicKeys, - keys.contentInitializationInfo, - ); - } else { - [outboundSessionCreationResult] = await Promise.all([ - await olmAPI.contentOutboundSessionCreator( - primaryIdentityPublicKeys, - keys.contentInitializationInfo, - ), - olmAPI.notificationsOutboundSessionCreator( - recipientDeviceID, - notificationIdentityPublicKeys, - keys.notifInitializationInfo, - ), - ]); - } + const keysResponse = await identityClient.getOutboundKeysForUser(userID); + + const devicePromises = filteredDevices.map(async sessionRequest => { + try { + const { deviceID, hasContentSession, hasNotifsSession } = sessionRequest; + const deviceKeysResponse = keysResponse.find( + keys => keys.deviceID === deviceID, + ); + + if (!deviceKeysResponse || !deviceKeysResponse.keys) { + console.log(`Keys missing for device ${deviceID}`); + return; + } + const { keys } = deviceKeysResponse; - const { sessionVersion, encryptedData } = outboundSessionCreationResult; - - const sessionCreationMessage: OutboundSessionCreation = { - type: peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION, - senderInfo: { - userID: authUserID, - deviceID: authDeviceID, - }, - encryptedData, - sessionVersion, - }; - - await sendMessage({ - deviceID: recipientDeviceID, - payload: JSON.stringify(sessionCreationMessage), + const { primaryIdentityPublicKeys, notificationIdentityPublicKeys } = + keys.identityKeysBlob; + const recipientDeviceID = primaryIdentityPublicKeys.ed25519; + + if (hasContentSession) { + await olmAPI.notificationsOutboundSessionCreator( + recipientDeviceID, + notificationIdentityPublicKeys, + keys.notifInitializationInfo, + ); + return; + } + + let outboundSessionCreationResult: OutboundSessionCreationResult; + if (hasNotifsSession) { + outboundSessionCreationResult = + await olmAPI.contentOutboundSessionCreator( + primaryIdentityPublicKeys, + keys.contentInitializationInfo, + ); + } else { + [outboundSessionCreationResult] = await Promise.all([ + await olmAPI.contentOutboundSessionCreator( + primaryIdentityPublicKeys, + keys.contentInitializationInfo, + ), + olmAPI.notificationsOutboundSessionCreator( + recipientDeviceID, + notificationIdentityPublicKeys, + keys.notifInitializationInfo, + ), + ]); + } + + const { sessionVersion, encryptedData } = outboundSessionCreationResult; + + const sessionCreationMessage: OutboundSessionCreation = { + type: peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION, + senderInfo: { + userID: authUserID, + deviceID: authDeviceID, + }, + encryptedData, + sessionVersion, + }; + + await sendMessage({ + deviceID: recipientDeviceID, + payload: JSON.stringify(sessionCreationMessage), + }); + console.log( + `Request to create a session with device ${recipientDeviceID} sent.`, + ); + } catch (e) { + console.error( + `Error creating session with ${sessionRequest.deviceID}: ${ + getMessageForException(e) ?? 'unknown error' + }`, + e, + ); + } }); - console.log( - `Request to create a session with device ${recipientDeviceID} sent.`, - ); + await Promise.all(devicePromises); } export { signedIdentityKeysBlobValidator, identityKeysBlobValidator, getContentSigningKey, createOlmSessionsWithOwnDevices, - createOlmSessionWithPeer, + createOlmSessionsWithUser, };