diff --git a/web/crypto/olm-api.js b/web/crypto/olm-api.js index 4112241fb..f60daf705 100644 --- a/web/crypto/olm-api.js +++ b/web/crypto/olm-api.js @@ -1,155 +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 { 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 }; +export { olmAPI, usingSharedWorker }; diff --git a/web/grpc/identity-service-client-proxy.js b/web/grpc/identity-service-client-proxy.js new file mode 100644 index 000000000..584feee86 --- /dev/null +++ b/web/grpc/identity-service-client-proxy.js @@ -0,0 +1,109 @@ +// @flow + +import type { + OneTimeKeysResultValues, + SignedPrekeys, +} from 'lib/types/crypto-types.js'; +import type { + IdentityServiceClient, + IdentityServiceAuthLayer, + DeviceOlmOutboundKeys, + IdentityAuthResult, + UserDevicesOlmInboundKeys, + UserDevicesOlmOutboundKeys, +} from 'lib/types/identity-service-types.js'; +import { getConfig } from 'lib/utils/config.js'; + +import { + type CommSharedWorker, + getCommSharedWorker, +} from '../shared-worker/shared-worker-provider.js'; +import { getOpaqueWasmPath } from '../shared-worker/utils/constants.js'; +import { + workerRequestMessageTypes, + workerResponseMessageTypes, +} from '../types/worker-types.js'; + +class IdentityServiceClientSharedProxy implements IdentityServiceClient { + sharedWorkerPromise: Promise; + + constructor(authLayer: ?IdentityServiceAuthLayer) { + this.sharedWorkerPromise = (async () => { + const sharedWorker = await getCommSharedWorker(); + await sharedWorker.schedule({ + type: workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT, + opaqueWasmPath: getOpaqueWasmPath(), + platformDetails: getConfig().platformDetails, + authLayer, + }); + + return sharedWorker; + })(); + } + + proxyToWorker( + method: $Keys, + ): (...args: $ReadOnlyArray) => Promise { + return async (...args: $ReadOnlyArray) => { + const sharedWorker = await this.sharedWorkerPromise; + const result = await sharedWorker.schedule({ + type: workerRequestMessageTypes.CALL_IDENTITY_CLIENT_METHOD, + method, + args, + }); + + if (!result) { + throw new Error(`Worker identity call didn't return expected message`); + } else if ( + result.type !== workerResponseMessageTypes.CALL_IDENTITY_CLIENT_METHOD + ) { + throw new Error( + `Worker identity call didn't return expected message. Instead got: ${JSON.stringify( + result, + )}`, + ); + } + + // Worker should return a message with the corresponding return type + return (result.result: any); + }; + } + + deleteUser: () => Promise = this.proxyToWorker('deleteUser'); + + getKeyserverKeys: (keyserverID: string) => Promise = + this.proxyToWorker('getKeyserverKeys'); + + getOutboundKeysForUser: ( + userID: string, + ) => Promise = this.proxyToWorker( + 'getOutboundKeysForUser', + ); + + getInboundKeysForUser: ( + userID: string, + ) => Promise = this.proxyToWorker( + 'getInboundKeysForUser', + ); + + uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise = + this.proxyToWorker('uploadOneTimeKeys'); + + logInPasswordUser: ( + username: string, + password: string, + ) => Promise = this.proxyToWorker('logInPasswordUser'); + + logInWalletUser: ( + walletAddress: string, + siweMessage: string, + siweSignature: string, + ) => Promise = this.proxyToWorker('logInWalletUser'); + + generateNonce: () => Promise = this.proxyToWorker('generateNonce'); + + publishWebPrekeys: (prekeys: SignedPrekeys) => Promise = + this.proxyToWorker('publishWebPrekeys'); +} + +export { IdentityServiceClientSharedProxy }; diff --git a/web/grpc/identity-service-context-provider.react.js b/web/grpc/identity-service-context-provider.react.js index 04079db6f..7ca2e7ee8 100644 --- a/web/grpc/identity-service-context-provider.react.js +++ b/web/grpc/identity-service-context-provider.react.js @@ -1,69 +1,75 @@ // @flow import * as React from 'react'; import { IdentityClientContext, type AuthMetadata, } from 'lib/shared/identity-client-context.js'; import { getConfig } from 'lib/utils/config.js'; +import { IdentityServiceClientSharedProxy } from './identity-service-client-proxy.js'; import { IdentityServiceClientWrapper } from './identity-service-client-wrapper.js'; import { useGetDeviceKeyUpload } from '../account/account-hooks.js'; +import { usingSharedWorker } from '../crypto/olm-api.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { +children: React.Node, }; function IdentityServiceContextProvider(props: Props): React.Node { const { children } = props; const userID = useSelector(state => state.currentUserInfo?.id); const accessToken = useSelector(state => state.commServicesAccessToken); const deviceID = useSelector( state => state.cryptoStore?.primaryIdentityKeys.ed25519, ); const getDeviceKeyUpload = useGetDeviceKeyUpload(); const client = React.useMemo(() => { let authLayer = null; if (userID && deviceID && accessToken) { authLayer = { userID, deviceID, commServicesAccessToken: accessToken, }; } - return new IdentityServiceClientWrapper( - getConfig().platformDetails, - null, - authLayer, - getDeviceKeyUpload, - ); + if (usingSharedWorker) { + return new IdentityServiceClientSharedProxy(authLayer); + } else { + return new IdentityServiceClientWrapper( + getConfig().platformDetails, + null, + authLayer, + getDeviceKeyUpload, + ); + } }, [accessToken, deviceID, getDeviceKeyUpload, userID]); const getAuthMetadata = React.useCallback<() => Promise>( async () => ({ userID, deviceID, accessToken, }), [accessToken, deviceID, userID], ); const value = React.useMemo( () => ({ identityClient: client, getAuthMetadata, }), [client, getAuthMetadata], ); return ( {children} ); } export default IdentityServiceContextProvider; diff --git a/web/shared-worker/shared-worker-provider.js b/web/shared-worker/shared-worker-provider.js index c68e74163..78a110c40 100644 --- a/web/shared-worker/shared-worker-provider.js +++ b/web/shared-worker/shared-worker-provider.js @@ -1,169 +1,169 @@ // @flow import invariant from 'invariant'; import localforage from 'localforage'; import { getConfig } from 'lib/utils/config.js'; import { DATABASE_WORKER_PATH, WORKERS_MODULES_DIR_PATH, SQLITE_ENCRYPTION_KEY, } from './utils/constants.js'; import { isDesktopSafari, isSQLiteSupported } from './utils/db-utils.js'; import WorkerConnectionProxy from './utils/worker-connection-proxy.js'; import { exportKeyToJWK, generateCryptoKey, } from '../crypto/aes-gcm-crypto-utils.js'; import { workerRequestMessageTypes, type WorkerRequestMessage, type WorkerResponseMessage, } from '../types/worker-types.js'; declare var baseURL: string; declare var commQueryExecutorFilename: string; declare var backupClientFilename: string; const sharedWorkerStatuses = Object.freeze({ notRunning: 'NOT_RUNNING', initSuccess: 'INIT_SUCCESS', initInProgress: 'INIT_IN_PROGRESS', initError: 'INIT_ERROR', }); type SharedWorkerStatus = | { +type: 'NOT_RUNNING' | 'INIT_SUCCESS' | 'INIT_ERROR' } | { +type: 'INIT_IN_PROGRESS', +initPromise: Promise }; type InitOptions = { +clearDatabase: boolean }; class CommSharedWorker { worker: ?SharedWorker; workerProxy: ?WorkerConnectionProxy; status: SharedWorkerStatus = { type: sharedWorkerStatuses.notRunning }; async init({ clearDatabase }: InitOptions): Promise { if (!isSQLiteSupported()) { console.warn('SQLite is not supported'); this.status = { type: sharedWorkerStatuses.initError }; return; } if (this.status.type === sharedWorkerStatuses.initInProgress) { await this.status.initPromise; return; } if ( (this.status.type === sharedWorkerStatuses.initSuccess && !clearDatabase) || this.status.type === sharedWorkerStatuses.initError ) { return; } const initPromise = (async () => { if ( clearDatabase && this.status.type === sharedWorkerStatuses.initSuccess ) { console.info('Clearing sensitive data'); invariant(this.workerProxy, 'Worker proxy should exist'); await this.workerProxy.scheduleOnWorker({ type: workerRequestMessageTypes.CLEAR_SENSITIVE_DATA, }); } const codeVersion = getConfig().platformDetails.codeVersion ?? ''; const workerName = `comm-app-shared-worker-${codeVersion}`; this.worker = new SharedWorker(DATABASE_WORKER_PATH, workerName); this.worker.onerror = console.error; this.workerProxy = new WorkerConnectionProxy( this.worker.port, console.error, ); const origin = window.location.origin; try { let encryptionKey = null; if (isDesktopSafari) { encryptionKey = await getSafariEncryptionKey(); } invariant(this.workerProxy, 'Worker proxy should exist'); await this.workerProxy.scheduleOnWorker({ type: workerRequestMessageTypes.INIT, webworkerModulesFilePath: `${origin}${baseURL}${WORKERS_MODULES_DIR_PATH}`, encryptionKey, commQueryExecutorFilename, backupClientFilename, }); this.status = { type: sharedWorkerStatuses.initSuccess }; console.info('Database initialization success'); } catch (error) { this.status = { type: sharedWorkerStatuses.initError }; console.error(`Database initialization failure`, error); } })(); this.status = { type: sharedWorkerStatuses.initInProgress, initPromise }; await initPromise; } async isSupported(): Promise { if (this.status.type === sharedWorkerStatuses.initInProgress) { await this.status.initPromise; } return this.status.type === sharedWorkerStatuses.initSuccess; } async schedule( payload: WorkerRequestMessage, ): Promise { if (this.status.type === sharedWorkerStatuses.notRunning) { throw new Error('Database not running'); } if (this.status.type === sharedWorkerStatuses.initInProgress) { await this.status.initPromise; } if (this.status.type === sharedWorkerStatuses.initError) { throw new Error('Database could not be initialized'); } invariant(this.workerProxy, 'Worker proxy should exist'); return this.workerProxy.scheduleOnWorker(payload); } } async function getSafariEncryptionKey(): Promise { const encryptionKey = await localforage.getItem( SQLITE_ENCRYPTION_KEY, ); if (encryptionKey) { return await exportKeyToJWK(encryptionKey); } const newEncryptionKey = await generateCryptoKey({ extractable: true, }); await localforage.setItem(SQLITE_ENCRYPTION_KEY, newEncryptionKey); return await exportKeyToJWK(newEncryptionKey); } let sharedWorker: ?CommSharedWorker = null; async function getCommSharedWorker(): Promise { if (sharedWorker) { return sharedWorker; } const newModule = new CommSharedWorker(); sharedWorker = newModule; await newModule.init({ clearDatabase: false }); return newModule; } -export { getCommSharedWorker }; +export { CommSharedWorker, getCommSharedWorker }; diff --git a/web/shared-worker/worker/identity-client.js b/web/shared-worker/worker/identity-client.js index c1b67e596..75339766e 100644 --- a/web/shared-worker/worker/identity-client.js +++ b/web/shared-worker/worker/identity-client.js @@ -1,36 +1,61 @@ // @flow import { getDeviceKeyUpload } from './worker-crypto.js'; import { IdentityServiceClientWrapper } from '../../grpc/identity-service-client-wrapper.js'; -import { workerRequestMessageTypes } from '../../types/worker-types.js'; -import type { - WorkerResponseMessage, - WorkerRequestMessage, +import { + type WorkerResponseMessage, + type WorkerRequestMessage, + workerRequestMessageTypes, + workerResponseMessageTypes, } from '../../types/worker-types.js'; import type { EmscriptenModule } from '../types/module.js'; import type { SQLiteQueryExecutor } from '../types/sqlite-query-executor.js'; let identityClient: ?IdentityServiceClientWrapper = null; async function processAppIdentityClientRequest( sqliteQueryExecutor: SQLiteQueryExecutor, dbModule: EmscriptenModule, message: WorkerRequestMessage, ): Promise { if ( message.type === workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT ) { identityClient = new IdentityServiceClientWrapper( message.platformDetails, message.opaqueWasmPath, message.authLayer, async () => getDeviceKeyUpload(), ); + return undefined; } + + if (!identityClient) { + throw new Error('Identity client not created'); + } + + if (message.type === workerRequestMessageTypes.CALL_IDENTITY_CLIENT_METHOD) { + // Flow doesn't allow us to access methods like this (it needs an index + // signature declaration in the object type) + // $FlowFixMe + const method = identityClient[message.method]; + if (typeof method !== 'function') { + throw new Error( + `Couldn't find identity client method with name '${message.method}'`, + ); + } + const result = await method(...message.args); + return { + type: workerResponseMessageTypes.CALL_IDENTITY_CLIENT_METHOD, + result, + }; + } + + return undefined; } function getIdentityClient(): ?IdentityServiceClientWrapper { return identityClient; } export { processAppIdentityClientRequest, getIdentityClient }; diff --git a/web/types/worker-types.js b/web/types/worker-types.js index 2d3bd7d01..2ae4e2e27 100644 --- a/web/types/worker-types.js +++ b/web/types/worker-types.js @@ -1,188 +1,207 @@ // @flow import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; import type { CryptoStore } from 'lib/types/crypto-types.js'; import type { PlatformDetails } from 'lib/types/device-types.js'; -import type { IdentityServiceAuthLayer } from 'lib/types/identity-service-types.js'; +import type { + IdentityServiceClient, + IdentityServiceAuthLayer, +} from 'lib/types/identity-service-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, CREATE_IDENTITY_SERVICE_CLIENT: 13, + CALL_IDENTITY_CLIENT_METHOD: 14, }); 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 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, +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 CreateIdentityServiceClientRequestMessage = { +type: 13, +opaqueWasmPath: string, +platformDetails: PlatformDetails, +authLayer: ?IdentityServiceAuthLayer, }; +export type CallIdentityClientMethodRequestMessage = { + +type: 14, + +method: $Keys, + +args: $ReadOnlyArray, +}; + export type WorkerRequestMessage = | PingWorkerRequestMessage | InitWorkerRequestMessage | GenerateDatabaseEncryptionKeyRequestMessage | ProcessStoreOperationsRequestMessage | GetClientStoreRequestMessage | SetCurrentUserIDRequestMessage | GetCurrentUserIDRequestMessage | GetPersistStorageItemRequestMessage | SetPersistStorageItemRequestMessage | RemovePersistStorageItemRequestMessage | ClearSensitiveDataRequestMessage | BackupRestoreRequestMessage | InitializeCryptoAccountRequestMessage - | CreateIdentityServiceClientRequestMessage; + | CreateIdentityServiceClientRequestMessage + | CallIdentityClientMethodRequestMessage; 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, + CALL_IDENTITY_CLIENT_METHOD: 4, }); 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 WorkerResponseMessage = | PongWorkerResponseMessage | ClientStoreResponseMessage | GetCurrentUserIDResponseMessage - | GetPersistStorageItemResponseMessage; + | GetPersistStorageItemResponseMessage + | CallIdentityClientMethodResponseMessage; export type WorkerResponseProxyMessage = { +id?: number, +message?: WorkerResponseMessage, +error?: string, }; // SharedWorker types export type SharedWorkerMessageEvent = MessageEvent & { +ports: $ReadOnlyArray, ... };