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 @@ -151,6 +151,10 @@ deviceID: string, messageID: string, ) => Promise, + +encryptNotification: ( + payload: string, + deviceID: string, + ) => Promise, +decrypt: (encryptedData: EncryptedData, deviceID: string) => Promise, +decryptAndPersist: ( encryptedData: EncryptedData, 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 @@ -17,6 +17,7 @@ getUserPublicKey: jest.fn(), encrypt: jest.fn(), encryptAndPersist: jest.fn(), + encryptNotification: jest.fn(), decrypt: jest.fn(), decryptAndPersist: jest.fn(), contentInboundSessionCreator: 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 @@ -21,6 +21,7 @@ getUserPublicKey: commCoreModule.getUserPublicKey, encrypt: commCoreModule.encrypt, encryptAndPersist: commCoreModule.encryptAndPersist, + encryptNotification: commCoreModule.encryptNotification, decrypt: commCoreModule.decrypt, decryptAndPersist: commCoreModule.decryptAndPersist, async contentInboundSessionCreator( 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 @@ -1,8 +1,9 @@ // @flow import type { EncryptedNotifUtilsAPI } from 'lib/types/notif-types.js'; +import { getConfig } from 'lib/utils/config.js'; -import { commUtilsModule, commCoreModule } from '../native-modules.js'; +import { commUtilsModule } from '../native-modules.js'; const encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI = { encryptSerializedNotifPayload: async ( @@ -13,8 +14,12 @@ type: '1' | '0', ) => boolean, ) => { - const { message: body, messageType: type } = - await commCoreModule.encryptNotification(unencryptedPayload, cryptoID); + const { initializeCryptoAccount, encryptNotification } = getConfig().olmAPI; + await initializeCryptoAccount(); + const { message: body, messageType: type } = await encryptNotification( + unencryptedPayload, + cryptoID, + ); return { encryptedData: { body, type }, sizeLimitViolated: encryptedPayloadSizeValidator 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 @@ -46,6 +46,7 @@ getUserPublicKey: proxyToWorker('getUserPublicKey'), encrypt: proxyToWorker('encrypt'), encryptAndPersist: proxyToWorker('encryptAndPersist'), + encryptNotification: proxyToWorker('encryptNotification'), decrypt: proxyToWorker('decrypt'), decryptAndPersist: proxyToWorker('decryptAndPersist'), contentInboundSessionCreator: proxyToWorker('contentInboundSessionCreator'), 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 @@ -1,6 +1,7 @@ // @flow import type { EncryptedNotifUtilsAPI } from 'lib/types/notif-types.js'; +import { getConfig } from 'lib/utils/config.js'; const encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI = { encryptSerializedNotifPayload: async ( @@ -11,16 +12,16 @@ type: '1' | '0', ) => boolean, ) => { - // The "mock" implementation below will be replaced with proper - // implementation after olm notif sessions initialization is - // implemented. for now it is actually beneficial to return - // original string as encrypted string since it allows for - // better testing as we can verify which data are encrypted - // and which aren't. + const { initializeCryptoAccount, encryptNotification } = getConfig().olmAPI; + await initializeCryptoAccount(); + const { message: body, messageType: type } = await encryptNotification( + unencryptedPayload, + cryptoID, + ); return { - encryptedData: { body: unencryptedPayload, type: 1 }, + encryptedData: { body, type }, sizeLimitViolated: encryptedPayloadSizeValidator - ? !encryptedPayloadSizeValidator(unencryptedPayload, '1') + ? !encryptedPayloadSizeValidator(body, type ? '1' : '0') : false, }; }, 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 @@ -1,6 +1,7 @@ // @flow import olm from '@commapp/olm'; +import type { EncryptResult } from '@commapp/olm'; import invariant from 'invariant'; import localforage from 'localforage'; @@ -13,6 +14,7 @@ EncryptedWebNotification, } from 'lib/types/notif-types.js'; import { getCookieIDFromCookie } from 'lib/utils/cookie-utils.js'; +import { getMessageForException } from 'lib/utils/errors.js'; import { type EncryptedData, @@ -86,9 +88,9 @@ displayErrorMessage: staffCanSee, }; } - const { olmDataContentKey, encryptionKeyDBKey } = olmDBKeys; + const { olmDataKey, encryptionKeyDBKey } = olmDBKeys; const [encryptedOlmData, encryptionKey] = await Promise.all([ - localforage.getItem(olmDataContentKey), + localforage.getItem(olmDataKey), retrieveEncryptionKey(encryptionKeyDBKey), ]); @@ -105,7 +107,7 @@ const decryptedNotification = await commonDecrypt( encryptedOlmData, - olmDataContentKey, + olmDataKey, encryptionKey, encryptedPayload, ); @@ -132,15 +134,15 @@ staffCanSee: boolean, keyserverID?: string, ): Promise<{ +[string]: mixed }> { - let encryptedOlmData, encryptionKey, olmDataContentKey; + let encryptedOlmData, encryptionKey, olmDataKey; try { - const { olmDataContentKey: olmDataContentKeyValue, encryptionKeyDBKey } = + const { olmDataKey: olmDataKeyValue, encryptionKeyDBKey } = await getNotifsOlmSessionDBKeys(keyserverID); - olmDataContentKey = olmDataContentKeyValue; + olmDataKey = olmDataKeyValue; [encryptedOlmData, encryptionKey] = await Promise.all([ - localforage.getItem(olmDataContentKey), + localforage.getItem(olmDataKey), retrieveEncryptionKey(encryptionKeyDBKey), initOlm(), ]); @@ -162,7 +164,7 @@ try { decryptedNotification = await commonDecrypt<{ +[string]: mixed }>( encryptedOlmData, - olmDataContentKey, + olmDataKey, encryptionKey, encryptedPayload, ); @@ -197,7 +199,7 @@ async function commonDecrypt( encryptedOlmData: EncryptedData, - olmDataContentKey: string, + olmDataKey: string, encryptionKey: CryptoKey, encryptedPayload: string, ): Promise { @@ -257,7 +259,7 @@ encryptionKey, ); - await localforage.setItem(olmDataContentKey, updatedEncryptedSession); + await localforage.setItem(olmDataKey, updatedEncryptedSession); return decryptedNotification; } @@ -309,6 +311,87 @@ } } +async function encryptNotification( + payload: string, + deviceID: string, +): Promise { + const olmDataKey = getOlmDataKeyForDeviceID(deviceID); + const olmEncryptionKeyDBLabel = + getOlmEncryptionKeyDBLabelForDeviceID(deviceID); + + let encryptedOlmData, encryptionKey; + try { + [encryptedOlmData, encryptionKey] = await Promise.all([ + localforage.getItem(olmDataKey), + retrieveEncryptionKey(olmEncryptionKeyDBLabel), + initOlm(), + ]); + } catch (e) { + throw new Error( + `Failed to fetch olm session from IndexedDB for device: ${deviceID}. Details: ${ + getMessageForException(e) ?? '' + }`, + ); + } + + if (!encryptionKey || !encryptedOlmData) { + throw new Error(`Session with device: ${deviceID} not initialized.`); + } + + let encryptedNotification; + try { + encryptedNotification = await encryptNotificationWithOlmSession( + payload, + encryptedOlmData, + olmDataKey, + encryptionKey, + ); + } catch (e) { + throw new Error( + `Failed encrypt notification for device: ${deviceID}. Details: ${ + getMessageForException(e) ?? '' + }`, + ); + } + return encryptedNotification; +} + +async function encryptNotificationWithOlmSession( + payload: string, + encryptedOlmData: EncryptedData, + olmDataKey: string, + encryptionKey: CryptoKey, +): Promise { + const serializedOlmData = await decryptData(encryptedOlmData, encryptionKey); + const { + mainSession, + picklingKey, + pendingSessionUpdate, + updateCreationTimestamp, + }: NotificationsOlmDataType = JSON.parse( + new TextDecoder().decode(serializedOlmData), + ); + + const session = new olm.Session(); + session.unpickle(picklingKey, pendingSessionUpdate); + const encryptedNotification = session.encrypt(payload); + + const newPendingSessionUpdate = session.pickle(picklingKey); + const updatedOlmData: NotificationsOlmDataType = { + mainSession, + pendingSessionUpdate: newPendingSessionUpdate, + picklingKey, + updateCreationTimestamp, + }; + const updatedEncryptedSession = await encryptData( + new TextEncoder().encode(JSON.stringify(updatedOlmData)), + encryptionKey, + ); + + await localforage.setItem(olmDataKey, updatedEncryptedSession); + return encryptedNotification; +} + async function retrieveEncryptionKey( encryptionKeyDBLabel: string, ): Promise { @@ -326,10 +409,10 @@ } async function getNotifsOlmSessionDBKeys(keyserverID?: string): Promise<{ - +olmDataContentKey: string, + +olmDataKey: string, +encryptionKeyDBKey: string, }> { - const olmDataContentKeyForKeyserverPrefix = getOlmDataContentKeyForCookie( + const olmDataKeyForKeyserverPrefix = getOlmDataKeyForCookie( undefined, keyserverID, ); @@ -338,8 +421,8 @@ getOlmEncryptionKeyDBLabelForCookie(undefined, keyserverID); const dbKeys = await localforage.keys(); - const olmDataContentKeys = sortOlmDBKeysArray( - dbKeys.filter(key => key.startsWith(olmDataContentKeyForKeyserverPrefix)), + const olmDataKeys = sortOlmDBKeysArray( + dbKeys.filter(key => key.startsWith(olmDataKeyForKeyserverPrefix)), ); const encryptionKeyDBLabels = sortOlmDBKeysArray( dbKeys.filter(key => @@ -347,38 +430,36 @@ ), ); - if (olmDataContentKeys.length === 0 || encryptionKeyDBLabels.length === 0) { + if (olmDataKeys.length === 0 || encryptionKeyDBLabels.length === 0) { throw new Error( 'Received encrypted notification but olm session was not created', ); } - const latestDataContentKey = - olmDataContentKeys[olmDataContentKeys.length - 1]; + const latestDataKey = olmDataKeys[olmDataKeys.length - 1]; const latestEncryptionKeyDBKey = encryptionKeyDBLabels[encryptionKeyDBLabels.length - 1]; - const latestDataContentCookieID = - getCookieIDFromOlmDBKey(latestDataContentKey); + const latestDataCookieID = getCookieIDFromOlmDBKey(latestDataKey); const latestEncryptionKeyCookieID = getCookieIDFromOlmDBKey( latestEncryptionKeyDBKey, ); - if (latestDataContentCookieID !== latestEncryptionKeyCookieID) { + if (latestDataCookieID !== latestEncryptionKeyCookieID) { throw new Error( 'Olm sessions and their encryption keys out of sync. Latest cookie ' + - `id for olm sessions ${latestDataContentCookieID}. Latest cookie ` + + `id for olm sessions ${latestDataCookieID}. Latest cookie ` + `id for olm session encryption keys ${latestEncryptionKeyCookieID}`, ); } const olmDBKeys = { - olmDataContentKey: latestDataContentKey, + olmDataKey: latestDataKey, encryptionKeyDBKey: latestEncryptionKeyDBKey, }; const keysToDelete: $ReadOnlyArray = [ - ...olmDataContentKeys.slice(0, olmDataContentKeys.length - 1), + ...olmDataKeys.slice(0, olmDataKeys.length - 1), ...encryptionKeyDBLabels.slice(0, encryptionKeyDBLabels.length - 1), ]; @@ -386,29 +467,26 @@ return olmDBKeys; } -function getOlmDataContentKeyForCookie( - cookie: ?string, - keyserverID?: string, -): string { - let olmDataContentKeyBase; +function getOlmDataKeyForCookie(cookie: ?string, keyserverID?: string): string { + let olmDataKeyBase; if (keyserverID) { - olmDataContentKeyBase = [ + olmDataKeyBase = [ INDEXED_DB_KEYSERVER_PREFIX, keyserverID, NOTIFICATIONS_OLM_DATA_CONTENT, ].join(INDEXED_DB_KEY_SEPARATOR); } else { - olmDataContentKeyBase = NOTIFICATIONS_OLM_DATA_CONTENT; + olmDataKeyBase = NOTIFICATIONS_OLM_DATA_CONTENT; } if (!cookie) { - return olmDataContentKeyBase; + return olmDataKeyBase; } const cookieID = getCookieIDFromCookie(cookie); - return [olmDataContentKeyBase, cookieID].join(INDEXED_DB_KEY_SEPARATOR); + return [olmDataKeyBase, cookieID].join(INDEXED_DB_KEY_SEPARATOR); } -function getOlmDataContentKeyForDeviceID(deviceID: string): string { +function getOlmDataKeyForDeviceID(deviceID: string): string { return [ INDEXED_DB_DEVICE_PREFIX, deviceID, @@ -480,7 +558,7 @@ let keyToInsert; if (key.startsWith(NOTIFICATIONS_OLM_DATA_CONTENT)) { const cookieID = getCookieIDFromOlmDBKey(key); - keyToInsert = getOlmDataContentKeyForCookie( + keyToInsert = getOlmDataKeyForCookie( cookieID, ASHOAT_KEYSERVER_ID_USED_ONLY_FOR_MIGRATION_FROM_LEGACY_NOTIF_STORAGE, ); @@ -557,9 +635,10 @@ export { decryptWebNotification, decryptDesktopNotification, - getOlmDataContentKeyForCookie, + encryptNotification, + getOlmDataKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, - getOlmDataContentKeyForDeviceID, + getOlmDataKeyForDeviceID, getOlmEncryptionKeyDBLabelForDeviceID, migrateLegacyOlmNotificationsSessions, updateNotifsUnreadCountStorage, 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 @@ -50,10 +50,11 @@ generateCryptoKey, } from '../../crypto/aes-gcm-crypto-utils.js'; import { - getOlmDataContentKeyForCookie, + getOlmDataKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, - getOlmDataContentKeyForDeviceID, + getOlmDataKeyForDeviceID, getOlmEncryptionKeyDBLabelForDeviceID, + encryptNotification, } from '../../push-notif/notif-crypto-utils.js'; import { type WorkerRequestMessage, @@ -439,16 +440,13 @@ cookie, keyserverID, ), - notifsOlmDataContentKey: getOlmDataContentKeyForCookie( - cookie, - keyserverID, - ), + notifsOlmDataContentKey: getOlmDataKeyForCookie(cookie, keyserverID), }; } else { return { notifsOlmDataEncryptionKeyDBLabel: getOlmEncryptionKeyDBLabelForCookie(cookie), - notifsOlmDataContentKey: getOlmDataContentKeyForCookie(cookie), + notifsOlmDataContentKey: getOlmDataKeyForCookie(cookie), }; } } @@ -567,6 +565,16 @@ return result; }, + async encryptNotification( + payload: string, + deviceID: string, + ): Promise { + const { body: message, type: messageType } = await encryptNotification( + payload, + deviceID, + ); + return { message, messageType }; + }, async decrypt( encryptedData: EncryptedData, deviceID: string, @@ -728,7 +736,7 @@ notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, ): Promise { - const dataPersistenceKey = getOlmDataContentKeyForDeviceID(deviceID); + const dataPersistenceKey = getOlmDataKeyForDeviceID(deviceID); const dataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForDeviceID(deviceID); return createAndPersistNotificationsOutboundSession(