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 @@ -46,14 +46,16 @@ encryptedNotification.id = notification.id; encryptedNotification.payload.id = notification.id; + encryptedNotification.payload.keyserverID = notification.payload.keyserverID; encryptedNotification.topic = notification.topic; encryptedNotification.sound = notification.aps.sound; encryptedNotification.pushType = 'alert'; encryptedNotification.mutableContent = true; - const { id, ...payloadSansId } = notification.payload; + const { id, keyserverID, ...payloadSansUnencryptedData } = + notification.payload; const unencryptedPayload = { - ...payloadSansId, + ...payloadSansUnencryptedData, badge: notification.aps.badge.toString(), merged: notification.body, }; @@ -197,8 +199,9 @@ +payloadSizeExceeded: boolean, +encryptionOrder?: number, }> { - const { id, badgeOnly, ...unencryptedPayload } = notification.data; - let unencryptedData = { badgeOnly }; + const { id, keyserverID, badgeOnly, ...unencryptedPayload } = + notification.data; + let unencryptedData = { badgeOnly, keyserverID }; if (id) { unencryptedData = { ...unencryptedData, id }; } @@ -238,12 +241,13 @@ // 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 { resultPayload } = await encryptAndroidNotificationPayload( cookieID, - notification.data, + unencryptedPayload, ); return { - data: resultPayload, + data: { keyserverID, ...resultPayload }, }; } 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 @@ -30,6 +30,7 @@ import createIDs from '../creators/id-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import type { SQLStatementType } from '../database/types.js'; +import { thisKeyserverID } from '../user/identity.js'; import { validateOutput } from '../utils/validation-utils.js'; type ParsedDelivery = { @@ -62,7 +63,6 @@ ) AS unread_count FROM notifications n LEFT JOIN memberships m ON m.user = n.user - AND m.last_message > m.last_read_message AND m.role > 0 AND JSON_EXTRACT(subscription, ${notificationExtractString}) AND JSON_EXTRACT(permissions, ${visPermissionExtractString}) @@ -70,7 +70,10 @@ `); fetchQuery.append(notifCondition); fetchQuery.append(SQL` GROUP BY n.id, m.user`); - const [fetchResult] = await dbQuery(fetchQuery); + const [fetchResult, keyserverID] = await Promise.all([ + dbQuery(fetchQuery), + thisKeyserverID(), + ]); const allDeviceTokens = new Set(); const parsedDeliveries: { [string]: $ReadOnlyArray } = {}; @@ -152,6 +155,7 @@ })); const deliveryPromise = (async () => { const targetedNotifications = await prepareIOSNotification( + keyserverID, delivery.notificationID, row.unread_count, threadID, @@ -174,6 +178,7 @@ })); const deliveryPromise = (async () => { const targetedNotifications = await prepareAndroidNotification( + keyserverID, delivery.notificationID, row.unread_count, threadID, @@ -303,6 +308,7 @@ } async function prepareIOSNotification( + keyserverID: string, iosID: string, unreadCount: number, threadID: string, @@ -334,6 +340,7 @@ notificationId: iosID, setUnreadStatus: true, threadID, + keyserverID, } : { managedAps: { @@ -350,6 +357,7 @@ } async function prepareAndroidNotification( + keyserverID: string, notifID: string, unreadCount: number, threadID: string, @@ -366,6 +374,7 @@ rescindID: notifID, setUnreadStatus: 'true', threadID, + keyserverID, }, }; return await conditionallyEncryptNotification( 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 @@ -78,6 +78,7 @@ import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js'; import { fetchUserInfos } from '../fetchers/user-fetchers.js'; import type { Viewer } from '../session/viewer.js'; +import { thisKeyserverID } from '../user/identity.js'; import { getENSNames } from '../utils/ens-cache.js'; import { validateOutput } from '../utils/validation-utils.js'; @@ -112,6 +113,8 @@ return; } + const keyserverID = await thisKeyserverID(); + const [ unreadCounts, { usersToCollapsableNotifInfo, serverThreadInfos, userInfos }, @@ -148,6 +151,7 @@ for (const notifInfo of usersToCollapsableNotifInfo[userID]) { preparePromises.push( preparePushNotif({ + keyserverID, notifInfo, userID, pushUserInfo: pushInfo[userID], @@ -192,6 +196,7 @@ }; async function preparePushNotif(input: { + keyserverID: string, notifInfo: CollapsableNotifInfo, userID: string, pushUserInfo: PushUserInfo, @@ -204,6 +209,7 @@ rowsToSave: Map, // mutable }): Promise> { const { + keyserverID, notifInfo, userID, pushUserInfo, @@ -319,6 +325,7 @@ (async () => { const targetedNotifications = await prepareAPNsNotification( { + keyserverID, notifTexts, newRawMessageInfos: shimmedNewRawMessageInfos, threadID: threadInfo.id, @@ -359,6 +366,7 @@ (async () => { const targetedNotifications = await prepareAndroidNotification( { + keyserverID, notifTexts, newRawMessageInfos: shimmedNewRawMessageInfos, threadID: threadInfo.id, @@ -437,6 +445,7 @@ (async () => { const targetedNotifications = await prepareAPNsNotification( { + keyserverID, notifTexts, newRawMessageInfos: shimmedNewRawMessageInfos, threadID: threadInfo.id, @@ -886,6 +895,7 @@ } type APNsNotifInputData = { + +keyserverID: string, +notifTexts: ResolvedNotifTexts, +newRawMessageInfos: RawMessageInfo[], +threadID: string, @@ -895,6 +905,7 @@ +platformDetails: PlatformDetails, }; const apnsNotifInputDataValidator = tShape({ + keyserverID: t.String, notifTexts: resolvedNotifTextsValidator, newRawMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, @@ -913,6 +924,7 @@ inputData, ); const { + keyserverID, notifTexts, newRawMessageInfos, threadID, @@ -960,10 +972,14 @@ notification.body = merged; notification.sound = 'default'; } + notification.payload = { ...notification.payload, ...rest, }; + if (platformDetails.platform !== 'macos') { + notification.payload.keyserverID = keyserverID; + } notification.badge = unreadCount; notification.threadId = threadID; @@ -1093,6 +1109,7 @@ inputData, ); const { + keyserverID, notifTexts, newRawMessageInfos, threadID, @@ -1118,6 +1135,7 @@ const { merged, ...rest } = notifTexts; const notification = { data: { + keyserverID, badge: unreadCount.toString(), ...rest, threadID, @@ -1628,11 +1646,13 @@ if (viewer.data.cookieID) { deviceTokenQuery.append(SQL`AND id != ${viewer.cookieID} `); } - const [unreadCounts, [deviceTokenResult], [dbID]] = await Promise.all([ - getUnreadCounts([userID]), - dbQuery(deviceTokenQuery), - createIDs('notifications', 1), - ]); + const [unreadCounts, [deviceTokenResult], [dbID], keyserverID] = + await Promise.all([ + getUnreadCounts([userID]), + dbQuery(deviceTokenQuery), + createIDs('notifications', 1), + thisKeyserverID(), + ]); const unreadCount = unreadCounts[userID]; const devices = deviceTokenResult.map(row => { const versions = JSON.parse(row.versions); @@ -1660,6 +1680,7 @@ }); notification.badge = unreadCount; notification.pushType = 'alert'; + notification.payload.keyserverID = keyserverID; const preparePromise: Promise = (async () => { let targetedNotifications: $ReadOnlyArray; if (codeVersion > 222) { @@ -1706,7 +1727,9 @@ codeVersion < 69 ? { badge: unreadCount.toString() } : { badge: unreadCount.toString(), badgeOnly: '1' }; - const notification = { data: notificationData }; + const notification = { + data: { ...notificationData, keyserverID }, + }; const preparePromise: Promise = (async () => { let targetedNotifications: $ReadOnlyArray; if (codeVersion > 222) { 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 @@ -39,21 +39,25 @@ +data: { +id?: string, +badgeOnly?: string, + +keyserverID: string, ...AndroidNotificationPayload | { +encryptedPayload: string }, }, }; export type AndroidNotificationRescind = { - +data: - | { - +badge: string, - +rescind: 'true', - +rescindID: string, - +setUnreadStatus: 'true', - +threadID: string, - +encryptionFailed?: string, - } - | { +encryptedPayload: string }, + +data: { + +keyserverID: string, + ... + | { + +badge: string, + +rescind: 'true', + +rescindID: string, + +setUnreadStatus: 'true', + +threadID: string, + +encryptionFailed?: string, + } + | { +encryptedPayload: string }, + }, }; export type TargetedAndroidNotification = { diff --git a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java --- a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java +++ b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java @@ -49,8 +49,7 @@ private static final String KEYSERVER_ID_KEY = "keyserverID"; private static final String CHANNEL_ID = "default"; private static final long[] VIBRATION_SPEC = {500, 500}; - // Introduced temporarily - private static final String ASHOAT_KEYSERVER_ID = "256"; + private static final String MMKV_KEYSERVER_PREFIX = "KEYSERVER."; private static final String MMKV_UNREAD_COUNT_SUFFIX = ".UNREAD_COUNT"; private Bitmap displayableNotificationLargeIcon; @@ -89,10 +88,11 @@ @Override public void onMessageReceived(RemoteMessage message) { - String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY); - if (senderKeyserverID == null) { - senderKeyserverID = ASHOAT_KEYSERVER_ID; + if (message.getData().get(KEYSERVER_ID_KEY) == null) { + throw new RuntimeException( + "Received encrypted notification without keyserver ID."); } + String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY); if (message.getData().get(ENCRYPTED_PAYLOAD_KEY) != null) { try { @@ -223,10 +223,10 @@ return; } - String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY); - if (senderKeyserverID == null) { - senderKeyserverID = ASHOAT_KEYSERVER_ID; + if (message.getData().get(KEYSERVER_ID_KEY) == null) { + throw new RuntimeException("Received badge update without keyserver ID."); } + String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY); String senderKeyserverUnreadCountKey = MMKV_KEYSERVER_PREFIX + senderKeyserverID + MMKV_UNREAD_COUNT_SUFFIX; diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm --- a/native/ios/NotificationService/NotificationService.mm +++ b/native/ios/NotificationService/NotificationService.mm @@ -22,8 +22,6 @@ CFStringRef newMessageInfosDarwinNotification = CFSTR("app.comm.darwin_new_message_infos"); -// Introduced temporarily -const std::string ashoatKeyserverID = "256"; // Implementation below was inspired by the // following discussion with Apple staff member: @@ -444,11 +442,11 @@ - (void)calculateTotalUnreadCountInPlace: (UNMutableNotificationContent *)content { - std::string senderKeyserverID = ashoatKeyserverID; - if (content.userInfo[keyserverIDKey]) { - senderKeyserverID = - std::string([content.userInfo[keyserverIDKey] UTF8String]); + if (!content.userInfo[keyserverIDKey]) { + throw std::runtime_error("Received badge update without keyserverID."); } + std::string senderKeyserverID = + std::string([content.userInfo[keyserverIDKey] UTF8String]); static const std::string keyserverPrefix = "KEYSERVER."; static const std::string unreadCountSuffix = ".UNREAD_COUNT"; @@ -534,11 +532,15 @@ decryptContentInPlace:(UNMutableNotificationContent *)content { std::string encryptedData = std::string([content.userInfo[encryptedPayloadKey] UTF8String]); - std::string senderKeyserverID = ashoatKeyserverID; - if (content.userInfo[keyserverIDKey]) { - senderKeyserverID = - std::string([content.userInfo[keyserverIDKey] UTF8String]); + + if (!content.userInfo[keyserverIDKey]) { + throw std::runtime_error( + "Received encrypted notification without keyserverID."); } + + std::string senderKeyserverID = + std::string([content.userInfo[keyserverIDKey] UTF8String]); + auto decryptResult = comm::NotificationsCryptoModule::statefulDecrypt( senderKeyserverID, encryptedData,