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,3 +1,81 @@ // @flow +import olm from '@commapp/olm'; +import invariant from 'invariant'; +import localforage from 'localforage'; + +import type { PickledOLMSession } from 'lib/types/crypto-types.js'; +import type { + PlainTextWebNotification, + EncryptedWebNotification, +} from 'lib/types/notif-types.js'; + +import { decryptData, encryptData } from '../crypto/aes-gcm-crypto-utils.js'; +import { + NOTIFICATIONS_OLM_SESSION_CONTENT, + NOTIFICATIONS_OLM_SESSION_ENCRYPTION_KEY, +} from '../database/utils/constants.js'; + +type PlainTextWebNotificationPayload = $Diff< + PlainTextWebNotification, + { +id: string }, +>; + export const OLM_WASM_PATH_KEY = 'olmWasmPath'; + +async function decryptWebNotification( + encryptedNotification: EncryptedWebNotification, +): Promise { + const { id, encryptedPayload } = encryptedNotification; + + const olmInitPromise = (async () => { + const olmWasmFilePath = await localforage.getItem(OLM_WASM_PATH_KEY); + await olm.init({ locateFile: () => olmWasmFilePath }); + })(); + const [encryptedOlmSession, encryptionKey] = await Promise.all([ + localforage.getItem(NOTIFICATIONS_OLM_SESSION_CONTENT), + localforage.getItem(NOTIFICATIONS_OLM_SESSION_ENCRYPTION_KEY), + olmInitPromise, + ]); + + // This is indeed an error since it means that the keyserver + // has established notification session, but the client hasn't + invariant( + encryptionKey && encryptedOlmSession, + 'Received encrypted notification but olm session was not created', + ); + + const serializedSession = await decryptData( + encryptedOlmSession, + encryptionKey, + ); + const { picklingKey, pickledSession }: PickledOLMSession = JSON.parse( + new TextDecoder().decode(serializedSession), + ); + + const session = new olm.Session(); + session.unpickle(picklingKey, pickledSession); + + const decryptedSerializedNotification = session.decrypt(1, encryptedPayload); + const decryptedNotification: PlainTextWebNotificationPayload = JSON.parse( + decryptedSerializedNotification, + ); + + const updatedPickledSession = { + picklingKey, + pickledSession: session.pickle(picklingKey), + }; + const updatedEncryptedSession = await encryptData( + new TextEncoder().encode(JSON.stringify(updatedPickledSession)), + encryptionKey, + ); + + await localforage.setItem( + NOTIFICATIONS_OLM_SESSION_CONTENT, + updatedEncryptedSession, + ); + + return { id, ...decryptedNotification }; +} + +export { decryptWebNotification }; diff --git a/web/push-notif/service-worker.js b/web/push-notif/service-worker.js --- a/web/push-notif/service-worker.js +++ b/web/push-notif/service-worker.js @@ -2,11 +2,17 @@ import localforage from 'localforage'; -import type { PlainTextWebNotification } from 'lib/types/notif-types.js'; +import type { + PlainTextWebNotification, + WebNotification, +} from 'lib/types/notif-types.js'; import { convertNonPendingIDToNewSchema } from 'lib/utils/migration-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; -import { OLM_WASM_PATH_KEY } from './notif-crypto-utils.js'; +import { + decryptWebNotification, + OLM_WASM_PATH_KEY, +} from './notif-crypto-utils.js'; import { localforageConfig } from '../database/utils/constants.js'; declare class PushMessageData { @@ -44,11 +50,30 @@ }); self.addEventListener('push', (event: PushEvent) => { - const data: PlainTextWebNotification = event.data.json(); + localforage.config(localforageConfig); + const data: WebNotification = event.data.json(); event.waitUntil( (async () => { - let body = data.body; + let plainTextData: PlainTextWebNotification; + + if (data.encryptedPayload) { + try { + plainTextData = await decryptWebNotification(data); + } catch (e) { + console.log(`Failed to decrypt notification with id: ${data.id}`, e); + return; + } + } else if (data.body) { + plainTextData = data; + } else { + // We will never enter ths branch. It is + // necessary since flow doesn't differentiate + // between union types out-of-the-box. + return; + } + + let body = plainTextData.body; if (data.prefix) { body = `${data.prefix} ${body}`; } @@ -56,10 +81,10 @@ body, badge: 'https://web.comm.app/favicon.ico', icon: 'https://web.comm.app/favicon.ico', - tag: data.id, + tag: plainTextData.id, data: { - unreadCount: data.unreadCount, - threadID: data.threadID, + unreadCount: plainTextData.unreadCount, + threadID: plainTextData.threadID, }, }); })(),