diff --git a/keyserver/src/updaters/olm-session-updater.js b/keyserver/src/updaters/olm-session-updater.js new file mode 100644 --- /dev/null +++ b/keyserver/src/updaters/olm-session-updater.js @@ -0,0 +1,88 @@ +// @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; + +async function encryptAndUpdateOlmSession( + cookieID: string, + olmSessionType: 'content' | 'notifications', + messagesToEncrypt: $ReadOnly<{ [string]: string }>, +): Promise<{ [string]: EncryptResult }> { + 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], + ); + } + + 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; + } + + await sleep(olmSessionUpdateRetryDelay); + } + + throw new ServerError('max_olm_account_update_retry_exceeded'); +} + +export { encryptAndUpdateOlmSession };