diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js --- a/keyserver/src/push/crypto.js +++ b/keyserver/src/push/crypto.js @@ -4,6 +4,11 @@ import invariant from 'invariant'; import _cloneDeep from 'lodash/fp/cloneDeep.js'; +import type { + PlainTextWebNotification, + WebNotification, +} from 'lib/types/notif-types.js'; + import type { AndroidNotification, AndroidNotificationPayload, @@ -213,6 +218,31 @@ }; } +async function encryptWebNotification( + cookieID: string, + notification: PlainTextWebNotification, +): Promise { + const { id, ...payloadSansId } = notification; + const unencryptedSerializedPayload = JSON.stringify(payloadSansId); + + try { + const { + encryptedMessages: { serializedPayload }, + } = await encryptAndUpdateOlmSession(cookieID, 'notifications', { + serializedPayload: unencryptedSerializedPayload, + }); + + return { id, encryptedPayload: serializedPayload.body }; + } catch (e) { + console.log('Notification encryption failed: ' + e); + return { + id, + encryptionFailed: '1', + ...payloadSansId, + }; + } +} + function prepareEncryptedIOSNotifications( devices: $ReadOnlyArray, notification: apn.Notification, @@ -312,9 +342,28 @@ return Promise.all(notificationPromises); } +function prepareEncryptedWebNotifications( + devices: $ReadOnlyArray, + notification: PlainTextWebNotification, +): Promise< + $ReadOnlyArray<{ + +deviceToken: string, + +notification: WebNotification, + }>, +> { + const notificationPromises = devices.map( + async ({ deviceToken, cookieID }) => { + const notif = await encryptWebNotification(cookieID, notification); + return { notification: notif, deviceToken }; + }, + ); + return Promise.all(notificationPromises); +} + export { prepareEncryptedIOSNotifications, prepareEncryptedIOSNotificationRescind, prepareEncryptedAndroidNotifications, prepareEncryptedAndroidNotificationRescinds, + prepareEncryptedWebNotifications, }; diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js --- a/keyserver/src/push/send.js +++ b/keyserver/src/push/send.js @@ -19,10 +19,15 @@ } from 'lib/shared/message-utils.js'; import { messageSpecs } from 'lib/shared/messages/message-specs.js'; import { notifTextsForMessageInfo } from 'lib/shared/notif-utils.js'; +import { isStaff } from 'lib/shared/staff-utils.js'; import { rawThreadInfoFromServerThreadInfo, threadInfoFromRawThreadInfo, } from 'lib/shared/thread-utils.js'; +import { + NEXT_CODE_VERSION, + hasMinCodeVersion, +} from 'lib/shared/version-utils.js'; import type { Platform, PlatformDetails } from 'lib/types/device-types.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { @@ -31,7 +36,6 @@ } from 'lib/types/message-types.js'; import { rawMessageInfoValidator } from 'lib/types/message-types.js'; import type { - WebNotification, WNSNotification, ResolvedNotifTexts, } from 'lib/types/notif-types.js'; @@ -39,12 +43,14 @@ import type { ServerThreadInfo, ThreadInfo } from 'lib/types/thread-types.js'; import { updateTypes } from 'lib/types/update-types-enum.js'; import { type GlobalUserInfo } from 'lib/types/user-types.js'; +import { isDev } from 'lib/utils/dev-utils.js'; import { promiseAll } from 'lib/utils/promises.js'; import { tID, tPlatformDetails, tShape } from 'lib/utils/validation-utils.js'; import { prepareEncryptedIOSNotifications, prepareEncryptedAndroidNotifications, + prepareEncryptedWebNotifications, } from './crypto.js'; import { getAPNsNotificationTopic } from './providers.js'; import { rescindPushNotifs } from './rescind.js'; @@ -52,6 +58,7 @@ NotificationTargetDevice, TargetedAPNsNotification, TargetedAndroidNotification, + TargetedWebNotification, } from './types.js'; import { apnPush, @@ -356,14 +363,18 @@ }; const deliveryPromise: Promise = (async () => { - const notification = await prepareWebNotification({ - notifTexts, - threadID: threadInfo.id, - unreadCount, - platformDetails, - }); - const deviceTokens = devices.map(({ deviceToken }) => deviceToken); - return await sendWebNotification(notification, deviceTokens, { + const targetedNotifications = await prepareWebNotification( + userID, + { + notifTexts, + threadID: threadInfo.id, + unreadCount, + platformDetails, + }, + devices, + ); + + return await sendWebNotifications(targetedNotifications, { ...notificationInfo, codeVersion, stateVersion, @@ -1053,8 +1064,10 @@ platformDetails: tPlatformDetails, }); async function prepareWebNotification( + userID: string, inputData: WebNotifInputData, -): Promise { + devices: $ReadOnlyArray, +): Promise<$ReadOnlyArray> { const convertedData = validateOutput( inputData.platformDetails, webNotifInputDataValidator, @@ -1069,7 +1082,18 @@ id, threadID, }; - return notification; + + const isStaffOrDev = isStaff(userID) || isDev; + const shouldBeEncrypted = + hasMinCodeVersion(convertedData.platformDetails, { + web: NEXT_CODE_VERSION, + }) && isStaffOrDev; + + if (!shouldBeEncrypted) { + return devices.map(({ deviceToken }) => ({ deviceToken, notification })); + } + + return prepareEncryptedWebNotifications(devices, notification); } type WNSNotifInputData = { @@ -1272,18 +1296,17 @@ +delivery: WebDelivery, +invalidTokens?: $ReadOnlyArray, }; -async function sendWebNotification( - notification: WebNotification, - deviceTokens: $ReadOnlyArray, +async function sendWebNotifications( + targetedNotifications: $ReadOnlyArray, notificationInfo: NotificationInfo, ): Promise { const { source, codeVersion, stateVersion } = notificationInfo; - const response = await webPush({ - notification, - deviceTokens, - }); + const response = await webPush(targetedNotifications); + const deviceTokens = targetedNotifications.map( + ({ deviceToken }) => deviceToken, + ); const delivery: WebDelivery = { source, deviceType: 'web', diff --git a/keyserver/src/push/types.js b/keyserver/src/push/types.js --- a/keyserver/src/push/types.js +++ b/keyserver/src/push/types.js @@ -2,6 +2,8 @@ import apn from '@parse/node-apn'; +import type { WebNotification } from 'lib/types/notif-types.js'; + export type TargetedAPNsNotification = { +notification: apn.Notification, +deviceToken: string, @@ -55,6 +57,11 @@ +deviceToken: string, }; +export type TargetedWebNotification = { + +notification: WebNotification, + +deviceToken: string, +}; + export type NotificationTargetDevice = { +cookieID: string, +deviceToken: string, diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js --- a/keyserver/src/push/utils.js +++ b/keyserver/src/push/utils.js @@ -11,10 +11,7 @@ import blobService from 'lib/facts/blob-service.js'; import type { PlatformDetails } from 'lib/types/device-types.js'; -import type { - WebNotification, - WNSNotification, -} from 'lib/types/notif-types.js'; +import type { WNSNotification } from 'lib/types/notif-types.js'; import { threadSubscriptions } from 'lib/types/subscription-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { toBase64URL } from 'lib/utils/base64.js'; @@ -32,6 +29,7 @@ import type { TargetedAPNsNotification, TargetedAndroidNotification, + TargetedWebNotification, } from './types.js'; import { dbQuery, SQL } from '../database/database.js'; import { generateKey, encrypt } from '../utils/aes-crypto-utils.js'; @@ -239,30 +237,31 @@ +errors?: $ReadOnlyArray, +invalidTokens?: $ReadOnlyArray, }; -async function webPush({ - notification, - deviceTokens, -}: { - +notification: WebNotification, - +deviceTokens: $ReadOnlyArray, -}): Promise { +async function webPush( + targetedNotifications: $ReadOnlyArray, +): Promise { await ensureWebPushInitialized(); - const notificationString = JSON.stringify(notification); const pushResults = await Promise.all( - deviceTokens.map(async deviceTokenString => { - const deviceToken: PushSubscriptionJSON = JSON.parse(deviceTokenString); - try { - await webpush.sendNotification(deviceToken, notificationString); - } catch (error) { - return { error }; - } - return {}; - }), + targetedNotifications.map( + async ({ notification, deviceToken: deviceTokenString }) => { + const deviceToken: PushSubscriptionJSON = JSON.parse(deviceTokenString); + const notificationString = JSON.stringify(notification); + try { + await webpush.sendNotification(deviceToken, notificationString); + } catch (error) { + return { error }; + } + return {}; + }, + ), ); const errors = []; const invalidTokens = []; + const deviceTokens = targetedNotifications.map( + ({ deviceToken }) => deviceToken, + ); for (let i = 0; i < pushResults.length; i++) { const pushResult = pushResults[i]; if (pushResult.error) { diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -26,15 +26,29 @@ prefix: t.maybe(t.String), }); -export type WebNotification = { +export type PlainTextWebNotificationPayload = { +body: string, +prefix?: string, +title: string, +unreadCount: number, - +id: string, +threadID: string, + +encryptionFailed?: '1', +}; + +export type PlainTextWebNotification = { + +id: string, + ...PlainTextWebNotificationPayload, }; +export type EncryptedWebNotification = { + +id: string, + +encryptedPayload: string, +}; + +export type WebNotification = + | PlainTextWebNotification + | EncryptedWebNotification; + export type WNSNotification = { +body: string, +prefix?: string, 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 @@ -1,6 +1,6 @@ // @flow -import type { WebNotification } from 'lib/types/notif-types.js'; +import type { PlainTextWebNotification } from 'lib/types/notif-types.js'; import { convertNonPendingIDToNewSchema } from 'lib/utils/migration-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; @@ -23,7 +23,7 @@ }); self.addEventListener('push', (event: PushEvent) => { - const data: WebNotification = event.data.json(); + const data: PlainTextWebNotification = event.data.json(); event.waitUntil( (async () => {