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 @@ -39,6 +39,7 @@ export type NotificationsSessionCreatorContextType = { +notificationsSessionCreator: ( + cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, ) => Promise, diff --git a/lib/utils/cookie-utils.js b/lib/utils/cookie-utils.js --- a/lib/utils/cookie-utils.js +++ b/lib/utils/cookie-utils.js @@ -11,4 +11,10 @@ return cookies; } -export { parseCookies }; +function getCookieIDFromCookie(cookie: string): string { + const cookieString = cookie.split('=').pop(); + const [cookieID] = cookieString.split(':'); + return cookieID; +} + +export { parseCookies, getCookieIDFromCookie }; diff --git a/web/account/account-hooks.js b/web/account/account-hooks.js --- a/web/account/account-hooks.js +++ b/web/account/account-hooks.js @@ -28,12 +28,12 @@ encryptData, exportKeyToJWK, } from '../crypto/aes-gcm-crypto-utils.js'; -import { - NOTIFICATIONS_OLM_DATA_CONTENT, - NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, -} from '../database/utils/constants.js'; import { isDesktopSafari } from '../database/utils/db-utils.js'; import { initOlm } from '../olm/olm-utils.js'; +import { + getOlmDataContentKeyForCookie, + getOlmEncryptionKeyDBLabelForCookie, +} from '../push-notif/notif-crypto-utils.js'; import { setCryptoStore } from '../redux/crypto-store-reducer.js'; import { useSelector } from '../redux/redux-utils.js'; @@ -183,6 +183,7 @@ const createNewNotificationsSession = React.useCallback( async ( + cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, ) => { @@ -228,6 +229,10 @@ encryptionKey, ); + const notifsOlmDataEncryptionKeyDBLabel = + getOlmEncryptionKeyDBLabelForCookie(cookie); + const notifsOlmDataContentKey = getOlmDataContentKeyForCookie(cookie); + const persistEncryptionKeyPromise = (async () => { let cryptoKeyPersistentForm; if (isDesktopSafari) { @@ -239,13 +244,13 @@ } await localforage.setItem( - NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, + notifsOlmDataEncryptionKeyDBLabel, cryptoKeyPersistentForm, ); })(); await Promise.all([ - localforage.setItem(NOTIFICATIONS_OLM_DATA_CONTENT, encryptedOlmData), + localforage.setItem(notifsOlmDataContentKey, encryptedOlmData), persistEncryptionKeyPromise, ]); @@ -257,6 +262,7 @@ const notificationsSessionPromise = React.useRef>(null); const createNotificationsSession = React.useCallback( async ( + cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, ) => { @@ -267,6 +273,7 @@ const newNotificationsSessionPromise = (async () => { try { return await createNewNotificationsSession( + cookie, notificationsIdentityKeys, notificationsInitializationInfo, ); @@ -304,6 +311,7 @@ } function useWebNotificationsSessionCreator(): ( + cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, ) => Promise { 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 @@ -11,6 +11,7 @@ PlainTextWebNotification, EncryptedWebNotification, } from 'lib/types/notif-types.js'; +import { getCookieIDFromCookie } from 'lib/utils/cookie-utils.js'; import { type EncryptedData, @@ -50,20 +51,31 @@ encryptedNotification: EncryptedWebNotification, ): Promise { const { id, encryptedPayload } = encryptedNotification; - - const [encryptedOlmData, encryptionKey, utilsData] = await Promise.all([ - localforage.getItem(NOTIFICATIONS_OLM_DATA_CONTENT), - retrieveEncryptionKey(), - localforage.getItem( - WEB_NOTIFS_SERVICE_UTILS_KEY, - ), - ]); + const utilsData = await localforage.getItem( + WEB_NOTIFS_SERVICE_UTILS_KEY, + ); if (!utilsData) { return { id, error: 'Necessary data not found in IndexedDB' }; } - const { olmWasmPath, staffCanSee } = (utilsData: WebNotifsServiceUtilsData); + + let olmDBKeys; + try { + olmDBKeys = await getNotifsOlmSessionDBKeys(); + } catch (e) { + return { + id, + error: e.message, + displayErrorMessage: staffCanSee, + }; + } + const { olmDataContentKey, encryptionKeyDBKey } = olmDBKeys; + const [encryptedOlmData, encryptionKey] = await Promise.all([ + localforage.getItem(olmDataContentKey), + retrieveEncryptionKey(encryptionKeyDBKey), + ]); + if (!encryptionKey || !encryptedOlmData) { return { id, @@ -77,6 +89,7 @@ const decryptedNotification = await commonDecrypt( encryptedOlmData, + olmDataContentKey, encryptionKey, encryptedPayload, ); @@ -95,11 +108,16 @@ encryptedPayload: string, staffCanSee: boolean, ): Promise<{ +[string]: mixed }> { - let encryptedOlmData, encryptionKey; + let encryptedOlmData, encryptionKey, olmDataContentKey; try { + const { olmDataContentKey: olmDataContentKeyValue, encryptionKeyDBKey } = + await getNotifsOlmSessionDBKeys(); + + olmDataContentKey = olmDataContentKeyValue; + [encryptedOlmData, encryptionKey] = await Promise.all([ - localforage.getItem(NOTIFICATIONS_OLM_DATA_CONTENT), - retrieveEncryptionKey(), + localforage.getItem(olmDataContentKey), + retrieveEncryptionKey(encryptionKeyDBKey), initOlm(), ]); } catch (e) { @@ -119,6 +137,7 @@ try { return await commonDecrypt( encryptedOlmData, + olmDataContentKey, encryptionKey, encryptedPayload, ); @@ -132,6 +151,7 @@ async function commonDecrypt( encryptedOlmData: EncryptedData, + olmDataContentKey: string, encryptionKey: CryptoKey, encryptedPayload: string, ): Promise { @@ -191,10 +211,7 @@ encryptionKey, ); - await localforage.setItem( - NOTIFICATIONS_OLM_DATA_CONTENT, - updatedEncryptedSession, - ); + await localforage.setItem(olmDataContentKey, updatedEncryptedSession); return decryptedNotification; } @@ -246,16 +263,16 @@ } } -async function retrieveEncryptionKey(): Promise { +async function retrieveEncryptionKey( + encryptionKeyDBLabel: string, +): Promise { if (!isDesktopSafari) { - return await localforage.getItem( - NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, - ); + return await localforage.getItem(encryptionKeyDBLabel); } // Safari doesn't support structured clone algorithm in service // worker context so we have to store CryptoKey as JSON const persistedCryptoKey = await localforage.getItem( - NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, + encryptionKeyDBLabel, ); if (!persistedCryptoKey) { return null; @@ -263,4 +280,96 @@ return await importJWKKey(persistedCryptoKey); } -export { decryptWebNotification, decryptDesktopNotification }; +async function getNotifsOlmSessionDBKeys(): Promise<{ + +olmDataContentKey: string, + +encryptionKeyDBKey: string, +}> { + const dbKeys = await localforage.keys(); + const olmDataContentKeys = sortOlmDBKeysArray( + dbKeys.filter(key => key.startsWith(NOTIFICATIONS_OLM_DATA_CONTENT)), + ); + const encryptionKeyDBLabels = sortOlmDBKeysArray( + dbKeys.filter(key => key.startsWith(NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY)), + ); + + if (olmDataContentKeys.length === 0 || encryptionKeyDBLabels.length === 0) { + throw new Error( + 'Received encrypted notification but olm session was not created', + ); + } + + const latestDataContentKey = + olmDataContentKeys[olmDataContentKeys.length - 1]; + const latestEncryptionKeyDBKey = + encryptionKeyDBLabels[encryptionKeyDBLabels.length - 1]; + + const latestDataContentCookieID = + getCookieIDFromOlmDBKey(latestDataContentKey); + const latestEncryptionKeyCookieID = getCookieIDFromOlmDBKey( + latestEncryptionKeyDBKey, + ); + + if (latestDataContentCookieID !== latestEncryptionKeyCookieID) { + throw new Error( + 'Olm sessions and their encryption keys out of sync. Latest cookie ' + + `id for olm sessions ${latestDataContentCookieID}. Latest cookie ` + + `id for olm session encryption keys ${latestEncryptionKeyCookieID}`, + ); + } + + const olmDBKeys = { + olmDataContentKey: latestDataContentKey, + encryptionKeyDBKey: latestEncryptionKeyDBKey, + }; + + const keysToDelete: $ReadOnlyArray = [ + ...olmDataContentKeys.slice(0, olmDataContentKeys.length - 1), + ...encryptionKeyDBLabels.slice(0, encryptionKeyDBLabels.length - 1), + ]; + + await Promise.all(keysToDelete.map(key => localforage.removeItem(key))); + return olmDBKeys; +} + +function getOlmDataContentKeyForCookie(cookie: ?string): string { + if (!cookie) { + return NOTIFICATIONS_OLM_DATA_CONTENT; + } + const cookieID = getCookieIDFromCookie(cookie); + return `${NOTIFICATIONS_OLM_DATA_CONTENT}:${cookieID}`; +} + +function getOlmEncryptionKeyDBLabelForCookie(cookie: ?string): string { + if (!cookie) { + return NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY; + } + const cookieID = getCookieIDFromCookie(cookie); + return `${NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY}:${cookieID}`; +} + +function getCookieIDFromOlmDBKey(olmDBKey: string): string | '0' { + const cookieID = olmDBKey.split(':')[1]; + return cookieID ?? '0'; +} + +function sortOlmDBKeysArray( + olmDBKeysArray: $ReadOnlyArray, +): $ReadOnlyArray { + return olmDBKeysArray + .map(key => ({ + cookieID: Number(getCookieIDFromOlmDBKey(key)), + key, + })) + .sort( + ({ cookieID: cookieID1 }, { cookieID: cookieID2 }) => + cookieID1 - cookieID2, + ) + .map(({ key }) => key); +} + +export { + decryptWebNotification, + decryptDesktopNotification, + getOlmDataContentKeyForCookie, + getOlmEncryptionKeyDBLabelForCookie, +}; diff --git a/web/socket.react.js b/web/socket.react.js --- a/web/socket.react.js +++ b/web/socket.react.js @@ -13,6 +13,8 @@ } from 'lib/selectors/keyserver-selectors.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import Socket, { type BaseSocketProps } from 'lib/socket/socket.react.js'; +import type { OLMIdentityKeys } from 'lib/types/crypto-types.js'; +import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; @@ -58,8 +60,20 @@ ); const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); const webNotificationsSessionCreator = useWebNotificationsSessionCreator(); + const webNotifsSessionCreatorForCookie = React.useCallback( + async ( + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + ) => + webNotificationsSessionCreator( + cookie, + notificationsIdentityKeys, + notificationsInitializationInfo, + ), + [webNotificationsSessionCreator, cookie], + ); const getInitialNotificationsEncryptedMessage = - useInitialNotificationsEncryptedMessage(webNotificationsSessionCreator); + useInitialNotificationsEncryptedMessage(webNotifsSessionCreatorForCookie); const getClientResponses = useSelector(state => webGetClientResponsesSelector({ state,