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 @@ -22,18 +22,19 @@ } from 'lib/types/crypto-types.js'; import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { generateCryptoKey, 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'; @@ -180,6 +181,9 @@ function WebNotificationsSessionCreatorProvider(props: Props): React.Node { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); const currentCryptoStore = useSelector(state => state.cryptoStore); + const cookie = useSelector( + state => state.keyserverStore.keyserverInfos[ashoatKeyserverID].cookie, + ); const createNewNotificationsSession = React.useCallback( async ( @@ -228,6 +232,10 @@ encryptionKey, ); + const notifsOlmDataEncryptionKeyDBLabel = + getOlmEncryptionKeyDBLabelForCookie(cookie); + const notifsOlmDataContentKey = getOlmDataContentKeyForCookie(cookie); + const persistEncryptionKeyPromise = (async () => { let cryptoKeyPersistentForm; if (isDesktopSafari) { @@ -239,19 +247,19 @@ } 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, ]); return initialNotificationsEncryptedMessage; }, - [getOrCreateCryptoStore], + [getOrCreateCryptoStore, cookie], ); const notificationsSessionPromise = React.useRef>(null); 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, encryptionKeyDBLabel } = olmDBKeys; + const [encryptedOlmData, encryptionKey] = await Promise.all([ + localforage.getItem(olmDataContentKey), + retrieveEncryptionKey(encryptionKeyDBLabel), + ]); + 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, encryptionKeyDBLabel } = + await getNotifsOlmSessionDBKeys(); + + olmDataContentKey = olmDataContentKeyValue; + [encryptedOlmData, encryptionKey] = await Promise.all([ - localforage.getItem(NOTIFICATIONS_OLM_DATA_CONTENT), - retrieveEncryptionKey(), + localforage.getItem(olmDataContentKey), + retrieveEncryptionKey(encryptionKeyDBLabel), 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,79 @@ return await importJWKKey(persistedCryptoKey); } -export { decryptWebNotification, decryptDesktopNotification }; +async function getNotifsOlmSessionDBKeys(): Promise<{ + +olmDataContentKey: string, + +encryptionKeyDBLabel: string, +}> { + const dbKeys = await localforage.keys(); + const olmDataContentKeys = dbKeys + .filter(key => key.startsWith(NOTIFICATIONS_OLM_DATA_CONTENT)) + .sort(); + const encryptionKeyDBLabels = dbKeys + .filter(key => key.startsWith(NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY)) + .sort(); + + if (olmDataContentKeys.length === 0 || encryptionKeyDBLabels.length === 0) { + throw 'Received encrypted notification but olm session was not created'; + } + + const latestDataContentKey = + olmDataContentKeys[olmDataContentKeys.length - 1]; + const latestEncryptionKeyDBLabel = + encryptionKeyDBLabels[encryptionKeyDBLabels.length - 1]; + + const latestDataContentCookieID = + getCookieIDFromOlmDBKey(latestDataContentKey); + const latestEncryptionKeyCookieID = getCookieIDFromOlmDBKey( + latestEncryptionKeyDBLabel, + ); + + if (latestDataContentCookieID !== latestEncryptionKeyCookieID) { + throw ( + '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, + encryptionKeyDBLabel: latestEncryptionKeyDBLabel, + }; + + 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 { + const cookieID = olmDBKey.split(':')[1]; + return cookieID ?? 'no_cookie'; +} + +export { + decryptWebNotification, + decryptDesktopNotification, + getOlmDataContentKeyForCookie, + getOlmEncryptionKeyDBLabelForCookie, +};