diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js index f59eec852..d2bc7edb5 100644 --- a/keyserver/src/push/crypto.js +++ b/keyserver/src/push/crypto.js @@ -1,318 +1,312 @@ // @flow import apn from '@parse/node-apn'; import invariant from 'invariant'; import _cloneDeep from 'lodash/fp/cloneDeep.js'; -import { NEXT_CODE_VERSION } from 'lib/shared/version-utils.js'; - import type { AndroidNotification, AndroidNotificationPayload, AndroidNotificationRescind, NotificationTargetDevice, } from './types.js'; import { encryptAndUpdateOlmSession } from '../updaters/olm-session-updater.js'; async function encryptIOSNotification( cookieID: string, notification: apn.Notification, codeVersion?: ?number, notificationSizeValidator?: apn.Notification => boolean, ): Promise<{ +notification: apn.Notification, +payloadSizeExceeded: boolean }> { invariant( !notification.collapseId, 'Collapsible notifications encryption currently not implemented', ); const encryptedNotification = new apn.Notification(); encryptedNotification.id = notification.id; encryptedNotification.payload.id = notification.id; encryptedNotification.topic = notification.topic; encryptedNotification.sound = notification.aps.sound; encryptedNotification.pushType = 'alert'; encryptedNotification.mutableContent = true; const { id, ...payloadSansId } = notification.payload; const unencryptedPayload = { ...payloadSansId, badge: notification.aps.badge.toString(), merged: notification.body, }; try { const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload); let dbPersistCondition; if (notificationSizeValidator) { dbPersistCondition = ({ serializedPayload }) => { const notifCopy = _cloneDeep(encryptedNotification); notifCopy.payload.encryptedPayload = serializedPayload.body; return notificationSizeValidator(notifCopy); }; } const { encryptedMessages: { serializedPayload }, dbPersistConditionViolated, } = await encryptAndUpdateOlmSession( cookieID, 'notifications', { serializedPayload: unencryptedSerializedPayload, }, dbPersistCondition, ); encryptedNotification.payload.encryptedPayload = serializedPayload.body; - if ( - codeVersion && - codeVersion >= NEXT_CODE_VERSION && - codeVersion % 2 === 0 - ) { + if (codeVersion && codeVersion >= 254 && codeVersion % 2 === 0) { encryptedNotification.aps = { alert: { body: 'ENCRYPTED' }, ...encryptedNotification.aps, }; } return { notification: encryptedNotification, payloadSizeExceeded: !!dbPersistConditionViolated, }; } catch (e) { console.log('Notification encryption failed: ' + e); encryptedNotification.body = notification.body; encryptedNotification.threadId = notification.payload.threadID; invariant( typeof notification.aps.badge === 'number', 'Unencrypted notification must have badge as a number', ); encryptedNotification.badge = notification.aps.badge; encryptedNotification.payload = { ...encryptedNotification.payload, ...notification.payload, encryptionFailed: 1, }; return { notification: encryptedNotification, payloadSizeExceeded: notificationSizeValidator ? notificationSizeValidator(_cloneDeep(encryptedNotification)) : false, }; } } async function encryptAndroidNotificationPayload( cookieID: string, unencryptedPayload: T, payloadSizeValidator?: (T | { +encryptedPayload: string }) => boolean, ): Promise<{ +resultPayload: T | { +encryptedPayload: string }, +payloadSizeExceeded: boolean, }> { try { const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload); if (!unencryptedSerializedPayload) { return { resultPayload: unencryptedPayload, payloadSizeExceeded: payloadSizeValidator ? payloadSizeValidator(unencryptedPayload) : false, }; } let dbPersistCondition; if (payloadSizeValidator) { dbPersistCondition = ({ serializedPayload }) => payloadSizeValidator({ encryptedPayload: serializedPayload.body }); } const { encryptedMessages: { serializedPayload }, dbPersistConditionViolated, } = await encryptAndUpdateOlmSession( cookieID, 'notifications', { serializedPayload: unencryptedSerializedPayload, }, dbPersistCondition, ); return { resultPayload: { encryptedPayload: serializedPayload.body }, payloadSizeExceeded: !!dbPersistConditionViolated, }; } catch (e) { console.log('Notification encryption failed: ' + e); const resultPayload = { encryptionFailed: '1', ...unencryptedPayload, }; return { resultPayload, payloadSizeExceeded: payloadSizeValidator ? payloadSizeValidator(resultPayload) : false, }; } } async function encryptAndroidNotification( cookieID: string, notification: AndroidNotification, notificationSizeValidator?: AndroidNotification => boolean, ): Promise<{ +notification: AndroidNotification, +payloadSizeExceeded: boolean, }> { const { id, badgeOnly, ...unencryptedPayload } = notification.data; let payloadSizeValidator; if (notificationSizeValidator) { payloadSizeValidator = ( payload: AndroidNotificationPayload | { +encryptedPayload: string }, ) => { return notificationSizeValidator({ data: { id, badgeOnly, ...payload } }); }; } const { resultPayload, payloadSizeExceeded } = await encryptAndroidNotificationPayload( cookieID, unencryptedPayload, payloadSizeValidator, ); return { notification: { data: { id, badgeOnly, ...resultPayload, }, }, payloadSizeExceeded, }; } async function encryptAndroidNotificationRescind( cookieID: string, notification: AndroidNotificationRescind, ): Promise { // We don't validate payload size for rescind // since they are expected to be small and // never exceed any FCM limit const { resultPayload } = await encryptAndroidNotificationPayload( cookieID, notification.data, ); return { data: resultPayload, }; } function prepareEncryptedIOSNotifications( devices: $ReadOnlyArray, notification: apn.Notification, codeVersion?: ?number, notificationSizeValidator?: apn.Notification => boolean, ): Promise< $ReadOnlyArray<{ +cookieID: string, +deviceToken: string, +notification: apn.Notification, +payloadSizeExceeded: boolean, }>, > { const notificationPromises = devices.map( async ({ cookieID, deviceToken }) => { const notif = await encryptIOSNotification( cookieID, notification, codeVersion, notificationSizeValidator, ); return { cookieID, deviceToken, ...notif }; }, ); return Promise.all(notificationPromises); } function prepareEncryptedIOSNotificationRescind( devices: $ReadOnlyArray, notification: apn.Notification, codeVersion?: ?number, ): Promise< $ReadOnlyArray<{ +cookieID: string, +deviceToken: string, +notification: apn.Notification, }>, > { const notificationPromises = devices.map( async ({ deviceToken, cookieID }) => { const { notification: notif } = await encryptIOSNotification( cookieID, notification, codeVersion, ); return { deviceToken, cookieID, notification: notif }; }, ); return Promise.all(notificationPromises); } function prepareEncryptedAndroidNotifications( devices: $ReadOnlyArray, notification: AndroidNotification, notificationSizeValidator?: (notification: AndroidNotification) => boolean, ): Promise< $ReadOnlyArray<{ +cookieID: string, +deviceToken: string, +notification: AndroidNotification, +payloadSizeExceeded: boolean, }>, > { const notificationPromises = devices.map( async ({ deviceToken, cookieID }) => { const notif = await encryptAndroidNotification( cookieID, notification, notificationSizeValidator, ); return { deviceToken, cookieID, ...notif }; }, ); return Promise.all(notificationPromises); } function prepareEncryptedAndroidNotificationRescinds( devices: $ReadOnlyArray, notification: AndroidNotificationRescind, ): Promise< $ReadOnlyArray<{ +cookieID: string, +deviceToken: string, +notification: AndroidNotificationRescind, }>, > { const notificationPromises = devices.map( async ({ deviceToken, cookieID }) => { const notif = await encryptAndroidNotificationRescind( cookieID, notification, ); return { deviceToken, cookieID, notification: notif }; }, ); return Promise.all(notificationPromises); } export { prepareEncryptedIOSNotifications, prepareEncryptedIOSNotificationRescind, prepareEncryptedAndroidNotifications, prepareEncryptedAndroidNotificationRescinds, }; diff --git a/lib/shared/messages/change-role-message-spec.js b/lib/shared/messages/change-role-message-spec.js index 3fd25db65..76cc799af 100644 --- a/lib/shared/messages/change-role-message-spec.js +++ b/lib/shared/messages/change-role-message-spec.js @@ -1,232 +1,232 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, type RobotextParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import type { PlatformDetails } from '../../types/device-types.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type ChangeRoleMessageData, type ChangeRoleMessageInfo, type RawChangeRoleMessageInfo, rawChangeRoleMessageInfoValidator, } from '../../types/messages/change-role.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { entityTextToRawString } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; import { constructChangeRoleEntityText } from '../message-utils.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; -import { NEXT_CODE_VERSION, hasMinCodeVersion } from '../version-utils.js'; +import { hasMinCodeVersion } from '../version-utils.js'; export const changeRoleMessageSpec: MessageSpec< ChangeRoleMessageData, RawChangeRoleMessageInfo, ChangeRoleMessageInfo, > = Object.freeze({ messageContentForServerDB( data: ChangeRoleMessageData | RawChangeRoleMessageInfo, ): string { return JSON.stringify({ userIDs: data.userIDs, newRole: data.newRole, roleName: data.roleName, }); }, messageContentForClientDB(data: RawChangeRoleMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawChangeRoleMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.CHANGE_ROLE, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), userIDs: content.userIDs, newRole: content.newRole, roleName: content.roleName, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawChangeRoleMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for ChangeRole', ); const content = JSON.parse(clientDBMessageInfo.content); const rawChangeRoleMessageInfo: RawChangeRoleMessageInfo = { type: messageTypes.CHANGE_ROLE, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, userIDs: content.userIDs, newRole: content.newRole, roleName: content.roleName, }; return rawChangeRoleMessageInfo; }, createMessageInfo( rawMessageInfo: RawChangeRoleMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ChangeRoleMessageInfo { const members = params.createRelativeUserInfos(rawMessageInfo.userIDs); return { type: messageTypes.CHANGE_ROLE, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, members, newRole: rawMessageInfo.newRole, roleName: rawMessageInfo.roleName, }; }, rawMessageInfoFromMessageData( messageData: ChangeRoleMessageData, id: ?string, ): RawChangeRoleMessageInfo { invariant(id, 'RawChangeRoleMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: ChangeRoleMessageInfo, params: RobotextParams, ): EntityText { const users = messageInfo.members; invariant(users.length !== 0, 'changed whose role??'); const creator = ET.user({ userInfo: messageInfo.creator }); const affectedUsers = pluralizeEntityText( users.map(user => ET`${ET.user({ userInfo: user })}`), ); const { threadInfo } = params; invariant(threadInfo, 'ThreadInfo should be set for CHANGE_ROLE message'); const threadRoleName = threadInfo.roles[messageInfo.newRole]?.name; const messageInfoRoleName = messageInfo.roleName; const roleName = threadRoleName ?? messageInfoRoleName; const constructedEntityText = constructChangeRoleEntityText( affectedUsers, roleName, ); return ET`${creator} ${constructedEntityText}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const membersObject = {}; for (const messageInfo of messageInfos) { invariant( messageInfo.type === messageTypes.CHANGE_ROLE, 'messageInfo should be messageTypes.CHANGE_ROLE!', ); for (const member of messageInfo.members) { membersObject[member.id] = member; } } const members = values(membersObject); const mostRecentMessageInfo = messageInfos[0]; invariant( mostRecentMessageInfo.type === messageTypes.CHANGE_ROLE, 'messageInfo should be messageTypes.CHANGE_ROLE!', ); const mergedMessageInfo = { ...mostRecentMessageInfo, members }; const robotext = notifRobotextForMessageInfo(mergedMessageInfo, threadInfo); const merged = ET`${robotext} of ${ET.thread({ display: 'shortName', threadInfo, })}`; return { merged, title: threadInfo.uiName, body: robotext, }; }, shimUnsupportedMessageInfo( rawMessageInfo: RawChangeRoleMessageInfo, platformDetails: ?PlatformDetails, ): RawChangeRoleMessageInfo | RawUnsupportedMessageInfo { - if (hasMinCodeVersion(platformDetails, { native: NEXT_CODE_VERSION })) { + if (hasMinCodeVersion(platformDetails, { native: 251 })) { return rawMessageInfo; } const { id, userIDs } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); const affectedUsers = userIDs.length === 1 ? 'a member' : 'some members'; const roleName = rawMessageInfo.roleName; const constructedEntityText = constructChangeRoleEntityText( affectedUsers, roleName, ); const stringifiedEntityText = entityTextToRawString(constructedEntityText); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: stringifiedEntityText, unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo( unwrapped: RawChangeRoleMessageInfo, ): RawChangeRoleMessageInfo { return unwrapped; }, notificationCollapseKey(rawMessageInfo: RawChangeRoleMessageInfo): string { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, rawMessageInfo.newRole, ); }, generatesNotifs: async () => pushTypes.NOTIF, validator: rawChangeRoleMessageInfoValidator, });