diff --git a/lib/keyserver-conn/keyserver-auth.js b/lib/keyserver-conn/keyserver-auth.js --- a/lib/keyserver-conn/keyserver-auth.js +++ b/lib/keyserver-conn/keyserver-auth.js @@ -100,7 +100,7 @@ const [notifsSession, contentSession, { userID, deviceID }] = await Promise.all([ - olmAPI.notificationsSessionCreator( + olmAPI.keyserverNotificationsSessionCreator( cookie, keyserverKeys.identityKeysBlob.notificationIdentityPublicKeys, keyserverKeys.notifInitializationInfo, diff --git a/lib/shared/crypto-utils.js b/lib/shared/crypto-utils.js --- a/lib/shared/crypto-utils.js +++ b/lib/shared/crypto-utils.js @@ -67,7 +67,7 @@ signedIdentityKeysBlob.payload, ); - return await olmAPI.notificationsSessionCreator( + return await olmAPI.keyserverNotificationsSessionCreator( cookie, notificationIdentityPublicKeys, notifInitializationInfo, 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 @@ -167,12 +167,17 @@ contentIdentityKeys: OLMIdentityKeys, contentInitializationInfo: OlmSessionInitializationInfo, ) => Promise, - +notificationsSessionCreator: ( + +keyserverNotificationsSessionCreator: ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => Promise, + +notificationsOutboundSessionCreator: ( + deviceID: string, + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + ) => Promise, +reassignNotificationsSession?: ( prevCookie: ?string, newCookie: ?string, 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 @@ -21,7 +21,8 @@ decryptAndPersist: jest.fn(), contentInboundSessionCreator: jest.fn(), contentOutboundSessionCreator: jest.fn(), - notificationsSessionCreator: jest.fn(), + keyserverNotificationsSessionCreator: jest.fn(), + notificationsOutboundSessionCreator: jest.fn(), getOneTimeKeys: jest.fn(), validateAndUploadPrekeys: jest.fn(), signMessage: 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 @@ -59,7 +59,7 @@ contentIdentityKeys.ed25519, ); }, - notificationsSessionCreator( + keyserverNotificationsSessionCreator( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, @@ -75,6 +75,25 @@ keyserverID, ); }, + async notificationsOutboundSessionCreator( + deviceID: string, + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + ): Promise { + const { prekey, prekeySignature, oneTimeKey } = + notificationsInitializationInfo; + const identityKeys = JSON.stringify({ + curve25519: notificationsIdentityKeys.curve25519, + ed25519: notificationsIdentityKeys.ed25519, + }); + return commCoreModule.initializeNotificationsOutboundSession( + identityKeys, + prekey, + prekeySignature, + oneTimeKey, + deviceID, + ); + }, 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 @@ -50,7 +50,12 @@ decryptAndPersist: proxyToWorker('decryptAndPersist'), contentInboundSessionCreator: proxyToWorker('contentInboundSessionCreator'), contentOutboundSessionCreator: proxyToWorker('contentOutboundSessionCreator'), - notificationsSessionCreator: proxyToWorker('notificationsSessionCreator'), + keyserverNotificationsSessionCreator: proxyToWorker( + 'keyserverNotificationsSessionCreator', + ), + notificationsOutboundSessionCreator: proxyToWorker( + 'notificationsOutboundSessionCreator', + ), reassignNotificationsSession: proxyToWorker('reassignNotificationsSession'), getOneTimeKeys: proxyToWorker('getOneTimeKeys'), validateAndUploadPrekeys: proxyToWorker('validateAndUploadPrekeys'), diff --git a/web/push-notif/notif-crypto-utils.js b/web/push-notif/notif-crypto-utils.js --- a/web/push-notif/notif-crypto-utils.js +++ b/web/push-notif/notif-crypto-utils.js @@ -49,6 +49,7 @@ const SESSION_UPDATE_MAX_PENDING_TIME = 10 * 1000; const INDEXED_DB_KEYSERVER_PREFIX = 'keyserver'; const INDEXED_DB_KEY_SEPARATOR = ':'; +const INDEXED_DB_DEVICE_PREFIX = 'device'; // This constant is only used to migrate the existing notifications // session with production keyserver to new IndexedDB key format. This @@ -407,6 +408,14 @@ return [olmDataContentKeyBase, cookieID].join(INDEXED_DB_KEY_SEPARATOR); } +function getOlmDataContentKeyForDeviceID(deviceID: string): string { + return [ + INDEXED_DB_DEVICE_PREFIX, + deviceID, + NOTIFICATIONS_OLM_DATA_CONTENT, + ].join(INDEXED_DB_KEY_SEPARATOR); +} + function getOlmEncryptionKeyDBLabelForCookie( cookie: ?string, keyserverID?: string, @@ -429,6 +438,14 @@ return [olmEncryptionKeyDBLabelBase, cookieID].join(INDEXED_DB_KEY_SEPARATOR); } +function getOlmEncryptionKeyDBLabelForDeviceID(deviceID: string): string { + return [ + INDEXED_DB_DEVICE_PREFIX, + deviceID, + NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, + ].join(INDEXED_DB_KEY_SEPARATOR); +} + function getCookieIDFromOlmDBKey(olmDBKey: string): string | '0' { // Olm DB keys comply to one of the following formats: // KEYSERVER::(OLM_CONTENT | OLM_ENCRYPTION_KEY): @@ -542,6 +559,8 @@ decryptDesktopNotification, getOlmDataContentKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, + getOlmDataContentKeyForDeviceID, + getOlmEncryptionKeyDBLabelForDeviceID, migrateLegacyOlmNotificationsSessions, updateNotifsUnreadCountStorage, queryNotifsUnreadCountStorage, 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 @@ -52,6 +52,8 @@ import { getOlmDataContentKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, + getOlmDataContentKeyForDeviceID, + getOlmEncryptionKeyDBLabelForDeviceID, } from '../../push-notif/notif-crypto-utils.js'; import { type WorkerRequestMessage, @@ -147,6 +149,81 @@ } } +async function createAndPersistNotificationsOutboundSession( + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + dataPersistenceKey: string, + dataEncryptionKeyDBLabel: string, +): Promise { + 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(); + if (notificationsInitializationInfo.oneTimeKey) { + session.create_outbound( + notificationAccount, + notificationsIdentityKeys.curve25519, + notificationsIdentityKeys.ed25519, + notificationsPrekey, + notificationsInitializationInfo.prekeySignature, + notificationsInitializationInfo.oneTimeKey, + ); + } else { + session.create_outbound_without_otk( + notificationAccount, + notificationsIdentityKeys.curve25519, + notificationsIdentityKeys.ed25519, + notificationsPrekey, + notificationsInitializationInfo.prekeySignature, + ); + } + const { body: message, type: messageType } = 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, + ); + + 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( + dataEncryptionKeyDBLabel, + cryptoKeyPersistentForm, + ); + })(); + + await Promise.all([ + localforage.setItem(dataPersistenceKey, encryptedOlmData), + persistEncryptionKeyPromise, + ]); + + return { message, messageType }; +} + function getOrCreateOlmAccount(accountIDInDB: number): { +picklingKey: string, +account: olm.Account, @@ -646,7 +723,22 @@ return { encryptedData, sessionVersion: newSessionVersion }; }, - async notificationsSessionCreator( + async notificationsOutboundSessionCreator( + deviceID: string, + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + ): Promise { + const dataPersistenceKey = getOlmDataContentKeyForDeviceID(deviceID); + const dataEncryptionKeyDBLabel = + getOlmEncryptionKeyDBLabelForDeviceID(deviceID); + return createAndPersistNotificationsOutboundSession( + notificationsIdentityKeys, + notificationsInitializationInfo, + dataPersistenceKey, + dataEncryptionKeyDBLabel, + ); + }, + async keyserverNotificationsSessionCreator( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, @@ -657,76 +749,17 @@ 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(); - if (notificationsInitializationInfo.oneTimeKey) { - session.create_outbound( - notificationAccount, - notificationsIdentityKeys.curve25519, - notificationsIdentityKeys.ed25519, - notificationsPrekey, - notificationsInitializationInfo.prekeySignature, - notificationsInitializationInfo.oneTimeKey, - ); - } else { - session.create_outbound_without_otk( - notificationAccount, - notificationsIdentityKeys.curve25519, - notificationsIdentityKeys.ed25519, - notificationsPrekey, - notificationsInitializationInfo.prekeySignature, - ); - } - 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, - ); - const { notifsOlmDataContentKey, notifsOlmDataEncryptionKeyDBLabel } = getNotifsPersistenceKeys(cookie, keyserverID, platformDetails); - 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, - ]); + const { message } = await createAndPersistNotificationsOutboundSession( + notificationsIdentityKeys, + notificationsInitializationInfo, + notifsOlmDataContentKey, + notifsOlmDataEncryptionKeyDBLabel, + ); - return initialNotificationsEncryptedMessage; + return message; }, async reassignNotificationsSession( prevCookie: ?string,