diff --git a/lib/components/prekeys-handler.react.js b/lib/components/prekeys-handler.react.js --- a/lib/components/prekeys-handler.react.js +++ b/lib/components/prekeys-handler.react.js @@ -24,8 +24,10 @@ const timeoutID = setTimeout(async () => { try { + const authMetadata = await identityContext.getAuthMetadata(); + const { olmAPI } = getConfig(); - await olmAPI.validateAndUploadPrekeys(identityContext); + await olmAPI.validateAndUploadPrekeys(authMetadata); } catch (e) { console.log('Prekey validation error: ', e.message); } diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -2,7 +2,7 @@ import t, { type TInterface } from 'tcomb'; -import { type IdentityClientContextType } from '../shared/identity-client-context.js'; +import { type AuthMetadata } from '../shared/identity-client-context.js'; import { tShape } from '../utils/validation-utils.js'; export type OLMIdentityKeys = { @@ -131,7 +131,5 @@ initialEncryptedContent: string, ) => Promise, +getOneTimeKeys: (numberOfKeys: number) => Promise, - +validateAndUploadPrekeys: ( - identityContext: IdentityClientContextType, - ) => Promise, + +validateAndUploadPrekeys: (authMetadata: AuthMetadata) => Promise, }; diff --git a/native/crypto/olm-api.js b/native/crypto/olm-api.js --- a/native/crypto/olm-api.js +++ b/native/crypto/olm-api.js @@ -1,7 +1,7 @@ // @flow import { getOneTimeKeyValues } from 'lib/shared/crypto-utils.js'; -import { type IdentityClientContextType } from 'lib/shared/identity-client-context.js'; +import { type AuthMetadata } from 'lib/shared/identity-client-context.js'; import type { OneTimeKeysResultValues, OlmAPI, @@ -38,15 +38,7 @@ notificationsOneTimeKeys: getOneTimeKeyValues(notificationsOneTimeKeys), }; }, - async validateAndUploadPrekeys( - identityContext: IdentityClientContextType, - ): Promise { - let authMetadata; - try { - authMetadata = await identityContext.getAuthMetadata(); - } catch (e) { - return; - } + async validateAndUploadPrekeys(authMetadata: AuthMetadata): Promise { const { userID, deviceID, accessToken } = authMetadata; if (!userID || !deviceID || !accessToken) { return; diff --git a/web/crypto/olm-api.js b/web/crypto/olm-api.js --- a/web/crypto/olm-api.js +++ b/web/crypto/olm-api.js @@ -1,43 +1,43 @@ // @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 { type OlmAPI } from 'lib/types/crypto-types.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'; +import { + workerRequestMessageTypes, + workerResponseMessageTypes, +} 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(); +function proxyToWorker( + method: $Keys, +): (...args: $ReadOnlyArray) => Promise { + return async (...args: $ReadOnlyArray) => { + const sharedWorker = await getCommSharedWorker(); + const result = await sharedWorker.schedule({ + type: workerRequestMessageTypes.CALL_OLM_API_METHOD, + method, + args, + }); + + if (!result) { + throw new Error(`Worker OlmAPI call didn't return expected message`); + } else if (result.type !== workerResponseMessageTypes.CALL_OLM_API_METHOD) { + throw new Error( + `Worker OlmAPI 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); + }; } -// 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 { @@ -51,105 +51,11 @@ 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); - }, + encrypt: proxyToWorker('encrypt'), + decrypt: proxyToWorker('decrypt'), + contentInboundSessionCreator: proxyToWorker('contentInboundSessionCreator'), + getOneTimeKeys: proxyToWorker('getOneTimeKeys'), + validateAndUploadPrekeys: proxyToWorker('validateAndUploadPrekeys'), }; export { olmAPI, usingSharedWorker }; diff --git a/web/shared-worker/worker/shared-worker.js b/web/shared-worker/worker/shared-worker.js --- a/web/shared-worker/worker/shared-worker.js +++ b/web/shared-worker/worker/shared-worker.js @@ -253,7 +253,7 @@ let result; if (isOlmAPIRequest) { - await processAppOlmApiRequest(message); + result = await processAppOlmApiRequest(message); } else if (isIdentityClientRequest) { result = await processAppIdentityClientRequest( sqliteQueryExecutor, diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -3,11 +3,15 @@ import olm from '@commapp/olm'; import uuid from 'uuid'; -import type { - CryptoStore, - PickledOLMAccount, - IdentityKeysBlob, - SignedIdentityKeysBlob, +import { + olmEncryptedMessageTypes, + type OLMIdentityKeys, + type CryptoStore, + type PickledOLMAccount, + type IdentityKeysBlob, + type SignedIdentityKeysBlob, + type OlmAPI, + type OneTimeKeysResultValues, } from 'lib/types/crypto-types.js'; import type { IdentityNewDeviceKeyUpload, @@ -15,16 +19,22 @@ } from 'lib/types/identity-service-types.js'; import { entries } from 'lib/utils/objects.js'; import { - retrieveIdentityKeysAndPrekeys, retrieveAccountKeysSet, + getAccountOneTimeKeys, + getAccountPrekeysSet, + shouldForgetPrekey, + shouldRotatePrekey, + retrieveIdentityKeysAndPrekeys, } from 'lib/utils/olm-utils.js'; +import { getIdentityClient } from './identity-client.js'; import { getProcessingStoreOpsExceptionMessage } from './process-operations.js'; import { getDBModule, getSQLiteQueryExecutor } from './worker-database.js'; import { type WorkerRequestMessage, type WorkerResponseMessage, workerRequestMessageTypes, + workerResponseMessageTypes, } from '../../types/worker-types.js'; import type { OlmPersistSession } from '../types/sqlite-query-executor.js'; @@ -194,23 +204,7 @@ return; } - const contentAccountResult = getOrCreateOlmAccount( - sqliteQueryExecutor.getContentAccountID(), - ); - const contentSessions = getOlmSessions(contentAccountResult.picklingKey); - const notificationAccountResult = getOrCreateOlmAccount( - sqliteQueryExecutor.getNotifsAccountID(), - ); - - cryptoStore = { - contentAccountPickleKey: contentAccountResult.picklingKey, - contentAccount: contentAccountResult.account, - contentSessions, - notificationAccountPickleKey: notificationAccountResult.picklingKey, - notificationAccount: notificationAccountResult.account, - }; - - persistCryptoStore(); + await olmAPI.initializeCryptoAccount(); } async function processAppOlmApiRequest( @@ -221,7 +215,19 @@ message.olmWasmPath, message.initialCryptoStore, ); + } else if (message.type === workerRequestMessageTypes.CALL_OLM_API_METHOD) { + const method: (...$ReadOnlyArray) => mixed = (olmAPI[ + message.method + ]: any); + // Flow doesn't allow us to bind the (stringified) method name with + // the argument types so we need to pass the args as mixed. + const result = await method(...message.args); + return { + type: workerResponseMessageTypes.CALL_OLM_API_METHOD, + result, + }; } + return undefined; } function getSignedIdentityKeysBlob(): SignedIdentityKeysBlob { @@ -298,6 +304,167 @@ }; } +const olmAPI: OlmAPI = { + async initializeCryptoAccount(): Promise { + const sqliteQueryExecutor = getSQLiteQueryExecutor(); + if (!sqliteQueryExecutor) { + throw new Error('Database not initialized'); + } + + const contentAccountResult = getOrCreateOlmAccount( + sqliteQueryExecutor.getContentAccountID(), + ); + const notificationAccountResult = getOrCreateOlmAccount( + sqliteQueryExecutor.getNotifsAccountID(), + ); + const contentSessions = getOlmSessions(contentAccountResult.picklingKey); + + cryptoStore = { + contentAccountPickleKey: contentAccountResult.picklingKey, + contentAccount: contentAccountResult.account, + contentSessions, + notificationAccountPickleKey: notificationAccountResult.picklingKey, + notificationAccount: notificationAccountResult.account, + }; + + persistCryptoStore(); + }, + async encrypt(content: string, deviceID: string): Promise { + if (!cryptoStore) { + throw new Error('Crypto account not initialized'); + } + const session = cryptoStore.contentSessions[deviceID]; + if (!session) { + throw new Error(`No session for deviceID: ${deviceID}`); + } + const { body } = session.encrypt(content); + + persistCryptoStore(); + + return body; + }, + async decrypt(encryptedContent: string, deviceID: string): Promise { + if (!cryptoStore) { + throw new Error('Crypto account not initialized'); + } + + const session = cryptoStore.contentSessions[deviceID]; + if (!session) { + throw new Error(`No session for deviceID: ${deviceID}`); + } + + const result = session.decrypt( + olmEncryptedMessageTypes.TEXT, + encryptedContent, + ); + + persistCryptoStore(); + + return result; + }, + async contentInboundSessionCreator( + contentIdentityKeys: OLMIdentityKeys, + initialEncryptedContent: string, + ): Promise { + if (!cryptoStore) { + throw new Error('Crypto account not initialized'); + } + const { contentAccount, contentSessions } = cryptoStore; + + const session = new olm.Session(); + session.create_inbound_from( + contentAccount, + contentIdentityKeys.curve25519, + initialEncryptedContent, + ); + + contentAccount.remove_one_time_keys(session); + const initialEncryptedMessage = session.decrypt( + olmEncryptedMessageTypes.PREKEY, + initialEncryptedContent, + ); + + contentSessions[contentIdentityKeys.ed25519] = session; + persistCryptoStore(); + + return initialEncryptedMessage; + }, + async getOneTimeKeys(numberOfKeys: number): Promise { + if (!cryptoStore) { + throw new Error('Crypto account not initialized'); + } + const { contentAccount, notificationAccount } = cryptoStore; + + const contentOneTimeKeys = getAccountOneTimeKeys( + contentAccount, + numberOfKeys, + ); + contentAccount.mark_keys_as_published(); + + const notificationsOneTimeKeys = getAccountOneTimeKeys( + notificationAccount, + numberOfKeys, + ); + notificationAccount.mark_keys_as_published(); + + persistCryptoStore(); + + return { contentOneTimeKeys, notificationsOneTimeKeys }; + }, + async validateAndUploadPrekeys(authMetadata): Promise { + const { userID, deviceID, accessToken } = authMetadata; + if (!userID || !deviceID || !accessToken) { + return; + } + const identityClient = getIdentityClient(); + + if (!identityClient) { + throw new Error('Identity client not initialized'); + } + + if (!cryptoStore) { + throw new Error('Crypto account not initialized'); + } + const { contentAccount, notificationAccount } = cryptoStore; + + // Content and notification accounts' keys are always rotated at the same + // time so we only need to check one of them. + if (shouldRotatePrekey(contentAccount)) { + contentAccount.generate_prekey(); + notificationAccount.generate_prekey(); + } + if (shouldForgetPrekey(contentAccount)) { + contentAccount.forget_old_prekey(); + notificationAccount.forget_old_prekey(); + } + persistCryptoStore(); + + if (!contentAccount.unpublished_prekey()) { + return; + } + + const { prekey: notifPrekey, prekeySignature: notifPrekeySignature } = + getAccountPrekeysSet(notificationAccount); + const { prekey: contentPrekey, prekeySignature: contentPrekeySignature } = + getAccountPrekeysSet(contentAccount); + + if (!notifPrekeySignature || !contentPrekeySignature) { + throw new Error('Prekey signature is missing'); + } + + await identityClient.publishWebPrekeys({ + contentPrekey, + contentPrekeySignature, + notifPrekey, + notifPrekeySignature, + }); + contentAccount.mark_prekey_as_published(); + notificationAccount.mark_prekey_as_published(); + + persistCryptoStore(); + }, +}; + export { clearCryptoStore, processAppOlmApiRequest, diff --git a/web/types/worker-types.js b/web/types/worker-types.js --- a/web/types/worker-types.js +++ b/web/types/worker-types.js @@ -1,7 +1,7 @@ // @flow import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; -import type { CryptoStore } from 'lib/types/crypto-types.js'; +import type { OlmAPI, CryptoStore } from 'lib/types/crypto-types.js'; import type { PlatformDetails } from 'lib/types/device-types.js'; import type { IdentityServiceClient, @@ -29,6 +29,7 @@ INITIALIZE_CRYPTO_ACCOUNT: 12, CREATE_IDENTITY_SERVICE_CLIENT: 13, CALL_IDENTITY_CLIENT_METHOD: 14, + CALL_OLM_API_METHOD: 15, }); export const workerWriteRequests: $ReadOnlyArray = [ @@ -42,6 +43,7 @@ export const workerOlmAPIRequests: $ReadOnlyArray = [ workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, + workerRequestMessageTypes.CALL_OLM_API_METHOD, ]; export const workerIdentityClientRequests: $ReadOnlyArray = [ @@ -131,6 +133,12 @@ +args: $ReadOnlyArray, }; +export type CallOLMApiMethodRequestMessage = { + +type: 15, + +method: $Keys, + +args: $ReadOnlyArray, +}; + export type WorkerRequestMessage = | PingWorkerRequestMessage | InitWorkerRequestMessage @@ -146,7 +154,8 @@ | BackupRestoreRequestMessage | InitializeCryptoAccountRequestMessage | CreateIdentityServiceClientRequestMessage - | CallIdentityClientMethodRequestMessage; + | CallIdentityClientMethodRequestMessage + | CallOLMApiMethodRequestMessage; export type WorkerRequestProxyMessage = { +id: number, @@ -160,6 +169,7 @@ GET_CURRENT_USER_ID: 2, GET_PERSIST_STORAGE_ITEM: 3, CALL_IDENTITY_CLIENT_METHOD: 4, + CALL_OLM_API_METHOD: 5, }); export type PongWorkerResponseMessage = { @@ -187,12 +197,18 @@ +result: mixed, }; +export type CallOLMApiMethodResponseMessage = { + +type: 5, + +result: mixed, +}; + export type WorkerResponseMessage = | PongWorkerResponseMessage | ClientStoreResponseMessage | GetCurrentUserIDResponseMessage | GetPersistStorageItemResponseMessage - | CallIdentityClientMethodResponseMessage; + | CallIdentityClientMethodResponseMessage + | CallOLMApiMethodResponseMessage; export type WorkerResponseProxyMessage = { +id?: number,