diff --git a/keyserver/src/utils/olm-utils.js b/keyserver/src/utils/olm-utils.js index 58443966f..ccf1b3a93 100644 --- a/keyserver/src/utils/olm-utils.js +++ b/keyserver/src/utils/olm-utils.js @@ -1,175 +1,198 @@ // @flow import olm from '@commapp/olm'; import type { Account as OlmAccount, Utility as OlmUtility, Session as OlmSession, } from '@commapp/olm'; import { getRustAPI } from 'rust-node-addon'; import uuid from 'uuid'; import { getOneTimeKeyValuesFromBlob } from 'lib/shared/crypto-utils.js'; import { olmEncryptedMessageTypes } from 'lib/types/crypto-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { fetchCallUpdateOlmAccount, fetchOlmAccount, } from '../updaters/olm-account-updater.js'; import { fetchIdentityInfo } from '../user/identity.js'; type PickledOlmAccount = { +picklingKey: string, +pickledAccount: string, }; const maxPublishedPrekeyAge = 30 * 24 * 60 * 60 * 1000; const maxOldPrekeyAge = 24 * 60 * 60 * 1000; +function getLastPrekeyPublishTime(account: OlmAccount): Date { + const olmLastPrekeyPublishTime = account.last_prekey_publish_time(); + + // Olm uses seconds, while in Node we need milliseconds. + return new Date(olmLastPrekeyPublishTime * 1000); +} + +function shouldRotatePrekey(account: OlmAccount): boolean { + // Our fork of Olm only remembers two prekeys at a time. + // If the new one hasn't been published, then the old one is still active. + // In that scenario, we need to avoid rotating the prekey because it will + // result in the old active prekey being discarded. + if (account.unpublished_prekey()) { + return false; + } + + const currentDate = new Date(); + const lastPrekeyPublishDate = getLastPrekeyPublishTime(account); + + return currentDate - lastPrekeyPublishDate >= maxPublishedPrekeyAge; +} + +function shouldForgetPrekey(account: OlmAccount): boolean { + // Our fork of Olm only remembers two prekeys at a time. + // We have to hold onto the old one until the new one is published. + if (account.unpublished_prekey()) { + return false; + } + + const currentDate = new Date(); + const lastPrekeyPublishDate = getLastPrekeyPublishTime(account); + + return currentDate - lastPrekeyPublishDate >= maxOldPrekeyAge; +} + async function createPickledOlmAccount(): Promise { await olm.init(); const account = new olm.Account(); account.create(); const picklingKey = uuid.v4(); const pickledAccount = account.pickle(picklingKey); return { picklingKey: picklingKey, pickledAccount: pickledAccount, }; } async function unpickleOlmAccount( pickledOlmAccount: PickledOlmAccount, ): Promise { await olm.init(); const account = new olm.Account(); account.unpickle( pickledOlmAccount.picklingKey, pickledOlmAccount.pickledAccount, ); return account; } async function createPickledOlmSession( account: OlmAccount, accountPicklingKey: string, initialEncryptedMessage: string, theirCurve25519Key?: string, ): Promise { await olm.init(); const session = new olm.Session(); if (theirCurve25519Key) { session.create_inbound_from( account, theirCurve25519Key, initialEncryptedMessage, ); } else { session.create_inbound(account, initialEncryptedMessage); } account.remove_one_time_keys(session); session.decrypt(olmEncryptedMessageTypes.PREKEY, initialEncryptedMessage); return session.pickle(accountPicklingKey); } async function unpickleOlmSession( pickledSession: string, picklingKey: string, ): Promise { await olm.init(); const session = new olm.Session(); session.unpickle(picklingKey, pickledSession); return session; } let cachedOLMUtility: OlmUtility; + function getOlmUtility(): OlmUtility { if (cachedOLMUtility) { return cachedOLMUtility; } cachedOLMUtility = new olm.Utility(); return cachedOLMUtility; } function validateAccountPrekey(account: OlmAccount) { - const currentDate = new Date(); - const lastPrekeyPublishDate = new Date(account.last_prekey_publish_time()); - - const prekeyPublished = !account.unpublished_prekey(); - if ( - prekeyPublished && - currentDate - lastPrekeyPublishDate > maxPublishedPrekeyAge - ) { - // If there is no prekey or the current prekey is older than month - // we need to generate new one. + if (shouldRotatePrekey(account)) { account.generate_prekey(); } - - if ( - prekeyPublished && - currentDate - lastPrekeyPublishDate >= maxOldPrekeyAge - ) { + if (shouldForgetPrekey(account)) { account.forget_old_prekey(); } } async function uploadNewOneTimeKeys(numberOfKeys: number) { const [rustAPI, identityInfo, deviceID] = await Promise.all([ getRustAPI(), fetchIdentityInfo(), getContentSigningKey(), ]); if (!identityInfo) { throw new ServerError('missing_identity_info'); } await fetchCallUpdateOlmAccount('content', (contentAccount: OlmAccount) => { contentAccount.generate_one_time_keys(numberOfKeys); const contentOneTimeKeys = getOneTimeKeyValuesFromBlob( contentAccount.one_time_keys(), ); return fetchCallUpdateOlmAccount( 'notifications', async (notifAccount: OlmAccount) => { notifAccount.generate_one_time_keys(numberOfKeys); const notifOneTimeKeys = getOneTimeKeyValuesFromBlob( notifAccount.one_time_keys(), ); await rustAPI.uploadOneTimeKeys( identityInfo.userId, deviceID, identityInfo.accessToken, contentOneTimeKeys, notifOneTimeKeys, ); notifAccount.mark_keys_as_published(); contentAccount.mark_keys_as_published(); }, ); }); } async function getContentSigningKey(): Promise { const accountInfo = await fetchOlmAccount('content'); return JSON.parse(accountInfo.account.identity_keys()).ed25519; } export { createPickledOlmAccount, createPickledOlmSession, getOlmUtility, unpickleOlmAccount, unpickleOlmSession, validateAccountPrekey, uploadNewOneTimeKeys, getContentSigningKey, };