diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -3,6 +3,7 @@ import invariant from 'invariant'; import * as React from 'react'; +import { usePeerOlmSessionsCreatorContext } from '../components/peer-olm-session-creator-provider.react.js'; import { useBroadcastDeviceListUpdates, useBroadcastAccountDeletion, @@ -90,7 +91,6 @@ } from '../types/user-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { getConfig } from '../utils/config.js'; -import { createOlmSessionWithPeer } from '../utils/crypto-utils.js'; import { getMessageForException } from '../utils/errors.js'; import { useSelector } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; @@ -273,6 +273,7 @@ const foreignPeerDevices = useSelector(getForeignPeerDevices); const logOut = useLogOut(primaryDeviceLogOutOptions); + const { createOlmSessionsWithPeer } = usePeerOlmSessionsCreatorContext(); return React.useCallback(async () => { const { identityClient, getAuthMetadata } = identityContext; const authMetadata = await getAuthMetadata(); @@ -310,13 +311,7 @@ }); } catch { try { - await createOlmSessionWithPeer( - authMetadata, - identityClient, - sendMessageToDevice, - userID, - deviceID, - ); + await createOlmSessionsWithPeer(userID, deviceID); const encryptedData = await olmAPI.encrypt( JSON.stringify(messageContents), deviceID, @@ -349,6 +344,7 @@ await broadcastDeviceListUpdates(foreignPeerDevices); return logOutResult; }, [ + createOlmSessionsWithPeer, broadcastDeviceListUpdates, foreignPeerDevices, identityContext, @@ -369,6 +365,7 @@ if (!identityContext) { throw new Error('Identity service client is not initialized'); } + const { createOlmSessionsWithPeer } = usePeerOlmSessionsCreatorContext(); return React.useCallback(async () => { const { identityClient, getAuthMetadata } = identityContext; @@ -407,13 +404,7 @@ }); } catch { try { - await createOlmSessionWithPeer( - authMetadata, - identityClient, - sendMessageToDevice, - userID, - primaryDeviceID, - ); + await createOlmSessionsWithPeer(userID, primaryDeviceID); const encryptedData = await olmAPI.encrypt( JSON.stringify(messageContents), primaryDeviceID, @@ -434,7 +425,7 @@ // log out of identity service, keyserver and visually return logOut(); - }, [identityContext, sendMessageToDevice, logOut]); + }, [createOlmSessionsWithPeer, identityContext, sendMessageToDevice, logOut]); } const claimUsernameActionTypes = Object.freeze({ diff --git a/lib/components/peer-olm-session-creator-provider.react.js b/lib/components/peer-olm-session-creator-provider.react.js new file mode 100644 --- /dev/null +++ b/lib/components/peer-olm-session-creator-provider.react.js @@ -0,0 +1,87 @@ +// @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 } from '../utils/crypto-utils.js'; + +export type PeerOlmSessionCreatorContextType = { + +createOlmSessionsWithPeer: ( + userID: string, + deviceID: string, + ) => 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) => { + if ( + runningPromises.current[userID] && + runningPromises.current[userID][deviceID] + ) { + return runningPromises.current[userID][deviceID]; + } + + const promise = (async () => { + const authMetadata = await getAuthMetadata(); + await createOlmSessionWithPeer( + authMetadata, + identityClient, + sendMessageToDevice, + userID, + deviceID, + ); + + 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/hooks/peer-list-hooks.js b/lib/hooks/peer-list-hooks.js --- a/lib/hooks/peer-list-hooks.js +++ b/lib/hooks/peer-list-hooks.js @@ -4,6 +4,7 @@ import * as React from 'react'; import { setPeerDeviceListsActionType } from '../actions/aux-user-actions.js'; +import { usePeerOlmSessionsCreatorContext } from '../components/peer-olm-session-creator-provider.react.js'; import { getAllPeerDevices, getForeignPeerDevices, @@ -26,10 +27,7 @@ type AccountDeletionP2PMessage, } from '../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; import { getConfig } from '../utils/config.js'; -import { - getContentSigningKey, - createOlmSessionWithPeer, -} from '../utils/crypto-utils.js'; +import { getContentSigningKey } from '../utils/crypto-utils.js'; import { convertSignedDeviceListsToRawDeviceLists } from '../utils/device-list-utils.js'; import { values } from '../utils/objects.js'; import { useDispatch, useSelector } from '../utils/redux-utils.js'; @@ -160,9 +158,10 @@ ? getAllPeerDevices : getForeignPeerDevices; const peerDevices = useSelector(devicesSelector); + const { createOlmSessionsWithPeer } = usePeerOlmSessionsCreatorContext(); return React.useCallback(async () => { - const { identityClient, getAuthMetadata } = identityContext; + const { getAuthMetadata } = identityContext; const authMetadata = await getAuthMetadata(); const { userID, deviceID: thisDeviceID } = authMetadata; if (!thisDeviceID || !userID) { @@ -194,13 +193,7 @@ }); } catch { try { - await createOlmSessionWithPeer( - authMetadata, - identityClient, - sendMessageToDevice, - userID, - deviceID, - ); + await createOlmSessionsWithPeer(userID, deviceID); const encryptedData = await olmAPI.encrypt(rawPayload, deviceID); const encryptedMessage: EncryptedMessage = { type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, diff --git a/lib/push/send-hooks.react.js b/lib/push/send-hooks.react.js --- a/lib/push/send-hooks.react.js +++ b/lib/push/send-hooks.react.js @@ -8,6 +8,7 @@ } from './send-utils.js'; import { ENSCacheContext } from '../components/ens-cache-provider.react.js'; import { NeynarClientContext } from '../components/neynar-client-provider.react.js'; +import { usePeerOlmSessionsCreatorContext } from '../components/peer-olm-session-creator-provider.react.js'; import { thickRawThreadInfosSelector } from '../selectors/thread-selectors.js'; import type { MessageData } from '../types/message-types.js'; import type { @@ -29,6 +30,9 @@ const { getENSNames } = React.useContext(ENSCacheContext); const getFCNames = React.useContext(NeynarClientContext)?.getFCNames; + const { createOlmSessionsWithPeer: olmSessionCreator } = + usePeerOlmSessionsCreatorContext(); + return React.useCallback( ( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, @@ -38,6 +42,7 @@ return preparePushNotifs({ encryptedNotifUtilsAPI, senderDeviceDescriptor, + olmSessionCreator, messageInfos: rawMessageInfos, thickRawThreadInfos, auxUserInfos, @@ -48,6 +53,7 @@ }); }, [ + olmSessionCreator, rawMessageInfos, thickRawThreadInfos, auxUserInfos, diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js --- a/lib/push/send-utils.js +++ b/lib/push/send-utils.js @@ -48,6 +48,7 @@ import type { ThreadSubscription } from '../types/subscription-types.js'; import type { ThickRawThreadInfos } from '../types/thread-types.js'; import type { UserInfos } from '../types/user-types.js'; +import { getConfig } from '../utils/config.js'; import { type GetENSNames } from '../utils/ens-helpers.js'; import { type GetFCNames } from '../utils/farcaster-helpers.js'; import { promiseAll } from '../utils/promises.js'; @@ -654,6 +655,7 @@ type PreparePushNotifsInputData = { +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +senderDeviceDescriptor: SenderDeviceDescriptor, + +olmSessionCreator: (userID: string, deviceID: string) => Promise, +messageInfos: { +[id: string]: RawMessageInfo }, +thickRawThreadInfos: ThickRawThreadInfos, +auxUserInfos: AuxUserInfos, @@ -669,6 +671,7 @@ const { encryptedNotifUtilsAPI, senderDeviceDescriptor, + olmSessionCreator, messageDatas, messageInfos, auxUserInfos, @@ -689,6 +692,43 @@ return null; } + const { + initializeCryptoAccount, + isNotificationsSessionInitializedWithDevices, + } = getConfig().olmAPI; + await initializeCryptoAccount(); + + const deviceIDsToUserIDs: { [string]: string } = {}; + for (const userID in pushInfos) { + for (const device of pushInfos[userID].devices) { + deviceIDsToUserIDs[device.cryptoID] = userID; + } + } + + const deviceIDsToSessionPresence = + await isNotificationsSessionInitializedWithDevices( + Object.keys(deviceIDsToUserIDs), + ); + + const olmSessionCreationPromises = []; + for (const deviceID in deviceIDsToSessionPresence) { + if (deviceIDsToSessionPresence[deviceID]) { + continue; + } + olmSessionCreationPromises.push( + olmSessionCreator(deviceIDsToUserIDs[deviceID], deviceID), + ); + } + + try { + await Promise.allSettled(olmSessionCreationPromises); + } catch (e) { + // session creation may fail for some devices + // but we should still pursue notification + // delivery for others + console.log(e); + } + return await buildNotifsFromPushInfo({ encryptedNotifUtilsAPI, senderDeviceDescriptor, diff --git a/lib/tunnelbroker/peer-to-peer-context.js b/lib/tunnelbroker/peer-to-peer-context.js --- a/lib/tunnelbroker/peer-to-peer-context.js +++ b/lib/tunnelbroker/peer-to-peer-context.js @@ -8,6 +8,7 @@ type TunnelbrokerClientMessageToDevice, useTunnelbroker, } from './tunnelbroker-context.js'; +import { usePeerOlmSessionsCreatorContext } from '../components/peer-olm-session-creator-provider.react.js'; import { createMessagesToPeersFromDMOp, type DMOperationSpecification, @@ -26,7 +27,6 @@ peerToPeerMessageTypes, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import { getConfig } from '../utils/config.js'; -import { createOlmSessionWithPeer } from '../utils/crypto-utils.js'; import { getMessageForException } from '../utils/errors.js'; import { useDispatch, useSelector } from '../utils/redux-utils.js'; @@ -51,6 +51,7 @@ messageID: ?string, ) => Promise, identityContext: IdentityClientContextType, + peerOlmSessionsCreator: (userID: string, deviceID: string) => Promise, messageIDs: ?$ReadOnlyArray, ): Promise { const authMetadata = await identityContext.getAuthMetadata(); @@ -125,13 +126,7 @@ await sendMessageToPeer(encryptedMessage); } catch (e) { try { - await createOlmSessionWithPeer( - authMetadata, - identityContext.identityClient, - sendMessage, - message.userID, - peerDeviceID, - ); + await peerOlmSessionsCreator(message.userID, peerDeviceID); const result = await olmAPI.encryptAndPersist( message.plaintext, message.deviceID, @@ -204,6 +199,9 @@ >([]); const promiseRunning = React.useRef(false); + const { createOlmSessionsWithPeer: peerOlmSessionsCreator } = + usePeerOlmSessionsCreatorContext(); + const processOutboundMessages = React.useCallback( (outboundMessageIDs: ?$ReadOnlyArray, dmOpID: ?string) => { processingQueue.current.push({ outboundMessageIDs, dmOpID }); @@ -216,6 +214,7 @@ await processOutboundP2PMessages( sendMessageToDevice, identityContext, + peerOlmSessionsCreator, queueFront?.outboundMessageIDs, ); if (queueFront.dmOpID) { @@ -244,7 +243,7 @@ })(); } }, - [identityContext, sendMessageToDevice], + [peerOlmSessionsCreator, identityContext, sendMessageToDevice], ); React.useEffect(() => { diff --git a/lib/tunnelbroker/tunnelbroker-context.js b/lib/tunnelbroker/tunnelbroker-context.js --- a/lib/tunnelbroker/tunnelbroker-context.js +++ b/lib/tunnelbroker/tunnelbroker-context.js @@ -8,6 +8,7 @@ import { PeerToPeerProvider } from './peer-to-peer-context.js'; import { PeerToPeerMessageHandler } from './peer-to-peer-message-handler.js'; import type { SecondaryTunnelbrokerConnection } from './secondary-tunnelbroker-connection.js'; +import { PeerOlmSessionCreatorProvider } from '../components/peer-olm-session-creator-provider.react.js'; import { tunnnelbrokerURL } from '../facts/tunnelbroker.js'; import { DMOpsQueueHandler } from '../shared/dm-ops/dm-ops-queue-handler.react.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; @@ -461,7 +462,9 @@ doesSocketExist={doesSocketExist} socketSend={socketSend} /> - {children} + + {children} + ); diff --git a/lib/utils/crypto-utils.js b/lib/utils/crypto-utils.js --- a/lib/utils/crypto-utils.js +++ b/lib/utils/crypto-utils.js @@ -11,8 +11,9 @@ import type { IdentityKeysBlob, OLMIdentityKeys, + OutboundSessionCreationResult, SignedIdentityKeysBlob, -} from '../types/crypto-types'; +} from '../types/crypto-types.js'; import type { IdentityServiceClient } from '../types/identity-service-types'; import { type OutboundSessionCreation, @@ -115,6 +116,14 @@ ): Promise { const { olmAPI } = getConfig(); await olmAPI.initializeCryptoAccount(); + const [hasContentSession, hasNotifsSession] = await Promise.all([ + olmAPI.isContentSessionInitialized(deviceID), + olmAPI.isDeviceNotificationsSessionInitialized(deviceID), + ]); + + if (hasContentSession && hasNotifsSession) { + return; + } const { userID: authUserID, @@ -134,14 +143,40 @@ } const { keys } = deviceKeysResponse; - const { primaryIdentityPublicKeys } = keys.identityKeysBlob; + const { primaryIdentityPublicKeys, notificationIdentityPublicKeys } = + keys.identityKeysBlob; const recipientDeviceID = primaryIdentityPublicKeys.ed25519; - const { sessionVersion, encryptedData } = - await olmAPI.contentOutboundSessionCreator( + 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, diff --git a/native/push/encrypted-notif-utils-api.js b/native/push/encrypted-notif-utils-api.js --- a/native/push/encrypted-notif-utils-api.js +++ b/native/push/encrypted-notif-utils-api.js @@ -14,8 +14,7 @@ type: '1' | '0', ) => boolean, ) => { - const { initializeCryptoAccount, encryptNotification } = getConfig().olmAPI; - await initializeCryptoAccount(); + const { encryptNotification } = getConfig().olmAPI; const { message: body, messageType: type } = await encryptNotification( unencryptedPayload, cryptoID, diff --git a/web/push-notif/encrypted-notif-utils-api.js b/web/push-notif/encrypted-notif-utils-api.js --- a/web/push-notif/encrypted-notif-utils-api.js +++ b/web/push-notif/encrypted-notif-utils-api.js @@ -12,8 +12,7 @@ type: '1' | '0', ) => boolean, ) => { - const { initializeCryptoAccount, encryptNotification } = getConfig().olmAPI; - await initializeCryptoAccount(); + const { encryptNotification } = getConfig().olmAPI; const { message: body, messageType: type } = await encryptNotification( unencryptedPayload, cryptoID,