From 90f0f8e2558c06838b9bf3c8d54e5fb563b59296 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Thu, 17 Aug 2023 16:35:41 -0700 Subject: [PATCH] eraseAllStorageServiceState: Delete everything, delete in memory --- ts/background.ts | 31 +++++++++++++++++------- ts/services/storage.ts | 37 +++++++++++++++++++++++++---- ts/sql/Client.ts | 5 ++++ ts/sql/Interface.ts | 3 ++- ts/sql/Server.ts | 48 +++++++++++++++++++++++++++----------- ts/state/ducks/stickers.ts | 39 ++++++++++++++++++++++++------- ts/state/ducks/user.ts | 16 ++++++++++++- 7 files changed, 143 insertions(+), 36 deletions(-) diff --git a/ts/background.ts b/ts/background.ts index 2aee3b034514..95c2e2e49c3c 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -2970,15 +2970,21 @@ export async function startApp(): Promise { try { log.info(`unlinkAndDisconnect: removing configuration, mode ${mode}`); - await window.textsecure.storage.protocol.removeAllConfiguration(mode); - // This was already done in the database with removeAllConfiguration; this does it - // for all the conversation models in memory. + // First, make changes to conversations in memory window.getConversations().forEach(conversation => { - // eslint-disable-next-line no-param-reassign - delete conversation.attributes.senderKeyInfo; + conversation.unset('senderKeyInfo'); }); + // Then make sure outstanding conversation saves are flushed + await window.Signal.Data.flushUpdateConversationBatcher(); + + // Then make sure that all previously-outstanding database saves are flushed + await window.Signal.Data.getItemById('manifestVersion'); + + // Finally, conversations in the database, and delete all config tables + await window.textsecure.storage.protocol.removeAllConfiguration(mode); + // These three bits of data are important to ensure that the app loads up // the conversation list, instead of showing just the QR code screen. if (previousNumberId !== undefined) { @@ -3102,10 +3108,17 @@ export async function startApp(): Promise { log.info( 'onKeysSync: updated storage service key, erasing state and fetching' ); - await window.storage.put('storageKey', storageServiceKeyBase64); - await StorageService.eraseAllStorageServiceState({ - keepUnknownFields: true, - }); + try { + await window.storage.put('storageKey', storageServiceKeyBase64); + await StorageService.eraseAllStorageServiceState({ + keepUnknownFields: true, + }); + } catch (error) { + log.info( + 'onKeysSync: Failed to erase storage service data, starting sync job anyway', + Errors.toLogFormat(error) + ); + } } await StorageService.runStorageServiceSyncJob(); diff --git a/ts/services/storage.ts b/ts/services/storage.ts index c64da3161ca9..80834ea963f4 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -69,7 +69,9 @@ import { isSignalConversation } from '../util/isSignalConversation'; type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier; const { - eraseStorageServiceStateFromConversations, + eraseStorageServiceState, + flushUpdateConversationBatcher, + getItemById, updateConversation, updateConversations, } = dataInterface; @@ -1893,12 +1895,12 @@ export function enableStorageService(): void { storageServiceEnabled = true; } -// Note: this function is meant to be called before ConversationController is hydrated. -// It goes directly to the database, so in-memory conversations will be out of date. export async function eraseAllStorageServiceState({ keepUnknownFields = false, }: { keepUnknownFields?: boolean } = {}): Promise { log.info('storageService.eraseAllStorageServiceState: starting...'); + + // First, update high-level storage service metadata await Promise.all([ window.storage.remove('manifestVersion'), keepUnknownFields @@ -1906,7 +1908,34 @@ export async function eraseAllStorageServiceState({ : window.storage.remove('storage-service-unknown-records'), window.storage.remove('storageCredentials'), ]); - await eraseStorageServiceStateFromConversations(); + + // Then, we make the changes to records in memory: + // - Conversations + // - Sticker packs + // - Uninstalled sticker packs + // - Story distribution lists + + // This call just erases stickers for now. Storage service data is not stored + // in memory for Story Distribution Lists. Uninstalled sticker packs are not + // kept in memory at all. + window.reduxActions.user.eraseStorageServiceState(); + + // Conversations. These properties are not present in redux. + window.getConversations().forEach(conversation => { + conversation.unset('storageID'); + conversation.unset('needsStorageServiceSync'); + conversation.unset('storageUnknownFields'); + }); + + // Then make sure outstanding conversation saves are flushed + await flushUpdateConversationBatcher(); + + // Then make sure that all previously-outstanding database saves are flushed + await getItemById('manifestVersion'); + + // Finally, we update the database directly for all record types: + await eraseStorageServiceState(); + log.info('storageService.eraseAllStorageServiceState: complete'); } diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index e532caf62e37..21f8b16a4234 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -107,6 +107,8 @@ const exclusiveInterface: ClientExclusiveInterface = { // Client-side only + flushUpdateConversationBatcher, + shutdown, removeAllMessagesInConversation, @@ -458,6 +460,9 @@ const updateConversationBatcher = createBatcher({ function updateConversation(data: ConversationType): void { updateConversationBatcher.add(data); } +async function flushUpdateConversationBatcher(): Promise { + await updateConversationBatcher.flushAndWait(); +} async function updateConversations( array: Array diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 6f12279b2425..62ba98c0685d 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -500,7 +500,6 @@ export type DataInterface = { removeAllSessions: () => Promise; getAllSessions: () => Promise>; - eraseStorageServiceStateFromConversations: () => Promise; getConversationCount: () => Promise; saveConversation: (data: ConversationType) => Promise; saveConversations: (array: Array) => Promise; @@ -794,6 +793,7 @@ export type DataInterface = { removeAll: () => Promise; removeAllConfiguration: (type?: RemoveAllConfiguration) => Promise; + eraseStorageServiceState: () => Promise; getMessagesNeedingUpgrade: ( limit: number, @@ -933,6 +933,7 @@ export type ClientExclusiveInterface = { updateConversation: (data: ConversationType) => void; removeConversation: (id: string) => Promise; + flushUpdateConversationBatcher: () => Promise; searchMessages: ({ query, diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 2772223d2c60..ab032ba01fcd 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -249,7 +249,6 @@ const dataInterface: ServerInterface = { removeAllSessions, getAllSessions, - eraseStorageServiceStateFromConversations, getConversationCount, saveConversation, saveConversations, @@ -384,6 +383,7 @@ const dataInterface: ServerInterface = { removeAll, removeAllConfiguration, + eraseStorageServiceState, getMessagesNeedingUpgrade, getMessagesWithVisualMediaAttachments, @@ -1621,18 +1621,6 @@ async function getConversationById( return jsonToObject(row.json); } -async function eraseStorageServiceStateFromConversations(): Promise { - const db = getInstance(); - - db.prepare( - ` - UPDATE conversations - SET - json = json_remove(json, '$.storageID', '$.needsStorageServiceSync', '$.unknownFields', '$.storageProfileKey'); - ` - ).run(); -} - function getAllConversationsSync(db = getInstance()): Array { const rows: ConversationRows = db .prepare( @@ -5583,6 +5571,40 @@ async function removeAllConfiguration( })(); } +async function eraseStorageServiceState(): Promise { + const db = getInstance(); + + db.exec(` + -- Conversations + UPDATE conversations + SET + json = json_remove(json, '$.storageID', '$.needsStorageServiceSync', '$.storageUnknownFields'); + + -- Stickers + UPDATE sticker_packs + SET + storageID = null, + storageVersion = null, + storageUnknownFields = null, + storageNeedsSync = 0; + + UPDATE uninstalled_sticker_packs + SET + storageID = null, + storageVersion = null, + storageUnknownFields = null, + storageNeedsSync = 0; + + -- Story Distribution Lists + UPDATE storyDistributions + SET + storageID = null, + storageVersion = null, + storageUnknownFields = null, + storageNeedsSync = 0; + `); +} + const MAX_MESSAGE_MIGRATION_ATTEMPTS = 5; async function getMessagesNeedingUpgrade( diff --git a/ts/state/ducks/stickers.ts b/ts/state/ducks/stickers.ts index 389f0bb2ae11..86ff7d158c93 100644 --- a/ts/state/ducks/stickers.ts +++ b/ts/state/ducks/stickers.ts @@ -18,6 +18,8 @@ import { import { storageServiceUploadJob } from '../../services/storage'; import { sendStickerPackSync } from '../../shims/textsecure'; import { trigger } from '../../shims/events'; +import { ERASE_STORAGE_SERVICE } from './user'; +import type { EraseStorageServiceStateAction } from './user'; import type { NoopActionType } from './noop'; @@ -128,27 +130,27 @@ type UseStickerFulfilledAction = ReadonlyDeep<{ export type StickersActionType = ReadonlyDeep< | ClearInstalledStickerPackAction + | InstallStickerPackFulfilledAction + | NoopActionType | StickerAddedAction | StickerPackAddedAction - | InstallStickerPackFulfilledAction - | UninstallStickerPackFulfilledAction - | StickerPackUpdatedAction | StickerPackRemovedAction + | StickerPackUpdatedAction + | UninstallStickerPackFulfilledAction | UseStickerFulfilledAction - | NoopActionType >; // Action Creators export const actions = { - downloadStickerPack, clearInstalledStickerPack, + downloadStickerPack, + installStickerPack, removeStickerPack, stickerAdded, stickerPackAdded, - installStickerPack, - uninstallStickerPack, stickerPackUpdated, + uninstallStickerPack, useSticker, }; @@ -356,7 +358,7 @@ export function getEmptyState(): StickersStateType { export function reducer( state: Readonly = getEmptyState(), - action: Readonly + action: Readonly ): StickersStateType { if (action.type === 'stickers/STICKER_PACK_ADDED') { // ts complains due to `stickers: {}` being overridden by the payload @@ -497,5 +499,26 @@ export function reducer( }; } + if (action.type === ERASE_STORAGE_SERVICE) { + const { packs } = state; + + const entries = Object.entries(packs).map(([id, pack]) => { + return [ + id, + omit(pack, [ + 'storageID', + 'storageVersion', + 'storageUnknownFields', + 'storageNeedsSync', + ]), + ]; + }); + + return { + ...state, + packs: Object.fromEntries(entries), + }; + } + return state; } diff --git a/ts/state/ducks/user.ts b/ts/state/ducks/user.ts index 279b0fea5f31..0111f07c7c72 100644 --- a/ts/state/ducks/user.ts +++ b/ts/state/ducks/user.ts @@ -56,15 +56,29 @@ type UserChangedActionType = ReadonlyDeep<{ }; }>; -export type UserActionType = ReadonlyDeep; +export const ERASE_STORAGE_SERVICE = 'user/ERASE_STORAGE_SERVICE_STATE'; +export type EraseStorageServiceStateAction = ReadonlyDeep<{ + type: typeof ERASE_STORAGE_SERVICE; +}>; + +export type UserActionType = ReadonlyDeep< + UserChangedActionType | EraseStorageServiceStateAction +>; // Action Creators export const actions = { + eraseStorageServiceState, userChanged, manualReconnect, }; +function eraseStorageServiceState(): EraseStorageServiceStateAction { + return { + type: ERASE_STORAGE_SERVICE, + }; +} + function userChanged(attributes: { interactionMode?: 'mouse' | 'keyboard'; ourConversationId?: string;