diff --git a/lib/shared/olm-session-creator-context.js b/lib/shared/olm-session-creator-context.js index fac2ea3ce..97058e07d 100644 --- a/lib/shared/olm-session-creator-context.js +++ b/lib/shared/olm-session-creator-context.js @@ -1,20 +1,24 @@ // @flow import * as React from 'react'; import type { OLMIdentityKeys } from '../types/crypto-types.js'; import type { OlmSessionInitializationInfo } from '../types/request-types.js'; export type OlmSessionCreatorContextType = { +notificationsSessionCreator: ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => Promise, + +contentSessionCreator: ( + contentIdentityKeys: OLMIdentityKeys, + contentInitializationInfo: OlmSessionInitializationInfo, + ) => Promise, }; const OlmSessionCreatorContext: React.Context = React.createContext(null); export { OlmSessionCreatorContext }; diff --git a/native/account/account-hooks.js b/native/account/account-hooks.js index b34eaed0e..2ba3fad49 100644 --- a/native/account/account-hooks.js +++ b/native/account/account-hooks.js @@ -1,41 +1,56 @@ // @flow import * as React from 'react'; import { OlmSessionCreatorContext } from 'lib/shared/olm-session-creator-context.js'; import type { OLMIdentityKeys } from 'lib/types/crypto-types.js'; import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; -import { nativeNotificationsSessionCreator } from '../utils/crypto-utils.js'; +import { + nativeNotificationsSessionCreator, + nativeOutboundContentSessionCreator, +} from '../utils/crypto-utils.js'; type Props = { +children: React.Node, }; function notificationsSessionCreator( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) { return nativeNotificationsSessionCreator( notificationsIdentityKeys, notificationsInitializationInfo, keyserverID, ); } +function contentSessionCreator( + contentIdentityKeys: OLMIdentityKeys, + contentInitializationInfo: OlmSessionInitializationInfo, +) { + return nativeOutboundContentSessionCreator( + contentIdentityKeys, + contentInitializationInfo, + contentIdentityKeys.ed25519, + ); +} + const contextValue = { notificationsSessionCreator, + contentSessionCreator, }; function OlmSessionCreatorProvider(props: Props): React.Node { const { children } = props; return ( {children} ); } export { OlmSessionCreatorProvider }; diff --git a/native/utils/crypto-utils.js b/native/utils/crypto-utils.js index 83191e733..5afc44873 100644 --- a/native/utils/crypto-utils.js +++ b/native/utils/crypto-utils.js @@ -1,180 +1,181 @@ // @flow import { type ClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { IdentityKeysBlob, OLMIdentityKeys, } from 'lib/types/crypto-types.js'; import type { OutboundKeyInfoResponse, InboundKeyInfoResponse, } from 'lib/types/identity-service-types'; import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; import { type OutboundSessionCreation, peerToPeerMessageTypes, } from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; import { commCoreModule, commRustModule } from '../native-modules.js'; function nativeNotificationsSessionCreator( notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ): Promise { const { prekey, prekeySignature, oneTimeKey } = notificationsInitializationInfo; return commCoreModule.initializeNotificationsSession( JSON.stringify(notificationsIdentityKeys), prekey, prekeySignature, oneTimeKey, keyserverID, ); } async function getContentSigningKey(): Promise { await commCoreModule.initializeCryptoAccount(); const { primaryIdentityPublicKeys: { ed25519 }, } = await commCoreModule.getUserPublicKey(); return ed25519; } async function nativeInboundContentSessionCreator( message: OutboundSessionCreation, ): Promise { const { senderInfo, encryptedContent } = message; const authMetadata = await commCoreModule.getCommServicesAuthMetadata(); const { userID, deviceID, accessToken } = authMetadata; if (!userID || !deviceID || !accessToken) { throw new Error('CommServicesAuthMetadata is missing'); } await commCoreModule.initializeCryptoAccount(); const keysResponse = await commRustModule.getInboundKeysForUser( userID, deviceID, accessToken, senderInfo.userID, ); const inboundKeys: InboundKeyInfoResponse[] = JSON.parse(keysResponse); const deviceKeys: ?InboundKeyInfoResponse = inboundKeys.find(keys => { const keysPayload: IdentityKeysBlob = JSON.parse(keys.payload); return ( keysPayload.primaryIdentityPublicKeys.ed25519 === senderInfo.deviceID ); }); if (!deviceKeys) { throw new Error( 'No keys for the device that requested creating a session, ' + `deviceID: ${senderInfo.deviceID}`, ); } const keysPayload: IdentityKeysBlob = JSON.parse(deviceKeys.payload); const identityKeys = JSON.stringify({ curve25519: keysPayload.primaryIdentityPublicKeys.curve25519, ed25519: keysPayload.primaryIdentityPublicKeys.ed25519, }); return commCoreModule.initializeContentInboundSession( identityKeys, encryptedContent, keysPayload.primaryIdentityPublicKeys.ed25519, ); } function nativeOutboundContentSessionCreator( contentIdentityKeys: OLMIdentityKeys, contentInitializationInfo: OlmSessionInitializationInfo, deviceID: string, ): Promise { const { prekey, prekeySignature, oneTimeKey } = contentInitializationInfo; const identityKeys = JSON.stringify({ curve25519: contentIdentityKeys.curve25519, ed25519: contentIdentityKeys.ed25519, }); return commCoreModule.initializeContentOutboundSession( identityKeys, prekey, prekeySignature, oneTimeKey, deviceID, ); } async function createOlmSessionsWithOwnDevices( sendMessage: (message: ClientMessageToDevice) => Promise, ): Promise { const authMetadata = await commCoreModule.getCommServicesAuthMetadata(); const { userID, deviceID, accessToken } = authMetadata; if (!userID || !deviceID || !accessToken) { throw new Error('CommServicesAuthMetadata is missing'); } await commCoreModule.initializeCryptoAccount(); const keysResponse = await commRustModule.getOutboundKeysForUser( userID, deviceID, accessToken, userID, ); const outboundKeys: OutboundKeyInfoResponse[] = JSON.parse(keysResponse); for (const deviceKeys: OutboundKeyInfoResponse of outboundKeys) { const keysPayload: IdentityKeysBlob = JSON.parse(deviceKeys.payload); if (keysPayload.primaryIdentityPublicKeys.ed25519 === deviceID) { continue; } const recipientDeviceID = keysPayload.primaryIdentityPublicKeys.ed25519; if (!deviceKeys.oneTimeContentPrekey) { console.log(`One-time key is missing for device ${recipientDeviceID}`); continue; } try { const encryptedContent = await nativeOutboundContentSessionCreator( keysPayload.primaryIdentityPublicKeys, { prekey: deviceKeys.contentPrekey, prekeySignature: deviceKeys.contentPrekeySignature, oneTimeKey: deviceKeys.oneTimeContentPrekey, }, recipientDeviceID, ); const sessionCreationMessage: OutboundSessionCreation = { type: peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION, senderInfo: { userID, deviceID, }, encryptedContent, }; 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 { getContentSigningKey, nativeNotificationsSessionCreator, nativeInboundContentSessionCreator, createOlmSessionsWithOwnDevices, + nativeOutboundContentSessionCreator, }; diff --git a/web/account/account-hooks.js b/web/account/account-hooks.js index 3bdcffb5d..ecea198fe 100644 --- a/web/account/account-hooks.js +++ b/web/account/account-hooks.js @@ -1,334 +1,373 @@ // @flow import olm from '@commapp/olm'; import invariant from 'invariant'; import localforage from 'localforage'; import * as React from 'react'; import uuid from 'uuid'; import { initialEncryptedMessageContent, getOneTimeKeyValuesFromBlob, getPrekeyValueFromBlob, } from 'lib/shared/crypto-utils.js'; import { OlmSessionCreatorContext } from 'lib/shared/olm-session-creator-context.js'; import type { SignedIdentityKeysBlob, CryptoStore, IdentityKeysBlob, CryptoStoreContextType, OLMIdentityKeys, NotificationsOlmDataType, } from 'lib/types/crypto-types.js'; import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { generateCryptoKey, encryptData, exportKeyToJWK, } from '../crypto/aes-gcm-crypto-utils.js'; import { isDesktopSafari } from '../database/utils/db-utils.js'; import { initOlm } from '../olm/olm-utils.js'; import { getOlmDataContentKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, } from '../push-notif/notif-crypto-utils.js'; import { setCryptoStore } from '../redux/crypto-store-reducer.js'; import { useSelector } from '../redux/redux-utils.js'; const CryptoStoreContext: React.Context = React.createContext(null); type Props = { +children: React.Node, }; function GetOrCreateCryptoStoreProvider(props: Props): React.Node { const dispatch = useDispatch(); const createCryptoStore = React.useCallback(async () => { await initOlm(); const identityAccount = new olm.Account(); identityAccount.create(); const { ed25519: identityED25519, curve25519: identityCurve25519 } = JSON.parse(identityAccount.identity_keys()); const identityAccountPicklingKey = uuid.v4(); const pickledIdentityAccount = identityAccount.pickle( identityAccountPicklingKey, ); const notificationAccount = new olm.Account(); notificationAccount.create(); const { ed25519: notificationED25519, curve25519: notificationCurve25519 } = JSON.parse(notificationAccount.identity_keys()); const notificationAccountPicklingKey = uuid.v4(); const pickledNotificationAccount = notificationAccount.pickle( notificationAccountPicklingKey, ); const newCryptoStore = { primaryAccount: { picklingKey: identityAccountPicklingKey, pickledAccount: pickledIdentityAccount, }, primaryIdentityKeys: { ed25519: identityED25519, curve25519: identityCurve25519, }, notificationAccount: { picklingKey: notificationAccountPicklingKey, pickledAccount: pickledNotificationAccount, }, notificationIdentityKeys: { ed25519: notificationED25519, curve25519: notificationCurve25519, }, }; dispatch({ type: setCryptoStore, payload: newCryptoStore }); return newCryptoStore; }, [dispatch]); const currentCryptoStore = useSelector(state => state.cryptoStore); const createCryptoStorePromiseRef = React.useRef>(null); const getCryptoStorePromise = React.useCallback(() => { if (currentCryptoStore) { return Promise.resolve(currentCryptoStore); } const currentCreateCryptoStorePromiseRef = createCryptoStorePromiseRef.current; if (currentCreateCryptoStorePromiseRef) { return currentCreateCryptoStorePromiseRef; } const newCreateCryptoStorePromise = (async () => { try { return await createCryptoStore(); } catch (e) { createCryptoStorePromiseRef.current = undefined; throw e; } })(); createCryptoStorePromiseRef.current = newCreateCryptoStorePromise; return newCreateCryptoStorePromise; }, [createCryptoStore, currentCryptoStore]); const isCryptoStoreSet = !!currentCryptoStore; React.useEffect(() => { if (!isCryptoStoreSet) { createCryptoStorePromiseRef.current = undefined; } }, [isCryptoStoreSet]); const contextValue = React.useMemo( () => ({ getInitializedCryptoStore: getCryptoStorePromise, }), [getCryptoStorePromise], ); return ( {props.children} ); } function useGetOrCreateCryptoStore(): () => Promise { const context = React.useContext(CryptoStoreContext); invariant(context, 'CryptoStoreContext not found'); return context.getInitializedCryptoStore; } function useGetSignedIdentityKeysBlob(): () => Promise { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); return React.useCallback(async () => { const { primaryAccount, primaryIdentityKeys, notificationIdentityKeys } = await getOrCreateCryptoStore(); await initOlm(); const primaryOLMAccount = new olm.Account(); primaryOLMAccount.unpickle( primaryAccount.picklingKey, primaryAccount.pickledAccount, ); const identityKeysBlob: IdentityKeysBlob = { primaryIdentityPublicKeys: primaryIdentityKeys, notificationIdentityPublicKeys: notificationIdentityKeys, }; const payloadToBeSigned: string = JSON.stringify(identityKeysBlob); const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: payloadToBeSigned, signature: primaryOLMAccount.sign(payloadToBeSigned), }; return signedIdentityKeysBlob; }, [getOrCreateCryptoStore]); } function OlmSessionCreatorProvider(props: Props): React.Node { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); const currentCryptoStore = useSelector(state => state.cryptoStore); const createNewNotificationsSession = React.useCallback( async ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => { const [{ notificationAccount }, encryptionKey] = await Promise.all([ getOrCreateCryptoStore(), generateCryptoKey({ extractable: isDesktopSafari }), initOlm(), ]); const account = new olm.Account(); const { picklingKey, pickledAccount } = notificationAccount; account.unpickle(picklingKey, pickledAccount); const notificationsPrekey = getPrekeyValueFromBlob( notificationsInitializationInfo.prekey, ); const [notificationsOneTimeKey] = getOneTimeKeyValuesFromBlob( notificationsInitializationInfo.oneTimeKey, ); const session = new olm.Session(); session.create_outbound( account, notificationsIdentityKeys.curve25519, notificationsIdentityKeys.ed25519, notificationsPrekey, notificationsInitializationInfo.prekeySignature, notificationsOneTimeKey, ); const { body: initialNotificationsEncryptedMessage } = session.encrypt( JSON.stringify(initialEncryptedMessageContent), ); const mainSession = session.pickle(picklingKey); const notificationsOlmData: NotificationsOlmDataType = { mainSession, pendingSessionUpdate: mainSession, updateCreationTimestamp: Date.now(), picklingKey, }; const encryptedOlmData = await encryptData( new TextEncoder().encode(JSON.stringify(notificationsOlmData)), encryptionKey, ); const notifsOlmDataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForCookie(cookie); const notifsOlmDataContentKey = getOlmDataContentKeyForCookie( cookie, keyserverID, ); const persistEncryptionKeyPromise = (async () => { let cryptoKeyPersistentForm; if (isDesktopSafari) { // Safari doesn't support structured clone algorithm in service // worker context so we have to store CryptoKey as JSON cryptoKeyPersistentForm = await exportKeyToJWK(encryptionKey); } else { cryptoKeyPersistentForm = encryptionKey; } await localforage.setItem( notifsOlmDataEncryptionKeyDBLabel, cryptoKeyPersistentForm, ); })(); await Promise.all([ localforage.setItem(notifsOlmDataContentKey, encryptedOlmData), persistEncryptionKeyPromise, ]); return initialNotificationsEncryptedMessage; }, [getOrCreateCryptoStore], ); + const createNewContentSession = React.useCallback( + async ( + contentIdentityKeys: OLMIdentityKeys, + contentInitializationInfo: OlmSessionInitializationInfo, + ) => { + const [{ primaryAccount }] = await Promise.all([ + getOrCreateCryptoStore(), + initOlm(), + ]); + + const account = new olm.Account(); + const { picklingKey, pickledAccount } = primaryAccount; + account.unpickle(picklingKey, pickledAccount); + + const contentPrekey = getPrekeyValueFromBlob( + contentInitializationInfo.prekey, + ); + const [contentOneTimeKey] = getOneTimeKeyValuesFromBlob( + contentInitializationInfo.oneTimeKey, + ); + + const session = new olm.Session(); + session.create_outbound( + account, + contentIdentityKeys.curve25519, + contentIdentityKeys.ed25519, + contentPrekey, + contentInitializationInfo.prekeySignature, + contentOneTimeKey, + ); + const { body: initialContentEncryptedMessage } = session.encrypt( + JSON.stringify(initialEncryptedMessageContent), + ); + return initialContentEncryptedMessage; + }, + [getOrCreateCryptoStore], + ); + const notificationsSessionPromise = React.useRef>(null); const createNotificationsSession = React.useCallback( async ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => { if (notificationsSessionPromise.current) { return notificationsSessionPromise.current; } const newNotificationsSessionPromise = (async () => { try { return await createNewNotificationsSession( cookie, notificationsIdentityKeys, notificationsInitializationInfo, keyserverID, ); } catch (e) { notificationsSessionPromise.current = undefined; throw e; } })(); notificationsSessionPromise.current = newNotificationsSessionPromise; return newNotificationsSessionPromise; }, [createNewNotificationsSession], ); const isCryptoStoreSet = !!currentCryptoStore; React.useEffect(() => { if (!isCryptoStoreSet) { notificationsSessionPromise.current = undefined; } }, [isCryptoStoreSet]); const contextValue = React.useMemo( () => ({ notificationsSessionCreator: createNotificationsSession, + contentSessionCreator: createNewContentSession, }), - [createNotificationsSession], + [createNewContentSession, createNotificationsSession], ); return ( {props.children} ); } function useWebNotificationsSessionCreator(): ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => Promise { const context = React.useContext(OlmSessionCreatorContext); invariant(context, 'WebNotificationsSessionCreator not found.'); return context.notificationsSessionCreator; } export { useGetSignedIdentityKeysBlob, useGetOrCreateCryptoStore, OlmSessionCreatorProvider, useWebNotificationsSessionCreator, GetOrCreateCryptoStoreProvider, };