diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -149,6 +149,12 @@ contentIdentityKeys: OLMIdentityKeys, contentInitializationInfo: OlmSessionInitializationInfo, ) => Promise, + +notificationsSessionCreator: ( + cookie: ?string, + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + keyserverID: string, + ) => Promise, +getOneTimeKeys: (numberOfKeys: number) => Promise, +validateAndUploadPrekeys: (authMetadata: AuthMetadata) => Promise, }; diff --git a/lib/utils/__mocks__/config.js b/lib/utils/__mocks__/config.js --- a/lib/utils/__mocks__/config.js +++ b/lib/utils/__mocks__/config.js @@ -19,6 +19,7 @@ decrypt: jest.fn(), contentInboundSessionCreator: jest.fn(), contentOutboundSessionCreator: jest.fn(), + notificationsSessionCreator: jest.fn(), getOneTimeKeys: jest.fn(), validateAndUploadPrekeys: jest.fn(), }, diff --git a/native/crypto/olm-api.js b/native/crypto/olm-api.js --- a/native/crypto/olm-api.js +++ b/native/crypto/olm-api.js @@ -50,6 +50,22 @@ contentIdentityKeys.ed25519, ); }, + notificationsSessionCreator( + cookie: ?string, + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + keyserverID: string, + ): Promise { + const { prekey, prekeySignature, oneTimeKey } = + notificationsInitializationInfo; + return commCoreModule.initializeNotificationsSession( + JSON.stringify(notificationsIdentityKeys), + prekey, + prekeySignature, + oneTimeKey, + keyserverID, + ); + }, async getOneTimeKeys(numberOfKeys: number): Promise { const { contentOneTimeKeys, notificationsOneTimeKeys } = await commCoreModule.getOneTimeKeys(numberOfKeys); diff --git a/web/crypto/olm-api.js b/web/crypto/olm-api.js --- a/web/crypto/olm-api.js +++ b/web/crypto/olm-api.js @@ -56,6 +56,7 @@ decrypt: proxyToWorker('decrypt'), contentInboundSessionCreator: proxyToWorker('contentInboundSessionCreator'), contentOutboundSessionCreator: proxyToWorker('contentOutboundSessionCreator'), + notificationsSessionCreator: proxyToWorker('notificationsSessionCreator'), getOneTimeKeys: proxyToWorker('getOneTimeKeys'), validateAndUploadPrekeys: proxyToWorker('validateAndUploadPrekeys'), }; diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -1,9 +1,14 @@ // @flow import olm from '@commapp/olm'; +import localforage from 'localforage'; import uuid from 'uuid'; import { initialEncryptedMessageContent } from 'lib/shared/crypto-utils.js'; +import { + hasMinCodeVersion, + NEXT_CODE_VERSION, +} from 'lib/shared/version-utils.js'; import { olmEncryptedMessageTypes, type OLMIdentityKeys, @@ -14,6 +19,7 @@ type OlmAPI, type OneTimeKeysResultValues, type ClientPublicKeys, + type NotificationsOlmDataType, } from 'lib/types/crypto-types.js'; import type { IdentityNewDeviceKeyUpload, @@ -32,7 +38,20 @@ import { getIdentityClient } from './identity-client.js'; import { getProcessingStoreOpsExceptionMessage } from './process-operations.js'; -import { getDBModule, getSQLiteQueryExecutor } from './worker-database.js'; +import { + getDBModule, + getSQLiteQueryExecutor, + getPlatformDetails, +} from './worker-database.js'; +import { + encryptData, + exportKeyToJWK, + generateCryptoKey, +} from '../../crypto/aes-gcm-crypto-utils.js'; +import { + getOlmDataContentKeyForCookie, + getOlmEncryptionKeyDBLabelForCookie, +} from '../../push-notif/notif-crypto-utils.js'; import { type WorkerRequestMessage, type WorkerResponseMessage, @@ -40,6 +59,7 @@ workerResponseMessageTypes, } from '../../types/worker-types.js'; import type { OlmPersistSession } from '../types/sqlite-query-executor.js'; +import { isDesktopSafari } from '../utils/db-utils.js'; type WorkerCryptoStore = { +contentAccountPickleKey: string, @@ -435,6 +455,95 @@ return initialContentEncryptedMessage; }, + async notificationsSessionCreator( + cookie: ?string, + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + keyserverID: string, + ): Promise { + const platformDetails = getPlatformDetails(); + if (!platformDetails) { + throw new Error('Worker not initialized'); + } + + if (!cryptoStore) { + throw new Error('Crypto account not initialized'); + } + + const { notificationAccountPickleKey, notificationAccount } = cryptoStore; + const encryptionKey = await generateCryptoKey({ + extractable: isDesktopSafari, + }); + + const notificationsPrekey = notificationsInitializationInfo.prekey; + const session = new olm.Session(); + session.create_outbound( + notificationAccount, + notificationsIdentityKeys.curve25519, + notificationsIdentityKeys.ed25519, + notificationsPrekey, + notificationsInitializationInfo.prekeySignature, + notificationsInitializationInfo.oneTimeKey, + ); + const { body: initialNotificationsEncryptedMessage } = session.encrypt( + JSON.stringify(initialEncryptedMessageContent), + ); + + const mainSession = session.pickle(notificationAccountPickleKey); + const notificationsOlmData: NotificationsOlmDataType = { + mainSession, + pendingSessionUpdate: mainSession, + updateCreationTimestamp: Date.now(), + picklingKey: notificationAccountPickleKey, + }; + const encryptedOlmData = await encryptData( + new TextEncoder().encode(JSON.stringify(notificationsOlmData)), + encryptionKey, + ); + + let notifsOlmDataContentKey; + let notifsOlmDataEncryptionKeyDBLabel; + + if ( + hasMinCodeVersion(platformDetails, { majorDesktop: NEXT_CODE_VERSION }) + ) { + notifsOlmDataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForCookie( + cookie, + keyserverID, + ); + notifsOlmDataContentKey = getOlmDataContentKeyForCookie( + cookie, + keyserverID, + ); + } else { + notifsOlmDataEncryptionKeyDBLabel = + getOlmEncryptionKeyDBLabelForCookie(cookie); + notifsOlmDataContentKey = getOlmDataContentKeyForCookie(cookie); + } + + 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; + }, async getOneTimeKeys(numberOfKeys: number): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized');