eraseAllStorageServiceState: Delete everything, delete in memory

This commit is contained in:
Scott Nonnenberg 2023-08-17 16:35:41 -07:00 committed by Jamie Kyle
parent b7b725f74c
commit 90f0f8e255
7 changed files with 143 additions and 36 deletions

View file

@ -2970,15 +2970,21 @@ export async function startApp(): Promise<void> {
try { try {
log.info(`unlinkAndDisconnect: removing configuration, mode ${mode}`); 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 // First, make changes to conversations in memory
// for all the conversation models in memory.
window.getConversations().forEach(conversation => { window.getConversations().forEach(conversation => {
// eslint-disable-next-line no-param-reassign conversation.unset('senderKeyInfo');
delete conversation.attributes.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 // 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. // the conversation list, instead of showing just the QR code screen.
if (previousNumberId !== undefined) { if (previousNumberId !== undefined) {
@ -3102,10 +3108,17 @@ export async function startApp(): Promise<void> {
log.info( log.info(
'onKeysSync: updated storage service key, erasing state and fetching' 'onKeysSync: updated storage service key, erasing state and fetching'
); );
await window.storage.put('storageKey', storageServiceKeyBase64); try {
await StorageService.eraseAllStorageServiceState({ await window.storage.put('storageKey', storageServiceKeyBase64);
keepUnknownFields: true, 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(); await StorageService.runStorageServiceSyncJob();

View file

@ -69,7 +69,9 @@ import { isSignalConversation } from '../util/isSignalConversation';
type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier; type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier;
const { const {
eraseStorageServiceStateFromConversations, eraseStorageServiceState,
flushUpdateConversationBatcher,
getItemById,
updateConversation, updateConversation,
updateConversations, updateConversations,
} = dataInterface; } = dataInterface;
@ -1893,12 +1895,12 @@ export function enableStorageService(): void {
storageServiceEnabled = true; 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({ export async function eraseAllStorageServiceState({
keepUnknownFields = false, keepUnknownFields = false,
}: { keepUnknownFields?: boolean } = {}): Promise<void> { }: { keepUnknownFields?: boolean } = {}): Promise<void> {
log.info('storageService.eraseAllStorageServiceState: starting...'); log.info('storageService.eraseAllStorageServiceState: starting...');
// First, update high-level storage service metadata
await Promise.all([ await Promise.all([
window.storage.remove('manifestVersion'), window.storage.remove('manifestVersion'),
keepUnknownFields keepUnknownFields
@ -1906,7 +1908,34 @@ export async function eraseAllStorageServiceState({
: window.storage.remove('storage-service-unknown-records'), : window.storage.remove('storage-service-unknown-records'),
window.storage.remove('storageCredentials'), 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'); log.info('storageService.eraseAllStorageServiceState: complete');
} }

View file

@ -107,6 +107,8 @@ const exclusiveInterface: ClientExclusiveInterface = {
// Client-side only // Client-side only
flushUpdateConversationBatcher,
shutdown, shutdown,
removeAllMessagesInConversation, removeAllMessagesInConversation,
@ -458,6 +460,9 @@ const updateConversationBatcher = createBatcher<ConversationType>({
function updateConversation(data: ConversationType): void { function updateConversation(data: ConversationType): void {
updateConversationBatcher.add(data); updateConversationBatcher.add(data);
} }
async function flushUpdateConversationBatcher(): Promise<void> {
await updateConversationBatcher.flushAndWait();
}
async function updateConversations( async function updateConversations(
array: Array<ConversationType> array: Array<ConversationType>

View file

@ -500,7 +500,6 @@ export type DataInterface = {
removeAllSessions: () => Promise<void>; removeAllSessions: () => Promise<void>;
getAllSessions: () => Promise<Array<SessionType>>; getAllSessions: () => Promise<Array<SessionType>>;
eraseStorageServiceStateFromConversations: () => Promise<void>;
getConversationCount: () => Promise<number>; getConversationCount: () => Promise<number>;
saveConversation: (data: ConversationType) => Promise<void>; saveConversation: (data: ConversationType) => Promise<void>;
saveConversations: (array: Array<ConversationType>) => Promise<void>; saveConversations: (array: Array<ConversationType>) => Promise<void>;
@ -794,6 +793,7 @@ export type DataInterface = {
removeAll: () => Promise<void>; removeAll: () => Promise<void>;
removeAllConfiguration: (type?: RemoveAllConfiguration) => Promise<void>; removeAllConfiguration: (type?: RemoveAllConfiguration) => Promise<void>;
eraseStorageServiceState: () => Promise<void>;
getMessagesNeedingUpgrade: ( getMessagesNeedingUpgrade: (
limit: number, limit: number,
@ -933,6 +933,7 @@ export type ClientExclusiveInterface = {
updateConversation: (data: ConversationType) => void; updateConversation: (data: ConversationType) => void;
removeConversation: (id: string) => Promise<void>; removeConversation: (id: string) => Promise<void>;
flushUpdateConversationBatcher: () => Promise<void>;
searchMessages: ({ searchMessages: ({
query, query,

View file

@ -249,7 +249,6 @@ const dataInterface: ServerInterface = {
removeAllSessions, removeAllSessions,
getAllSessions, getAllSessions,
eraseStorageServiceStateFromConversations,
getConversationCount, getConversationCount,
saveConversation, saveConversation,
saveConversations, saveConversations,
@ -384,6 +383,7 @@ const dataInterface: ServerInterface = {
removeAll, removeAll,
removeAllConfiguration, removeAllConfiguration,
eraseStorageServiceState,
getMessagesNeedingUpgrade, getMessagesNeedingUpgrade,
getMessagesWithVisualMediaAttachments, getMessagesWithVisualMediaAttachments,
@ -1621,18 +1621,6 @@ async function getConversationById(
return jsonToObject(row.json); return jsonToObject(row.json);
} }
async function eraseStorageServiceStateFromConversations(): Promise<void> {
const db = getInstance();
db.prepare<EmptyQuery>(
`
UPDATE conversations
SET
json = json_remove(json, '$.storageID', '$.needsStorageServiceSync', '$.unknownFields', '$.storageProfileKey');
`
).run();
}
function getAllConversationsSync(db = getInstance()): Array<ConversationType> { function getAllConversationsSync(db = getInstance()): Array<ConversationType> {
const rows: ConversationRows = db const rows: ConversationRows = db
.prepare<EmptyQuery>( .prepare<EmptyQuery>(
@ -5583,6 +5571,40 @@ async function removeAllConfiguration(
})(); })();
} }
async function eraseStorageServiceState(): Promise<void> {
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; const MAX_MESSAGE_MIGRATION_ATTEMPTS = 5;
async function getMessagesNeedingUpgrade( async function getMessagesNeedingUpgrade(

View file

@ -18,6 +18,8 @@ import {
import { storageServiceUploadJob } from '../../services/storage'; import { storageServiceUploadJob } from '../../services/storage';
import { sendStickerPackSync } from '../../shims/textsecure'; import { sendStickerPackSync } from '../../shims/textsecure';
import { trigger } from '../../shims/events'; import { trigger } from '../../shims/events';
import { ERASE_STORAGE_SERVICE } from './user';
import type { EraseStorageServiceStateAction } from './user';
import type { NoopActionType } from './noop'; import type { NoopActionType } from './noop';
@ -128,27 +130,27 @@ type UseStickerFulfilledAction = ReadonlyDeep<{
export type StickersActionType = ReadonlyDeep< export type StickersActionType = ReadonlyDeep<
| ClearInstalledStickerPackAction | ClearInstalledStickerPackAction
| InstallStickerPackFulfilledAction
| NoopActionType
| StickerAddedAction | StickerAddedAction
| StickerPackAddedAction | StickerPackAddedAction
| InstallStickerPackFulfilledAction
| UninstallStickerPackFulfilledAction
| StickerPackUpdatedAction
| StickerPackRemovedAction | StickerPackRemovedAction
| StickerPackUpdatedAction
| UninstallStickerPackFulfilledAction
| UseStickerFulfilledAction | UseStickerFulfilledAction
| NoopActionType
>; >;
// Action Creators // Action Creators
export const actions = { export const actions = {
downloadStickerPack,
clearInstalledStickerPack, clearInstalledStickerPack,
downloadStickerPack,
installStickerPack,
removeStickerPack, removeStickerPack,
stickerAdded, stickerAdded,
stickerPackAdded, stickerPackAdded,
installStickerPack,
uninstallStickerPack,
stickerPackUpdated, stickerPackUpdated,
uninstallStickerPack,
useSticker, useSticker,
}; };
@ -356,7 +358,7 @@ export function getEmptyState(): StickersStateType {
export function reducer( export function reducer(
state: Readonly<StickersStateType> = getEmptyState(), state: Readonly<StickersStateType> = getEmptyState(),
action: Readonly<StickersActionType> action: Readonly<StickersActionType | EraseStorageServiceStateAction>
): StickersStateType { ): StickersStateType {
if (action.type === 'stickers/STICKER_PACK_ADDED') { if (action.type === 'stickers/STICKER_PACK_ADDED') {
// ts complains due to `stickers: {}` being overridden by the payload // 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; return state;
} }

View file

@ -56,15 +56,29 @@ type UserChangedActionType = ReadonlyDeep<{
}; };
}>; }>;
export type UserActionType = ReadonlyDeep<UserChangedActionType>; 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 // Action Creators
export const actions = { export const actions = {
eraseStorageServiceState,
userChanged, userChanged,
manualReconnect, manualReconnect,
}; };
function eraseStorageServiceState(): EraseStorageServiceStateAction {
return {
type: ERASE_STORAGE_SERVICE,
};
}
function userChanged(attributes: { function userChanged(attributes: {
interactionMode?: 'mouse' | 'keyboard'; interactionMode?: 'mouse' | 'keyboard';
ourConversationId?: string; ourConversationId?: string;