diff --git a/web/crypto/olm-api.js b/web/crypto/olm-api.js index 4a84aa86f..4112241fb 100644 --- a/web/crypto/olm-api.js +++ b/web/crypto/olm-api.js @@ -1,142 +1,155 @@ // @flow import olm from '@commapp/olm'; import type { Account, Session } from '@commapp/olm'; import { type IdentityClientContextType } from 'lib/shared/identity-client-context.js'; import { type OlmAPI, olmEncryptedMessageTypes, type OLMIdentityKeys, type OneTimeKeysResultValues, } from 'lib/types/crypto-types.js'; import { getAccountOneTimeKeys, getAccountPrekeysSet, shouldForgetPrekey, shouldRotatePrekey, } from 'lib/utils/olm-utils.js'; +import { getCommSharedWorker } from '../shared-worker/shared-worker-provider.js'; +import { getOlmWasmPath } from '../shared-worker/utils/constants.js'; +import { workerRequestMessageTypes } from '../types/worker-types.js'; + +const usingSharedWorker = false; // methods below are just mocks to SQLite API // implement proper methods tracked in ENG-6462 function getOlmAccount(): Account { const account = new olm.Account(); account.create(); return account; } // eslint-disable-next-line no-unused-vars function getOlmSession(deviceID: string): Session { return new olm.Session(); } // eslint-disable-next-line no-unused-vars function storeOlmAccount(account: Account): void {} // eslint-disable-next-line no-unused-vars function storeOlmSession(session: Session): void {} const olmAPI: OlmAPI = { async initializeCryptoAccount(): Promise { - await olm.init(); + if (usingSharedWorker) { + const sharedWorker = await getCommSharedWorker(); + await sharedWorker.schedule({ + type: workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, + olmWasmPath: getOlmWasmPath(), + }); + } else { + await olm.init(); + } }, async encrypt(content: string, deviceID: string): Promise { const session = getOlmSession(deviceID); const { body } = session.encrypt(content); storeOlmSession(session); return body; }, async decrypt(encryptedContent: string, deviceID: string): Promise { const session = getOlmSession(deviceID); const result = session.decrypt( olmEncryptedMessageTypes.TEXT, encryptedContent, ); storeOlmSession(session); return result; }, async contentInboundSessionCreator( contentIdentityKeys: OLMIdentityKeys, initialEncryptedContent: string, ): Promise { const account = getOlmAccount(); const session = new olm.Session(); session.create_inbound_from( account, contentIdentityKeys.curve25519, initialEncryptedContent, ); account.remove_one_time_keys(session); const initialEncryptedMessage = session.decrypt( olmEncryptedMessageTypes.PREKEY, initialEncryptedContent, ); storeOlmAccount(account); storeOlmSession(session); return initialEncryptedMessage; }, async getOneTimeKeys(numberOfKeys: number): Promise { const contentAccount = getOlmAccount(); const notifAccount = getOlmAccount(); const contentOneTimeKeys = getAccountOneTimeKeys( contentAccount, numberOfKeys, ); contentAccount.mark_keys_as_published(); storeOlmAccount(contentAccount); const notificationsOneTimeKeys = getAccountOneTimeKeys( notifAccount, numberOfKeys, ); notifAccount.mark_keys_as_published(); storeOlmAccount(notifAccount); return { contentOneTimeKeys, notificationsOneTimeKeys }; }, async validateAndUploadPrekeys( identityContext: IdentityClientContextType, ): Promise { const authMetadata = await identityContext.getAuthMetadata(); const { userID, deviceID, accessToken } = authMetadata; if (!userID || !deviceID || !accessToken) { return; } const contentAccount = getOlmAccount(); if (shouldRotatePrekey(contentAccount)) { contentAccount.generate_prekey(); } if (shouldForgetPrekey(contentAccount)) { contentAccount.forget_old_prekey(); } await storeOlmAccount(contentAccount); if (!contentAccount.unpublished_prekey()) { return; } const notifAccount = getOlmAccount(); const { prekey: notifPrekey, prekeySignature: notifPrekeySignature } = getAccountPrekeysSet(notifAccount); const { prekey: contentPrekey, prekeySignature: contentPrekeySignature } = getAccountPrekeysSet(contentAccount); if (!notifPrekeySignature || !contentPrekeySignature) { throw new Error('Prekey signature is missing'); } if (!identityContext.identityClient.publishWebPrekeys) { throw new Error('Publish prekeys method unimplemented'); } await identityContext.identityClient.publishWebPrekeys({ contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, }); contentAccount.mark_keys_as_published(); await storeOlmAccount(contentAccount); }, }; export { olmAPI }; diff --git a/web/push-notif/push-notifs-handler.js b/web/push-notif/push-notifs-handler.js index fc3cd4319..f9ec81169 100644 --- a/web/push-notif/push-notifs-handler.js +++ b/web/push-notif/push-notifs-handler.js @@ -1,231 +1,224 @@ // @flow import * as React from 'react'; import { useSetDeviceTokenFanout, setDeviceTokenActionTypes, } from 'lib/actions/device-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { convertNonPendingIDToNewSchema } from 'lib/utils/migration-utils.js'; import { shouldSkipPushPermissionAlert, recordNotifPermissionAlertActionType, } from 'lib/utils/push-alerts.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { decryptDesktopNotification } from './notif-crypto-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import electron from '../electron.js'; import PushNotifModal from '../modals/push-notif-modal.react.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; -import { - WORKERS_MODULES_DIR_PATH, - DEFAULT_OLM_FILENAME, -} from '../shared-worker/utils/constants.js'; +import { getOlmWasmPath } from '../shared-worker/utils/constants.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; -declare var baseURL: string; -declare var olmFilename: string; - function useCreateDesktopPushSubscription() { const dispatchActionPromise = useDispatchActionPromise(); const callSetDeviceToken = useSetDeviceTokenFanout(); const staffCanSee = useStaffCanSee(); React.useEffect( () => electron?.onDeviceTokenRegistered?.((token: ?string) => { void dispatchActionPromise( setDeviceTokenActionTypes, callSetDeviceToken(token), ); }), [callSetDeviceToken, dispatchActionPromise], ); React.useEffect(() => { electron?.fetchDeviceToken?.(); }, []); React.useEffect( () => electron?.onEncryptedNotification?.( async ({ encryptedPayload }: { encryptedPayload: string }) => { const decryptedPayload = await decryptDesktopNotification( encryptedPayload, staffCanSee, ); electron?.showDecryptedNotification(decryptedPayload); }, ), [staffCanSee], ); const dispatch = useDispatch(); React.useEffect( () => electron?.onNotificationClicked?.( ({ threadID }: { +threadID: string }) => { const convertedThreadID = convertNonPendingIDToNewSchema( threadID, authoritativeKeyserverID, ); const payload = { chatMode: 'view', activeChatThreadID: convertedThreadID, tab: 'chat', }; dispatch({ type: updateNavInfoActionType, payload }); }, ), [dispatch], ); } function useCreatePushSubscription(): () => Promise { const publicKey = useSelector(state => state.pushApiPublicKey); const dispatchActionPromise = useDispatchActionPromise(); const callSetDeviceToken = useSetDeviceTokenFanout(); const staffCanSee = useStaffCanSee(); return React.useCallback(async () => { if (!publicKey) { return; } const workerRegistration = await navigator.serviceWorker?.ready; if (!workerRegistration || !workerRegistration.pushManager) { return; } - const origin = window.location.origin; - const olmWasmDirPath = `${origin}${baseURL}${WORKERS_MODULES_DIR_PATH}`; - const olmWasmFilename = olmFilename ? olmFilename : DEFAULT_OLM_FILENAME; - const olmWasmPath = `${olmWasmDirPath}/${olmWasmFilename}`; - workerRegistration.active?.postMessage({ olmWasmPath, staffCanSee }); + workerRegistration.active?.postMessage({ + olmWasmPath: getOlmWasmPath(), + staffCanSee, + }); const subscription = await workerRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: publicKey, }); void dispatchActionPromise( setDeviceTokenActionTypes, callSetDeviceToken(JSON.stringify(subscription)), ); }, [callSetDeviceToken, dispatchActionPromise, publicKey, staffCanSee]); } function PushNotificationsHandler(): React.Node { useCreateDesktopPushSubscription(); const createPushSubscription = useCreatePushSubscription(); const notifPermissionAlertInfo = useSelector( state => state.notifPermissionAlertInfo, ); const modalContext = useModalContext(); const loggedIn = useSelector(isLoggedIn); const dispatch = useDispatch(); const supported = 'Notification' in window && !electron; React.useEffect(() => { void (async () => { if (!navigator.serviceWorker || !supported) { return; } await navigator.serviceWorker.register('worker/notif', { scope: '/' }); if (Notification.permission === 'granted') { // Make sure the subscription is current if we have the permissions await createPushSubscription(); } else if ( Notification.permission === 'default' && loggedIn && !shouldSkipPushPermissionAlert(notifPermissionAlertInfo) ) { // Ask existing users that are already logged in for permission modalContext.pushModal(); dispatch({ type: recordNotifPermissionAlertActionType, payload: { time: Date.now() }, }); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Ask for permission on login const prevLoggedIn = React.useRef(loggedIn); React.useEffect(() => { if (!navigator.serviceWorker || !supported) { return; } if (!prevLoggedIn.current && loggedIn) { if (Notification.permission === 'granted') { void createPushSubscription(); } else if ( Notification.permission === 'default' && !shouldSkipPushPermissionAlert(notifPermissionAlertInfo) ) { modalContext.pushModal(); dispatch({ type: recordNotifPermissionAlertActionType, payload: { time: Date.now() }, }); } } prevLoggedIn.current = loggedIn; }, [ createPushSubscription, dispatch, loggedIn, modalContext, notifPermissionAlertInfo, prevLoggedIn, supported, ]); // Redirect to thread on notification click React.useEffect(() => { if (!navigator.serviceWorker || !supported) { return undefined; } const callback = (event: MessageEvent) => { if (typeof event.data !== 'object' || !event.data) { return; } if (event.data.targetThreadID) { const payload = { chatMode: 'view', activeChatThreadID: event.data.targetThreadID, tab: 'chat', }; dispatch({ type: updateNavInfoActionType, payload }); } }; navigator.serviceWorker.addEventListener('message', callback); return () => navigator.serviceWorker?.removeEventListener('message', callback); }, [dispatch, supported]); return null; } export { PushNotificationsHandler, useCreatePushSubscription }; diff --git a/web/shared-worker/utils/constants.js b/web/shared-worker/utils/constants.js index defc1c6e8..b82bb9ad6 100644 --- a/web/shared-worker/utils/constants.js +++ b/web/shared-worker/utils/constants.js @@ -1,48 +1,57 @@ // @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', }; + +declare var baseURL: string; +declare var olmFilename: string; +export function getOlmWasmPath(): string { + const origin = window.location.origin; + const olmWasmDirPath = `${origin}${baseURL}${WORKERS_MODULES_DIR_PATH}`; + const olmWasmFilename = olmFilename ? olmFilename : DEFAULT_OLM_FILENAME; + return `${olmWasmDirPath}/${olmWasmFilename}`; +} diff --git a/web/shared-worker/worker/shared-worker.js b/web/shared-worker/worker/shared-worker.js index c2221e10c..4f51ff21d 100644 --- a/web/shared-worker/worker/shared-worker.js +++ b/web/shared-worker/worker/shared-worker.js @@ -1,308 +1,316 @@ // @flow import localforage from 'localforage'; import { restoreBackup } from './backup.js'; import { getClientStoreFromQueryExecutor, processDBStoreOperations, } from './process-operations.js'; +import { clearCryptoStore, processAppOlmApiRequest } from './worker-crypto.js'; import { getDBModule, getSQLiteQueryExecutor, setDBModule, setSQLiteQueryExecutor, } 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 { getDatabaseModule } from '../db-module.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 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) { 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_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'); + const isOlmAPIRequest = workerOlmAPIRequests.includes(message.type); + if (!workerWriteRequests.includes(message.type) && !isOlmAPIRequest) { + 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}`, ); } - if (message.type === workerRequestMessageTypes.PROCESS_STORE_OPERATIONS) { + if (isOlmAPIRequest) { + await processAppOlmApiRequest(message); + } else 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, message.backupLogDataKey, ); } 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/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js new file mode 100644 index 000000000..2ca467a86 --- /dev/null +++ b/web/shared-worker/worker/worker-crypto.js @@ -0,0 +1,169 @@ +// @flow + +import olm from '@commapp/olm'; +import uuid from 'uuid'; + +import type { CryptoStore, PickledOLMAccount } from 'lib/types/crypto-types.js'; + +import { getProcessingStoreOpsExceptionMessage } from './process-operations.js'; +import { getDBModule, getSQLiteQueryExecutor } from './worker-database.js'; +import { + type WorkerRequestMessage, + type WorkerResponseMessage, + workerRequestMessageTypes, +} from '../../types/worker-types.js'; + +type WorkerCryptoStore = { + +contentAccountPickleKey: string, + +contentAccount: olm.Account, + +notificationAccountPickleKey: string, + +notificationAccount: olm.Account, +}; + +let cryptoStore: ?WorkerCryptoStore = null; + +function clearCryptoStore() { + cryptoStore = null; +} + +function persistCryptoStore() { + const sqliteQueryExecutor = getSQLiteQueryExecutor(); + const dbModule = getDBModule(); + if (!sqliteQueryExecutor || !dbModule) { + throw new Error( + "Couldn't persist crypto store because database is not initialized", + ); + } + if (!cryptoStore) { + throw new Error("Couldn't persist crypto store because it doesn't exist"); + } + + const { + contentAccountPickleKey, + contentAccount, + notificationAccountPickleKey, + notificationAccount, + } = cryptoStore; + + const pickledContentAccount: PickledOLMAccount = { + picklingKey: contentAccountPickleKey, + pickledAccount: contentAccount.pickle(contentAccountPickleKey), + }; + + const pickledNotificationAccount: PickledOLMAccount = { + picklingKey: notificationAccountPickleKey, + pickledAccount: notificationAccount.pickle(notificationAccountPickleKey), + }; + + try { + sqliteQueryExecutor.storeOlmPersistAccount( + sqliteQueryExecutor.getContentAccountID(), + JSON.stringify(pickledContentAccount), + ); + sqliteQueryExecutor.storeOlmPersistAccount( + sqliteQueryExecutor.getNotifsAccountID(), + JSON.stringify(pickledNotificationAccount), + ); + } catch (err) { + throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); + } +} + +function getOrCreateOlmAccount(accountIDInDB: number): { + +picklingKey: string, + +account: olm.Account, +} { + const sqliteQueryExecutor = getSQLiteQueryExecutor(); + const dbModule = getDBModule(); + if (!sqliteQueryExecutor || !dbModule) { + throw new Error('Database not initialized'); + } + + const account = new olm.Account(); + let picklingKey; + + let accountDBString; + try { + accountDBString = + sqliteQueryExecutor.getOlmPersistAccountDataWeb(accountIDInDB); + } catch (err) { + throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); + } + + if (accountDBString.isNull) { + picklingKey = uuid.v4(); + account.create(); + } else { + const dbAccount: PickledOLMAccount = JSON.parse(accountDBString.value); + picklingKey = dbAccount.picklingKey; + account.unpickle(picklingKey, dbAccount.pickledAccount); + } + + return { picklingKey, account }; +} + +function unpickleInitialCryptoStoreAccount( + account: PickledOLMAccount, +): olm.Account { + const { picklingKey, pickledAccount } = account; + const olmAccount = new olm.Account(); + olmAccount.unpickle(picklingKey, pickledAccount); + return olmAccount; +} + +async function initializeCryptoAccount( + olmWasmPath: string, + initialCryptoStore: ?CryptoStore, +) { + const sqliteQueryExecutor = getSQLiteQueryExecutor(); + if (!sqliteQueryExecutor) { + throw new Error('Database not initialized'); + } + + await olm.init({ locateFile: () => olmWasmPath }); + + if (initialCryptoStore) { + cryptoStore = { + contentAccountPickleKey: initialCryptoStore.primaryAccount.picklingKey, + contentAccount: unpickleInitialCryptoStoreAccount( + initialCryptoStore.primaryAccount, + ), + notificationAccountPickleKey: + initialCryptoStore.notificationAccount.picklingKey, + notificationAccount: unpickleInitialCryptoStoreAccount( + initialCryptoStore.notificationAccount, + ), + }; + persistCryptoStore(); + return; + } + + const contentAccountResult = getOrCreateOlmAccount( + sqliteQueryExecutor.getContentAccountID(), + ); + const notificationAccountResult = getOrCreateOlmAccount( + sqliteQueryExecutor.getNotifsAccountID(), + ); + + cryptoStore = { + contentAccountPickleKey: contentAccountResult.picklingKey, + contentAccount: contentAccountResult.account, + notificationAccountPickleKey: notificationAccountResult.picklingKey, + notificationAccount: notificationAccountResult.account, + }; + + persistCryptoStore(); +} + +async function processAppOlmApiRequest( + message: WorkerRequestMessage, +): Promise { + if (message.type === workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT) { + await initializeCryptoAccount( + message.olmWasmPath, + message.initialCryptoStore, + ); + } +} + +export { clearCryptoStore, processAppOlmApiRequest }; diff --git a/web/types/worker-types.js b/web/types/worker-types.js index 06875359f..db97fdb81 100644 --- a/web/types/worker-types.js +++ b/web/types/worker-types.js @@ -1,159 +1,173 @@ // @flow import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; +import type { CryptoStore } from 'lib/types/crypto-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, 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, + INITIALIZE_CRYPTO_ACCOUNT: 12, }); 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, + workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, +]; + +export const workerOlmAPIRequests: $ReadOnlyArray = [ + workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, ]; 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, +backupLogDataKey: string, }; +export type InitializeCryptoAccountRequestMessage = { + +type: 12, + +olmWasmPath: string, + +initialCryptoStore?: CryptoStore, +}; + export type WorkerRequestMessage = | PingWorkerRequestMessage | InitWorkerRequestMessage | GenerateDatabaseEncryptionKeyRequestMessage | ProcessStoreOperationsRequestMessage | GetClientStoreRequestMessage | SetCurrentUserIDRequestMessage | GetCurrentUserIDRequestMessage | GetPersistStorageItemRequestMessage | SetPersistStorageItemRequestMessage | RemovePersistStorageItemRequestMessage | ClearSensitiveDataRequestMessage - | BackupRestoreRequestMessage; + | BackupRestoreRequestMessage + | InitializeCryptoAccountRequestMessage; 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, ... };