diff --git a/lib/types/sqlite-types.js b/lib/types/sqlite-types.js index 2dfb9206b..29a1c6996 100644 --- a/lib/types/sqlite-types.js +++ b/lib/types/sqlite-types.js @@ -1,62 +1,67 @@ // @flow import type { ClientDBMessageInfo } from './message-types.js'; import type { StoreOperations } from './store-ops-types.js'; export const outboundP2PMessageStatuses = Object.freeze({ // The message was prepared to be sent to other peers, but it's not encrypted. // It was inserted into DB in the same transaction as making changes to // the store. persisted: 'persisted', // Encryption is done in the same transaction as persisting the CryptoModule, // and message order is also tracked on the client side, // which means the message can be sent. encrypted: 'encrypted', // The message was sent to another peer (Tunnelbroker owns it), // waiting for the confirmation (handled in `peerToPeerMessageHandler`). sent: 'sent', }); export type OutboundP2PMessageStatuses = $Values< typeof outboundP2PMessageStatuses, >; export type InboundP2PMessage = { +messageID: string, +senderDeviceID: string, +plaintext: string, +status: string, }; export type OutboundP2PMessage = { +messageID: string, +deviceID: string, +userID: string, +timestamp: string, +plaintext: string, +ciphertext: string, +status: OutboundP2PMessageStatuses, +supportsAutoRetry: boolean, }; export type SQLiteAPI = { // read operations - +getAllInboundP2PMessage: () => Promise, - +getAllOutboundP2PMessage: () => Promise, - +getRelatedMessages: (messageID: string) => Promise, + +getAllInboundP2PMessage: () => Promise>, + +getAllOutboundP2PMessage: () => Promise>, + +getRelatedMessages: ( + messageID: string, + ) => Promise>, + +getOutboundP2PMessagesByID: ( + ids: $ReadOnlyArray, + ) => Promise>, // write operations +removeInboundP2PMessages: (ids: $ReadOnlyArray) => Promise, +markOutboundP2PMessageAsSent: ( messageID: string, deviceID: string, ) => Promise, +removeOutboundP2PMessagesOlderThan: ( messageID: string, deviceID: string, ) => Promise, +processDBStoreOperations: ( operations: StoreOperations, userID?: ?string, ) => Promise, }; diff --git a/lib/utils/__mocks__/config.js b/lib/utils/__mocks__/config.js index 2de245d37..b35cad26f 100644 --- a/lib/utils/__mocks__/config.js +++ b/lib/utils/__mocks__/config.js @@ -1,44 +1,45 @@ // @flow import { type Config } from '../config.js'; const getConfig = (): Config => ({ resolveKeyserverSessionInvalidationUsingNativeCredentials: null, setSessionIDOnRequest: true, calendarRangeInactivityLimit: null, platformDetails: { platform: 'web', codeVersion: 70, stateVersion: 50, }, authoritativeKeyserverID: '123', olmAPI: { initializeCryptoAccount: jest.fn(), getUserPublicKey: jest.fn(), encrypt: jest.fn(), encryptAndPersist: jest.fn(), decrypt: jest.fn(), decryptSequentialAndPersist: jest.fn(), contentInboundSessionCreator: jest.fn(), contentOutboundSessionCreator: jest.fn(), notificationsSessionCreator: jest.fn(), getOneTimeKeys: jest.fn(), validateAndUploadPrekeys: jest.fn(), signMessage: jest.fn(), verifyMessage: jest.fn(), markPrekeysAsPublished: jest.fn(), }, sqliteAPI: { getAllInboundP2PMessage: jest.fn(), removeInboundP2PMessages: jest.fn(), processDBStoreOperations: jest.fn(), getAllOutboundP2PMessage: jest.fn(), markOutboundP2PMessageAsSent: jest.fn(), removeOutboundP2PMessagesOlderThan: jest.fn(), getRelatedMessages: jest.fn(), + getOutboundP2PMessagesByID: jest.fn(), }, }); const hasConfig = (): boolean => true; export { getConfig, hasConfig }; diff --git a/native/database/sqlite-api.js b/native/database/sqlite-api.js index d3c67ab63..ceaabd521 100644 --- a/native/database/sqlite-api.js +++ b/native/database/sqlite-api.js @@ -1,23 +1,24 @@ // @flow import type { SQLiteAPI } from 'lib/types/sqlite-types.js'; import { commCoreModule } from '../native-modules.js'; import { processDBStoreOperations } from '../redux/redux-utils.js'; const sqliteAPI: SQLiteAPI = { // read operations getAllInboundP2PMessage: commCoreModule.getAllInboundP2PMessage, getAllOutboundP2PMessage: commCoreModule.getAllOutboundP2PMessage, getRelatedMessages: commCoreModule.getRelatedMessages, + getOutboundP2PMessagesByID: commCoreModule.getOutboundP2PMessagesByID, // write operations removeInboundP2PMessages: commCoreModule.removeInboundP2PMessages, markOutboundP2PMessageAsSent: commCoreModule.markOutboundP2PMessageAsSent, removeOutboundP2PMessagesOlderThan: commCoreModule.removeOutboundP2PMessagesOlderThan, processDBStoreOperations, }; export { sqliteAPI }; diff --git a/native/schema/CommCoreModuleSchema.js b/native/schema/CommCoreModuleSchema.js index 94a30ad0c..de1de3622 100644 --- a/native/schema/CommCoreModuleSchema.js +++ b/native/schema/CommCoreModuleSchema.js @@ -1,203 +1,203 @@ // @flow 'use strict'; import { TurboModuleRegistry } from 'react-native'; import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport.js'; import type { ClientDBMessageStoreOperation } from 'lib/ops/message-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 { OneTimeKeysResult, SignedPrekeys, ClientPublicKeys, EncryptedData, OutboundSessionCreationResult, } from 'lib/types/crypto-types.js'; import type { ClientDBMessageInfo } from 'lib/types/message-types.js'; import type { SIWEBackupSecrets } from 'lib/types/siwe-types.js'; import type { InboundP2PMessage, OutboundP2PMessage, } from 'lib/types/sqlite-types.js'; import type { ClientDBStore, ClientDBStoreOperations, } from 'lib/types/store-ops-types'; import type { ClientDBThreadInfo } from 'lib/types/thread-types.js'; type CommServicesAuthMetadata = { +userID?: ?string, +deviceID?: ?string, +accessToken?: ?string, }; interface Spec extends TurboModule { +getDraft: (key: string) => Promise; +updateDraft: (key: string, text: string) => Promise; +moveDraft: (oldKey: string, newKey: string) => Promise; +getClientDBStore: () => Promise; +removeAllDrafts: () => Promise; +getAllMessagesSync: () => $ReadOnlyArray; +processMessageStoreOperationsSync: ( operations: $ReadOnlyArray, ) => void; +getAllThreadsSync: () => $ReadOnlyArray; +processReportStoreOperationsSync: ( operations: $ReadOnlyArray, ) => void; +processThreadStoreOperationsSync: ( operations: $ReadOnlyArray, ) => void; +processDBStoreOperations: (operations: Object) => Promise; +initializeCryptoAccount: () => Promise; +getUserPublicKey: () => Promise; +getOneTimeKeys: (oneTimeKeysAmount: number) => Promise; +validateAndGetPrekeys: () => Promise; +validateAndUploadPrekeys: ( authUserID: string, authDeviceID: string, authAccessToken: string, ) => Promise; +initializeNotificationsSession: ( identityKeys: string, prekey: string, prekeySignature: string, oneTimeKey: ?string, keyserverID: string, ) => Promise; +isNotificationsSessionInitialized: () => Promise; +updateKeyserverDataInNotifStorage: ( keyserversData: $ReadOnlyArray<{ +id: string, +unreadCount: number }>, ) => Promise; +removeKeyserverDataFromNotifStorage: ( keyserverIDsToDelete: $ReadOnlyArray, ) => Promise; +getKeyserverDataFromNotifStorage: ( keyserverIDs: $ReadOnlyArray, ) => Promise<$ReadOnlyArray<{ +id: string, +unreadCount: number }>>; +initializeContentOutboundSession: ( identityKeys: string, prekey: string, prekeySignature: string, oneTimeKey: ?string, deviceID: string, ) => Promise; +initializeContentInboundSession: ( identityKeys: string, encryptedContent: Object, deviceID: string, sessionVersion: number, overwrite: boolean, ) => Promise; +encrypt: (message: string, deviceID: string) => Promise; +encryptAndPersist: ( message: string, deviceID: string, messageID: string, ) => Promise; +decrypt: (encryptedData: Object, deviceID: string) => Promise; +decryptSequentialAndPersist: ( encryptedData: Object, deviceID: string, messageID: string, ) => Promise; +signMessage: (message: string) => Promise; +verifySignature: ( publicKey: string, message: string, signature: string, ) => Promise; +getCodeVersion: () => number; +terminate: () => void; +setNotifyToken: (token: string) => Promise; +clearNotifyToken: () => Promise; +stampSQLiteDBUserID: (userID: string) => Promise; +getSQLiteStampedUserID: () => Promise; +clearSensitiveData: () => Promise; +checkIfDatabaseNeedsDeletion: () => boolean; +reportDBOperationsFailure: () => void; +computeBackupKey: (password: string, backupID: string) => Promise; +generateRandomString: (size: number) => Promise; +setCommServicesAuthMetadata: ( userID: string, deviceID: string, accessToken: string, ) => Promise; +getCommServicesAuthMetadata: () => Promise; +clearCommServicesAuthMetadata: () => Promise; +setCommServicesAccessToken: (accessToken: string) => Promise; +clearCommServicesAccessToken: () => Promise; +startBackupHandler: () => void; +stopBackupHandler: () => void; +createNewBackup: (backupSecret: string) => Promise; +createNewSIWEBackup: ( backupSecret: string, siweBackupMsg: string, ) => Promise; +restoreBackup: (backupSecret: string, maxVersion: string) => Promise; +restoreSIWEBackup: ( backupSecret: string, backupID: string, maxVersion: string, ) => Promise; +restoreBackupData: ( backupID: string, backupDataKey: string, backupLogDataKey: string, maxVersion: string, ) => Promise; +retrieveBackupKeys: (backupSecret: string) => Promise; +retrieveLatestSIWEBackupData: () => Promise; +setSIWEBackupSecrets: (siweBackupSecrets: Object) => Promise; +getSIWEBackupSecrets: () => Promise; +getAllInboundP2PMessage: () => Promise; +removeInboundP2PMessages: (ids: $ReadOnlyArray) => Promise; +getOutboundP2PMessagesByID: ( ids: $ReadOnlyArray, - ) => Promise<$ReadOnlyArray>; + ) => Promise>; +getAllOutboundP2PMessage: () => Promise; +markOutboundP2PMessageAsSent: ( messageID: string, deviceID: string, ) => Promise; +removeOutboundP2PMessagesOlderThan: ( messageID: string, deviceID: string, ) => Promise; +getSyncedDatabaseVersion: () => Promise; +markPrekeysAsPublished: () => Promise; +getRelatedMessages: (messageID: string) => Promise; } export interface CoreModuleSpec extends Spec { +computeBackupKey: ( password: string, backupID: string, ) => Promise; +decrypt: (encryptedData: EncryptedData, deviceID: string) => Promise; +decryptSequentialAndPersist: ( encryptedData: EncryptedData, deviceID: string, messageID: string, ) => Promise; +initializeContentInboundSession: ( identityKeys: string, encryptedContent: EncryptedData, deviceID: string, sessionVersion: number, overwrite: boolean, ) => Promise; +setSIWEBackupSecrets: ( siweBackupSecrets: SIWEBackupSecrets, ) => Promise; +getSIWEBackupSecrets: () => Promise; +processDBStoreOperations: ( operations: ClientDBStoreOperations, ) => Promise; } export default (TurboModuleRegistry.getEnforcing( 'CommTurboModule', ): Spec); diff --git a/web/database/sqlite-api.js b/web/database/sqlite-api.js index a0a317099..037ad3860 100644 --- a/web/database/sqlite-api.js +++ b/web/database/sqlite-api.js @@ -1,88 +1,102 @@ // @flow import type { ClientDBMessageInfo } from 'lib/types/message-types.js'; import type { SQLiteAPI, InboundP2PMessage, OutboundP2PMessage, } from 'lib/types/sqlite-types.js'; import { getCommSharedWorker } from '../shared-worker/shared-worker-provider.js'; import { processDBStoreOperations } from '../shared-worker/utils/store.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; const sqliteAPI: SQLiteAPI = { // read operations async getAllInboundP2PMessage(): Promise { const sharedWorker = await getCommSharedWorker(); const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_INBOUND_P2P_MESSAGES, }); const messages: ?$ReadOnlyArray = data?.inboundP2PMessages; return messages ? [...messages] : []; }, async getAllOutboundP2PMessage(): Promise { const sharedWorker = await getCommSharedWorker(); const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_OUTBOUND_P2P_MESSAGES, }); const messages: ?$ReadOnlyArray = data?.outboundP2PMessages; return messages ? [...messages] : []; }, async getRelatedMessages(messageID: string): Promise { const sharedWorker = await getCommSharedWorker(); const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_RELATED_MESSAGES, messageID, }); const messages: ?$ReadOnlyArray = data?.messages; return messages ? [...messages] : []; }, + async getOutboundP2PMessagesByID( + ids: $ReadOnlyArray, + ): Promise> { + const sharedWorker = await getCommSharedWorker(); + + const data = await sharedWorker.schedule({ + type: workerRequestMessageTypes.GET_OUTBOUND_P2P_MESSAGES_BY_ID, + messageIDs: ids, + }); + const messages: ?$ReadOnlyArray = + data?.outboundP2PMessages; + return messages ? [...messages] : []; + }, + // write operations async removeInboundP2PMessages(ids: $ReadOnlyArray): Promise { const sharedWorker = await getCommSharedWorker(); await sharedWorker.schedule({ type: workerRequestMessageTypes.REMOVE_INBOUND_P2P_MESSAGES, ids, }); }, async markOutboundP2PMessageAsSent( messageID: string, deviceID: string, ): Promise { const sharedWorker = await getCommSharedWorker(); await sharedWorker.schedule({ type: workerRequestMessageTypes.MARK_OUTBOUND_P2P_MESSAGE_AS_SENT, messageID, deviceID, }); }, async removeOutboundP2PMessagesOlderThan( messageID: string, deviceID: string, ): Promise { const sharedWorker = await getCommSharedWorker(); await sharedWorker.schedule({ type: workerRequestMessageTypes.REMOVE_OUTBOUND_P2P_MESSAGES, messageID, deviceID, }); }, processDBStoreOperations, }; export { sqliteAPI }; diff --git a/web/shared-worker/worker/shared-worker.js b/web/shared-worker/worker/shared-worker.js index 113020c3a..5f67494ca 100644 --- a/web/shared-worker/worker/shared-worker.js +++ b/web/shared-worker/worker/shared-worker.js @@ -1,394 +1,403 @@ // @flow import localforage from 'localforage'; import { getMessageForException } from 'lib/utils/errors.js'; import { restoreBackup } from './backup.js'; import { processAppIdentityClientRequest } from './identity-client.js'; import { getClientStoreFromQueryExecutor, processDBStoreOperations, } from './process-operations.js'; import { clearCryptoStore, processAppOlmApiRequest } from './worker-crypto.js'; import { getDBModule, getSQLiteQueryExecutor, setDBModule, setSQLiteQueryExecutor, getPlatformDetails, setPlatformDetails, } from './worker-database.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, workerOlmAPIRequests, } from '../../types/worker-types.js'; import { workerIdentityClientRequests } from '../../types/worker-types.js'; import { getDatabaseModule } from '../db-module.js'; import { webMessageToClientDBMessageInfo } from '../types/entities.js'; import { COMM_SQLITE_DATABASE_PATH, SQLITE_STAMPED_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 persistNeeded: boolean = false; let persistInProgress: boolean = false; async function initDatabase( webworkerModulesFilePath: string, commQueryExecutorFilename: ?string, encryptionKeyJWK?: ?SubtleCrypto$JsonWebKey, ) { const dbModule = getDBModule(); const sqliteQueryExecutor = getSQLiteQueryExecutor(); if (!!dbModule && !!sqliteQueryExecutor) { console.log('Database already initialized'); return; } const newModule = dbModule ? dbModule : getDatabaseModule(commQueryExecutorFilename, webworkerModulesFilePath); if (!dbModule) { setDBModule(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'); } setSQLiteQueryExecutor( 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 sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { 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(dbModule, 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; } const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); // database operations if (message.type === workerRequestMessageTypes.INIT) { setPlatformDetails(message.platformDetails); 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) { clearCryptoStore(); encryptionKey = null; await localforage.clear(); if (dbModule && sqliteQueryExecutor) { clearSensitiveData( dbModule, COMM_SQLITE_DATABASE_PATH, sqliteQueryExecutor, ); } setSQLiteQueryExecutor(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_SQLITE_STAMPED_USER_ID ) { return { type: workerResponseMessageTypes.GET_SQLITE_STAMPED_USER_ID, userID: sqliteQueryExecutor.getMetadata(SQLITE_STAMPED_USER_ID_KEY), }; } else if ( message.type === workerRequestMessageTypes.GET_PERSIST_STORAGE_ITEM ) { return { type: workerResponseMessageTypes.GET_PERSIST_STORAGE_ITEM, item: sqliteQueryExecutor.getPersistStorageItem(message.key), }; } else if ( message.type === workerRequestMessageTypes.GET_INBOUND_P2P_MESSAGES ) { return { type: workerResponseMessageTypes.GET_INBOUND_P2P_MESSAGES, inboundP2PMessages: sqliteQueryExecutor.getAllInboundP2PMessage(), }; } else if ( message.type === workerRequestMessageTypes.GET_OUTBOUND_P2P_MESSAGES ) { return { type: workerResponseMessageTypes.GET_OUTBOUND_P2P_MESSAGES, outboundP2PMessages: sqliteQueryExecutor.getAllOutboundP2PMessages(), }; } else if (message.type === workerRequestMessageTypes.GET_RELATED_MESSAGES) { const webMessageEntities = sqliteQueryExecutor.getRelatedMessagesWeb( message.messageID, ); return { type: workerResponseMessageTypes.GET_RELATED_MESSAGES, messages: webMessageEntities.map(webMessageToClientDBMessageInfo), }; + } else if ( + message.type === workerRequestMessageTypes.GET_OUTBOUND_P2P_MESSAGES_BY_ID + ) { + return { + type: workerResponseMessageTypes.GET_OUTBOUND_P2P_MESSAGES, + outboundP2PMessages: sqliteQueryExecutor.getOutboundP2PMessagesByID( + message.messageIDs, + ), + }; } // write operations const isOlmAPIRequest = workerOlmAPIRequests.includes(message.type); const isIdentityClientRequest = workerIdentityClientRequests.includes( message.type, ); if ( !workerWriteRequests.includes(message.type) && !isOlmAPIRequest && !isIdentityClientRequest ) { throw new Error(`Request type ${message.type} not supported`); } if (!sqliteQueryExecutor || !dbModule) { throw new Error( `Database not initialized, unable to process request type: ${message.type}`, ); } let result; if (isOlmAPIRequest) { result = await processAppOlmApiRequest(message); } else if (isIdentityClientRequest) { const platformDetails = getPlatformDetails(); if (!platformDetails) { throw new Error( 'Platform details not set, unable to process identity client request', ); } result = await processAppIdentityClientRequest( sqliteQueryExecutor, dbModule, platformDetails, message, ); } else if ( message.type === workerRequestMessageTypes.PROCESS_STORE_OPERATIONS ) { processDBStoreOperations( sqliteQueryExecutor, message.storeOperations, dbModule, ); } else if ( message.type === workerRequestMessageTypes.STAMP_SQLITE_DB_USER_ID ) { sqliteQueryExecutor.setMetadata(SQLITE_STAMPED_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, message.backupLogDataKey, ); } else if ( message.type === workerRequestMessageTypes.REMOVE_INBOUND_P2P_MESSAGES ) { sqliteQueryExecutor.removeInboundP2PMessages(message.ids); } else if ( message.type === workerRequestMessageTypes.MARK_OUTBOUND_P2P_MESSAGE_AS_SENT ) { sqliteQueryExecutor.markOutboundP2PMessageAsSent( message.messageID, message.deviceID, ); } else if ( message.type === workerRequestMessageTypes.REMOVE_OUTBOUND_P2P_MESSAGES ) { sqliteQueryExecutor.removeOutboundP2PMessagesOlderThan( message.messageID, message.deviceID, ); } persistNeeded = true; if (!persistInProgress) { void persist(); } return result; } let currentlyProcessedMessage: ?Promise = null; 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', }); } currentlyProcessedMessage = (async () => { await currentlyProcessedMessage; try { const result = await processAppRequest(message); port.postMessage({ id, message: result, }); } catch (e) { port.postMessage({ id, error: getMessageForException(e), }); } })(); }; } self.addEventListener('connect', connectHandler); diff --git a/web/types/worker-types.js b/web/types/worker-types.js index 576a4dd7c..50981ee2e 100644 --- a/web/types/worker-types.js +++ b/web/types/worker-types.js @@ -1,306 +1,313 @@ // @flow import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; import type { PickledOLMAccount, OLMIdentityKeys, OlmAPI, } from 'lib/types/crypto-types.js'; import type { PlatformDetails } from 'lib/types/device-types.js'; import type { IdentityServiceClient, IdentityServiceAuthLayer, } from 'lib/types/identity-service-types.js'; import type { ClientDBMessageInfo } from 'lib/types/message-types.js'; import type { InboundP2PMessage, OutboundP2PMessage, } from 'lib/types/sqlite-types.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, STAMP_SQLITE_DB_USER_ID: 5, GET_SQLITE_STAMPED_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, INITIALIZE_CRYPTO_ACCOUNT: 12, CREATE_IDENTITY_SERVICE_CLIENT: 13, CALL_IDENTITY_CLIENT_METHOD: 14, CALL_OLM_API_METHOD: 15, GET_INBOUND_P2P_MESSAGES: 16, REMOVE_INBOUND_P2P_MESSAGES: 17, GET_OUTBOUND_P2P_MESSAGES: 18, MARK_OUTBOUND_P2P_MESSAGE_AS_SENT: 19, REMOVE_OUTBOUND_P2P_MESSAGES: 20, GET_RELATED_MESSAGES: 21, + GET_OUTBOUND_P2P_MESSAGES_BY_ID: 22, }); export const workerWriteRequests: $ReadOnlyArray = [ workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, workerRequestMessageTypes.STAMP_SQLITE_DB_USER_ID, workerRequestMessageTypes.SET_PERSIST_STORAGE_ITEM, workerRequestMessageTypes.REMOVE_PERSIST_STORAGE_ITEM, workerRequestMessageTypes.BACKUP_RESTORE, workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, workerRequestMessageTypes.REMOVE_INBOUND_P2P_MESSAGES, workerRequestMessageTypes.MARK_OUTBOUND_P2P_MESSAGE_AS_SENT, workerRequestMessageTypes.REMOVE_OUTBOUND_P2P_MESSAGES, ]; export const workerOlmAPIRequests: $ReadOnlyArray = [ workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, workerRequestMessageTypes.CALL_OLM_API_METHOD, ]; export const workerIdentityClientRequests: $ReadOnlyArray = [ workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT, workerRequestMessageTypes.CALL_IDENTITY_CLIENT_METHOD, ]; export type PingWorkerRequestMessage = { +type: 0, +text: string, }; export type InitWorkerRequestMessage = { +type: 1, +platformDetails: PlatformDetails, +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, +backupLogDataKey: string, }; // Previously used on web in redux. Now only used // in a migration to the shared worker. export type LegacyCryptoStore = { +primaryAccount: PickledOLMAccount, +primaryIdentityKeys: OLMIdentityKeys, +notificationAccount: PickledOLMAccount, +notificationIdentityKeys: OLMIdentityKeys, }; export type InitializeCryptoAccountRequestMessage = { +type: 12, +olmWasmPath: string, +initialCryptoStore?: LegacyCryptoStore, }; export type CreateIdentityServiceClientRequestMessage = { +type: 13, +opaqueWasmPath: string, +authLayer: ?IdentityServiceAuthLayer, }; export type CallIdentityClientMethodRequestMessage = { +type: 14, +method: $Keys, +args: $ReadOnlyArray, }; export type CallOLMApiMethodRequestMessage = { +type: 15, +method: $Keys, +args: $ReadOnlyArray, }; export type GetInboundP2PMessagesRequestMessage = { +type: 16, }; export type RemoveInboundP2PMessagesRequestMessage = { +type: 17, +ids: $ReadOnlyArray, }; export type GetOutboundP2PMessagesRequestMessage = { +type: 18, }; export type MarkOutboundP2PMessageAsSentRequestMessage = { +type: 19, +messageID: string, +deviceID: string, }; export type RemoveOutboundP2PMessagesRequestMessage = { +type: 20, +messageID: string, +deviceID: string, }; export type GetRelatedMessagesRequestMessage = { +type: 21, +messageID: string, }; +export type GetOutboundP2PMessagesByIDRequestMessage = { + +type: 22, + +messageIDs: $ReadOnlyArray, +}; + export type WorkerRequestMessage = | PingWorkerRequestMessage | InitWorkerRequestMessage | GenerateDatabaseEncryptionKeyRequestMessage | ProcessStoreOperationsRequestMessage | GetClientStoreRequestMessage | SetCurrentUserIDRequestMessage | GetCurrentUserIDRequestMessage | GetPersistStorageItemRequestMessage | SetPersistStorageItemRequestMessage | RemovePersistStorageItemRequestMessage | ClearSensitiveDataRequestMessage | BackupRestoreRequestMessage | InitializeCryptoAccountRequestMessage | CreateIdentityServiceClientRequestMessage | CallIdentityClientMethodRequestMessage | CallOLMApiMethodRequestMessage | GetInboundP2PMessagesRequestMessage | RemoveInboundP2PMessagesRequestMessage | GetOutboundP2PMessagesRequestMessage | MarkOutboundP2PMessageAsSentRequestMessage | RemoveOutboundP2PMessagesRequestMessage - | GetRelatedMessagesRequestMessage; + | GetRelatedMessagesRequestMessage + | GetOutboundP2PMessagesByIDRequestMessage; 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_SQLITE_STAMPED_USER_ID: 2, GET_PERSIST_STORAGE_ITEM: 3, CALL_IDENTITY_CLIENT_METHOD: 4, CALL_OLM_API_METHOD: 5, GET_INBOUND_P2P_MESSAGES: 6, GET_OUTBOUND_P2P_MESSAGES: 7, GET_RELATED_MESSAGES: 8, }); 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 CallIdentityClientMethodResponseMessage = { +type: 4, +result: mixed, }; export type CallOLMApiMethodResponseMessage = { +type: 5, +result: mixed, }; export type GetInboundP2PMessagesResponseMessage = { +type: 6, +inboundP2PMessages: $ReadOnlyArray, }; export type GetOutboundP2PMessagesResponseMessage = { +type: 7, +outboundP2PMessages: $ReadOnlyArray, }; export type GetRelatedMessagesResponseMessage = { +type: 8, +messages: $ReadOnlyArray, }; export type WorkerResponseMessage = | PongWorkerResponseMessage | ClientStoreResponseMessage | GetCurrentUserIDResponseMessage | GetPersistStorageItemResponseMessage | CallIdentityClientMethodResponseMessage | CallOLMApiMethodResponseMessage | GetInboundP2PMessagesResponseMessage | GetOutboundP2PMessagesResponseMessage | GetRelatedMessagesResponseMessage; export type WorkerResponseProxyMessage = { +id?: number, +message?: WorkerResponseMessage, +error?: string, }; // SharedWorker types export type SharedWorkerMessageEvent = MessageEvent & { +ports: $ReadOnlyArray, ... };