diff --git a/lib/components/prekeys-handler.react.js b/lib/components/prekeys-handler.react.js index 871a48fbb..fa0e692e4 100644 --- a/lib/components/prekeys-handler.react.js +++ b/lib/components/prekeys-handler.react.js @@ -1,40 +1,42 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { isLoggedIn } from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { getConfig } from '../utils/config.js'; import { useSelector } from '../utils/redux-utils.js'; // Time after which rotation is started const PREKEY_ROTATION_TIMEOUT = 3 * 1000; // in milliseconds function PrekeysHandler(): null { const loggedIn = useSelector(isLoggedIn); const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); React.useEffect(() => { if (!loggedIn) { return undefined; } 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); } }, PREKEY_ROTATION_TIMEOUT); return () => clearTimeout(timeoutID); }, [identityContext, loggedIn]); return null; } export default PrekeysHandler; diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js index 1b3ae002d..836b4b32d 100644 --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -1,137 +1,135 @@ // @flow 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 = { +ed25519: string, +curve25519: string, }; const olmIdentityKeysValidator: TInterface = tShape({ ed25519: t.String, curve25519: t.String, }); export type OLMPrekey = { +curve25519: { +[key: string]: string, }, }; export type SignedPrekeys = { +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, }; export const signedPrekeysValidator: TInterface = tShape({ contentPrekey: t.String, contentPrekeySignature: t.String, notifPrekey: t.String, notifPrekeySignature: t.String, }); export type OLMOneTimeKeys = { +curve25519: { +[string]: string }, }; export type OneTimeKeysResult = { +contentOneTimeKeys: OLMOneTimeKeys, +notificationsOneTimeKeys: OLMOneTimeKeys, }; export type OneTimeKeysResultValues = { +contentOneTimeKeys: $ReadOnlyArray, +notificationsOneTimeKeys: $ReadOnlyArray, }; export type PickledOLMAccount = { +picklingKey: string, +pickledAccount: string, }; export type CryptoStore = { +primaryAccount: PickledOLMAccount, +primaryIdentityKeys: OLMIdentityKeys, +notificationAccount: PickledOLMAccount, +notificationIdentityKeys: OLMIdentityKeys, }; export type CryptoStoreContextType = { +getInitializedCryptoStore: () => Promise, }; export type NotificationsOlmDataType = { +mainSession: string, +picklingKey: string, +pendingSessionUpdate: string, +updateCreationTimestamp: number, }; export type IdentityKeysBlob = { +primaryIdentityPublicKeys: OLMIdentityKeys, +notificationIdentityPublicKeys: OLMIdentityKeys, }; export const identityKeysBlobValidator: TInterface = tShape({ primaryIdentityPublicKeys: olmIdentityKeysValidator, notificationIdentityPublicKeys: olmIdentityKeysValidator, }); export type SignedIdentityKeysBlob = { +payload: string, +signature: string, }; export const signedIdentityKeysBlobValidator: TInterface = tShape({ payload: t.String, signature: t.String, }); export type UserDetail = { +username: string, +userID: string, }; // This type should not be changed without making equivalent changes to // `Message` in Identity service's `reserved_users` module export type ReservedUsernameMessage = | { +statement: 'Add the following usernames to reserved list', +payload: $ReadOnlyArray, +issuedAt: string, } | { +statement: 'Remove the following username from reserved list', +payload: string, +issuedAt: string, } | { +statement: 'This user is the owner of the following username and user ID', +payload: UserDetail, +issuedAt: string, }; export const olmEncryptedMessageTypes = Object.freeze({ PREKEY: 0, TEXT: 1, }); export type OlmAPI = { +initializeCryptoAccount: () => Promise, +encrypt: (content: string, deviceID: string) => Promise, +decrypt: (encryptedContent: string, deviceID: string) => Promise, +contentInboundSessionCreator: ( contentIdentityKeys: OLMIdentityKeys, 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 index 5b3e433a4..efba59b71 100644 --- a/native/crypto/olm-api.js +++ b/native/crypto/olm-api.js @@ -1,62 +1,54 @@ // @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, OLMIdentityKeys, } from 'lib/types/crypto-types'; import { commCoreModule } from '../native-modules.js'; const olmAPI: OlmAPI = { async initializeCryptoAccount(): Promise { await commCoreModule.initializeCryptoAccount(); }, encrypt: commCoreModule.encrypt, decrypt: commCoreModule.decrypt, async contentInboundSessionCreator( contentIdentityKeys: OLMIdentityKeys, initialEncryptedContent: string, ): Promise { const identityKeys = JSON.stringify({ curve25519: contentIdentityKeys.curve25519, ed25519: contentIdentityKeys.ed25519, }); return commCoreModule.initializeContentInboundSession( identityKeys, initialEncryptedContent, contentIdentityKeys.ed25519, ); }, async getOneTimeKeys(numberOfKeys: number): Promise { const { contentOneTimeKeys, notificationsOneTimeKeys } = await commCoreModule.getOneTimeKeys(numberOfKeys); return { contentOneTimeKeys: getOneTimeKeyValues(contentOneTimeKeys), 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; } await commCoreModule.validateAndUploadPrekeys( userID, deviceID, accessToken, ); }, }; export { olmAPI }; diff --git a/web/crypto/olm-api.js b/web/crypto/olm-api.js index f60daf705..cdff26c1c 100644 --- a/web/crypto/olm-api.js +++ b/web/crypto/olm-api.js @@ -1,155 +1,61 @@ // @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 { 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); - }, + 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 index d72df88c8..db332cba6 100644 --- a/web/shared-worker/worker/shared-worker.js +++ b/web/shared-worker/worker/shared-worker.js @@ -1,337 +1,337 @@ // @flow import localforage from 'localforage'; 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, } 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 { 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 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) { - await processAppOlmApiRequest(message); + result = await processAppOlmApiRequest(message); } else if (isIdentityClientRequest) { result = await processAppIdentityClientRequest( sqliteQueryExecutor, dbModule, 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 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: e.message, }); } })(); }; } self.addEventListener('connect', connectHandler); diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js index c53db509e..5a5ed0f79 100644 --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -1,307 +1,474 @@ // @flow 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, IdentityExistingDeviceKeyUpload, } 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'; type WorkerCryptoStore = { +contentAccountPickleKey: string, +contentAccount: olm.Account, +contentSessions: { [deviceID: string]: olm.Session }, +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, contentSessions, notificationAccountPickleKey, notificationAccount, } = cryptoStore; const pickledContentAccount: PickledOLMAccount = { picklingKey: contentAccountPickleKey, pickledAccount: contentAccount.pickle(contentAccountPickleKey), }; const pickledContentSessions: OlmPersistSession[] = entries( contentSessions, ).map(([deviceID, session]) => ({ targetUserID: deviceID, sessionData: session.pickle(contentAccountPickleKey), })); const pickledNotificationAccount: PickledOLMAccount = { picklingKey: notificationAccountPickleKey, pickledAccount: notificationAccount.pickle(notificationAccountPickleKey), }; try { sqliteQueryExecutor.storeOlmPersistAccount( sqliteQueryExecutor.getContentAccountID(), JSON.stringify(pickledContentAccount), ); for (const pickledSession of pickledContentSessions) { sqliteQueryExecutor.storeOlmPersistSession(pickledSession); } 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 getOlmSessions(picklingKey: string): { [deviceID: string]: olm.Session, } { const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { throw new Error( "Couldn't get olm sessions because database is not initialized", ); } let sessionsData; try { sessionsData = sqliteQueryExecutor.getOlmPersistSessionsData(); } catch (err) { throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); } const sessions: { [deviceID: string]: olm.Session } = {}; for (const sessionData of sessionsData) { const session = new olm.Session(); session.unpickle(picklingKey, sessionData.sessionData); sessions[sessionData.targetUserID] = session; } return sessions; } 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, ), contentSessions: {}, notificationAccountPickleKey: initialCryptoStore.notificationAccount.picklingKey, notificationAccount: unpickleInitialCryptoStoreAccount( initialCryptoStore.notificationAccount, ), }; persistCryptoStore(); 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( message: WorkerRequestMessage, ): Promise { if (message.type === workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT) { await initializeCryptoAccount( 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 { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount, notificationAccount } = cryptoStore; const identityKeysBlob: IdentityKeysBlob = { notificationIdentityPublicKeys: JSON.parse( notificationAccount.identity_keys(), ), primaryIdentityPublicKeys: JSON.parse(contentAccount.identity_keys()), }; const payloadToBeSigned: string = JSON.stringify(identityKeysBlob); const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: payloadToBeSigned, signature: contentAccount.sign(payloadToBeSigned), }; return signedIdentityKeysBlob; } function getNewDeviceKeyUpload(): IdentityNewDeviceKeyUpload { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount, notificationAccount } = cryptoStore; const signedIdentityKeysBlob = getSignedIdentityKeysBlob(); const primaryAccountKeysSet = retrieveAccountKeysSet(contentAccount); const notificationAccountKeysSet = retrieveAccountKeysSet(notificationAccount); persistCryptoStore(); 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, }; } function getExistingDeviceKeyUpload(): IdentityExistingDeviceKeyUpload { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount, notificationAccount } = cryptoStore; const signedIdentityKeysBlob = getSignedIdentityKeysBlob(); const { prekey: contentPrekey, prekeySignature: contentPrekeySignature } = retrieveIdentityKeysAndPrekeys(contentAccount); const { prekey: notifPrekey, prekeySignature: notifPrekeySignature } = retrieveIdentityKeysAndPrekeys(notificationAccount); persistCryptoStore(); return { keyPayload: signedIdentityKeysBlob.payload, keyPayloadSignature: signedIdentityKeysBlob.signature, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, }; } +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, getSignedIdentityKeysBlob, getNewDeviceKeyUpload, getExistingDeviceKeyUpload, }; diff --git a/web/types/worker-types.js b/web/types/worker-types.js index 2ae4e2e27..abbb88879 100644 --- a/web/types/worker-types.js +++ b/web/types/worker-types.js @@ -1,207 +1,223 @@ // @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, 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, + CALL_OLM_API_METHOD: 15, }); 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, + 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, +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 CallOLMApiMethodRequestMessage = { + +type: 15, + +method: $Keys, + +args: $ReadOnlyArray, +}; + export type WorkerRequestMessage = | PingWorkerRequestMessage | InitWorkerRequestMessage | GenerateDatabaseEncryptionKeyRequestMessage | ProcessStoreOperationsRequestMessage | GetClientStoreRequestMessage | SetCurrentUserIDRequestMessage | GetCurrentUserIDRequestMessage | GetPersistStorageItemRequestMessage | SetPersistStorageItemRequestMessage | RemovePersistStorageItemRequestMessage | ClearSensitiveDataRequestMessage | BackupRestoreRequestMessage | InitializeCryptoAccountRequestMessage | CreateIdentityServiceClientRequestMessage - | CallIdentityClientMethodRequestMessage; + | CallIdentityClientMethodRequestMessage + | CallOLMApiMethodRequestMessage; 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, + CALL_OLM_API_METHOD: 5, }); 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 WorkerResponseMessage = | PongWorkerResponseMessage | ClientStoreResponseMessage | GetCurrentUserIDResponseMessage | GetPersistStorageItemResponseMessage - | CallIdentityClientMethodResponseMessage; + | CallIdentityClientMethodResponseMessage + | CallOLMApiMethodResponseMessage; export type WorkerResponseProxyMessage = { +id?: number, +message?: WorkerResponseMessage, +error?: string, }; // SharedWorker types export type SharedWorkerMessageEvent = MessageEvent & { +ports: $ReadOnlyArray, ... };