diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js index 567a087f7..0bfdf0add 100644 --- a/keyserver/src/push/crypto.js +++ b/keyserver/src/push/crypto.js @@ -1,163 +1,159 @@ // @flow import apn from '@parse/node-apn'; import invariant from 'invariant'; import type { AndroidNotification, AndroidNotificationRescind, } from './types.js'; import { encryptAndUpdateOlmSession } from '../updaters/olm-session-updater.js'; async function encryptIOSNotification( cookieID: string, notification: apn.Notification, ): Promise { 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, }; let encryptedSerializedPayload; try { const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload); - const { serializedPayload } = await encryptAndUpdateOlmSession( - cookieID, - 'notifications', - { - serializedPayload: unencryptedSerializedPayload, - }, - ); + const { + encryptedMessages: { serializedPayload }, + } = await encryptAndUpdateOlmSession(cookieID, 'notifications', { + serializedPayload: unencryptedSerializedPayload, + }); encryptedSerializedPayload = serializedPayload; } 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 encryptedNotification; } encryptedNotification.payload.encryptedPayload = encryptedSerializedPayload.body; return encryptedNotification; } async function encryptAndroidNotificationPayload( cookieID: string, unencryptedPayload: T, ): Promise { try { const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload); if (!unencryptedSerializedPayload) { return unencryptedPayload; } - const { serializedPayload } = await encryptAndUpdateOlmSession( - cookieID, - 'notifications', - { - serializedPayload: unencryptedSerializedPayload, - }, - ); + const { + encryptedMessages: { serializedPayload }, + } = await encryptAndUpdateOlmSession(cookieID, 'notifications', { + serializedPayload: unencryptedSerializedPayload, + }); return { encryptedPayload: serializedPayload.body }; } catch (e) { console.log('Notification encryption failed: ' + e); return { encryptionFailed: '1', ...unencryptedPayload, }; } } async function encryptAndroidNotification( cookieID: string, notification: AndroidNotification, ): Promise { const { id, badgeOnly, ...unencryptedPayload } = notification.data; const encryptedSerializedPayload = await encryptAndroidNotificationPayload( cookieID, unencryptedPayload, ); return { data: { id, badgeOnly, ...encryptedSerializedPayload, }, }; } async function encryptAndroidNotificationRescind( cookieID: string, notification: AndroidNotificationRescind, ): Promise { const encryptedPayload = await encryptAndroidNotificationPayload( cookieID, notification.data, ); return { data: encryptedPayload, }; } function prepareEncryptedIOSNotifications( cookieIDs: $ReadOnlyArray, notification: apn.Notification, ): Promise<$ReadOnlyArray> { const notificationPromises = cookieIDs.map(cookieID => encryptIOSNotification(cookieID, notification), ); return Promise.all(notificationPromises); } function prepareEncryptedAndroidNotifications( cookieIDs: $ReadOnlyArray, notification: AndroidNotification, ): Promise<$ReadOnlyArray> { const notificationPromises = cookieIDs.map(cookieID => encryptAndroidNotification(cookieID, notification), ); return Promise.all(notificationPromises); } function prepareEncryptedAndroidNotificationRescinds( cookieIDs: $ReadOnlyArray, notification: AndroidNotificationRescind, ): Promise<$ReadOnlyArray> { const notificationPromises = cookieIDs.map(cookieID => encryptAndroidNotificationRescind(cookieID, notification), ); return Promise.all(notificationPromises); } export { prepareEncryptedIOSNotifications, prepareEncryptedAndroidNotifications, prepareEncryptedAndroidNotificationRescinds, }; diff --git a/keyserver/src/updaters/olm-session-updater.js b/keyserver/src/updaters/olm-session-updater.js index 6f7fbbfae..218b8a986 100644 --- a/keyserver/src/updaters/olm-session-updater.js +++ b/keyserver/src/updaters/olm-session-updater.js @@ -1,88 +1,98 @@ // @flow import type { EncryptResult } from '@commapp/olm'; import { ServerError } from 'lib/utils/errors.js'; import sleep from 'lib/utils/sleep.js'; import { fetchOlmAccount } from './olm-account-updater.js'; import { SQL, dbQuery } from '../database/database.js'; import { unpickleOlmSession } from '../utils/olm-utils.js'; const maxOlmSessionUpdateAttemptTime = 30000; const olmSessionUpdateRetryDelay = 50; +type OlmEncryptionResult = { + +encryptedMessages: { +[string]: EncryptResult }, + +dbPersistConditionViolated?: boolean, +}; + async function encryptAndUpdateOlmSession( cookieID: string, olmSessionType: 'content' | 'notifications', messagesToEncrypt: $ReadOnly<{ [string]: string }>, -): Promise<{ [string]: EncryptResult }> { + dbPersistCondition?: ({ +[string]: EncryptResult }) => boolean, +): Promise { const isContent = olmSessionType === 'content'; const { picklingKey } = await fetchOlmAccount(olmSessionType); const olmUpdateAttemptStartTime = Date.now(); while ( Date.now() - olmUpdateAttemptStartTime < maxOlmSessionUpdateAttemptTime ) { const [olmSessionResult] = await dbQuery( SQL` SELECT version, pickled_olm_session FROM olm_sessions WHERE cookie_id = ${cookieID} AND is_content = ${isContent} `, ); if (olmSessionResult.length === 0) { throw new ServerError('missing_olm_session'); } const [{ version, pickled_olm_session: pickledSession }] = olmSessionResult; const session = await unpickleOlmSession(pickledSession, picklingKey); const encryptedMessages = {}; for (const messageName in messagesToEncrypt) { encryptedMessages[messageName] = session.encrypt( messagesToEncrypt[messageName], ); } + if (dbPersistCondition && !dbPersistCondition(encryptedMessages)) { + return { encryptedMessages, dbPersistConditionViolated: true }; + } + const updatedSession = session.pickle(picklingKey); const [transactionResult] = await dbQuery( SQL` START TRANSACTION; SELECT version INTO @currentVersion FROM olm_sessions WHERE cookie_id = ${cookieID} AND is_content = ${isContent} FOR UPDATE; UPDATE olm_sessions SET pickled_olm_session = ${updatedSession}, version = ${version} + 1 WHERE version = ${version} AND is_content = ${isContent} AND cookie_id = ${cookieID}; COMMIT; SELECT @currentVersion AS versionOnUpdateAttempt; `, { multipleStatements: true }, ); const selectResult = transactionResult.pop(); const [{ versionOnUpdateAttempt }] = selectResult; if (version === versionOnUpdateAttempt) { - return encryptedMessages; + return { encryptedMessages }; } await sleep(olmSessionUpdateRetryDelay); } throw new ServerError('max_olm_account_update_retry_exceeded'); } export { encryptAndUpdateOlmSession };