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 {
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<void> {
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();

View file

@ -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<void> {
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');
}

View file

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

View file

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

View file

@ -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<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> {
const rows: ConversationRows = db
.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;
async function getMessagesNeedingUpgrade(

View file

@ -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<StickersStateType> = getEmptyState(),
action: Readonly<StickersActionType>
action: Readonly<StickersActionType | EraseStorageServiceStateAction>
): 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;
}

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
export const actions = {
eraseStorageServiceState,
userChanged,
manualReconnect,
};
function eraseStorageServiceState(): EraseStorageServiceStateAction {
return {
type: ERASE_STORAGE_SERVICE,
};
}
function userChanged(attributes: {
interactionMode?: 'mouse' | 'keyboard';
ourConversationId?: string;