diff --git a/web/crypto/aes-gcm-crypto-utils.js b/web/crypto/aes-gcm-crypto-utils.js --- a/web/crypto/aes-gcm-crypto-utils.js +++ b/web/crypto/aes-gcm-crypto-utils.js @@ -1,5 +1,9 @@ // @flow +import t, { type TInterface, type TUnion } from 'tcomb'; + +import { tShape } from 'lib/utils/validation-utils.js'; + const ENCRYPTION_ALGORITHM = 'AES-GCM'; const ENCRYPTION_KEY_USAGES: $ReadOnlyArray = [ 'encrypt', @@ -11,6 +15,45 @@ +ciphertext: Uint8Array, }; +export const encryptedAESDataValidator: TInterface = + tShape({ + iv: t.irreducible('Uint8Array', x => x instanceof Uint8Array), + ciphertext: t.irreducible('Uint8Array', x => x instanceof Uint8Array), + }); + +export const cryptoKeyValidator: TInterface = tShape({ + algorithm: t.Object, + extractable: t.Boolean, + type: t.String, + usages: t.list(t.String), +}); + +export const subtleCrypto$JsonWebKeyValidator: TInterface = + tShape({ + alg: t.maybe(t.String), + crv: t.maybe(t.String), + d: t.maybe(t.String), + dp: t.maybe(t.String), + dq: t.maybe(t.String), + e: t.maybe(t.String), + ext: t.maybe(t.Boolean), + k: t.maybe(t.String), + key_ops: t.maybe(t.list(t.String)), + kty: t.maybe(t.String), + n: t.maybe(t.String), + oth: t.maybe(t.list(t.Object)), + p: t.maybe(t.String), + q: t.maybe(t.String), + qi: t.maybe(t.String), + use: t.maybe(t.String), + x: t.maybe(t.String), + y: t.maybe(t.String), + }); + +export const extendedCryptoKeyValidator: TUnion< + CryptoKey | SubtleCrypto$JsonWebKey, +> = t.union([cryptoKeyValidator, subtleCrypto$JsonWebKeyValidator]); + function generateCryptoKey({ extractable, }: { @@ -26,7 +69,7 @@ ); } -function generateIV(): BufferSource { +function generateIV(): Uint8Array { return crypto.getRandomValues(new Uint8Array(12)); } 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,18 @@ } 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 { assertWithValidator } from 'lib/utils/validation-utils.js'; import { type EncryptedData, decryptData, encryptData, importJWKKey, + exportKeyToJWK, + generateCryptoKey, + encryptedAESDataValidator, + extendedCryptoKeyValidator, } from '../crypto/aes-gcm-crypto-utils.js'; import { initOlm } from '../olm/olm-utils.js'; import { @@ -40,6 +48,13 @@ +staffCanSee: boolean, }; +export type NotificationAccountWithPicklingKey = { + +notificationAccount: olm.Account, + +picklingKey: string, + +synchronizationValue: ?string, + +accountEncryptionKey?: CryptoKey, +}; + type DecryptionResult = { +newPendingSessionUpdate: string, +newUpdateCreationTimestamp: number, @@ -52,6 +67,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 +79,146 @@ '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 { + 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 (!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 || olmEncryptionKeyDBLabel); + const shouldPersistAccount = !!accountWithPicklingKey; + + if (!shouldPersistOlmData && !shouldPersistAccount) { + return; + } + + const serializationPromises: { + [string]: Promise, + } = {}; + + 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 shared worker persisted its own data + console.log(e); + } +} async function decryptWebNotification( encryptedNotification: EncryptedWebNotification, @@ -319,13 +475,21 @@ const olmEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForDeviceID(deviceID); - let encryptedOlmData, encryptionKey; + let encryptedOlmData, encryptionKey, synchronizationValue; try { - [encryptedOlmData, encryptionKey] = await Promise.all([ - localforage.getItem(olmDataKey), - retrieveEncryptionKey(olmEncryptionKeyDBLabel), - initOlm(), - ]); + const { + values: { + [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 +502,22 @@ throw new Error(`Session with device: ${deviceID} not initialized.`); } + const validatedEncryptedOlmData = assertWithValidator( + encryptedOlmData, + encryptedAESDataValidator, + ); + const validatedEncryptionKey = await validateCryptoKey( + assertWithValidator(encryptionKey, extendedCryptoKeyValidator), + ); + let encryptedNotification; try { encryptedNotification = await encryptNotificationWithOlmSession( payload, - encryptedOlmData, + validatedEncryptedOlmData, olmDataKey, - encryptionKey, + validatedEncryptionKey, + synchronizationValue, ); } catch (e) { throw new Error( @@ -361,6 +534,7 @@ encryptedOlmData: EncryptedData, olmDataKey: string, encryptionKey: CryptoKey, + synchronizationValue: ?string, ): Promise { const serializedOlmData = await decryptData(encryptedOlmData, encryptionKey); const { @@ -388,10 +562,89 @@ 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 the shared worker which must always win race + // condition against push notifications service-worker. + true, + ); + return encryptedNotification; } +// notifications account manipulation + +async function isNotifsCryptoAccountInitialized(): Promise { + const { + values: { + 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 { + values: { + [INDEXED_DB_NOTIFS_ACCOUNT_KEY]: encryptedNotifsAccount, + [INDEXED_DB_NOTIFS_ACCOUNT_ENCRYPTION_KEY_DB_LABEL]: + notifsAccountEncryptionKey, + }, + synchronizationValue, + } = await localforage.getMultipleItems<{ + +notificationAccount: ?EncryptedData, + +notificationAccountEncryptionKey: ?CryptoKey | ?SubtleCrypto$JsonWebKey, + }>( + [ + INDEXED_DB_NOTIFS_ACCOUNT_KEY, + INDEXED_DB_NOTIFS_ACCOUNT_ENCRYPTION_KEY_DB_LABEL, + ], + INDEXED_DB_NOTIFS_SYNC_KEY, + ); + + if (!encryptedNotifsAccount || !notifsAccountEncryptionKey) { + throw new Error( + 'Attempt to retrieve notifs olm account but account not created.', + ); + } + + const validatedNotifsAccountEncryptionKey = await validateCryptoKey( + notifsAccountEncryptionKey, + ); + + const pickledOLMAccount = await deserializeEncryptedData( + encryptedNotifsAccount, + validatedNotifsAccountEncryptionKey, + ); + + 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 +661,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 +912,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,17 +44,15 @@ getSQLiteQueryExecutor, getPlatformDetails, } from './worker-database.js'; -import { - encryptData, - exportKeyToJWK, - generateCryptoKey, -} from '../../crypto/aes-gcm-crypto-utils.js'; import { getOlmDataKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, getOlmDataKeyForDeviceID, getOlmEncryptionKeyDBLabelForDeviceID, encryptNotification, + type NotificationAccountWithPicklingKey, + getNotifsCryptoAccount, + persistNotifsAccountWithOlmData, } from '../../push-notif/notif-crypto-utils.js'; import { type WorkerRequestMessage, @@ -64,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 = { @@ -75,8 +72,6 @@ +contentAccountPickleKey: string, +contentAccount: olm.Account, +contentSessions: OlmSessions, - +notificationAccountPickleKey: string, - +notificationAccount: olm.Account, }; let cryptoStore: ?WorkerCryptoStore = null; @@ -86,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) { @@ -98,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, @@ -119,11 +112,6 @@ version: sessionData.version, })); - const pickledNotificationAccount: PickledOLMAccount = { - picklingKey: notificationAccountPickleKey, - pickledAccount: notificationAccount.pickle(notificationAccountPickleKey), - }; - try { if (!withoutTransaction) { sqliteQueryExecutor.beginTransaction(); @@ -135,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(); } @@ -160,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, @@ -178,7 +180,7 @@ ); } else { session.create_outbound_without_otk( - notificationAccount, + notificationAccountWithPicklingKey.notificationAccount, notificationsIdentityKeys.curve25519, notificationsIdentityKeys.ed25519, notificationsPrekey, @@ -189,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) { @@ -246,6 +249,31 @@ throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); } + const maybeNotifsCryptoAccount: ?NotificationAccountWithPicklingKey = + await (async () => { + if (accountIDInDB !== sqliteQueryExecutor.getNotifsAccountID()) { + return undefined; + } + try { + return await getNotifsCryptoAccount(); + } catch (e) { + return undefined; + } + })(); + + if (maybeNotifsCryptoAccount) { + const { + notificationAccount, + picklingKey: notificationAccountPicklingKey, + synchronizationValue, + } = maybeNotifsCryptoAccount; + return { + account: notificationAccount, + picklingKey: notificationAccountPicklingKey, + synchronizationValue, + }; + } + if (accountDBString.isNull) { picklingKey = uuid.v4(); account.create(); @@ -255,6 +283,10 @@ account.unpickle(picklingKey, dbAccount.pickledAccount); } + if (accountIDInDB === sqliteQueryExecutor.getNotifsAccountID()) { + return { picklingKey, account, synchronizationValue: uuid.v4() }; + } + return { picklingKey, account }; } @@ -316,13 +348,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; } @@ -352,12 +386,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( @@ -375,22 +410,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, @@ -404,20 +442,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, @@ -470,30 +510,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()), @@ -514,7 +560,7 @@ } const encryptedContent = olmSession.session.encrypt(content); - persistCryptoStore(); + await persistCryptoStore(); return { message: encryptedContent.body, @@ -556,7 +602,7 @@ deviceID, JSON.stringify(result), ); - persistCryptoStore(true); + await persistCryptoStore(undefined, true); sqliteQueryExecutor.commitTransaction(); } catch (e) { sqliteQueryExecutor.rollbackTransaction(); @@ -593,7 +639,7 @@ encryptedData.message, ); - persistCryptoStore(); + await persistCryptoStore(); return result; }, @@ -634,7 +680,7 @@ sqliteQueryExecutor.beginTransaction(); try { sqliteQueryExecutor.addInboundP2PMessage(receivedMessage); - persistCryptoStore(true); + await persistCryptoStore(undefined, true); sqliteQueryExecutor.commitTransaction(); } catch (e) { sqliteQueryExecutor.rollbackTransaction(); @@ -680,7 +726,7 @@ session, version: sessionVersion, }; - persistCryptoStore(); + await persistCryptoStore(); return initialEncryptedMessage; }, @@ -722,7 +768,7 @@ session, version: newSessionVersion, }; - persistCryptoStore(); + await persistCryptoStore(); const encryptedData: EncryptedData = { message: initialEncryptedData.body, @@ -844,7 +890,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, @@ -853,12 +900,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 }; }, @@ -876,26 +923,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); @@ -910,9 +958,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) { @@ -945,12 +993,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); }, };