diff --git a/web/shared-worker/utils/constants.js b/web/shared-worker/utils/constants.js --- a/web/shared-worker/utils/constants.js +++ b/web/shared-worker/utils/constants.js @@ -55,3 +55,15 @@ const olmWasmFilename = olmFilename ? olmFilename : DEFAULT_OLM_FILENAME; return `${olmWasmDirPath}/${olmWasmFilename}`; } + +declare var opaqueURL: string; +export function opaqueWasmPath(): string { + try { + // Check if it's a valid url + new URL(opaqueURL); + return opaqueURL; + } catch (err) {} + + const origin = window.location.origin; + return `${origin}${baseURL}/${opaqueURL}`; +} diff --git a/web/shared-worker/worker/identity-client.js b/web/shared-worker/worker/identity-client.js new file mode 100644 --- /dev/null +++ b/web/shared-worker/worker/identity-client.js @@ -0,0 +1,52 @@ +// @flow + +import type { PlatformDetails } from 'lib/types/device-types.js'; +import type { IdentityServiceAuthLayer } from 'lib/types/identity-service-types.js'; + +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, +} from '../../types/worker-types.js'; +import type { EmscriptenModule } from '../types/module'; +import type { SQLiteQueryExecutor } from '../types/sqlite-query-executor'; + +// eslint-disable-next-line no-unused-vars +let identityClient: ?IdentityServiceClientWrapper = null; + +async function processAppIdentityClientRequest( + sqliteQueryExecutor: SQLiteQueryExecutor, + dbModule: EmscriptenModule, + message: WorkerRequestMessage, +): Promise { + if ( + message.type === workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT + ) { + createIdentityServiceClient( + sqliteQueryExecutor, + dbModule, + message.opaqueWasmPath, + message.platformDetails, + message.authLayer, + ); + } +} + +function createIdentityServiceClient( + sqliteQueryExecutor: SQLiteQueryExecutor, + dbModule: EmscriptenModule, + opaqueWasmPath: string, + platformDetails: PlatformDetails, + authLayer: ?IdentityServiceAuthLayer, +) { + identityClient = new IdentityServiceClientWrapper( + platformDetails, + opaqueWasmPath, + authLayer, + () => Promise.resolve(getDeviceKeyUpload(sqliteQueryExecutor, dbModule)), + ); +} + +export { processAppIdentityClientRequest }; 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 @@ -3,6 +3,7 @@ import localforage from 'localforage'; import { restoreBackup } from './backup.js'; +import { processAppIdentityClientRequest } from './identity-client.js'; import { getClientStoreFromQueryExecutor, processDBStoreOperations, @@ -25,6 +26,7 @@ type WorkerRequestProxyMessage, workerWriteRequests, } from '../../types/worker-types.js'; +import { workerIdentityClientRequests } from '../../types/worker-types.js'; import { getDatabaseModule } from '../db-module.js'; import { type EmscriptenModule } from '../types/module.js'; import { type SQLiteQueryExecutor } from '../types/sqlite-query-executor.js'; @@ -223,8 +225,10 @@ }; } - // write operations - if (!workerWriteRequests.includes(message.type)) { + const isIdentityClientRequest = workerIdentityClientRequests.includes( + message.type, + ); + if (!workerWriteRequests.includes(message.type) && !isIdentityClientRequest) { throw new Error('Request type not supported'); } if (!sqliteQueryExecutor || !dbModule) { @@ -233,6 +237,15 @@ ); } + if (isIdentityClientRequest) { + return processAppIdentityClientRequest( + sqliteQueryExecutor, + dbModule, + message, + ); + } + + // write operations if (message.type === workerRequestMessageTypes.PROCESS_STORE_OPERATIONS) { processDBStoreOperations( 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 @@ -7,7 +7,11 @@ CryptoStore, PickledOLMAccount, OLMIdentityKeys, + IdentityKeysBlob, + SignedIdentityKeysBlob, } from 'lib/types/crypto-types.js'; +import type { IdentityDeviceKeyUpload } from 'lib/types/identity-service-types.js'; +import { retrieveAccountKeysSet } from 'lib/utils/olm-utils.js'; import { getProcessingStoreOpsExceptionMessage } from './process-operations.js'; import type { EmscriptenModule } from '../types/module'; @@ -117,4 +121,97 @@ persistCryptoStore(sqliteQueryExecutor, dbModule); } -export { clearCryptoStore, initializeCryptoAccount }; +function getSignedIdentityKeysBlob(): SignedIdentityKeysBlob { + if (!cryptoStore) { + throw new Error('Crypto account not initialized'); + } + + const { primaryAccount, primaryIdentityKeys, notificationIdentityKeys } = + cryptoStore; + + const primaryOLMAccount = new olm.Account(); + primaryOLMAccount.unpickle( + primaryAccount.picklingKey, + primaryAccount.pickledAccount, + ); + + const identityKeysBlob: IdentityKeysBlob = { + primaryIdentityPublicKeys: primaryIdentityKeys, + notificationIdentityPublicKeys: notificationIdentityKeys, + }; + + const payloadToBeSigned: string = JSON.stringify(identityKeysBlob); + const signedIdentityKeysBlob: SignedIdentityKeysBlob = { + payload: payloadToBeSigned, + signature: primaryOLMAccount.sign(payloadToBeSigned), + }; + + return signedIdentityKeysBlob; +} + +function getDeviceKeyUpload( + sqliteQueryExecutor: SQLiteQueryExecutor, + dbModule: EmscriptenModule, +): IdentityDeviceKeyUpload { + if (!cryptoStore) { + throw new Error('Crypto account not initialized'); + } + const { primaryAccount, notificationAccount, ...rest } = cryptoStore; + + const signedIdentityKeysBlob = getSignedIdentityKeysBlob(); + + const primaryOLMAccount = new olm.Account(); + const notificationOLMAccount = new olm.Account(); + primaryOLMAccount.unpickle( + primaryAccount.picklingKey, + primaryAccount.pickledAccount, + ); + notificationOLMAccount.unpickle( + notificationAccount.picklingKey, + notificationAccount.pickledAccount, + ); + + const primaryAccountKeysSet = retrieveAccountKeysSet(primaryOLMAccount); + const notificationAccountKeysSet = retrieveAccountKeysSet( + notificationOLMAccount, + ); + + const pickledPrimaryAccount = primaryOLMAccount.pickle( + primaryAccount.picklingKey, + ); + const pickledNotificationAccount = notificationOLMAccount.pickle( + notificationAccount.picklingKey, + ); + + cryptoStore = { + ...rest, + primaryAccount: { + picklingKey: primaryAccount.picklingKey, + pickledAccount: pickledPrimaryAccount, + }, + notificationAccount: { + picklingKey: notificationAccount.picklingKey, + pickledAccount: pickledNotificationAccount, + }, + }; + + persistCryptoStore(sqliteQueryExecutor, dbModule); + + return { + keyPayload: signedIdentityKeysBlob.payload, + keyPayloadSignature: signedIdentityKeysBlob.signature, + contentPrekey: primaryAccountKeysSet.prekey, + contentPrekeySignature: primaryAccountKeysSet.prekeySignature, + notifPrekey: notificationAccountKeysSet.prekey, + notifPrekeySignature: notificationAccountKeysSet.prekeySignature, + contentOneTimeKeys: primaryAccountKeysSet.oneTimeKeys, + notifOneTimeKeys: notificationAccountKeysSet.oneTimeKeys, + }; +} + +export { + clearCryptoStore, + initializeCryptoAccount, + getSignedIdentityKeysBlob, + getDeviceKeyUpload, +}; 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 @@ -2,6 +2,8 @@ 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 { ClientDBStore, ClientDBStoreOperations, @@ -22,6 +24,7 @@ CLEAR_SENSITIVE_DATA: 10, BACKUP_RESTORE: 11, INITIALIZE_CRYPTO_ACCOUNT: 12, + CREATE_IDENTITY_SERVICE_CLIENT: 13, }); export const workerWriteRequests: $ReadOnlyArray = [ @@ -33,6 +36,10 @@ workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, ]; +export const workerIdentityClientRequests: $ReadOnlyArray = [ + workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT, +]; + export type PingWorkerRequestMessage = { +type: 0, +text: string, @@ -103,6 +110,13 @@ +initialCryptoStore?: CryptoStore, }; +export type CreateIdentityServiceClientRequestMessage = { + +type: 13, + +opaqueWasmPath: string, + +platformDetails: PlatformDetails, + +authLayer: ?IdentityServiceAuthLayer, +}; + export type WorkerRequestMessage = | PingWorkerRequestMessage | InitWorkerRequestMessage @@ -116,7 +130,8 @@ | RemovePersistStorageItemRequestMessage | ClearSensitiveDataRequestMessage | BackupRestoreRequestMessage - | InitializeCryptoAccountRequestMessage; + | InitializeCryptoAccountRequestMessage + | CreateIdentityServiceClientRequestMessage; export type WorkerRequestProxyMessage = { +id: number,