diff --git a/lib/facts/backup-service.js b/lib/facts/backup-service.js new file mode 100644 index 000000000..3aa21955a --- /dev/null +++ b/lib/facts/backup-service.js @@ -0,0 +1,15 @@ +// @flow + +import { isDev } from '../utils/dev-utils.js'; + +type BackupServiceConfig = { + +url: string, +}; + +const config: BackupServiceConfig = { + url: isDev + ? 'https://backup.staging.commtechnologies.org' + : 'https://backup.commtechnologies.org', +}; + +export default config; diff --git a/web/database/utils/constants.js b/web/database/utils/constants.js index f930a7088..defc1c6e8 100644 --- a/web/database/utils/constants.js +++ b/web/database/utils/constants.js @@ -1,46 +1,48 @@ // @flow import localforage from 'localforage'; export const SQLITE_CONTENT = 'sqliteFileContent'; export const SQLITE_ENCRYPTION_KEY = 'encryptionKey'; export const CURRENT_USER_ID_KEY = 'current_user_id'; export const DATABASE_WORKER_PATH = 'worker/database'; export const WORKERS_MODULES_DIR_PATH = '/compiled/webworkers'; export const DEFAULT_COMM_QUERY_EXECUTOR_FILENAME = 'comm_query_executor.wasm'; export const DEFAULT_BACKUP_CLIENT_FILENAME = 'backup-client-wasm_bg.wasm'; export const DEFAULT_OLM_FILENAME = 'olm.wasm'; export const COMM_SQLITE_DATABASE_PATH = 'comm.sqlite'; +export const COMM_SQLITE_BACKUP_RESTORE_DATABASE_PATH = + 'comm_backup_restore.sqlite'; export const NOTIFICATIONS_OLM_DATA_CONTENT = 'notificationsOlmDataContent'; export const NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY = 'notificationsOlmDataEncryptionKey'; export const DB_SUPPORTED_OS: $ReadOnlyArray = [ 'Windows 10', 'Linux', 'Mac OS', ]; export const DB_SUPPORTED_BROWSERS: $ReadOnlyArray = [ 'edge', 'edge-chromium', 'chrome', 'firefox', 'opera', 'safari', ]; export const localforageConfig: PartialConfig = { driver: localforage.INDEXEDDB, name: 'comm', storeName: 'commStorage', description: 'Comm encrypted database storage', version: '1.0', }; diff --git a/web/database/worker/backup.js b/web/database/worker/backup.js new file mode 100644 index 000000000..aa76fc55b --- /dev/null +++ b/web/database/worker/backup.js @@ -0,0 +1,64 @@ +// @flow + +import backupService from 'lib/facts/backup-service.js'; +import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; + +import { getProcessingStoreOpsExceptionMessage } from './process-operations.js'; +import { + BackupClient, + RequestedData, +} from '../../backup-client-wasm/wasm/backup-client-wasm.js'; +import { completeRootKey } from '../../redux/persist-constants.js'; +import type { EmscriptenModule } from '../types/module.js'; +import type { SQLiteQueryExecutor } from '../types/sqlite-query-executor.js'; +import { COMM_SQLITE_BACKUP_RESTORE_DATABASE_PATH } from '../utils/constants.js'; +import { importDatabaseContent } from '../utils/db-utils.js'; + +async function restoreBackup( + sqliteQueryExecutor: SQLiteQueryExecutor, + dbModule: EmscriptenModule, + authMetadata: AuthMetadata, + backupID: string, + backupDataKey: string, +) { + const { userID, deviceID, accessToken } = authMetadata; + if (!userID || !deviceID || !accessToken) { + throw new Error('Backup restore requires full authMetadata'); + } + const userIdentity = { userID, deviceID, accessToken }; + + const client = new BackupClient(backupService.url); + const result = await client.downloadBackupData( + { + type: 'BackupID', + backupID, + userIdentity, + }, + RequestedData.UserData, + ); + + importDatabaseContent( + result, + dbModule, + COMM_SQLITE_BACKUP_RESTORE_DATABASE_PATH, + ); + + try { + const reduxPersistData = + sqliteQueryExecutor.getPersistStorageItem(completeRootKey); + + sqliteQueryExecutor.restoreFromMainCompaction( + COMM_SQLITE_BACKUP_RESTORE_DATABASE_PATH, + backupDataKey, + ); + + sqliteQueryExecutor.setPersistStorageItem( + completeRootKey, + reduxPersistData, + ); + } catch (err) { + throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); + } +} + +export { restoreBackup }; diff --git a/web/database/worker/db-worker.js b/web/database/worker/db-worker.js index dedf491bf..11457cd2a 100644 --- a/web/database/worker/db-worker.js +++ b/web/database/worker/db-worker.js @@ -1,291 +1,300 @@ // @flow import localforage from 'localforage'; +import { restoreBackup } from './backup.js'; import { getClientStoreFromQueryExecutor, processDBStoreOperations, } from './process-operations.js'; import initBackupClientModule from '../../backup-client-wasm/wasm/backup-client-wasm.js'; import { decryptData, encryptData, generateCryptoKey, importJWKKey, type EncryptedData, } from '../../crypto/aes-gcm-crypto-utils.js'; import { type SharedWorkerMessageEvent, type WorkerRequestMessage, type WorkerResponseMessage, workerRequestMessageTypes, workerResponseMessageTypes, type WorkerRequestProxyMessage, workerWriteRequests, } from '../../types/worker-types.js'; import { getDatabaseModule } from '../db-module.js'; import { type EmscriptenModule } from '../types/module.js'; import { type SQLiteQueryExecutor } from '../types/sqlite-query-executor.js'; import { COMM_SQLITE_DATABASE_PATH, CURRENT_USER_ID_KEY, localforageConfig, SQLITE_CONTENT, SQLITE_ENCRYPTION_KEY, DEFAULT_BACKUP_CLIENT_FILENAME, } from '../utils/constants.js'; import { clearSensitiveData, exportDatabaseContent, importDatabaseContent, } from '../utils/db-utils.js'; localforage.config(localforageConfig); let encryptionKey: ?CryptoKey = null; let sqliteQueryExecutor: ?SQLiteQueryExecutor = null; let dbModule: ?EmscriptenModule = null; let persistNeeded: boolean = false; let persistInProgress: boolean = false; async function initDatabase( webworkerModulesFilePath: string, commQueryExecutorFilename: ?string, encryptionKeyJWK?: ?SubtleCrypto$JsonWebKey, ) { if (!!dbModule && !!sqliteQueryExecutor) { console.log('Database already initialized'); return; } const newModule = dbModule ? dbModule : getDatabaseModule(commQueryExecutorFilename, webworkerModulesFilePath); if (!dbModule) { dbModule = newModule; } if (encryptionKeyJWK) { encryptionKey = await importJWKKey(encryptionKeyJWK); } else { encryptionKey = await localforage.getItem(SQLITE_ENCRYPTION_KEY); if (!encryptionKey) { const cryptoKey = await generateCryptoKey({ extractable: false }); await localforage.setItem(SQLITE_ENCRYPTION_KEY, cryptoKey); } } const encryptedContent = await localforage.getItem(SQLITE_CONTENT); let dbContent = null; try { if (encryptionKey && encryptedContent) { dbContent = await decryptData(encryptedContent, encryptionKey); } } catch (e) { console.error('Error while decrypting content, clearing database content'); await localforage.removeItem(SQLITE_CONTENT); } if (dbContent) { importDatabaseContent(dbContent, newModule, COMM_SQLITE_DATABASE_PATH); console.info( 'Database exists and is properly encrypted, using persisted data', ); } else { console.info('Creating fresh database'); } sqliteQueryExecutor = new newModule.SQLiteQueryExecutor( COMM_SQLITE_DATABASE_PATH, ); } async function initBackupClient( webworkerModulesFilePath: string, backupClientFilename: ?string, ) { let modulePath; if (backupClientFilename) { modulePath = `${webworkerModulesFilePath}/${backupClientFilename}`; } else { modulePath = `${webworkerModulesFilePath}/${DEFAULT_BACKUP_CLIENT_FILENAME}`; } await initBackupClientModule(modulePath); } async function persist() { persistInProgress = true; const module = dbModule; if (!sqliteQueryExecutor || !module) { persistInProgress = false; throw new Error( 'Database not initialized while persisting database content', ); } if (!encryptionKey) { encryptionKey = await localforage.getItem(SQLITE_ENCRYPTION_KEY); } while (persistNeeded) { persistNeeded = false; const dbData = exportDatabaseContent(module, COMM_SQLITE_DATABASE_PATH); if (!encryptionKey) { persistInProgress = false; throw new Error('Encryption key is missing'); } const encryptedData = await encryptData(dbData, encryptionKey); await localforage.setItem(SQLITE_CONTENT, encryptedData); } persistInProgress = false; } async function processAppRequest( message: WorkerRequestMessage, ): Promise { // non-database operations if (message.type === workerRequestMessageTypes.PING) { return { type: workerResponseMessageTypes.PONG, text: 'PONG', }; } else if ( message.type === workerRequestMessageTypes.GENERATE_DATABASE_ENCRYPTION_KEY ) { const cryptoKey = await generateCryptoKey({ extractable: false }); await localforage.setItem(SQLITE_ENCRYPTION_KEY, cryptoKey); return undefined; } // database operations if (message.type === workerRequestMessageTypes.INIT) { const promises = [ initDatabase( message.webworkerModulesFilePath, message.commQueryExecutorFilename, message.encryptionKey, ), ]; if (message.backupClientFilename !== undefined) { promises.push( initBackupClient( message.webworkerModulesFilePath, message.backupClientFilename, ), ); } await Promise.all(promises); return undefined; } else if (message.type === workerRequestMessageTypes.CLEAR_SENSITIVE_DATA) { encryptionKey = null; await localforage.clear(); if (dbModule && sqliteQueryExecutor) { clearSensitiveData( dbModule, COMM_SQLITE_DATABASE_PATH, sqliteQueryExecutor, ); } sqliteQueryExecutor = null; return undefined; } if (!sqliteQueryExecutor) { throw new Error( `Database not initialized, unable to process request type: ${message.type}`, ); } // read-only operations if (message.type === workerRequestMessageTypes.GET_CLIENT_STORE) { return { type: workerResponseMessageTypes.CLIENT_STORE, store: getClientStoreFromQueryExecutor(sqliteQueryExecutor), }; } else if (message.type === workerRequestMessageTypes.GET_CURRENT_USER_ID) { return { type: workerResponseMessageTypes.GET_CURRENT_USER_ID, userID: sqliteQueryExecutor.getMetadata(CURRENT_USER_ID_KEY), }; } else if ( message.type === workerRequestMessageTypes.GET_PERSIST_STORAGE_ITEM ) { return { type: workerResponseMessageTypes.GET_PERSIST_STORAGE_ITEM, item: sqliteQueryExecutor.getPersistStorageItem(message.key), }; } // write operations if (!workerWriteRequests.includes(message.type)) { throw new Error('Request type not supported'); } if (!sqliteQueryExecutor || !dbModule) { throw new Error( `Database not initialized, unable to process request type: ${message.type}`, ); } if (message.type === workerRequestMessageTypes.PROCESS_STORE_OPERATIONS) { processDBStoreOperations( sqliteQueryExecutor, message.storeOperations, dbModule, ); } else if (message.type === workerRequestMessageTypes.SET_CURRENT_USER_ID) { sqliteQueryExecutor.setMetadata(CURRENT_USER_ID_KEY, message.userID); } else if ( message.type === workerRequestMessageTypes.SET_PERSIST_STORAGE_ITEM ) { sqliteQueryExecutor.setPersistStorageItem(message.key, message.item); } else if ( message.type === workerRequestMessageTypes.REMOVE_PERSIST_STORAGE_ITEM ) { sqliteQueryExecutor.removePersistStorageItem(message.key); + } else if (message.type === workerRequestMessageTypes.BACKUP_RESTORE) { + await restoreBackup( + sqliteQueryExecutor, + dbModule, + message.authMetadata, + message.backupID, + message.backupDataKey, + ); } persistNeeded = true; if (!persistInProgress) { void persist(); } return undefined; } function connectHandler(event: SharedWorkerMessageEvent) { if (!event.ports.length) { return; } const port: MessagePort = event.ports[0]; console.log('Web database worker alive!'); port.onmessage = async function (messageEvent: MessageEvent) { const data: WorkerRequestProxyMessage = (messageEvent.data: any); const { id, message } = data; if (!id) { port.postMessage({ error: 'Request without identifier', }); } try { const result = await processAppRequest(message); port.postMessage({ id, message: result, }); } catch (e) { port.postMessage({ id, error: e.message, }); } }; } self.addEventListener('connect', connectHandler); diff --git a/web/database/worker/process-operations.js b/web/database/worker/process-operations.js index 9e1ea5a68..7c3c74ada 100644 --- a/web/database/worker/process-operations.js +++ b/web/database/worker/process-operations.js @@ -1,264 +1,268 @@ // @flow import type { ClientDBCommunityStoreOperation } from 'lib/ops/community-store-ops.js'; import type { ClientDBKeyserverStoreOperation } from 'lib/ops/keyserver-store-ops.js'; import type { ClientDBReportStoreOperation } from 'lib/ops/report-store-ops.js'; import type { ClientDBThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import type { ClientDBDraftStoreOperation, DraftStoreOperation, } from 'lib/types/draft-types.js'; import type { ClientDBStore, ClientDBStoreOperations, } from 'lib/types/store-ops-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { clientDBThreadInfoToWebThread, webThreadToClientDBThreadInfo, } from '../types/entities.js'; import type { EmscriptenModule } from '../types/module.js'; import type { SQLiteQueryExecutor } from '../types/sqlite-query-executor.js'; function getProcessingStoreOpsExceptionMessage( e: mixed, module: EmscriptenModule, ): string { if (typeof e === 'number') { return module.getExceptionMessage(e); } return getMessageForException(e) ?? 'unknown error'; } function processDraftStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: DraftStoreOperation of operations) { try { if (operation.type === 'remove_all') { sqliteQueryExecutor.removeAllDrafts(); } else if (operation.type === 'remove') { const { ids } = operation.payload; sqliteQueryExecutor.removeDrafts(ids); } else if (operation.type === 'update') { const { key, text } = operation.payload; sqliteQueryExecutor.updateDraft(key, text); } else if (operation.type === 'move') { const { oldKey, newKey } = operation.payload; sqliteQueryExecutor.moveDraft(oldKey, newKey); } else { throw new Error('Unsupported draft operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } draft operation: ${getProcessingStoreOpsExceptionMessage(e, module)}`, ); } } } function processReportStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBReportStoreOperation of operations) { try { if (operation.type === 'remove_all_reports') { sqliteQueryExecutor.removeAllReports(); } else if (operation.type === 'remove_reports') { const { ids } = operation.payload; sqliteQueryExecutor.removeReports(ids); } else if (operation.type === 'replace_report') { const { id, report } = operation.payload; sqliteQueryExecutor.replaceReport({ id, report }); } else { throw new Error('Unsupported report operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } report operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } function processThreadStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBThreadStoreOperation of operations) { try { if (operation.type === 'remove_all') { sqliteQueryExecutor.removeAllThreads(); } else if (operation.type === 'remove') { const { ids } = operation.payload; sqliteQueryExecutor.removeThreads(ids); } else if (operation.type === 'replace') { sqliteQueryExecutor.replaceThreadWeb( clientDBThreadInfoToWebThread(operation.payload), ); } else { throw new Error('Unsupported thread operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } thread operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } function processKeyserverStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBKeyserverStoreOperation of operations) { try { if (operation.type === 'remove_all_keyservers') { sqliteQueryExecutor.removeAllKeyservers(); } else if (operation.type === 'remove_keyservers') { const { ids } = operation.payload; sqliteQueryExecutor.removeKeyservers(ids); } else if (operation.type === 'replace_keyserver') { const { id, keyserverInfo } = operation.payload; sqliteQueryExecutor.replaceKeyserver({ id, keyserverInfo }); } else { throw new Error('Unsupported keyserver operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } keyserver operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } function processCommunityStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBCommunityStoreOperation of operations) { try { if (operation.type === 'remove_all_communities') { sqliteQueryExecutor.removeAllCommunities(); } else if (operation.type === 'remove_communities') { const { ids } = operation.payload; sqliteQueryExecutor.removeCommunities(ids); } else if (operation.type === 'replace_community') { const { id, communityInfo } = operation.payload; sqliteQueryExecutor.replaceCommunity({ id, communityInfo }); } else { throw new Error('Unsupported community operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } community operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } function processDBStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, storeOperations: ClientDBStoreOperations, module: EmscriptenModule, ) { const { draftStoreOperations, reportStoreOperations, threadStoreOperations, keyserverStoreOperations, communityStoreOperations, } = storeOperations; try { sqliteQueryExecutor.beginTransaction(); if (draftStoreOperations && draftStoreOperations.length > 0) { processDraftStoreOperations( sqliteQueryExecutor, draftStoreOperations, module, ); } if (reportStoreOperations && reportStoreOperations.length > 0) { processReportStoreOperations( sqliteQueryExecutor, reportStoreOperations, module, ); } if (threadStoreOperations && threadStoreOperations.length > 0) { processThreadStoreOperations( sqliteQueryExecutor, threadStoreOperations, module, ); } if (keyserverStoreOperations && keyserverStoreOperations.length > 0) { processKeyserverStoreOperations( sqliteQueryExecutor, keyserverStoreOperations, module, ); } if (communityStoreOperations && communityStoreOperations.length > 0) { processCommunityStoreOperations( sqliteQueryExecutor, communityStoreOperations, module, ); } sqliteQueryExecutor.commitTransaction(); } catch (e) { sqliteQueryExecutor.rollbackTransaction(); console.log('Error while processing store ops: ', e); throw e; } } function getClientStoreFromQueryExecutor( sqliteQueryExecutor: SQLiteQueryExecutor, ): ClientDBStore { return { drafts: sqliteQueryExecutor.getAllDrafts(), messages: [], threads: sqliteQueryExecutor .getAllThreadsWeb() .map(t => webThreadToClientDBThreadInfo(t)), messageStoreThreads: [], reports: sqliteQueryExecutor.getAllReports(), users: [], keyservers: sqliteQueryExecutor.getAllKeyservers(), communities: sqliteQueryExecutor.getAllCommunities(), }; } -export { processDBStoreOperations, getClientStoreFromQueryExecutor }; +export { + processDBStoreOperations, + getProcessingStoreOpsExceptionMessage, + getClientStoreFromQueryExecutor, +}; diff --git a/web/redux/persist-constants.js b/web/redux/persist-constants.js new file mode 100644 index 000000000..1c24fc4f1 --- /dev/null +++ b/web/redux/persist-constants.js @@ -0,0 +1,7 @@ +// @flow + +const rootKey = 'root'; +const rootKeyPrefix = 'persist:'; +const completeRootKey = `${rootKeyPrefix}${rootKey}`; + +export { rootKey, rootKeyPrefix, completeRootKey }; diff --git a/web/redux/persist.js b/web/redux/persist.js index 135fb1541..3a1293f93 100644 --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -1,314 +1,314 @@ // @flow import invariant from 'invariant'; import { getStoredState, purgeStoredState } from 'redux-persist'; import storage from 'redux-persist/es/storage/index.js'; import type { PersistConfig } from 'redux-persist/src/types.js'; import { type ClientDBKeyserverStoreOperation, keyserverStoreOpsHandlers, type ReplaceKeyserverOperation, } from 'lib/ops/keyserver-store-ops.js'; import { createAsyncMigrate, type StorageMigrationFunction, } from 'lib/shared/create-async-migrate.js'; import { keyserverStoreTransform } from 'lib/shared/transforms/keyserver-store-transform.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import { cookieTypes } from 'lib/types/session-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js'; import { parseCookies } from 'lib/utils/cookie-utils.js'; import { isDev } from 'lib/utils/dev-utils.js'; import { wipeKeyserverStore } from 'lib/utils/keyserver-store-utils.js'; import { generateIDSchemaMigrationOpsForDrafts, convertDraftStoreToNewIDSchema, } from 'lib/utils/migration-utils.js'; import { entries } from 'lib/utils/objects.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import commReduxStorageEngine from './comm-redux-storage-engine.js'; import { defaultWebState } from './default-state.js'; +import { rootKey, rootKeyPrefix } from './persist-constants.js'; import type { AppState } from './redux-setup.js'; import { nonUserSpecificFieldsWeb } from './redux-setup.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { getDatabaseModule } from '../database/database-module-provider.js'; import { isSQLiteSupported } from '../database/utils/db-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; declare var keyserverURL: string; const persistWhitelist = [ 'enabledApps', 'cryptoStore', 'notifPermissionAlertInfo', 'commServicesAccessToken', 'keyserverStore', 'globalThemeInfo', 'customServer', ]; function handleReduxMigrationFailure(oldState: AppState): AppState { const persistedNonUserSpecificFields = nonUserSpecificFieldsWeb.filter( field => persistWhitelist.includes(field) || field === '_persist', ); const stateAfterReset = resetUserSpecificState( oldState, defaultWebState, persistedNonUserSpecificFields, ); return { ...stateAfterReset, keyserverStore: wipeKeyserverStore(stateAfterReset.keyserverStore), }; } const migrations = { [1]: async (state: any) => { const { primaryIdentityPublicKey, ...stateWithoutPrimaryIdentityPublicKey } = state; return { ...stateWithoutPrimaryIdentityPublicKey, cryptoStore: { primaryAccount: null, primaryIdentityKeys: null, notificationAccount: null, notificationIdentityKeys: null, }, }; }, [2]: async (state: AppState) => { return state; }, [3]: async (state: AppState) => { let newState = state; if (state.draftStore) { newState = { ...newState, draftStore: convertDraftStoreToNewIDSchema(state.draftStore), }; } const databaseModule = await getDatabaseModule(); const isDatabaseSupported = await databaseModule.isDatabaseSupported(); if (!isDatabaseSupported) { return newState; } const stores = await databaseModule.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); invariant(stores?.store, 'Stores should exist'); await databaseModule.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations: generateIDSchemaMigrationOpsForDrafts( stores.store.drafts, ), }, }); return newState; }, [4]: async (state: any) => { const { lastCommunicatedPlatformDetails, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], lastCommunicatedPlatformDetails, }, }, }, }; }, [5]: async (state: any) => { const databaseModule = await getDatabaseModule(); const isDatabaseSupported = await databaseModule.isDatabaseSupported(); if (!isDatabaseSupported) { return state; } if (!state.draftStore) { return state; } const { drafts } = state.draftStore; const draftStoreOperations = []; for (const key in drafts) { const text = drafts[key]; draftStoreOperations.push({ type: 'update', payload: { key, text }, }); } await databaseModule.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations }, }); return state; }, [6]: async (state: AppState) => ({ ...state, integrityStore: { threadHashes: {}, threadHashingStatus: 'starting' }, }), [7]: async (state: AppState): Promise => { if (!document.cookie) { return state; } const params = parseCookies(document.cookie); let cookie = null; if (params[cookieTypes.USER]) { cookie = `${cookieTypes.USER}=${params[cookieTypes.USER]}`; } else if (params[cookieTypes.ANONYMOUS]) { cookie = `${cookieTypes.ANONYMOUS}=${params[cookieTypes.ANONYMOUS]}`; } return { ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: { ...state.keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...state.keyserverStore.keyserverInfos[authoritativeKeyserverID], cookie, }, }, }, }; }, [8]: async (state: AppState) => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [9]: async (state: AppState) => ({ ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: { ...state.keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...state.keyserverStore.keyserverInfos[authoritativeKeyserverID], urlPrefix: keyserverURL, }, }, }, }), [10]: async (state: AppState) => { const { keyserverInfos } = state.keyserverStore; const newKeyserverInfos: { [string]: KeyserverInfo } = {}; for (const key in keyserverInfos) { newKeyserverInfos[key] = { ...keyserverInfos[key], connection: { ...defaultConnectionInfo }, updatesCurrentAsOf: 0, sessionID: null, }; } return { ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: newKeyserverInfos, }, }; }, [11]: async (state: AppState) => { const databaseModule = await getDatabaseModule(); const isDatabaseSupported = await databaseModule.isDatabaseSupported(); if (!isDatabaseSupported) { return state; } const replaceOps: $ReadOnlyArray = entries( state.keyserverStore.keyserverInfos, ).map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo, }, })); const keyserverStoreOperations: $ReadOnlyArray = keyserverStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_keyservers' }, ...replaceOps, ]); try { await databaseModule.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { keyserverStoreOperations }, }); return state; } catch (e) { console.log(e); return handleReduxMigrationFailure(state); } }, }; -const rootKey = 'root'; - const migrateStorageToSQLite: StorageMigrationFunction = async debug => { const databaseModule = await getDatabaseModule(); const isSupported = await databaseModule.isDatabaseSupported(); if (!isSupported) { return undefined; } const oldStorage = await getStoredState({ storage, key: rootKey }); if (!oldStorage) { return undefined; } purgeStoredState({ storage, key: rootKey }); if (debug) { console.log('redux-persist: migrating state to SQLite storage'); } const allKeys = Object.keys(oldStorage); const transforms = persistConfig.transforms ?? []; const newStorage = { ...oldStorage }; for (const transform of transforms) { for (const key of allKeys) { const transformedStore = transform.out(newStorage[key], key, newStorage); newStorage[key] = transformedStore; } } return newStorage; }; const persistConfig: PersistConfig = { + keyPrefix: rootKeyPrefix, key: rootKey, storage: commReduxStorageEngine, whitelist: isSQLiteSupported() ? persistWhitelist : [...persistWhitelist, 'draftStore'], migrate: (createAsyncMigrate( migrations, { debug: isDev }, migrateStorageToSQLite, ): any), version: 11, transforms: [keyserverStoreTransform], }; export { persistConfig }; diff --git a/web/types/worker-types.js b/web/types/worker-types.js index c4a205129..ad0ceaff3 100644 --- a/web/types/worker-types.js +++ b/web/types/worker-types.js @@ -1,147 +1,158 @@ // @flow +import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; import type { ClientDBStore, ClientDBStoreOperations, } from 'lib/types/store-ops-types.js'; // The types of messages sent from app to worker export const workerRequestMessageTypes = Object.freeze({ PING: 0, INIT: 1, GENERATE_DATABASE_ENCRYPTION_KEY: 2, PROCESS_STORE_OPERATIONS: 3, GET_CLIENT_STORE: 4, SET_CURRENT_USER_ID: 5, GET_CURRENT_USER_ID: 6, GET_PERSIST_STORAGE_ITEM: 7, SET_PERSIST_STORAGE_ITEM: 8, REMOVE_PERSIST_STORAGE_ITEM: 9, CLEAR_SENSITIVE_DATA: 10, + BACKUP_RESTORE: 11, }); export const workerWriteRequests: $ReadOnlyArray = [ workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, workerRequestMessageTypes.SET_CURRENT_USER_ID, workerRequestMessageTypes.SET_PERSIST_STORAGE_ITEM, workerRequestMessageTypes.REMOVE_PERSIST_STORAGE_ITEM, + workerRequestMessageTypes.BACKUP_RESTORE, ]; export type PingWorkerRequestMessage = { +type: 0, +text: string, }; export type InitWorkerRequestMessage = { +type: 1, +webworkerModulesFilePath: string, +commQueryExecutorFilename: ?string, +encryptionKey?: ?SubtleCrypto$JsonWebKey, +backupClientFilename?: ?string, }; export type GenerateDatabaseEncryptionKeyRequestMessage = { +type: 2, }; export type ProcessStoreOperationsRequestMessage = { +type: 3, +storeOperations: ClientDBStoreOperations, }; export type GetClientStoreRequestMessage = { +type: 4, }; export type SetCurrentUserIDRequestMessage = { +type: 5, +userID: string, }; export type GetCurrentUserIDRequestMessage = { +type: 6, }; export type GetPersistStorageItemRequestMessage = { +type: 7, +key: string, }; export type SetPersistStorageItemRequestMessage = { +type: 8, +key: string, +item: string, }; export type RemovePersistStorageItemRequestMessage = { +type: 9, +key: string, }; export type ClearSensitiveDataRequestMessage = { +type: 10, }; +export type BackupRestoreRequestMessage = { + +type: 11, + +authMetadata: AuthMetadata, + +backupID: string, + +backupDataKey: string, +}; + export type WorkerRequestMessage = | PingWorkerRequestMessage | InitWorkerRequestMessage | GenerateDatabaseEncryptionKeyRequestMessage | ProcessStoreOperationsRequestMessage | GetClientStoreRequestMessage | SetCurrentUserIDRequestMessage | GetCurrentUserIDRequestMessage | GetPersistStorageItemRequestMessage | SetPersistStorageItemRequestMessage | RemovePersistStorageItemRequestMessage - | ClearSensitiveDataRequestMessage; + | ClearSensitiveDataRequestMessage + | BackupRestoreRequestMessage; export type WorkerRequestProxyMessage = { +id: number, +message: WorkerRequestMessage, }; // The types of messages sent from worker to app export const workerResponseMessageTypes = Object.freeze({ PONG: 0, CLIENT_STORE: 1, GET_CURRENT_USER_ID: 2, GET_PERSIST_STORAGE_ITEM: 3, }); export type PongWorkerResponseMessage = { +type: 0, +text: string, }; export type ClientStoreResponseMessage = { +type: 1, +store: ClientDBStore, }; export type GetCurrentUserIDResponseMessage = { +type: 2, +userID: ?string, }; export type GetPersistStorageItemResponseMessage = { +type: 3, +item: string, }; export type WorkerResponseMessage = | PongWorkerResponseMessage | ClientStoreResponseMessage | GetCurrentUserIDResponseMessage | GetPersistStorageItemResponseMessage; export type WorkerResponseProxyMessage = { +id?: number, +message?: WorkerResponseMessage, +error?: string, }; // SharedWorker types export type SharedWorkerMessageEvent = MessageEvent & { +ports: $ReadOnlyArray, ... };