diff --git a/web/crypto/aes-gcm-crypto-utils.js b/web/crypto/aes-gcm-crypto-utils.js --- a/web/crypto/aes-gcm-crypto-utils.js +++ b/web/crypto/aes-gcm-crypto-utils.js @@ -6,7 +6,7 @@ 'decrypt', ]; -type EncryptedData = { +export type EncryptedData = { +iv: BufferSource, +ciphertext: Uint8Array, }; 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 @@ -9,11 +9,11 @@ } from 'lib/types/crypto-types.js'; import type { PlainTextWebNotification, - PlainTextWebNotificationPayload, EncryptedWebNotification, } from 'lib/types/notif-types.js'; import { + type EncryptedData, decryptData, encryptData, importJWKKey, @@ -23,6 +23,7 @@ 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, @@ -35,10 +36,10 @@ +staffCanSee: boolean, }; -type DecryptionResult = { +type DecryptionResult = { +newPendingSessionUpdate: string, +newUpdateCreationTimestamp: number, - +decryptedNotification: PlainTextWebNotificationPayload, + +decryptedNotification: T, }; export const WEB_NOTIFS_SERVICE_UTILS_KEY = 'webNotifsServiceUtils'; @@ -50,21 +51,9 @@ ): Promise { const { id, encryptedPayload } = encryptedNotification; - const retrieveEncryptionKeyPromise: Promise = (async () => { - const persistedCryptoKey = await localforage.getItem( - NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, - ); - if (isDesktopSafari && persistedCryptoKey) { - // Safari doesn't support structured clone algorithm in service - // worker context so we have to store CryptoKey as JSON - return await importJWKKey(persistedCryptoKey); - } - return persistedCryptoKey; - })(); - const [encryptedOlmData, encryptionKey, utilsData] = await Promise.all([ localforage.getItem(NOTIFICATIONS_OLM_DATA_CONTENT), - retrieveEncryptionKeyPromise, + retrieveEncryptionKey(), localforage.getItem(WEB_NOTIFS_SERVICE_UTILS_KEY), ]); @@ -84,91 +73,131 @@ 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 }> { + const [encryptedOlmData, encryptionKey] = await Promise.all([ + localforage.getItem(NOTIFICATIONS_OLM_DATA_CONTENT), + retrieveEncryptionKey(), + ]); - 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 { + await initOlm(); + 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), ); @@ -182,11 +211,11 @@ }; } -function decryptWithPendingSession( +function decryptWithPendingSession( pendingSessionUpdate: string, picklingKey: string, encryptedPayload: string, -): DecryptionResult | { +error: string } { +): DecryptionResult | { +error: string } { try { const { decryptedNotification, @@ -202,4 +231,17 @@ return { error: e.message }; } } -export { decryptWebNotification }; + +async function retrieveEncryptionKey(): Promise { + const persistedCryptoKey = await localforage.getItem( + NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, + ); + if (isDesktopSafari && persistedCryptoKey) { + // Safari doesn't support structured clone algorithm in service + // worker context so we have to store CryptoKey as JSON + return await importJWKKey(persistedCryptoKey); + } + return persistedCryptoKey; +} + +export { decryptWebNotification, decryptDesktopNotification };