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,83 @@ +// @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 maxOlmSessionUpdateRetriesCount = 5; +const olmSessionUpdateRetryDelay = 100; + +async function encryptAndUpdateOlmSession( + cookieID: string, + olmSessionType: 'content' | 'notifications', + messagesToEncrypt: $ReadOnlyArray, +): Promise<$ReadOnlyArray> { + const isContent = olmSessionType === 'content'; + const { picklingKey } = await fetchOlmAccount(olmSessionType); + + let retriesLeft = maxOlmSessionUpdateRetriesCount; + + while (retriesLeft > 0) { + 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 message of messagesToEncrypt) { + encryptedMessages.push(session.encrypt(message)); + } + + 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; + } + + retriesLeft = retriesLeft - 1; + await sleep(olmSessionUpdateRetryDelay); + } + + throw new ServerError('max_olm_account_update_retry_exceeded'); +} + +export { encryptAndUpdateOlmSession };