Page MenuHomePhabricator

D9661.diff
No OneTemporary

D9661.diff

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,8 +1,205 @@
// @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 {
+ decryptData,
+ encryptData,
+ importJWKKey,
+} 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';
+
+export type WebNotifDecryptionError = {
+ +id: string,
+ +error: string,
+ +displayErrorMessage?: boolean,
+};
+
export type WebNotifsServiceUtilsData = {
+olmWasmPath: string,
+staffCanSee: boolean,
};
+type DecryptionResult = {
+ +newPendingSessionUpdate: string,
+ +newUpdateCreationTimestamp: number,
+ +decryptedNotification: PlainTextWebNotificationPayload,
+};
+
export const WEB_NOTIFS_SERVICE_UTILS_KEY = 'webNotifsServiceUtils';
+
+const SESSION_UPDATE_MAX_PENDING_TIME = 10 * 1000;
+
+async function decryptWebNotification(
+ encryptedNotification: EncryptedWebNotification,
+): Promise<PlainTextWebNotification | WebNotifDecryptionError> {
+ const { id, encryptedPayload } = encryptedNotification;
+
+ const retrieveEncryptionKeyPromise: Promise<?CryptoKey> = (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,
+ 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(
+ encryptedOlmData,
+ encryptionKey,
+ );
+ const {
+ mainSession,
+ picklingKey,
+ pendingSessionUpdate,
+ updateCreationTimestamp,
+ }: NotificationsOlmDataType = JSON.parse(
+ new TextDecoder().decode(serializedOlmData),
+ );
+
+ let updatedOlmData: NotificationsOlmDataType;
+ let decryptedNotification: PlainTextWebNotificationPayload;
+
+ 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 { id, ...decryptedNotification };
+ } catch (e) {
+ return {
+ id,
+ error: e.message,
+ displayErrorMessage: staffCanSee,
+ };
+ }
+}
+
+function decryptWithSession(
+ pickledSession: string,
+ picklingKey: string,
+ encryptedPayload: string,
+): DecryptionResult {
+ const session = new olm.Session();
+
+ session.unpickle(picklingKey, pickledSession);
+ const decryptedNotification: PlainTextWebNotificationPayload = JSON.parse(
+ session.decrypt(olmEncryptedMessageTypes.TEXT, encryptedPayload),
+ );
+
+ const newPendingSessionUpdate = session.pickle(picklingKey);
+ const newUpdateCreationTimestamp = Date.now();
+
+ return {
+ decryptedNotification,
+ newUpdateCreationTimestamp,
+ newPendingSessionUpdate,
+ };
+}
+
+function decryptWithPendingSession(
+ pendingSessionUpdate: string,
+ picklingKey: string,
+ encryptedPayload: string,
+): DecryptionResult | { +error: string } {
+ try {
+ const {
+ decryptedNotification,
+ newPendingSessionUpdate,
+ newUpdateCreationTimestamp,
+ } = decryptWithSession(pendingSessionUpdate, picklingKey, encryptedPayload);
+ return {
+ newPendingSessionUpdate,
+ newUpdateCreationTimestamp,
+ decryptedNotification,
+ };
+ } catch (e) {
+ return { error: e.message };
+ }
+}
+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,13 +2,18 @@
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 {
+ decryptWebNotification,
WEB_NOTIFS_SERVICE_UTILS_KEY,
type WebNotifsServiceUtilsData,
+ type WebNotifDecryptionError,
} from './notif-crypto-utils.js';
import { localforageConfig } from '../database/utils/constants.js';
@@ -26,6 +31,30 @@
declare var clients: Clients;
declare function skipWaiting(): Promise<void>;
+const commIconUrl = 'https://web.comm.app/favicon.ico';
+
+function buildDecryptionErrorNotification(
+ decryptionError: WebNotifDecryptionError,
+) {
+ const baseErrorPayload = {
+ badge: commIconUrl,
+ icon: commIconUrl,
+ tag: decryptionError.id,
+ data: {
+ isError: true,
+ },
+ };
+
+ if (decryptionError.displayErrorMessage && decryptionError.error) {
+ return {
+ body: decryptionError.error,
+ ...baseErrorPayload,
+ };
+ }
+
+ return baseErrorPayload;
+}
+
self.addEventListener('install', () => {
skipWaiting();
});
@@ -41,6 +70,7 @@
if (!event.data.olmWasmPath || event.data.staffCanSee === undefined) {
return;
}
+
const webNotifsServiceUtils: WebNotifsServiceUtilsData = {
olmWasmPath: event.data.olmWasmPath,
staffCanSee: event.data.staffCanSee,
@@ -55,22 +85,49 @@
});
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;
+ let decryptionResult: PlainTextWebNotification | WebNotifDecryptionError;
+
+ if (data.encryptedPayload) {
+ decryptionResult = await decryptWebNotification(data);
+ }
+
+ if (decryptionResult && decryptionResult.error) {
+ const decryptionErrorNotification =
+ buildDecryptionErrorNotification(decryptionResult);
+ await self.registration.showNotification(
+ 'Comm notification',
+ decryptionErrorNotification,
+ );
+ return;
+ } else if (decryptionResult && decryptionResult.body) {
+ plainTextData = decryptionResult;
+ } 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}`;
}
- await self.registration.showNotification(data.title, {
+ await self.registration.showNotification(plainTextData.title, {
body,
- badge: 'https://web.comm.app/favicon.ico',
- icon: 'https://web.comm.app/favicon.ico',
- tag: data.id,
+ badge: commIconUrl,
+ icon: commIconUrl,
+ tag: plainTextData.id,
data: {
- unreadCount: data.unreadCount,
- threadID: data.threadID,
+ unreadCount: plainTextData.unreadCount,
+ threadID: plainTextData.threadID,
},
});
})(),
@@ -88,23 +145,32 @@
const selectedClient =
clientList.find(client => client.focused) ?? clientList[0];
- const threadID = convertNonPendingIDToNewSchema(
- event.notification.data.threadID,
- ashoatKeyserverID,
- );
+ // Decryption error notifications don't contain threadID
+ // but we still want them to be interactive in terms of basic
+ // navigation.
+ let threadID;
+ if (!event.notification.data.isError) {
+ threadID = convertNonPendingIDToNewSchema(
+ event.notification.data.threadID,
+ ashoatKeyserverID,
+ );
+ }
if (selectedClient) {
if (!selectedClient.focused) {
await selectedClient.focus();
}
- selectedClient.postMessage({
- targetThreadID: threadID,
- });
+ if (threadID) {
+ selectedClient.postMessage({
+ targetThreadID: threadID,
+ });
+ }
} else {
- const url =
- (process.env.NODE_ENV === 'production'
+ const baseURL =
+ process.env.NODE_ENV === 'production'
? 'https://web.comm.app'
- : 'http://localhost:3000/webapp') + `/chat/thread/${threadID}/`;
+ : 'http://localhost:3000/webapp';
+ const url = threadID ? baseURL + `/chat/thread/${threadID}/` : baseURL;
clients.openWindow(url);
}
})(),

File Metadata

Mime Type
text/plain
Expires
Wed, Nov 27, 5:20 AM (21 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2588437
Default Alt Text
D9661.diff (11 KB)

Event Timeline