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 @@ -288,10 +288,10 @@ ); const notifsOlmDataEncryptionKeyDBLabel = - getOlmEncryptionKeyDBLabelForCookie(cookie, keyserverID); + getOlmEncryptionKeyDBLabelForCookie(keyserverID, cookie); const notifsOlmDataContentKey = getOlmDataContentKeyForCookie( - cookie, keyserverID, + cookie, ); const persistEncryptionKeyPromise = (async () => { @@ -355,7 +355,10 @@ [getOrCreateCryptoStore], ); - const notificationsSessionPromise = React.useRef>(null); + const perKeyserverNotificationsSessionPromises = React.useRef<{ + [keyserverID: string]: ?Promise, + }>({}); + const createNotificationsSession = React.useCallback( async ( cookie: ?string, @@ -363,8 +366,8 @@ notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => { - if (notificationsSessionPromise.current) { - return notificationsSessionPromise.current; + if (perKeyserverNotificationsSessionPromises.current[keyserverID]) { + return perKeyserverNotificationsSessionPromises.current[keyserverID]; } const newNotificationsSessionPromise = (async () => { @@ -376,12 +379,14 @@ keyserverID, ); } catch (e) { - notificationsSessionPromise.current = undefined; + perKeyserverNotificationsSessionPromises.current[keyserverID] = + undefined; throw e; } })(); - notificationsSessionPromise.current = newNotificationsSessionPromise; + perKeyserverNotificationsSessionPromises.current[keyserverID] = + newNotificationsSessionPromise; return newNotificationsSessionPromise; }, [createNewNotificationsSession], @@ -390,7 +395,7 @@ const isCryptoStoreSet = !!currentCryptoStore; React.useEffect(() => { if (!isCryptoStoreSet) { - notificationsSessionPromise.current = undefined; + perKeyserverNotificationsSessionPromises.current = {}; } }, [isCryptoStoreSet]); 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 @@ -46,11 +46,21 @@ export const WEB_NOTIFS_SERVICE_UTILS_KEY = 'webNotifsServiceUtils'; const SESSION_UPDATE_MAX_PENDING_TIME = 10 * 1000; +const INDEXED_DB_KEYSERVER_PREFIX = 'keyserver'; +const INDEXED_DB_KEY_SEPARATOR = ':'; + +// This constant is only used to migrate the existing notifications +// session with production keyserver to new IndexedDB key format. This +// migration will fire when user updates the app. It will also fire +// on dev env provided old keyserver set up is used. Developers willing +// to use new keyserver set up must log out before updating the app. +// Do not introduce new usages of this constant in the code!!! +const AUTHORITATIVE_KEYSERVER_ID = '256'; async function decryptWebNotification( encryptedNotification: EncryptedWebNotification, ): Promise { - const { id, encryptedPayload } = encryptedNotification; + const { id, keyserverID, encryptedPayload } = encryptedNotification; const utilsData = await localforage.getItem( WEB_NOTIFS_SERVICE_UTILS_KEY, ); @@ -62,7 +72,7 @@ let olmDBKeys; try { - olmDBKeys = await getNotifsOlmSessionDBKeys(); + olmDBKeys = await getNotifsOlmSessionDBKeys(keyserverID); } catch (e) { return { id, @@ -112,7 +122,7 @@ let encryptedOlmData, encryptionKey, olmDataContentKey; try { const { olmDataContentKey: olmDataContentKeyValue, encryptionKeyDBKey } = - await getNotifsOlmSessionDBKeys(); + await getNotifsOlmSessionDBKeys(keyserverID); olmDataContentKey = olmDataContentKeyValue; @@ -280,16 +290,24 @@ return await importJWKKey(persistedCryptoKey); } -async function getNotifsOlmSessionDBKeys(): Promise<{ +async function getNotifsOlmSessionDBKeys(keyserverID: string): Promise<{ +olmDataContentKey: string, +encryptionKeyDBKey: string, }> { + const olmDataContentKeyForKeyserverPrefix = + getOlmDataContentKeyForCookie(keyserverID); + + const olmEncryptionKeyDBLabelForKeyserverPrefix = + getOlmEncryptionKeyDBLabelForCookie(keyserverID); + const dbKeys = await localforage.keys(); const olmDataContentKeys = sortOlmDBKeysArray( - dbKeys.filter(key => key.startsWith(NOTIFICATIONS_OLM_DATA_CONTENT)), + dbKeys.filter(key => key.startsWith(olmDataContentKeyForKeyserverPrefix)), ); const encryptionKeyDBLabels = sortOlmDBKeysArray( - dbKeys.filter(key => key.startsWith(NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY)), + dbKeys.filter(key => + key.startsWith(olmEncryptionKeyDBLabelForKeyserverPrefix), + ), ); if (olmDataContentKeys.length === 0 || encryptionKeyDBLabels.length === 0) { @@ -332,31 +350,51 @@ } function getOlmDataContentKeyForCookie( - cookie: ?string, - // eslint-disable-next-line no-unused-vars keyserverID: string, + cookie: ?string, ): string { + const olmDataContentKeyBase = [ + INDEXED_DB_KEYSERVER_PREFIX, + keyserverID, + NOTIFICATIONS_OLM_DATA_CONTENT, + ].join(INDEXED_DB_KEY_SEPARATOR); + if (!cookie) { - return NOTIFICATIONS_OLM_DATA_CONTENT; + return olmDataContentKeyBase; } const cookieID = getCookieIDFromCookie(cookie); - return `${NOTIFICATIONS_OLM_DATA_CONTENT}:${cookieID}`; + return [olmDataContentKeyBase, cookieID].join(INDEXED_DB_KEY_SEPARATOR); } function getOlmEncryptionKeyDBLabelForCookie( - cookie: ?string, - // eslint-disable-next-line no-unused-vars keyserverID: string, + cookie: ?string, ): string { + const olmEncryptionKeyDBLabelBase = [ + INDEXED_DB_KEYSERVER_PREFIX, + keyserverID, + NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, + ].join(INDEXED_DB_KEY_SEPARATOR); + if (!cookie) { - return NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY; + return olmEncryptionKeyDBLabelBase; } const cookieID = getCookieIDFromCookie(cookie); - return `${NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY}:${cookieID}`; + return [olmEncryptionKeyDBLabelBase, cookieID].join(INDEXED_DB_KEY_SEPARATOR); } function getCookieIDFromOlmDBKey(olmDBKey: string): string | '0' { - const cookieID = olmDBKey.split(':')[1]; + // Olm DB keys comply to the following format: + // KEYSERVER::(OLM_CONTENT | OLM_ENCRYPTION_KEY): + const cookieID = olmDBKey.split(INDEXED_DB_KEY_SEPARATOR)[3]; + return cookieID ?? '0'; +} + +function getCookieIDFromLegacyOlmDBKey(olmDBKey: string): string | '0' { + // Legacy (prior to multi-keyserver) olm DB keys + // complied to the following format: + // (OLM_CONTENT | OLM_ENCRYPTION_KEY): + const cookieID = olmDBKey.split(INDEXED_DB_KEY_SEPARATOR)[1]; return cookieID ?? '0'; } @@ -375,9 +413,51 @@ .map(({ key }) => key); } +async function migrateLegacyOlmNotificationsSessions() { + const keysToInsert = []; + const valuesToInsert = []; + const keysToDelete = []; + + await localforage.iterate((value: EncryptedData | CryptoKey, key) => { + if (key.startsWith(NOTIFICATIONS_OLM_DATA_CONTENT)) { + const cookieID = getCookieIDFromLegacyOlmDBKey(key); + keysToInsert.push( + getOlmDataContentKeyForCookie(AUTHORITATIVE_KEYSERVER_ID, cookieID), + ); + } else if (key.startsWith(NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY)) { + const cookieID = getCookieIDFromLegacyOlmDBKey(key); + keysToInsert.push( + getOlmEncryptionKeyDBLabelForCookie( + AUTHORITATIVE_KEYSERVER_ID, + cookieID, + ), + ); + } else { + return undefined; + } + + valuesToInsert.push(value); + keysToDelete.push(key); + return undefined; + }); + + const insertionPromises = keysToInsert.map((key, idx) => + (async () => { + await localforage.setItem(key, valuesToInsert[idx]); + })(), + ); + + const deletionPromises = keysToDelete.map(key => + (async () => await localforage.removeItem(key))(), + ); + + await Promise.all([...insertionPromises, ...deletionPromises]); +} + export { decryptWebNotification, decryptDesktopNotification, getOlmDataContentKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, + migrateLegacyOlmNotificationsSessions, }; diff --git a/web/push-notif/push-notifs-handler.js b/web/push-notif/push-notifs-handler.js --- a/web/push-notif/push-notifs-handler.js +++ b/web/push-notif/push-notifs-handler.js @@ -16,7 +16,10 @@ import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; -import { decryptDesktopNotification } from './notif-crypto-utils.js'; +import { + decryptDesktopNotification, + migrateLegacyOlmNotificationsSessions, +} from './notif-crypto-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { WORKERS_MODULES_DIR_PATH, @@ -35,6 +38,15 @@ const dispatchActionPromise = useDispatchActionPromise(); const callSetDeviceToken = useSetDeviceTokenFanout(); const staffCanSee = useStaffCanSee(); + const [notifsOlmSessionMigrated, setNotifsSessionsMigrated] = + React.useState(false); + + React.useEffect(() => { + void (async () => { + await migrateLegacyOlmNotificationsSessions(); + setNotifsSessionsMigrated(true); + })(); + }, []); React.useEffect( () => @@ -51,26 +63,28 @@ electron?.fetchDeviceToken?.(); }, []); - React.useEffect( - () => - electron?.onEncryptedNotification?.( - async ({ + React.useEffect(() => { + if (!notifsOlmSessionMigrated) { + return undefined; + } + + return electron?.onEncryptedNotification?.( + async ({ + keyserverID, + encryptedPayload, + }: { + keyserverID: string, + encryptedPayload: string, + }) => { + const decryptedPayload = await decryptDesktopNotification( keyserverID, encryptedPayload, - }: { - keyserverID: string, - encryptedPayload: string, - }) => { - const decryptedPayload = await decryptDesktopNotification( - keyserverID, - encryptedPayload, - staffCanSee, - ); - electron?.showDecryptedNotification(decryptedPayload); - }, - ), - [staffCanSee], - ); + staffCanSee, + ); + electron?.showDecryptedNotification(decryptedPayload); + }, + ); + }, [staffCanSee, notifsOlmSessionMigrated]); const dispatch = useDispatch(); diff --git a/web/push-notif/service-worker.js b/web/push-notif/service-worker.js --- a/web/push-notif/service-worker.js +++ b/web/push-notif/service-worker.js @@ -10,6 +10,7 @@ import { decryptWebNotification, + migrateLegacyOlmNotificationsSessions, WEB_NOTIFS_SERVICE_UTILS_KEY, type WebNotifsServiceUtilsData, type WebNotifDecryptionError, @@ -78,6 +79,8 @@ WEB_NOTIFS_SERVICE_UTILS_KEY, webNotifsServiceUtils, ); + + await migrateLegacyOlmNotificationsSessions(); })(), ); });