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 @@ -11,13 +11,13 @@ PlainTextWebNotificationPayload, WebNotification, PlainTextWNSNotification, - PlainTextWNSNotificationPayload, WNSNotification, AndroidVisualNotification, AndroidVisualNotificationPayload, AndroidBadgeOnlyNotification, AndroidNotificationRescind, NotificationTargetDevice, + SenderDeviceDescriptor, } from 'lib/types/notif-types.js'; import { toBase64URL } from 'lib/utils/base64.js'; @@ -27,6 +27,7 @@ async function encryptAPNsNotification( cookieID: string, + senderDeviceID: SenderDeviceDescriptor, notification: apn.Notification, codeVersion?: ?number, notificationSizeValidator?: apn.Notification => boolean, @@ -58,8 +59,7 @@ encryptedNotification.pushType = 'alert'; encryptedNotification.mutableContent = true; - const { id, keyserverID, ...payloadSansUnencryptedData } = - notification.payload; + const { id, ...payloadSansUnencryptedData } = notification.payload; const unencryptedPayload = { ...payloadSansUnencryptedData, badge: notification.aps.badge.toString(), @@ -95,6 +95,10 @@ ); encryptedNotification.payload.encryptedPayload = serializedPayload.body; + encryptedNotification.payload = { + ...senderDeviceID, + ...encryptedNotification.payload, + }; if (codeVersion && codeVersion >= 254 && codeVersion % 2 === 0) { encryptedNotification.aps = { @@ -137,10 +141,15 @@ async function encryptAndroidNotificationPayload( cookieID: string, + senderDeviceID: SenderDeviceDescriptor, unencryptedPayload: T, - payloadSizeValidator?: (T | { +encryptedPayload: string }) => boolean, + payloadSizeValidator?: ( + T | $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string }>, + ) => boolean, ): Promise<{ - +resultPayload: T | { +encryptedPayload: string }, + +resultPayload: + | T + | $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string }>, +payloadSizeExceeded: boolean, +encryptionOrder?: number, }> { @@ -161,7 +170,11 @@ serializedPayload, }: { +[string]: EncryptResult, - }) => payloadSizeValidator({ encryptedPayload: serializedPayload.body }); + }) => + payloadSizeValidator({ + encryptedPayload: serializedPayload.body, + ...senderDeviceID, + }); } const { @@ -177,7 +190,10 @@ dbPersistCondition, ); return { - resultPayload: { encryptedPayload: serializedPayload.body }, + resultPayload: { + encryptedPayload: serializedPayload.body, + ...senderDeviceID, + }, payloadSizeExceeded: !!dbPersistConditionViolated, encryptionOrder, }; @@ -197,6 +213,7 @@ } async function encryptAndroidVisualNotification( + senderDeviceID: SenderDeviceDescriptor, cookieID: string, notification: AndroidVisualNotification, notificationSizeValidator?: AndroidVisualNotification => boolean, @@ -206,11 +223,11 @@ +payloadSizeExceeded: boolean, +encryptionOrder?: number, }> { - const { id, keyserverID, ...rest } = notification.data; + const { id, ...rest } = notification.data; - let unencryptedData = { keyserverID }; + let unencryptedData = {}; if (id) { - unencryptedData = { ...unencryptedData, id }; + unencryptedData = { id }; } let unencryptedPayload = rest; @@ -221,7 +238,9 @@ let payloadSizeValidator; if (notificationSizeValidator) { payloadSizeValidator = ( - payload: AndroidVisualNotificationPayload | { +encryptedPayload: string }, + payload: + | AndroidVisualNotificationPayload + | $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string }>, ) => { return notificationSizeValidator({ data: { ...unencryptedData, ...payload }, @@ -231,6 +250,7 @@ const { resultPayload, payloadSizeExceeded, encryptionOrder } = await encryptAndroidNotificationPayload( cookieID, + senderDeviceID, unencryptedPayload, payloadSizeValidator, ); @@ -248,31 +268,32 @@ async function encryptAndroidSilentNotification( cookieID: string, + senderDeviceID: SenderDeviceDescriptor, notification: AndroidNotificationRescind | AndroidBadgeOnlyNotification, ): Promise { // We don't validate payload size for rescind // since they are expected to be small and // never exceed any FCM limit - const { keyserverID, ...unencryptedPayload } = notification.data; + const { ...unencryptedPayload } = notification.data; const { resultPayload } = await encryptAndroidNotificationPayload( cookieID, + senderDeviceID, unencryptedPayload, ); if (resultPayload.encryptedPayload) { return { - data: { keyserverID, ...resultPayload }, + data: { ...resultPayload }, }; } if (resultPayload.rescind) { return { - data: { keyserverID, ...resultPayload }, + data: { ...resultPayload }, }; } return { data: { - keyserverID, ...resultPayload, }, }; @@ -280,9 +301,14 @@ async function encryptBasicPayload( cookieID: string, + senderDeviceID: SenderDeviceDescriptor, basicPayload: T, ): Promise< - | { +encryptedPayload: string, +encryptionOrder?: number } + | $ReadOnly<{ + ...SenderDeviceDescriptor, + +encryptedPayload: string, + +encryptionOrder?: number, + }> | { ...T, +encryptionFailed: '1' }, > { const unencryptedSerializedPayload = JSON.stringify(basicPayload); @@ -300,6 +326,7 @@ }); return { + ...senderDeviceID, encryptedPayload: serializedPayload.body, encryptionOrder, }; @@ -314,37 +341,42 @@ async function encryptWebNotification( cookieID: string, + senderDeviceID: SenderDeviceDescriptor, notification: PlainTextWebNotification, ): Promise<{ +notification: WebNotification, +encryptionOrder?: number }> { - const { id, keyserverID, ...payloadSansId } = notification; + const { id, ...payloadSansId } = notification; const { encryptionOrder, ...encryptionResult } = await encryptBasicPayload( cookieID, + senderDeviceID, payloadSansId, ); + return { - notification: { id, keyserverID, ...encryptionResult }, + notification: { id, ...encryptionResult }, encryptionOrder, }; } async function encryptWNSNotification( cookieID: string, + senderDeviceID: SenderDeviceDescriptor, notification: PlainTextWNSNotification, ): Promise<{ +notification: WNSNotification, +encryptionOrder?: number }> { - const { keyserverID, ...payloadSansKeyserverID } = notification; const { encryptionOrder, ...encryptionResult } = - await encryptBasicPayload( + await encryptBasicPayload( cookieID, - payloadSansKeyserverID, + senderDeviceID, + notification, ); return { - notification: { keyserverID, ...encryptionResult }, + notification: { ...encryptionResult }, encryptionOrder, }; } function prepareEncryptedAPNsNotifications( + senderDeviceID: SenderDeviceDescriptor, devices: $ReadOnlyArray, notification: apn.Notification, codeVersion?: ?number, @@ -363,6 +395,7 @@ async ({ cookieID, deviceToken, blobHolder }) => { const notif = await encryptAPNsNotification( cookieID, + senderDeviceID, notification, codeVersion, notificationSizeValidator, @@ -375,6 +408,7 @@ } function prepareEncryptedIOSNotificationRescind( + senderDeviceID: SenderDeviceDescriptor, devices: $ReadOnlyArray, notification: apn.Notification, codeVersion?: ?number, @@ -389,6 +423,7 @@ async ({ deviceToken, cookieID }) => { const { notification: notif } = await encryptAPNsNotification( cookieID, + senderDeviceID, notification, codeVersion, ); @@ -399,6 +434,7 @@ } function prepareEncryptedAndroidVisualNotifications( + senderDeviceID: SenderDeviceDescriptor, devices: $ReadOnlyArray, notification: AndroidVisualNotification, notificationSizeValidator?: ( @@ -416,6 +452,7 @@ const notificationPromises = devices.map( async ({ deviceToken, cookieID, blobHolder }) => { const notif = await encryptAndroidVisualNotification( + senderDeviceID, cookieID, notification, notificationSizeValidator, @@ -428,6 +465,7 @@ } function prepareEncryptedAndroidSilentNotifications( + senderDeviceID: SenderDeviceDescriptor, devices: $ReadOnlyArray, notification: AndroidNotificationRescind | AndroidBadgeOnlyNotification, ): Promise< @@ -442,6 +480,7 @@ async ({ deviceToken, cookieID }) => { const notif = await encryptAndroidSilentNotification( cookieID, + senderDeviceID, notification, ); return { deviceToken, cookieID, notification: notif }; @@ -451,6 +490,7 @@ } function prepareEncryptedWebNotifications( + senderDeviceID: SenderDeviceDescriptor, devices: $ReadOnlyArray, notification: PlainTextWebNotification, ): Promise< @@ -462,7 +502,11 @@ > { const notificationPromises = devices.map( async ({ deviceToken, cookieID }) => { - const notif = await encryptWebNotification(cookieID, notification); + const notif = await encryptWebNotification( + cookieID, + senderDeviceID, + notification, + ); return { ...notif, deviceToken }; }, ); @@ -470,6 +514,7 @@ } function prepareEncryptedWNSNotifications( + senderDeviceID: SenderDeviceDescriptor, devices: $ReadOnlyArray, notification: PlainTextWNSNotification, ): Promise< @@ -481,7 +526,11 @@ > { const notificationPromises = devices.map( async ({ deviceToken, cookieID }) => { - const notif = await encryptWNSNotification(cookieID, notification); + const notif = await encryptWNSNotification( + cookieID, + senderDeviceID, + notification, + ); return { ...notif, deviceToken }; }, ); diff --git a/keyserver/src/push/rescind.js b/keyserver/src/push/rescind.js --- a/keyserver/src/push/rescind.js +++ b/keyserver/src/push/rescind.js @@ -9,6 +9,7 @@ import type { NotificationTargetDevice, TargetedAndroidNotification, + SenderDeviceDescriptor, } from 'lib/types/notif-types.js'; import { threadSubscriptions } from 'lib/types/subscription-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; @@ -274,10 +275,12 @@ } async function conditionallyEncryptNotification( + senderDeviceID: SenderDeviceDescriptor, notification: T, codeVersion: ?number, devices: $ReadOnlyArray, encryptCallback: ( + senderDeviceID: SenderDeviceDescriptor, devices: $ReadOnlyArray, notification: T, codeVersion?: ?number, @@ -298,6 +301,7 @@ })); } const notifications = await encryptCallback( + senderDeviceID, devices, notification, codeVersion, @@ -350,6 +354,7 @@ }, }; return await conditionallyEncryptNotification( + { keyserverID }, notification, codeVersion, devices, @@ -375,10 +380,10 @@ rescindID: notifID, setUnreadStatus: 'true', threadID, - keyserverID, }, }; const targetedRescinds = await conditionallyEncryptNotification( + { keyserverID }, notification, codeVersion, devices, 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 @@ -992,7 +992,6 @@ notification.pushType = 'alert'; notification.payload.id = uniqueID; notification.payload.threadID = threadID; - notification.payload.keyserverID = keyserverID; if (platformDetails.codeVersion && platformDetails.codeVersion > 198) { notification.mutableContent = true; @@ -1033,6 +1032,7 @@ if (platformDetails.platform === 'macos') { const macOSNotifsWithoutMessageInfos = await prepareEncryptedAPNsNotifications( + { keyserverID }, devices, notification, platformDetails.codeVersion, @@ -1046,6 +1046,7 @@ } const notifsWithMessageInfos = await prepareEncryptedAPNsNotifications( + { keyserverID }, devices, copyWithMessageInfos, platformDetails.codeVersion, @@ -1054,7 +1055,10 @@ const devicesWithExcessiveSizeNoHolders = notifsWithMessageInfos .filter(({ payloadSizeExceeded }) => payloadSizeExceeded) - .map(({ deviceToken, cookieID }) => ({ deviceToken, cookieID })); + .map(({ deviceToken, cookieID }) => ({ + deviceToken, + cookieID, + })); if (devicesWithExcessiveSizeNoHolders.length === 0) { return notifsWithMessageInfos.map( @@ -1112,6 +1116,7 @@ } const notifsWithoutMessageInfos = await prepareEncryptedAPNsNotifications( + { keyserverID }, devicesWithExcessiveSize, notification, platformDetails.codeVersion, @@ -1202,7 +1207,6 @@ const { merged, ...rest } = notifTexts; const notification = { data: { - keyserverID, badge: unreadCount.toString(), ...rest, threadID, @@ -1259,6 +1263,7 @@ const notifsWithMessageInfos = await prepareEncryptedAndroidVisualNotifications( + { keyserverID }, devices, copyWithMessageInfos, notificationsSizeValidator, @@ -1320,6 +1325,7 @@ const notifsWithoutMessageInfos = await prepareEncryptedAndroidVisualNotifications( + { keyserverID }, devicesWithExcessiveSize, notification, ); @@ -1379,7 +1385,6 @@ unreadCount, id, threadID, - keyserverID, }; const shouldBeEncrypted = hasMinCodeVersion(convertedData.platformDetails, { @@ -1390,7 +1395,11 @@ return devices.map(({ deviceToken }) => ({ deviceToken, notification })); } - return prepareEncryptedWebNotifications(devices, notification); + return prepareEncryptedWebNotifications( + { keyserverID }, + devices, + notification, + ); } type WNSNotifInputData = { @@ -1422,7 +1431,6 @@ ...rest, unreadCount, threadID, - keyserverID, }; if ( @@ -1442,7 +1450,11 @@ notification, })); } - return await prepareEncryptedWNSNotifications(devices, notification); + return await prepareEncryptedWNSNotifications( + { keyserverID }, + devices, + notification, + ); } type NotificationInfo = @@ -1791,11 +1803,11 @@ }); notification.badge = unreadCount; notification.pushType = 'alert'; - notification.payload.keyserverID = keyserverID; const preparePromise: Promise = (async () => { let targetedNotifications: $ReadOnlyArray; if (codeVersion > 222) { const notificationsArray = await prepareEncryptedAPNsNotifications( + { keyserverID }, deviceInfos, notification, codeVersion, @@ -1839,7 +1851,7 @@ badgeOnly: '1', }; const notification = { - data: { ...notificationData, keyserverID }, + data: { ...notificationData }, }; const preparePromise: Promise = (async () => { let targetedNotifications: $ReadOnlyArray; @@ -1847,6 +1859,7 @@ if (codeVersion > 222) { const notificationsArray = await prepareEncryptedAndroidSilentNotifications( + { keyserverID }, deviceInfos, notification, ); @@ -1904,6 +1917,7 @@ let targetedNotifications: $ReadOnlyArray; if (shouldBeEncrypted) { const notificationsArray = await prepareEncryptedAPNsNotifications( + { keyserverID }, deviceInfos, notification, codeVersion, 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,6 +26,10 @@ prefix: t.maybe(t.String), }); +export type SenderDeviceDescriptor = + | { +keyserverID: string } + | { +senderDeviceID: string }; + export type PlainTextWebNotificationPayload = { +body: string, +prefix?: string, @@ -35,23 +39,23 @@ +encryptionFailed?: '1', }; -export type PlainTextWebNotification = { +export type PlainTextWebNotification = $ReadOnly<{ +id: string, - +keyserverID: string, ...PlainTextWebNotificationPayload, -}; +}>; -export type EncryptedWebNotification = { +export type EncryptedWebNotification = $ReadOnly<{ + ...SenderDeviceDescriptor, +id: string, - +keyserverID: string, +encryptedPayload: string, -}; + +type?: '0' | '1', +}>; export type WebNotification = | PlainTextWebNotification | EncryptedWebNotification; -export type PlainTextWNSNotificationPayload = { +export type PlainTextWNSNotification = { +body: string, +prefix?: string, +title: string, @@ -60,15 +64,11 @@ +encryptionFailed?: '1', }; -export type PlainTextWNSNotification = { - +keyserverID: string, - ...PlainTextWNSNotificationPayload, -}; - -export type EncryptedWNSNotification = { - +keyserverID: string, +export type EncryptedWNSNotification = $ReadOnly<{ + ...SenderDeviceDescriptor, +encryptedPayload: string, -}; + +type?: '0' | '1', +}>; export type WNSNotification = | PlainTextWNSNotification @@ -85,39 +85,46 @@ +encryptionFailed?: '1', }>; -export type AndroidVisualNotificationPayload = $ReadOnly< - | { - ...AndroidVisualNotificationPayloadBase, - +messageInfos?: string, - } - | { - ...AndroidVisualNotificationPayloadBase, - +blobHash: string, - +encryptionKey: string, - }, ->; +type AndroidSmallVisualNotificationPayload = $ReadOnly<{ + ...AndroidVisualNotificationPayloadBase, + +messageInfos?: string, +}>; + +type AndroidLargeVisualNotificationPayload = $ReadOnly<{ + ...AndroidVisualNotificationPayloadBase, + +blobHash: string, + +encryptionKey: string, +}>; + +export type AndroidVisualNotificationPayload = + | AndroidSmallVisualNotificationPayload + | AndroidLargeVisualNotificationPayload; + +type EncryptedThinThreadPayload = { + +keyserverID: string, + +encryptedPayload: string, + +type?: '0' | '1', +}; + +type EncryptedThickThreadPayload = { + +senderDeviceID: string, + +encryptedPayload: string, + +type?: '0' | '1', +}; export type AndroidVisualNotification = { +data: $ReadOnly<{ +id?: string, - +keyserverID: string, ... - | { - ...AndroidVisualNotificationPayloadBase, - +messageInfos?: string, - } - | { - ...AndroidVisualNotificationPayloadBase, - +blobHash: string, - +encryptionKey: string, - } - | { +encryptedPayload: string }, + | AndroidSmallVisualNotificationPayload + | AndroidLargeVisualNotificationPayload + | EncryptedThinThreadPayload + | EncryptedThickThreadPayload, }>, }; export type AndroidNotificationRescind = { +data: $ReadOnly<{ - +keyserverID: string, ... | { +badge: string, @@ -127,20 +134,21 @@ +threadID: string, +encryptionFailed?: string, } - | { +encryptedPayload: string }, + | EncryptedThinThreadPayload + | EncryptedThickThreadPayload, }>, }; export type AndroidBadgeOnlyNotification = { +data: $ReadOnly<{ - +keyserverID: string, ... | { +badge: string, +badgeOnly: '1', +encryptionFailed?: string, } - | { +encryptedPayload: string }, + | EncryptedThinThreadPayload + | EncryptedThickThreadPayload, }>, }; 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,6 +1,7 @@ // @flow import olm from '@commapp/olm'; +import invariant from 'invariant'; import localforage from 'localforage'; import { @@ -64,6 +65,7 @@ encryptedNotification: EncryptedWebNotification, ): Promise { const { id, keyserverID, encryptedPayload } = encryptedNotification; + invariant(keyserverID, 'KeyserverID must be present to decrypt a notif'); const utilsData = await localforage.getItem( WEB_NOTIFS_SERVICE_UTILS_KEY, ); @@ -108,6 +110,8 @@ ); const { unreadCount } = decryptedNotification; + + invariant(keyserverID, 'Keyserver ID must be set to update badge counts'); await updateNotifsUnreadCountStorage({ [keyserverID]: unreadCount, });