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 @@ -8,6 +8,10 @@ import { initialEncryptedMessageContent } from 'lib/shared/crypto-utils.js'; import { OlmSessionCreatorContext } from 'lib/shared/olm-session-creator-context.js'; +import { + hasMinCodeVersion, + NEXT_CODE_VERSION, +} from 'lib/shared/version-utils.js'; import type { SignedIdentityKeysBlob, CryptoStore, @@ -18,6 +22,7 @@ } from 'lib/types/crypto-types.js'; import { type IdentityDeviceKeyUpload } from 'lib/types/identity-service-types.js'; import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; +import { getConfig } from 'lib/utils/config.js'; import { retrieveAccountKeysSet } from 'lib/utils/olm-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; @@ -237,6 +242,7 @@ function OlmSessionCreatorProvider(props: Props): React.Node { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); const currentCryptoStore = useSelector(state => state.cryptoStore); + const platformDetails = getConfig().platformDetails; const createNewNotificationsSession = React.useCallback( async ( @@ -282,12 +288,25 @@ encryptionKey, ); - const notifsOlmDataEncryptionKeyDBLabel = - getOlmEncryptionKeyDBLabelForCookie(cookie, keyserverID); - const notifsOlmDataContentKey = getOlmDataContentKeyForCookie( - cookie, - keyserverID, - ); + let notifsOlmDataContentKey; + let notifsOlmDataEncryptionKeyDBLabel; + + if ( + hasMinCodeVersion(platformDetails, { majorDesktop: NEXT_CODE_VERSION }) + ) { + notifsOlmDataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForCookie( + cookie, + keyserverID, + ); + notifsOlmDataContentKey = getOlmDataContentKeyForCookie( + cookie, + keyserverID, + ); + } else { + notifsOlmDataEncryptionKeyDBLabel = + getOlmEncryptionKeyDBLabelForCookie(cookie); + notifsOlmDataContentKey = getOlmDataContentKeyForCookie(cookie); + } const persistEncryptionKeyPromise = (async () => { let cryptoKeyPersistentForm; @@ -312,7 +331,7 @@ return initialNotificationsEncryptedMessage; }, - [getOrCreateCryptoStore], + [getOrCreateCryptoStore, platformDetails], ); const createNewContentSession = React.useCallback( @@ -348,7 +367,10 @@ [getOrCreateCryptoStore], ); - const notificationsSessionPromise = React.useRef>(null); + const perKeyserverNotificationsSessionPromises = React.useRef<{ + [keyserverID: string]: ?Promise, + }>({}); + const createNotificationsSession = React.useCallback( async ( cookie: ?string, @@ -356,8 +378,8 @@ notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => { - if (notificationsSessionPromise.current) { - return notificationsSessionPromise.current; + if (perKeyserverNotificationsSessionPromises.current[keyserverID]) { + return perKeyserverNotificationsSessionPromises.current[keyserverID]; } const newNotificationsSessionPromise = (async () => { @@ -369,12 +391,14 @@ keyserverID, ); } catch (e) { - notificationsSessionPromise.current = undefined; + perKeyserverNotificationsSessionPromises.current[keyserverID] = + undefined; throw e; } })(); - notificationsSessionPromise.current = newNotificationsSessionPromise; + perKeyserverNotificationsSessionPromises.current[keyserverID] = + newNotificationsSessionPromise; return newNotificationsSessionPromise; }, [createNewNotificationsSession], @@ -383,7 +407,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,22 @@ 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 ASHOAT_KEYSERVER_ID_USED_ONLY_FOR_MIGRATION_FROM_LEGACY_NOTIF_STORAGE = + '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 +73,7 @@ let olmDBKeys; try { - olmDBKeys = await getNotifsOlmSessionDBKeys(); + olmDBKeys = await getNotifsOlmSessionDBKeys(keyserverID); } catch (e) { return { id, @@ -107,13 +118,12 @@ async function decryptDesktopNotification( encryptedPayload: string, staffCanSee: boolean, - // eslint-disable-next-line no-unused-vars keyserverID?: string, ): Promise<{ +[string]: mixed }> { let encryptedOlmData, encryptionKey, olmDataContentKey; try { const { olmDataContentKey: olmDataContentKeyValue, encryptionKeyDBKey } = - await getNotifsOlmSessionDBKeys(); + await getNotifsOlmSessionDBKeys(keyserverID); olmDataContentKey = olmDataContentKeyValue; @@ -281,16 +291,26 @@ return await importJWKKey(persistedCryptoKey); } -async function getNotifsOlmSessionDBKeys(): Promise<{ +async function getNotifsOlmSessionDBKeys(keyserverID?: string): Promise<{ +olmDataContentKey: string, +encryptionKeyDBKey: string, }> { + const olmDataContentKeyForKeyserverPrefix = getOlmDataContentKeyForCookie( + undefined, + keyserverID, + ); + + const olmEncryptionKeyDBLabelForKeyserverPrefix = + getOlmEncryptionKeyDBLabelForCookie(undefined, 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) { @@ -334,30 +354,55 @@ function getOlmDataContentKeyForCookie( cookie: ?string, - // eslint-disable-next-line no-unused-vars - keyserverID: string, + keyserverID?: string, ): string { + let olmDataContentKeyBase; + if (keyserverID) { + olmDataContentKeyBase = [ + INDEXED_DB_KEYSERVER_PREFIX, + keyserverID, + NOTIFICATIONS_OLM_DATA_CONTENT, + ].join(INDEXED_DB_KEY_SEPARATOR); + } else { + olmDataContentKeyBase = NOTIFICATIONS_OLM_DATA_CONTENT; + } + 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, + keyserverID?: string, ): string { + let olmEncryptionKeyDBLabelBase; + if (keyserverID) { + olmEncryptionKeyDBLabelBase = [ + INDEXED_DB_KEYSERVER_PREFIX, + keyserverID, + NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, + ].join(INDEXED_DB_KEY_SEPARATOR); + } else { + olmEncryptionKeyDBLabelBase = NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY; + } + 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 one of the following formats: + // KEYSERVER::(OLM_CONTENT | OLM_ENCRYPTION_KEY): + // or legacy (OLM_CONTENT | OLM_ENCRYPTION_KEY):. + // Legacy format may be used in case a new version of the web app + // is running on a old desktop version that uses legacy key format. + const cookieID = olmDBKey.split(INDEXED_DB_KEY_SEPARATOR).slice(-1)[0]; return cookieID ?? '0'; } @@ -376,9 +421,52 @@ .map(({ key }) => key); } +async function migrateLegacyOlmNotificationsSessions() { + const keyValuePairsToInsert: { [key: string]: EncryptedData | CryptoKey } = + {}; + const keysToDelete = []; + + await localforage.iterate((value: EncryptedData | CryptoKey, key) => { + let keyToInsert; + if (key.startsWith(NOTIFICATIONS_OLM_DATA_CONTENT)) { + const cookieID = getCookieIDFromOlmDBKey(key); + keyToInsert = getOlmDataContentKeyForCookie( + cookieID, + ASHOAT_KEYSERVER_ID_USED_ONLY_FOR_MIGRATION_FROM_LEGACY_NOTIF_STORAGE, + ); + } else if (key.startsWith(NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY)) { + const cookieID = getCookieIDFromOlmDBKey(key); + keyToInsert = getOlmEncryptionKeyDBLabelForCookie( + cookieID, + ASHOAT_KEYSERVER_ID_USED_ONLY_FOR_MIGRATION_FROM_LEGACY_NOTIF_STORAGE, + ); + } else { + return undefined; + } + + keyValuePairsToInsert[keyToInsert] = value; + keysToDelete.push(key); + return undefined; + }); + + const insertionPromises = Object.entries(keyValuePairsToInsert).map( + ([key, value]) => + (async () => { + await localforage.setItem(key, value); + })(), + ); + + 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 @@ -8,6 +8,12 @@ } from 'lib/actions/device-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; +import { + hasMinCodeVersion, + NEXT_CODE_VERSION, +} from 'lib/shared/version-utils.js'; +import { isDesktopPlatform } from 'lib/types/device-types.js'; +import { getConfig } from 'lib/utils/config.js'; import { convertNonPendingIDToNewSchema } from 'lib/utils/migration-utils.js'; import { shouldSkipPushPermissionAlert, @@ -16,7 +22,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 electron from '../electron.js'; import PushNotifModal from '../modals/push-notif-modal.react.js'; @@ -29,6 +38,22 @@ const dispatchActionPromise = useDispatchActionPromise(); const callSetDeviceToken = useSetDeviceTokenFanout(); const staffCanSee = useStaffCanSee(); + const [notifsOlmSessionMigrated, setNotifsSessionsMigrated] = + React.useState(false); + const platformDetails = getConfig().platformDetails; + + React.useEffect(() => { + if ( + !isDesktopPlatform(platformDetails.platform) || + !hasMinCodeVersion(platformDetails, { majorDesktop: NEXT_CODE_VERSION }) + ) { + return; + } + void (async () => { + await migrateLegacyOlmNotificationsSessions(); + setNotifsSessionsMigrated(true); + })(); + }, [platformDetails]); React.useEffect( () => @@ -45,26 +70,31 @@ electron?.fetchDeviceToken?.(); }, []); - React.useEffect( - () => - electron?.onEncryptedNotification?.( - async ({ + React.useEffect(() => { + if ( + hasMinCodeVersion(platformDetails, { majorDesktop: NEXT_CODE_VERSION }) && + !notifsOlmSessionMigrated + ) { + return undefined; + } + + return electron?.onEncryptedNotification?.( + async ({ + encryptedPayload, + keyserverID, + }: { + encryptedPayload: string, + keyserverID?: string, + }) => { + const decryptedPayload = await decryptDesktopNotification( encryptedPayload, + staffCanSee, keyserverID, - }: { - encryptedPayload: string, - keyserverID?: string, - }) => { - const decryptedPayload = await decryptDesktopNotification( - encryptedPayload, - staffCanSee, - keyserverID, - ); - electron?.showDecryptedNotification(decryptedPayload); - }, - ), - [staffCanSee], - ); + ); + electron?.showDecryptedNotification(decryptedPayload); + }, + ); + }, [staffCanSee, notifsOlmSessionMigrated, platformDetails]); 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(); })(), ); });