diff --git a/web/push-notif/notif-crypto-utils.js b/web/push-notif/notif-crypto-utils.js index e14df9676..04686d310 100644 --- a/web/push-notif/notif-crypto-utils.js +++ b/web/push-notif/notif-crypto-utils.js @@ -1,214 +1,266 @@ // @flow import olm from '@commapp/olm'; import localforage from 'localforage'; import { olmEncryptedMessageTypes, type NotificationsOlmDataType, } from 'lib/types/crypto-types.js'; import type { PlainTextWebNotification, - PlainTextWebNotificationPayload, EncryptedWebNotification, } from 'lib/types/notif-types.js'; import { + type EncryptedData, decryptData, encryptData, importJWKKey, - type EncryptedData, } from '../crypto/aes-gcm-crypto-utils.js'; import { NOTIFICATIONS_OLM_DATA_CONTENT, NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, } from '../database/utils/constants.js'; import { isDesktopSafari } from '../database/utils/db-utils.js'; +import { initOlm } from '../olm/olm-utils.js'; export type WebNotifDecryptionError = { +id: string, +error: string, +displayErrorMessage?: boolean, }; export type WebNotifsServiceUtilsData = { +olmWasmPath: string, +staffCanSee: boolean, }; -type DecryptionResult = { +type DecryptionResult = { +newPendingSessionUpdate: string, +newUpdateCreationTimestamp: number, - +decryptedNotification: PlainTextWebNotificationPayload, + +decryptedNotification: T, }; export const WEB_NOTIFS_SERVICE_UTILS_KEY = 'webNotifsServiceUtils'; const SESSION_UPDATE_MAX_PENDING_TIME = 10 * 1000; async function decryptWebNotification( encryptedNotification: EncryptedWebNotification, ): Promise { const { id, encryptedPayload } = encryptedNotification; - const retrieveEncryptionKeyPromise: Promise = (async () => { - if (!isDesktopSafari) { - return await localforage.getItem( - NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, - ); - } - // Safari doesn't support structured clone algorithm in service - // worker context so we have to store CryptoKey as JSON - const persistedCryptoKey = - await localforage.getItem( - NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, - ); - if (!persistedCryptoKey) { - return null; - } - return await importJWKKey(persistedCryptoKey); - })(); - const [encryptedOlmData, encryptionKey, utilsData] = await Promise.all([ localforage.getItem(NOTIFICATIONS_OLM_DATA_CONTENT), - retrieveEncryptionKeyPromise, + retrieveEncryptionKey(), localforage.getItem( WEB_NOTIFS_SERVICE_UTILS_KEY, ), ]); if (!utilsData) { return { id, error: 'Necessary data not found in IndexedDB' }; } const { olmWasmPath, staffCanSee } = (utilsData: WebNotifsServiceUtilsData); if (!encryptionKey || !encryptedOlmData) { return { id, error: 'Received encrypted notification but olm session was not created', displayErrorMessage: staffCanSee, }; } try { await olm.init({ locateFile: () => olmWasmPath }); - const serializedOlmData = await decryptData( + const decryptedNotification = await commonDecrypt( encryptedOlmData, encryptionKey, - ); - const { - mainSession, - picklingKey, - pendingSessionUpdate, - updateCreationTimestamp, - }: NotificationsOlmDataType = JSON.parse( - new TextDecoder().decode(serializedOlmData), + encryptedPayload, ); - let updatedOlmData: NotificationsOlmDataType; - let decryptedNotification: PlainTextWebNotificationPayload; + return { id, ...decryptedNotification }; + } catch (e) { + return { + id, + error: e.message, + displayErrorMessage: staffCanSee, + }; + } +} - const shouldUpdateMainSession = - Date.now() - updateCreationTimestamp > SESSION_UPDATE_MAX_PENDING_TIME; +async function decryptDesktopNotification( + encryptedPayload: string, + staffCanSee: boolean, +): Promise<{ +[string]: mixed }> { + let encryptedOlmData, encryptionKey; + try { + [encryptedOlmData, encryptionKey] = await Promise.all([ + localforage.getItem(NOTIFICATIONS_OLM_DATA_CONTENT), + retrieveEncryptionKey(), + initOlm(), + ]); + } catch (e) { + return { + error: e.message, + displayErrorMessage: staffCanSee, + }; + } - const decryptionWithPendingSessionResult = decryptWithPendingSession( - pendingSessionUpdate, - picklingKey, - encryptedPayload, - ); + if (!encryptionKey || !encryptedOlmData) { + return { + error: 'Received encrypted notification but olm session was not created', + displayErrorMessage: staffCanSee, + }; + } - if (decryptionWithPendingSessionResult.decryptedNotification) { - const { - decryptedNotification: notifDecryptedWithPendingSession, - newPendingSessionUpdate, - newUpdateCreationTimestamp, - } = decryptionWithPendingSessionResult; - - decryptedNotification = notifDecryptedWithPendingSession; - updatedOlmData = { - mainSession: shouldUpdateMainSession - ? pendingSessionUpdate - : mainSession, - pendingSessionUpdate: newPendingSessionUpdate, - updateCreationTimestamp: newUpdateCreationTimestamp, - picklingKey, - }; - } else { - const { - newUpdateCreationTimestamp, - decryptedNotification: notifDecryptedWithMainSession, - } = decryptWithSession(mainSession, picklingKey, encryptedPayload); - - decryptedNotification = notifDecryptedWithMainSession; - updatedOlmData = { - mainSession: mainSession, - pendingSessionUpdate, - updateCreationTimestamp: newUpdateCreationTimestamp, - picklingKey, - }; - } - - const updatedEncryptedSession = await encryptData( - new TextEncoder().encode(JSON.stringify(updatedOlmData)), + try { + return await commonDecrypt( + encryptedOlmData, encryptionKey, + encryptedPayload, ); - - await localforage.setItem( - NOTIFICATIONS_OLM_DATA_CONTENT, - updatedEncryptedSession, - ); - - return { id, ...decryptedNotification }; } catch (e) { return { - id, error: e.message, - displayErrorMessage: staffCanSee, + staffCanSee, }; } } -function decryptWithSession( +async function commonDecrypt( + encryptedOlmData: EncryptedData, + encryptionKey: CryptoKey, + encryptedPayload: string, +): Promise { + const serializedOlmData = await decryptData(encryptedOlmData, encryptionKey); + const { + mainSession, + picklingKey, + pendingSessionUpdate, + updateCreationTimestamp, + }: NotificationsOlmDataType = JSON.parse( + new TextDecoder().decode(serializedOlmData), + ); + + let updatedOlmData: NotificationsOlmDataType; + let decryptedNotification: T; + + const shouldUpdateMainSession = + Date.now() - updateCreationTimestamp > SESSION_UPDATE_MAX_PENDING_TIME; + + const decryptionWithPendingSessionResult = decryptWithPendingSession( + pendingSessionUpdate, + picklingKey, + encryptedPayload, + ); + + if (decryptionWithPendingSessionResult.decryptedNotification) { + const { + decryptedNotification: notifDecryptedWithPendingSession, + newPendingSessionUpdate, + newUpdateCreationTimestamp, + } = decryptionWithPendingSessionResult; + + decryptedNotification = notifDecryptedWithPendingSession; + updatedOlmData = { + mainSession: shouldUpdateMainSession ? pendingSessionUpdate : mainSession, + pendingSessionUpdate: newPendingSessionUpdate, + updateCreationTimestamp: newUpdateCreationTimestamp, + picklingKey, + }; + } else { + const { + newUpdateCreationTimestamp, + decryptedNotification: notifDecryptedWithMainSession, + } = decryptWithSession(mainSession, picklingKey, encryptedPayload); + + decryptedNotification = notifDecryptedWithMainSession; + updatedOlmData = { + mainSession: mainSession, + pendingSessionUpdate, + updateCreationTimestamp: newUpdateCreationTimestamp, + picklingKey, + }; + } + + const updatedEncryptedSession = await encryptData( + new TextEncoder().encode(JSON.stringify(updatedOlmData)), + encryptionKey, + ); + + await localforage.setItem( + NOTIFICATIONS_OLM_DATA_CONTENT, + updatedEncryptedSession, + ); + + return decryptedNotification; +} + +function decryptWithSession( pickledSession: string, picklingKey: string, encryptedPayload: string, -): DecryptionResult { +): DecryptionResult { const session = new olm.Session(); session.unpickle(picklingKey, pickledSession); - const decryptedNotification: PlainTextWebNotificationPayload = JSON.parse( + const decryptedNotification: T = JSON.parse( session.decrypt(olmEncryptedMessageTypes.TEXT, encryptedPayload), ); const newPendingSessionUpdate = session.pickle(picklingKey); const newUpdateCreationTimestamp = Date.now(); return { decryptedNotification, newUpdateCreationTimestamp, newPendingSessionUpdate, }; } -function decryptWithPendingSession( +function decryptWithPendingSession( pendingSessionUpdate: string, picklingKey: string, encryptedPayload: string, -): DecryptionResult | { +error: string } { +): DecryptionResult | { +error: string } { try { const { decryptedNotification, newPendingSessionUpdate, newUpdateCreationTimestamp, - } = decryptWithSession(pendingSessionUpdate, picklingKey, encryptedPayload); + } = decryptWithSession( + pendingSessionUpdate, + picklingKey, + encryptedPayload, + ); return { newPendingSessionUpdate, newUpdateCreationTimestamp, decryptedNotification, }; } catch (e) { return { error: e.message }; } } -export { decryptWebNotification }; + +async function retrieveEncryptionKey(): Promise { + if (!isDesktopSafari) { + return await localforage.getItem( + NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, + ); + } + // Safari doesn't support structured clone algorithm in service + // worker context so we have to store CryptoKey as JSON + const persistedCryptoKey = await localforage.getItem( + NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, + ); + if (!persistedCryptoKey) { + return null; + } + return await importJWKKey(persistedCryptoKey); +} + +export { decryptWebNotification, decryptDesktopNotification };