diff --git a/web/push-notif/notif-crypto-utils.js b/web/push-notif/notif-crypto-utils.js --- a/web/push-notif/notif-crypto-utils.js +++ b/web/push-notif/notif-crypto-utils.js @@ -4,10 +4,12 @@ import type { EncryptResult } from '@commapp/olm'; import invariant from 'invariant'; import localforage from 'localforage'; +import uuid from 'uuid'; import { olmEncryptedMessageTypes, type NotificationsOlmDataType, + type PickledOLMAccount, } from 'lib/types/crypto-types.js'; import type { PlainTextWebNotification, @@ -15,12 +17,15 @@ } from 'lib/types/notif-types.js'; import { getCookieIDFromCookie } from 'lib/utils/cookie-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; +import { promiseAll } from 'lib/utils/promises.js'; import { type EncryptedData, decryptData, encryptData, importJWKKey, + exportKeyToJWK, + generateCryptoKey, } from '../crypto/aes-gcm-crypto-utils.js'; import { initOlm } from '../olm/olm-utils.js'; import { @@ -40,6 +45,13 @@ +staffCanSee: boolean, }; +export type NotificationAccountWithPicklingKey = { + +notificationAccount: olm.Account, + +picklingKey: string, + +synchronizationValue: ?string, + +accountEncryptionKey?: CryptoKey, +}; + type DecryptionResult = { +newPendingSessionUpdate: string, +newUpdateCreationTimestamp: number, @@ -52,6 +64,7 @@ const INDEXED_DB_KEYSERVER_PREFIX = 'keyserver'; const INDEXED_DB_KEY_SEPARATOR = ':'; const INDEXED_DB_DEVICE_PREFIX = 'device'; +const INDEXED_DB_NOTIFS_SYNC_KEY = 'notifsSyncKey'; // This constant is only used to migrate the existing notifications // session with production keyserver to new IndexedDB key format. This @@ -63,6 +76,156 @@ '256'; const INDEXED_DB_UNREAD_COUNT_SUFFIX = 'unreadCount'; +const INDEXED_DB_NOTIFS_ACCOUNT_KEY = 'notificationAccount'; +const INDEXED_DB_NOTIFS_ACCOUNT_ENCRYPTION_KEY_DB_LABEL = + 'notificationAccountEncryptionKey'; + +async function deserializeEncryptedData( + encryptedData: ?EncryptedData, + encryptionKey: ?CryptoKey, +): Promise { + if (!encryptedData || !encryptionKey) { + return undefined; + } + const serializedData = await decryptData(encryptedData, encryptionKey); + const data: T = JSON.parse(new TextDecoder().decode(serializedData)); + return data; +} + +async function serializeUnencryptedData( + data: T, + encryptionKey: CryptoKey, +): Promise { + const dataAsString = JSON.stringify(data); + invariant( + dataAsString, + 'Attempt to serialize null or undefined is forbidden', + ); + return await encryptData( + new TextEncoder().encode(dataAsString), + encryptionKey, + ); +} + +async function validateCryptoKey( + cryptoKey: ?CryptoKey | ?SubtleCrypto$JsonWebKey, +): Promise { + if (!cryptoKey) { + return cryptoKey; + } + + if (!isDesktopSafari) { + return ((cryptoKey: any): CryptoKey); + } + return await importJWKKey(((cryptoKey: any): SubtleCrypto$JsonWebKey)); +} + +async function getCryptoKeyPersistentForm( + cryptoKey: CryptoKey, +): Promise { + if (!isDesktopSafari) { + return cryptoKey; + } + + // Safari doesn't support structured clone algorithm in service + // worker context so we have to store CryptoKey as JSON + return await exportKeyToJWK(cryptoKey); +} + +async function persistNotifsAccountWithOlmData(input: { + olmDataKey?: string, + olmEncryptionKeyDBLabel?: string, + olmData?: ?NotificationsOlmDataType, + encryptionKey?: ?CryptoKey, + accountEncryptionKey?: ?CryptoKey, + accountWithPicklingKey?: PickledOLMAccount, + synchronizationValue: ?string, + forceWrite: boolean, +}): Promise { + const { + olmData, + olmDataKey, + accountEncryptionKey, + accountWithPicklingKey, + encryptionKey, + synchronizationValue, + olmEncryptionKeyDBLabel, + forceWrite, + } = input; + + const shouldPersistOlmData = olmDataKey && olmData && encryptionKey; + const shouldPersistAccount = accountWithPicklingKey && accountEncryptionKey; + + if (!shouldPersistOlmData && !shouldPersistAccount) { + return; + } + + const serializationPromises: { + [string]: Promise, + } = {}; + + if (!olmData && !accountWithPicklingKey) { + return; + } + + if (olmDataKey && olmData && encryptionKey) { + serializationPromises[olmDataKey] = + serializeUnencryptedData( + olmData, + encryptionKey, + ); + } else if (olmData && olmDataKey && olmEncryptionKeyDBLabel) { + const newEncryptionKey = await generateCryptoKey({ + extractable: isDesktopSafari, + }); + + serializationPromises[olmDataKey] = + serializeUnencryptedData( + olmData, + newEncryptionKey, + ); + + serializationPromises[olmEncryptionKeyDBLabel] = + getCryptoKeyPersistentForm(newEncryptionKey); + } + + if (accountWithPicklingKey && accountEncryptionKey) { + serializationPromises[INDEXED_DB_NOTIFS_ACCOUNT_KEY] = + serializeUnencryptedData( + accountWithPicklingKey, + accountEncryptionKey, + ); + } else if (accountWithPicklingKey) { + const newEncryptionKey = await generateCryptoKey({ + extractable: isDesktopSafari, + }); + + serializationPromises[INDEXED_DB_NOTIFS_ACCOUNT_KEY] = + serializeUnencryptedData( + accountWithPicklingKey, + newEncryptionKey, + ); + + serializationPromises[INDEXED_DB_NOTIFS_ACCOUNT_ENCRYPTION_KEY_DB_LABEL] = + getCryptoKeyPersistentForm(newEncryptionKey); + } + + const setMultipleItemsInput = await promiseAll(serializationPromises); + const newSynchronizationValue = uuid.v4(); + + try { + await localforage.setMultipleItems( + setMultipleItemsInput, + INDEXED_DB_NOTIFS_SYNC_KEY, + synchronizationValue, + newSynchronizationValue, + forceWrite, + ); + } catch (e) { + // likely worker crypt persisted its own data + console.log(e); + } +} async function decryptWebNotification( encryptedNotification: EncryptedWebNotification, @@ -319,13 +482,19 @@ const olmEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForDeviceID(deviceID); - let encryptedOlmData, encryptionKey; + let encryptedOlmData, encryptionKey, synchronizationValue; try { - [encryptedOlmData, encryptionKey] = await Promise.all([ - localforage.getItem(olmDataKey), - retrieveEncryptionKey(olmEncryptionKeyDBLabel), - initOlm(), - ]); + const { + [olmDataKey]: fetchedEncryptedOlmData, + [olmEncryptionKeyDBLabel]: fetchedEncryptionKey, + synchronizationValue: fetchedSynchronizationValue, + } = await localforage.getMultipleItems<{ + +[string]: ?EncryptedData | ?CryptoKey | ?SubtleCrypto$JsonWebKey, + }>([olmDataKey, olmEncryptionKeyDBLabel], INDEXED_DB_NOTIFS_SYNC_KEY); + + encryptedOlmData = fetchedEncryptedOlmData; + encryptionKey = fetchedEncryptionKey; + synchronizationValue = fetchedSynchronizationValue; } catch (e) { throw new Error( `Failed to fetch olm session from IndexedDB for device: ${deviceID}. Details: ${ @@ -338,13 +507,28 @@ throw new Error(`Session with device: ${deviceID} not initialized.`); } + // type refinement + if (!encryptedOlmData.ciphertext) { + throw new Error('Invalid encrypted olm data format'); + } + + if (encryptionKey.ciphertext) { + throw new Error('Invalid encryption key format'); + } + + const validatedEncryptionKey = await validateCryptoKey(encryptionKey); + if (!validatedEncryptionKey) { + throw new Error(`Session with device: ${deviceID} not initialized.`); + } + let encryptedNotification; try { encryptedNotification = await encryptNotificationWithOlmSession( payload, encryptedOlmData, olmDataKey, - encryptionKey, + validatedEncryptionKey, + synchronizationValue, ); } catch (e) { throw new Error( @@ -361,6 +545,7 @@ encryptedOlmData: EncryptedData, olmDataKey: string, encryptionKey: CryptoKey, + synchronizationValue: ?string, ): Promise { const serializedOlmData = await decryptData(encryptedOlmData, encryptionKey); const { @@ -388,10 +573,85 @@ encryptionKey, ); - await localforage.setItem(olmDataKey, updatedEncryptedSession); + const newSynchronizationValue = uuid.v4(); + await localforage.setMultipleItems( + { [olmDataKey]: updatedEncryptedSession }, + INDEXED_DB_NOTIFS_SYNC_KEY, + synchronizationValue, + newSynchronizationValue, + // This method (encryptNotification) is expected to be called + // exclusively from shared worker-crypto which must always win race + // condition against push notifications service-worker + true, + ); + return encryptedNotification; } +// notifications account manipulation + +async function isNotifsCryptoAccountInitialized(): Promise { + const { + notificationAccount: encryptedNotifsAccount, + notificationAccountEncryptionKey: notifsAccountEncryptionKey, + } = await localforage.getMultipleItems<{ + +notificationAccount: ?EncryptedData, + +notificationAccountEncryptionKey: ?CryptoKey, + }>( + [ + INDEXED_DB_NOTIFS_ACCOUNT_KEY, + INDEXED_DB_NOTIFS_ACCOUNT_ENCRYPTION_KEY_DB_LABEL, + ], + INDEXED_DB_NOTIFS_SYNC_KEY, + ); + return !!encryptedNotifsAccount && !!notifsAccountEncryptionKey; +} + +async function getNotifsCryptoAccount(): Promise { + const { + [INDEXED_DB_NOTIFS_ACCOUNT_KEY]: encryptedNotifsAccount, + [INDEXED_DB_NOTIFS_ACCOUNT_ENCRYPTION_KEY_DB_LABEL]: + notifsAccountEncryptionKey, + synchronizationValue, + } = await localforage.getMultipleItems<{ + +notificationAccount: ?EncryptedData, + +notificationAccountEncryptionKey: ?CryptoKey, + }>( + [ + INDEXED_DB_NOTIFS_ACCOUNT_KEY, + INDEXED_DB_NOTIFS_ACCOUNT_ENCRYPTION_KEY_DB_LABEL, + ], + INDEXED_DB_NOTIFS_SYNC_KEY, + ); + + const validatedNotifsAccountEncryptionKey = await validateCryptoKey( + notifsAccountEncryptionKey, + ); + + const pickledOLMAccount = await deserializeEncryptedData( + encryptedNotifsAccount, + validatedNotifsAccountEncryptionKey, + ); + + if (!pickledOLMAccount || !validatedNotifsAccountEncryptionKey) { + throw new Error( + 'Attempt to retrieve notifs olm account but account not created.', + ); + } + + const { pickledAccount, picklingKey } = pickledOLMAccount; + + const notificationAccount = new olm.Account(); + notificationAccount.unpickle(picklingKey, pickledAccount); + + return { + notificationAccount, + picklingKey, + synchronizationValue, + accountEncryptionKey: validatedNotifsAccountEncryptionKey, + }; +} + async function retrieveEncryptionKey( encryptionKeyDBLabel: string, ): Promise { @@ -408,6 +668,22 @@ return await importJWKKey(persistedCryptoKey); } +async function persistEncryptionKey( + encryptionKeyDBLabel: string, + encryptionKey: CryptoKey, +): Promise { + let cryptoKeyPersistentForm; + if (isDesktopSafari) { + // Safari doesn't support structured clone algorithm in service + // worker context so we have to store CryptoKey as JSON + cryptoKeyPersistentForm = await exportKeyToJWK(encryptionKey); + } else { + cryptoKeyPersistentForm = encryptionKey; + } + + await localforage.setItem(encryptionKeyDBLabel, cryptoKeyPersistentForm); +} + async function getNotifsOlmSessionDBKeys(keyserverID?: string): Promise<{ +olmDataKey: string, +encryptionKeyDBKey: string, @@ -643,4 +919,9 @@ migrateLegacyOlmNotificationsSessions, updateNotifsUnreadCountStorage, queryNotifsUnreadCountStorage, + isNotifsCryptoAccountInitialized, + getNotifsCryptoAccount, + persistEncryptionKey, + retrieveEncryptionKey, + persistNotifsAccountWithOlmData, }; diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -44,16 +44,15 @@ getSQLiteQueryExecutor, getPlatformDetails, } from './worker-database.js'; -import { - encryptData, - exportKeyToJWK, - generateCryptoKey, -} from '../../crypto/aes-gcm-crypto-utils.js'; import { getOlmDataKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, getOlmDataKeyForDeviceID, getOlmEncryptionKeyDBLabelForDeviceID, + isNotifsCryptoAccountInitialized, + type NotificationAccountWithPicklingKey, + getNotifsCryptoAccount, + persistNotifsAccountWithOlmData, } from '../../push-notif/notif-crypto-utils.js'; import { type WorkerRequestMessage, @@ -63,7 +62,6 @@ type LegacyCryptoStore, } from '../../types/worker-types.js'; import type { OlmPersistSession } from '../types/sqlite-query-executor.js'; -import { isDesktopSafari } from '../utils/db-utils.js'; type OlmSession = { +session: olm.Session, +version: number }; type OlmSessions = { @@ -74,8 +72,6 @@ +contentAccountPickleKey: string, +contentAccount: olm.Account, +contentSessions: OlmSessions, - +notificationAccountPickleKey: string, - +notificationAccount: olm.Account, }; let cryptoStore: ?WorkerCryptoStore = null; @@ -85,7 +81,10 @@ cryptoStore = null; } -function persistCryptoStore(withoutTransaction: boolean = false) { +async function persistCryptoStore( + notifsCryptoAccount?: NotificationAccountWithPicklingKey, + withoutTransaction: boolean = false, +) { const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { @@ -97,13 +96,8 @@ throw new Error("Couldn't persist crypto store because it doesn't exist"); } - const { - contentAccountPickleKey, - contentAccount, - contentSessions, - notificationAccountPickleKey, - notificationAccount, - } = cryptoStore; + const { contentAccountPickleKey, contentAccount, contentSessions } = + cryptoStore; const pickledContentAccount: PickledOLMAccount = { picklingKey: contentAccountPickleKey, @@ -118,11 +112,6 @@ version: sessionData.version, })); - const pickledNotificationAccount: PickledOLMAccount = { - picklingKey: notificationAccountPickleKey, - pickledAccount: notificationAccount.pickle(notificationAccountPickleKey), - }; - try { if (!withoutTransaction) { sqliteQueryExecutor.beginTransaction(); @@ -134,10 +123,27 @@ for (const pickledSession of pickledContentSessions) { sqliteQueryExecutor.storeOlmPersistSession(pickledSession); } - sqliteQueryExecutor.storeOlmPersistAccount( - sqliteQueryExecutor.getNotifsAccountID(), - JSON.stringify(pickledNotificationAccount), - ); + if (notifsCryptoAccount) { + const { + notificationAccount, + picklingKey, + synchronizationValue, + accountEncryptionKey, + } = notifsCryptoAccount; + + const pickledAccount = notificationAccount.pickle(picklingKey); + const accountWithPicklingKey: PickledOLMAccount = { + pickledAccount, + picklingKey, + }; + + await persistNotifsAccountWithOlmData({ + accountEncryptionKey, + accountWithPicklingKey, + synchronizationValue, + forceWrite: true, + }); + } if (!withoutTransaction) { sqliteQueryExecutor.commitTransaction(); } @@ -159,16 +165,13 @@ throw new Error('Crypto account not initialized'); } - const { notificationAccountPickleKey, notificationAccount } = cryptoStore; - const encryptionKey = await generateCryptoKey({ - extractable: isDesktopSafari, - }); + const notificationAccountWithPicklingKey = await getNotifsCryptoAccount(); const notificationsPrekey = notificationsInitializationInfo.prekey; const session = new olm.Session(); if (notificationsInitializationInfo.oneTimeKey) { session.create_outbound( - notificationAccount, + notificationAccountWithPicklingKey.notificationAccount, notificationsIdentityKeys.curve25519, notificationsIdentityKeys.ed25519, notificationsPrekey, @@ -177,7 +180,7 @@ ); } else { session.create_outbound_without_otk( - notificationAccount, + notificationAccountWithPicklingKey.notificationAccount, notificationsIdentityKeys.curve25519, notificationsIdentityKeys.ed25519, notificationsPrekey, @@ -188,46 +191,47 @@ JSON.stringify(initialEncryptedMessageContent), ); - const mainSession = session.pickle(notificationAccountPickleKey); + const mainSession = session.pickle( + notificationAccountWithPicklingKey.picklingKey, + ); const notificationsOlmData: NotificationsOlmDataType = { mainSession, pendingSessionUpdate: mainSession, updateCreationTimestamp: Date.now(), - picklingKey: notificationAccountPickleKey, + picklingKey: notificationAccountWithPicklingKey.picklingKey, }; - const encryptedOlmData = await encryptData( - new TextEncoder().encode(JSON.stringify(notificationsOlmData)), - encryptionKey, - ); - const persistEncryptionKeyPromise = (async () => { - let cryptoKeyPersistentForm; - if (isDesktopSafari) { - // Safari doesn't support structured clone algorithm in service - // worker context so we have to store CryptoKey as JSON - cryptoKeyPersistentForm = await exportKeyToJWK(encryptionKey); - } else { - cryptoKeyPersistentForm = encryptionKey; - } - - await localforage.setItem( - dataEncryptionKeyDBLabel, - cryptoKeyPersistentForm, - ); - })(); + const { + notificationAccount, + picklingKey, + synchronizationValue, + accountEncryptionKey, + } = notificationAccountWithPicklingKey; + + const pickledAccount = notificationAccount.pickle(picklingKey); + const accountWithPicklingKey: PickledOLMAccount = { + pickledAccount, + picklingKey, + }; - await Promise.all([ - localforage.setItem(dataPersistenceKey, encryptedOlmData), - persistEncryptionKeyPromise, - ]); + await persistNotifsAccountWithOlmData({ + accountEncryptionKey, + accountWithPicklingKey, + olmDataKey: dataPersistenceKey, + olmData: notificationsOlmData, + olmEncryptionKeyDBLabel: dataEncryptionKeyDBLabel, + synchronizationValue, + forceWrite: true, + }); return { message, messageType }; } -function getOrCreateOlmAccount(accountIDInDB: number): { +async function getOrCreateOlmAccount(accountIDInDB: number): Promise<{ +picklingKey: string, +account: olm.Account, -} { + +synchronizationValue?: ?string, +}> { const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { @@ -245,6 +249,26 @@ throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); } + const notifsCryptoModuleInitialized = await (async () => { + if (accountIDInDB !== sqliteQueryExecutor.getNotifsAccountID()) { + return false; + } + return await isNotifsCryptoAccountInitialized(); + })(); + + if (notifsCryptoModuleInitialized) { + const { + notificationAccount, + picklingKey: notificationAccountPicklingKey, + synchronizationValue, + } = await getNotifsCryptoAccount(); + return { + account: notificationAccount, + picklingKey: notificationAccountPicklingKey, + synchronizationValue, + }; + } + if (accountDBString.isNull) { picklingKey = uuid.v4(); account.create(); @@ -254,6 +278,10 @@ account.unpickle(picklingKey, dbAccount.pickledAccount); } + if (accountIDInDB === sqliteQueryExecutor.getNotifsAccountID()) { + return { picklingKey, account, synchronizationValue: uuid.v4() }; + } + return { picklingKey, account }; } @@ -315,13 +343,15 @@ initialCryptoStore.primaryAccount, ), contentSessions: {}, - notificationAccountPickleKey: - initialCryptoStore.notificationAccount.picklingKey, + }; + const notifsCryptoAccount = { + picklingKey: initialCryptoStore.notificationAccount.picklingKey, notificationAccount: unpickleInitialCryptoStoreAccount( initialCryptoStore.notificationAccount, ), + synchronizationValue: uuid.v4(), }; - persistCryptoStore(); + await persistCryptoStore(notifsCryptoAccount); return; } @@ -351,12 +381,13 @@ return undefined; } -function getSignedIdentityKeysBlob(): SignedIdentityKeysBlob { +async function getSignedIdentityKeysBlob(): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } - const { contentAccount, notificationAccount } = cryptoStore; + const { contentAccount } = cryptoStore; + const { notificationAccount } = await getNotifsCryptoAccount(); const identityKeysBlob: IdentityKeysBlob = { notificationIdentityPublicKeys: JSON.parse( @@ -374,22 +405,25 @@ return signedIdentityKeysBlob; } -function getNewDeviceKeyUpload(): IdentityNewDeviceKeyUpload { +async function getNewDeviceKeyUpload(): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } - const { contentAccount, notificationAccount } = cryptoStore; - - const signedIdentityKeysBlob = getSignedIdentityKeysBlob(); + const { contentAccount } = cryptoStore; + const [notifsCryptoAccount, signedIdentityKeysBlob] = await Promise.all([ + getNotifsCryptoAccount(), + getSignedIdentityKeysBlob(), + ]); const primaryAccountKeysSet = retrieveAccountKeysSet(contentAccount); - const notificationAccountKeysSet = - retrieveAccountKeysSet(notificationAccount); + const notificationAccountKeysSet = retrieveAccountKeysSet( + notifsCryptoAccount.notificationAccount, + ); contentAccount.mark_keys_as_published(); - notificationAccount.mark_keys_as_published(); + notifsCryptoAccount.notificationAccount.mark_keys_as_published(); - persistCryptoStore(); + await persistCryptoStore(notifsCryptoAccount); return { keyPayload: signedIdentityKeysBlob.payload, @@ -403,20 +437,22 @@ }; } -function getExistingDeviceKeyUpload(): IdentityExistingDeviceKeyUpload { +async function getExistingDeviceKeyUpload(): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } - const { contentAccount, notificationAccount } = cryptoStore; - - const signedIdentityKeysBlob = getSignedIdentityKeysBlob(); + const { contentAccount } = cryptoStore; + const [notifsCryptoAccount, signedIdentityKeysBlob] = await Promise.all([ + getNotifsCryptoAccount(), + getSignedIdentityKeysBlob(), + ]); const { prekey: contentPrekey, prekeySignature: contentPrekeySignature } = retrieveIdentityKeysAndPrekeys(contentAccount); const { prekey: notifPrekey, prekeySignature: notifPrekeySignature } = - retrieveIdentityKeysAndPrekeys(notificationAccount); + retrieveIdentityKeysAndPrekeys(notifsCryptoAccount.notificationAccount); - persistCryptoStore(); + await persistCryptoStore(notifsCryptoAccount); return { keyPayload: signedIdentityKeysBlob.payload, @@ -469,30 +505,36 @@ throw new Error('Database not initialized'); } - const contentAccountResult = getOrCreateOlmAccount( - sqliteQueryExecutor.getContentAccountID(), - ); - const notificationAccountResult = getOrCreateOlmAccount( - sqliteQueryExecutor.getNotifsAccountID(), + const [contentAccountResult, notificationAccountResult] = await Promise.all( + [ + getOrCreateOlmAccount(sqliteQueryExecutor.getContentAccountID()), + getOrCreateOlmAccount(sqliteQueryExecutor.getNotifsAccountID()), + ], ); + const contentSessions = getOlmSessions(contentAccountResult.picklingKey); cryptoStore = { contentAccountPickleKey: contentAccountResult.picklingKey, contentAccount: contentAccountResult.account, contentSessions, - notificationAccountPickleKey: notificationAccountResult.picklingKey, + }; + const notifsCryptoAccount = { + picklingKey: notificationAccountResult.picklingKey, notificationAccount: notificationAccountResult.account, + synchronizationValue: notificationAccountResult.synchronizationValue, }; - persistCryptoStore(); + await persistCryptoStore(notifsCryptoAccount); }, async getUserPublicKey(): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } - const { contentAccount, notificationAccount } = cryptoStore; - const { payload, signature } = getSignedIdentityKeysBlob(); + const { contentAccount } = cryptoStore; + const [{ notificationAccount }, { payload, signature }] = await Promise.all( + [getNotifsCryptoAccount(), getSignedIdentityKeysBlob()], + ); return { primaryIdentityPublicKeys: JSON.parse(contentAccount.identity_keys()), @@ -513,7 +555,7 @@ } const encryptedContent = olmSession.session.encrypt(content); - persistCryptoStore(); + await persistCryptoStore(); return { message: encryptedContent.body, @@ -555,7 +597,7 @@ deviceID, JSON.stringify(result), ); - persistCryptoStore(true); + await persistCryptoStore(undefined, true); sqliteQueryExecutor.commitTransaction(); } catch (e) { sqliteQueryExecutor.rollbackTransaction(); @@ -582,7 +624,7 @@ encryptedData.message, ); - persistCryptoStore(); + await persistCryptoStore(); return result; }, @@ -623,7 +665,7 @@ sqliteQueryExecutor.beginTransaction(); try { sqliteQueryExecutor.addInboundP2PMessage(receivedMessage); - persistCryptoStore(true); + await persistCryptoStore(undefined, true); sqliteQueryExecutor.commitTransaction(); } catch (e) { sqliteQueryExecutor.rollbackTransaction(); @@ -669,7 +711,7 @@ session, version: sessionVersion, }; - persistCryptoStore(); + await persistCryptoStore(); return initialEncryptedMessage; }, @@ -711,7 +753,7 @@ session, version: newSessionVersion, }; - persistCryptoStore(); + await persistCryptoStore(); const encryptedData: EncryptedData = { message: initialEncryptedData.body, @@ -817,7 +859,8 @@ if (!cryptoStore) { throw new Error('Crypto account not initialized'); } - const { contentAccount, notificationAccount } = cryptoStore; + const { contentAccount } = cryptoStore; + const notifsCryptoAccount = await getNotifsCryptoAccount(); const contentOneTimeKeys = getAccountOneTimeKeys( contentAccount, @@ -826,12 +869,12 @@ contentAccount.mark_keys_as_published(); const notificationsOneTimeKeys = getAccountOneTimeKeys( - notificationAccount, + notifsCryptoAccount.notificationAccount, numberOfKeys, ); - notificationAccount.mark_keys_as_published(); + notifsCryptoAccount.notificationAccount.mark_keys_as_published(); - persistCryptoStore(); + await persistCryptoStore(notifsCryptoAccount); return { contentOneTimeKeys, notificationsOneTimeKeys }; }, @@ -849,26 +892,27 @@ if (!cryptoStore) { throw new Error('Crypto account not initialized'); } - const { contentAccount, notificationAccount } = cryptoStore; + const { contentAccount } = cryptoStore; + const notifsCryptoAccount = await getNotifsCryptoAccount(); // Content and notification accounts' keys are always rotated at the same // time so we only need to check one of them. if (shouldRotatePrekey(contentAccount)) { contentAccount.generate_prekey(); - notificationAccount.generate_prekey(); + notifsCryptoAccount.notificationAccount.generate_prekey(); } if (shouldForgetPrekey(contentAccount)) { contentAccount.forget_old_prekey(); - notificationAccount.forget_old_prekey(); + notifsCryptoAccount.notificationAccount.forget_old_prekey(); } - persistCryptoStore(); + await persistCryptoStore(notifsCryptoAccount); if (!contentAccount.unpublished_prekey()) { return; } const { prekey: notifPrekey, prekeySignature: notifPrekeySignature } = - getAccountPrekeysSet(notificationAccount); + getAccountPrekeysSet(notifsCryptoAccount.notificationAccount); const { prekey: contentPrekey, prekeySignature: contentPrekeySignature } = getAccountPrekeysSet(contentAccount); @@ -883,9 +927,9 @@ notifPrekeySignature, }); contentAccount.mark_prekey_as_published(); - notificationAccount.mark_prekey_as_published(); + notifsCryptoAccount.notificationAccount.mark_prekey_as_published(); - persistCryptoStore(); + await persistCryptoStore(notifsCryptoAccount); }, async signMessage(message: string): Promise { if (!cryptoStore) { @@ -918,12 +962,13 @@ if (!cryptoStore) { throw new Error('Crypto account not initialized'); } - const { contentAccount, notificationAccount } = cryptoStore; + const { contentAccount } = cryptoStore; + const notifsCryptoAccount = await getNotifsCryptoAccount(); contentAccount.mark_prekey_as_published(); - notificationAccount.mark_prekey_as_published(); + notifsCryptoAccount.notificationAccount.mark_prekey_as_published(); - persistCryptoStore(); + await persistCryptoStore(notifsCryptoAccount); }, };