diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -55,6 +55,11 @@ +picklingKey: string, +pickledAccount: string, }; +export const pickledOLMAccountValidator: TInterface = + tShape({ + picklingKey: t.String, + pickledAccount: t.String, + }); export type NotificationsOlmDataType = { +mainSession: string, @@ -63,6 +68,14 @@ +updateCreationTimestamp: number, }; +export const notificationsOlmDataTypeValidator: TInterface = + tShape({ + mainSession: t.String, + picklingKey: t.String, + pendingSessionUpdate: t.String, + updateCreationTimestamp: t.Number, + }); + export type IdentityKeysBlob = { +primaryIdentityPublicKeys: OLMIdentityKeys, +notificationIdentityPublicKeys: OLMIdentityKeys, @@ -178,11 +191,26 @@ notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => Promise, + +ephemeralKeyserverNotifsSessionCreator?: ( + cookie: ?string, + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + keyserverID: string, + ) => Promise<{ + +initialEncryptedMessage: string, + +sessionPersistenceData: { +[string]: mixed }, + }>, + +persistEphemeralKeyserverNotifsSession?: (sessionPersistenceData: { + +[string]: mixed, + }) => Promise, +notificationsOutboundSessionCreator: ( deviceID: string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, ) => Promise, + +isKeyserverNotificationsSessionInitialized?: ( + keyserverID: string, + ) => Promise, +isDeviceNotificationsSessionInitialized: ( deviceID: string, ) => Promise, diff --git a/lib/utils/__mocks__/config.js b/lib/utils/__mocks__/config.js --- a/lib/utils/__mocks__/config.js +++ b/lib/utils/__mocks__/config.js @@ -25,6 +25,7 @@ keyserverNotificationsSessionCreator: jest.fn(), notificationsOutboundSessionCreator: jest.fn(), isContentSessionInitialized: jest.fn(), + isKeyserverNotificationsSessionInitialized: jest.fn(), isDeviceNotificationsSessionInitialized: jest.fn(), isNotificationsSessionInitializedWithDevices: jest.fn(), getOneTimeKeys: jest.fn(), diff --git a/web/crypto/olm-api.js b/web/crypto/olm-api.js --- a/web/crypto/olm-api.js +++ b/web/crypto/olm-api.js @@ -52,6 +52,9 @@ contentInboundSessionCreator: proxyToWorker('contentInboundSessionCreator'), contentOutboundSessionCreator: proxyToWorker('contentOutboundSessionCreator'), isContentSessionInitialized: proxyToWorker('isContentSessionInitialized'), + isKeyserverNotificationsSessionInitialized: proxyToWorker( + 'isKeyserverNotificationsSessionInitialized', + ), isDeviceNotificationsSessionInitialized: proxyToWorker( 'isDeviceNotificationsSessionInitialized', ), @@ -61,6 +64,12 @@ keyserverNotificationsSessionCreator: proxyToWorker( 'keyserverNotificationsSessionCreator', ), + ephemeralKeyserverNotifsSessionCreator: proxyToWorker( + 'ephemeralKeyserverNotifsSessionCreator', + ), + persistEphemeralKeyserverNotifsSession: proxyToWorker( + 'persistEphemeralKeyserverNotifsSession', + ), notificationsOutboundSessionCreator: proxyToWorker( 'notificationsOutboundSessionCreator', ), 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,12 +4,15 @@ import type { EncryptResult } from '@commapp/olm'; import invariant from 'invariant'; import localforage from 'localforage'; +import t, { type TInterface } from 'tcomb'; import uuid from 'uuid'; import { olmEncryptedMessageTypes, type NotificationsOlmDataType, type PickledOLMAccount, + pickledOLMAccountValidator, + notificationsOlmDataTypeValidator, } from 'lib/types/crypto-types.js'; import type { PlainTextWebNotification, @@ -19,7 +22,7 @@ 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 { assertWithValidator, tShape } from 'lib/utils/validation-utils.js'; import { fetchAuthMetadata, @@ -32,6 +35,7 @@ importJWKKey, exportKeyToJWK, generateCryptoKey, + cryptoKeyValidator, encryptedAESDataValidator, extendedCryptoKeyValidator, } from '../crypto/aes-gcm-crypto-utils.js'; @@ -235,7 +239,7 @@ }; } -async function persistNotifsAccountWithOlmData(input: { +type PersistNotifsAccountWithOlmDataInput = { +olmDataKey?: string, +olmEncryptionKeyDBLabel?: string, +olmData?: ?NotificationsOlmDataType, @@ -244,7 +248,23 @@ +accountWithPicklingKey?: PickledOLMAccount, +synchronizationValue: ?string, +forceWrite: boolean, -}): Promise { +}; + +export const persistNotifsAccountWithOlmDataInputValidator: TInterface = + tShape({ + olmDataKey: t.maybe(t.String), + olmEncryptionKeyDBLabel: t.maybe(t.String), + olmData: t.maybe(notificationsOlmDataTypeValidator), + encryptionKey: t.maybe(cryptoKeyValidator), + accountEncryptionKey: t.maybe(cryptoKeyValidator), + accountWithPicklingKey: t.maybe(pickledOLMAccountValidator), + synchronizationValue: t.maybe(t.String), + forceWrite: t.Boolean, + }); + +async function persistNotifsAccountWithOlmData( + input: PersistNotifsAccountWithOlmDataInput, +): Promise { const { olmData, olmDataKey, @@ -1022,9 +1042,11 @@ await localforage.setItem(encryptionKeyDBLabel, cryptoKeyPersistentForm); } -async function getNotifsOlmSessionDBKeys(keyserverID?: string): Promise<{ - +olmDataKey: string, - +encryptionKeyDBKey: string, +async function getAllNotifsOlmSessionDBKeys(keyserverID?: string): Promise, + +encryptionKeyDBLabels: $ReadOnlyArray, + +latestDataKey: string, + +latestEncryptionKeyDBKey: string, }> { const olmDataKeyForKeyserverPrefix = getOlmDataKeyForCookie( undefined, @@ -1045,9 +1067,7 @@ ); if (olmDataKeys.length === 0 || encryptionKeyDBLabels.length === 0) { - throw new Error( - 'Received encrypted notification but olm session was not created', - ); + return undefined; } const latestDataKey = olmDataKeys[olmDataKeys.length - 1]; @@ -1067,6 +1087,32 @@ ); } + return { + olmDataKeys, + encryptionKeyDBLabels, + latestDataKey, + latestEncryptionKeyDBKey, + }; +} + +async function getNotifsOlmSessionDBKeys(keyserverID?: string): Promise<{ + +olmDataKey: string, + +encryptionKeyDBKey: string, +}> { + const allNotifsKeys = await getAllNotifsOlmSessionDBKeys(keyserverID); + if (!allNotifsKeys) { + throw new Error( + 'Received encrypted notification but olm session was not created', + ); + } + + const { + olmDataKeys, + encryptionKeyDBLabels, + latestDataKey, + latestEncryptionKeyDBKey, + } = allNotifsKeys; + const olmDBKeys = { olmDataKey: latestDataKey, encryptionKeyDBKey: latestEncryptionKeyDBKey, @@ -1261,4 +1307,5 @@ persistEncryptionKey, retrieveEncryptionKey, persistNotifsAccountWithOlmData, + getAllNotifsOlmSessionDBKeys, }; 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 @@ -36,6 +36,7 @@ retrieveIdentityKeysAndPrekeys, olmSessionErrors, } from 'lib/utils/olm-utils.js'; +import { assertWithValidator } from 'lib/utils/validation-utils.js'; import { getIdentityClient } from './identity-client.js'; import { getProcessingStoreOpsExceptionMessage } from './process-operations.js'; @@ -53,6 +54,8 @@ type NotificationAccountWithPicklingKey, getNotifsCryptoAccount, persistNotifsAccountWithOlmData, + getAllNotifsOlmSessionDBKeys, + persistNotifsAccountWithOlmDataInputValidator, } from '../../push-notif/notif-crypto-utils.js'; import { type WorkerRequestMessage, @@ -155,12 +158,21 @@ } } -async function createAndPersistNotificationsOutboundSession( +async function createNotificationsOutboundSession( notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, dataPersistenceKey: string, dataEncryptionKeyDBLabel: string, -): Promise { +): Promise<{ + +accountEncryptionKey?: ?CryptoKey, + +accountWithPicklingKey?: PickledOLMAccount, + +encryptionKey?: ?CryptoKey, + +olmData?: ?NotificationsOlmDataType, + +olmDataKey?: string, + +olmEncryptionKeyDBLabel?: string, + +synchronizationValue: ?string, + +initialEncryptedData: EncryptedData, +}> { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } @@ -213,17 +225,37 @@ picklingKey, }; - await persistNotifsAccountWithOlmData({ + return { accountEncryptionKey, accountWithPicklingKey, olmDataKey: dataPersistenceKey, olmData: notificationsOlmData, olmEncryptionKeyDBLabel: dataEncryptionKeyDBLabel, synchronizationValue, + initialEncryptedData: { message, messageType }, + }; +} + +async function createAndPersistNotificationsOutboundSession( + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + dataPersistenceKey: string, + dataEncryptionKeyDBLabel: string, +): Promise { + const sessionCreationOutput = await createNotificationsOutboundSession( + notificationsIdentityKeys, + notificationsInitializationInfo, + dataPersistenceKey, + dataEncryptionKeyDBLabel, + ); + + const { initialEncryptedData, ...rest } = sessionCreationOutput; + await persistNotifsAccountWithOlmData({ + ...rest, forceWrite: true, }); - return { message, messageType }; + return initialEncryptedData; } async function getOrCreateOlmAccount(accountIDInDB: number): Promise<{ @@ -798,6 +830,19 @@ dataEncryptionKeyDBLabel, ); }, + async isKeyserverNotificationsSessionInitialized(keyserverID: string) { + try { + const allNotifsKeys = await getAllNotifsOlmSessionDBKeys(keyserverID); + return !!allNotifsKeys; + } catch (e) { + console.log( + `Detected inconsistency with notifs olm data with keyserver ${keyserverID}. Details ${ + getMessageForException(e) ?? '' + }`, + ); + return false; + } + }, async isDeviceNotificationsSessionInitialized(deviceID: string) { const dataPersistenceKey = getOlmDataKeyForDeviceID(deviceID); const dataEncryptionKeyDBLabel = @@ -852,6 +897,51 @@ return message; }, + async ephemeralKeyserverNotifsSessionCreator( + cookie: ?string, + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + keyserverID: string, + ): Promise<{ + +initialEncryptedMessage: string, + +sessionPersistenceData: { +[string]: mixed }, + }> { + const platformDetails = getPlatformDetails(); + if (!platformDetails) { + throw new Error('Worker not initialized'); + } + + const { notifsOlmDataContentKey, notifsOlmDataEncryptionKeyDBLabel } = + getNotifsPersistenceKeys(cookie, keyserverID, platformDetails); + + const sessionCreationOutput = await createNotificationsOutboundSession( + notificationsIdentityKeys, + notificationsInitializationInfo, + notifsOlmDataContentKey, + notifsOlmDataEncryptionKeyDBLabel, + ); + + const { + initialEncryptedData: { message: initialEncryptedMessage }, + ...rest + } = sessionCreationOutput; + + const sessionPersistenceData = { + ...rest, + forceWrite: false, + }; + + return { initialEncryptedMessage, sessionPersistenceData }; + }, + async persistEphemeralKeyserverNotifsSession(sessionPersistenceData: { + +[string]: mixed, + }): Promise { + const persistNotifsAccountWithOlmDataInput = assertWithValidator( + sessionPersistenceData, + persistNotifsAccountWithOlmDataInputValidator, + ); + await persistNotifsAccountWithOlmData(persistNotifsAccountWithOlmDataInput); + }, async reassignNotificationsSession( prevCookie: ?string, newCookie: ?string,