diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js index 171458384..6dc3f9c72 100644 --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -1,179 +1,184 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { OlmSessionInitializationInfo } from './request-types.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 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 OlmEncryptedMessageTypes = $Values; export type EncryptedData = { +message: string, +messageType: OlmEncryptedMessageTypes, }; export const encryptedDataValidator: TInterface = tShape({ message: t.String, messageType: t.Number, }); export type ClientPublicKeys = { +primaryIdentityPublicKeys: { +ed25519: string, +curve25519: string, }, +notificationIdentityPublicKeys: { +ed25519: string, +curve25519: string, }, +blobPayload: string, +signature: string, }; export type OutboundSessionCreationResult = { +encryptedData: EncryptedData, +sessionVersion: number, }; export type OlmAPI = { +initializeCryptoAccount: () => Promise, +getUserPublicKey: () => Promise, +encrypt: (content: string, deviceID: string) => Promise, +decrypt: (encryptedData: EncryptedData, deviceID: string) => Promise, +decryptSequentialAndPersist: ( encryptedData: EncryptedData, deviceID: string, messageID: string, ) => Promise, +contentInboundSessionCreator: ( contentIdentityKeys: OLMIdentityKeys, initialEncryptedData: EncryptedData, sessionVersion: number, overwrite: boolean, ) => Promise, +contentOutboundSessionCreator: ( contentIdentityKeys: OLMIdentityKeys, contentInitializationInfo: OlmSessionInitializationInfo, ) => Promise, +notificationsSessionCreator: ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => Promise, +reassignNotificationsSession?: ( prevCookie: ?string, newCookie: ?string, keyserverID: string, ) => Promise, +getOneTimeKeys: (numberOfKeys: number) => Promise, +validateAndUploadPrekeys: (authMetadata: AuthMetadata) => Promise, +signMessage: (message: string) => Promise, + +verifyMessage: ( + message: string, + signature: string, + signingPublicKey: string, + ) => Promise, }; diff --git a/lib/utils/__mocks__/config.js b/lib/utils/__mocks__/config.js index 68a9d94d9..833a52fbb 100644 --- a/lib/utils/__mocks__/config.js +++ b/lib/utils/__mocks__/config.js @@ -1,36 +1,37 @@ // @flow import { type Config } from '../config.js'; const getConfig = (): Config => ({ resolveKeyserverSessionInvalidationUsingNativeCredentials: null, setSessionIDOnRequest: true, calendarRangeInactivityLimit: null, platformDetails: { platform: 'web', codeVersion: 70, stateVersion: 50, }, authoritativeKeyserverID: '123', olmAPI: { initializeCryptoAccount: jest.fn(), getUserPublicKey: jest.fn(), encrypt: jest.fn(), decrypt: jest.fn(), decryptSequentialAndPersist: jest.fn(), contentInboundSessionCreator: jest.fn(), contentOutboundSessionCreator: jest.fn(), notificationsSessionCreator: jest.fn(), getOneTimeKeys: jest.fn(), validateAndUploadPrekeys: jest.fn(), signMessage: jest.fn(), + verifyMessage: jest.fn(), }, sqliteAPI: { getAllReceivedMessageToDevice: jest.fn(), removeReceivedMessagesToDevice: jest.fn(), }, }); const hasConfig = (): boolean => true; export { getConfig, hasConfig }; diff --git a/native/crypto/olm-api.js b/native/crypto/olm-api.js index 229506fed..6a1c4ab22 100644 --- a/native/crypto/olm-api.js +++ b/native/crypto/olm-api.js @@ -1,98 +1,120 @@ // @flow import { getOneTimeKeyValues } from 'lib/shared/crypto-utils.js'; import { type AuthMetadata } from 'lib/shared/identity-client-context.js'; import { type OneTimeKeysResultValues, type OlmAPI, type OLMIdentityKeys, type EncryptedData, type OutboundSessionCreationResult, } from 'lib/types/crypto-types.js'; import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; +import { getMessageForException } from 'lib/utils/errors.js'; import { commCoreModule } from '../native-modules.js'; const olmAPI: OlmAPI = { async initializeCryptoAccount(): Promise { await commCoreModule.initializeCryptoAccount(); }, getUserPublicKey: commCoreModule.getUserPublicKey, encrypt: commCoreModule.encrypt, decrypt: commCoreModule.decrypt, decryptSequentialAndPersist: commCoreModule.decryptSequentialAndPersist, async contentInboundSessionCreator( contentIdentityKeys: OLMIdentityKeys, initialEncryptedData: EncryptedData, sessionVersion: number, overwrite: boolean, ): Promise { const identityKeys = JSON.stringify({ curve25519: contentIdentityKeys.curve25519, ed25519: contentIdentityKeys.ed25519, }); return commCoreModule.initializeContentInboundSession( identityKeys, initialEncryptedData, contentIdentityKeys.ed25519, sessionVersion, overwrite, ); }, async contentOutboundSessionCreator( contentIdentityKeys: OLMIdentityKeys, contentInitializationInfo: OlmSessionInitializationInfo, ): Promise { const { prekey, prekeySignature, oneTimeKey } = contentInitializationInfo; const identityKeys = JSON.stringify({ curve25519: contentIdentityKeys.curve25519, ed25519: contentIdentityKeys.ed25519, }); return commCoreModule.initializeContentOutboundSession( identityKeys, prekey, prekeySignature, oneTimeKey, contentIdentityKeys.ed25519, ); }, notificationsSessionCreator( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ): Promise { const { prekey, prekeySignature, oneTimeKey } = notificationsInitializationInfo; return commCoreModule.initializeNotificationsSession( JSON.stringify(notificationsIdentityKeys), prekey, prekeySignature, oneTimeKey, keyserverID, ); }, async getOneTimeKeys(numberOfKeys: number): Promise { const { contentOneTimeKeys, notificationsOneTimeKeys } = await commCoreModule.getOneTimeKeys(numberOfKeys); return { contentOneTimeKeys: getOneTimeKeyValues(contentOneTimeKeys), notificationsOneTimeKeys: getOneTimeKeyValues(notificationsOneTimeKeys), }; }, async validateAndUploadPrekeys(authMetadata: AuthMetadata): Promise { const { userID, deviceID, accessToken } = authMetadata; if (!userID || !deviceID || !accessToken) { return; } await commCoreModule.validateAndUploadPrekeys( userID, deviceID, accessToken, ); }, signMessage: commCoreModule.signMessage, + async verifyMessage( + message: string, + signature: string, + signingPublicKey: string, + ): Promise { + try { + await commCoreModule.verifySignature( + signingPublicKey, + message, + signature, + ); + return true; + } catch (err) { + const isSignatureInvalid = + getMessageForException(err)?.includes('BAD_MESSAGE_MAC'); + if (isSignatureInvalid) { + return false; + } + throw err; + } + }, }; export { olmAPI }; diff --git a/web/crypto/olm-api.js b/web/crypto/olm-api.js index 4d7249e2a..8bfa2aa12 100644 --- a/web/crypto/olm-api.js +++ b/web/crypto/olm-api.js @@ -1,59 +1,60 @@ // @flow 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, workerResponseMessageTypes, } from '../types/worker-types.js'; 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); }; } const olmAPI: OlmAPI = { async initializeCryptoAccount(): Promise { const sharedWorker = await getCommSharedWorker(); await sharedWorker.schedule({ type: workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, olmWasmPath: getOlmWasmPath(), }); }, getUserPublicKey: proxyToWorker('getUserPublicKey'), encrypt: proxyToWorker('encrypt'), decrypt: proxyToWorker('decrypt'), decryptSequentialAndPersist: proxyToWorker('decryptSequentialAndPersist'), contentInboundSessionCreator: proxyToWorker('contentInboundSessionCreator'), contentOutboundSessionCreator: proxyToWorker('contentOutboundSessionCreator'), notificationsSessionCreator: proxyToWorker('notificationsSessionCreator'), reassignNotificationsSession: proxyToWorker('reassignNotificationsSession'), getOneTimeKeys: proxyToWorker('getOneTimeKeys'), validateAndUploadPrekeys: proxyToWorker('validateAndUploadPrekeys'), signMessage: proxyToWorker('signMessage'), + verifyMessage: proxyToWorker('verifyMessage'), }; export { olmAPI }; diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js index 7c7140d6d..9aad2150d 100644 --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -1,783 +1,806 @@ // @flow -import olm from '@commapp/olm'; +import olm, { type Utility } from '@commapp/olm'; import localforage from 'localforage'; import uuid from 'uuid'; import { initialEncryptedMessageContent } from 'lib/shared/crypto-utils.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import { type OLMIdentityKeys, type PickledOLMAccount, type IdentityKeysBlob, type SignedIdentityKeysBlob, type OlmAPI, type OneTimeKeysResultValues, type ClientPublicKeys, type NotificationsOlmDataType, type EncryptedData, type OutboundSessionCreationResult, } from 'lib/types/crypto-types.js'; import type { PlatformDetails } from 'lib/types/device-types.js'; import type { IdentityNewDeviceKeyUpload, IdentityExistingDeviceKeyUpload, } from 'lib/types/identity-service-types.js'; import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; import type { ReceivedMessageToDevice } from 'lib/types/sqlite-types.js'; +import { getMessageForException } from 'lib/utils/errors.js'; import { entries } from 'lib/utils/objects.js'; import { retrieveAccountKeysSet, getAccountOneTimeKeys, getAccountPrekeysSet, shouldForgetPrekey, shouldRotatePrekey, retrieveIdentityKeysAndPrekeys, olmSessionErrors, } from 'lib/utils/olm-utils.js'; import { getIdentityClient } from './identity-client.js'; import { getProcessingStoreOpsExceptionMessage } from './process-operations.js'; import { getDBModule, getSQLiteQueryExecutor, getPlatformDetails, } from './worker-database.js'; import { encryptData, exportKeyToJWK, generateCryptoKey, } from '../../crypto/aes-gcm-crypto-utils.js'; import { getOlmDataContentKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, } from '../../push-notif/notif-crypto-utils.js'; import { type WorkerRequestMessage, type WorkerResponseMessage, workerRequestMessageTypes, workerResponseMessageTypes, type LegacyCryptoStore, } from '../../types/worker-types.js'; import type { OlmPersistSession } from '../types/sqlite-query-executor.js'; import { isDesktopSafari } from '../utils/db-utils.js'; type OlmSession = { +session: olm.Session, +version: number }; type OlmSessions = { [deviceID: string]: OlmSession, }; type WorkerCryptoStore = { +contentAccountPickleKey: string, +contentAccount: olm.Account, +contentSessions: OlmSessions, +notificationAccountPickleKey: string, +notificationAccount: olm.Account, }; let cryptoStore: ?WorkerCryptoStore = null; +let olmUtility: ?Utility = null; function clearCryptoStore() { cryptoStore = null; } function persistCryptoStore(withoutTransaction: boolean = false) { 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(([targetDeviceID, sessionData]) => ({ targetDeviceID, sessionData: sessionData.session.pickle(contentAccountPickleKey), version: sessionData.version, })); const pickledNotificationAccount: PickledOLMAccount = { picklingKey: notificationAccountPickleKey, pickledAccount: notificationAccount.pickle(notificationAccountPickleKey), }; try { if (!withoutTransaction) { sqliteQueryExecutor.beginTransaction(); } sqliteQueryExecutor.storeOlmPersistAccount( sqliteQueryExecutor.getContentAccountID(), JSON.stringify(pickledContentAccount), ); for (const pickledSession of pickledContentSessions) { sqliteQueryExecutor.storeOlmPersistSession(pickledSession); } sqliteQueryExecutor.storeOlmPersistAccount( sqliteQueryExecutor.getNotifsAccountID(), JSON.stringify(pickledNotificationAccount), ); if (!withoutTransaction) { sqliteQueryExecutor.commitTransaction(); } } catch (err) { if (!withoutTransaction) { sqliteQueryExecutor.rollbackTransaction(); } 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): OlmSessions { const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { throw new Error( "Couldn't get olm sessions because database is not initialized", ); } let dbSessionsData; try { dbSessionsData = sqliteQueryExecutor.getOlmPersistSessionsData(); } catch (err) { throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); } const sessionsData: OlmSessions = {}; for (const persistedSession: OlmPersistSession of dbSessionsData) { const { sessionData, version } = persistedSession; const session = new olm.Session(); session.unpickle(picklingKey, sessionData); sessionsData[persistedSession.targetDeviceID] = { session, version, }; } return sessionsData; } 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: ?LegacyCryptoStore, ) { const sqliteQueryExecutor = getSQLiteQueryExecutor(); if (!sqliteQueryExecutor) { throw new Error('Database not initialized'); } await olm.init({ locateFile: () => olmWasmPath }); + olmUtility = new olm.Utility(); if (initialCryptoStore) { cryptoStore = { contentAccountPickleKey: initialCryptoStore.primaryAccount.picklingKey, contentAccount: unpickleInitialCryptoStoreAccount( initialCryptoStore.primaryAccount, ), contentSessions: {}, notificationAccountPickleKey: initialCryptoStore.notificationAccount.picklingKey, notificationAccount: unpickleInitialCryptoStoreAccount( initialCryptoStore.notificationAccount, ), }; persistCryptoStore(); return; } 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, }; } function getNotifsPersistenceKeys( cookie: ?string, keyserverID: string, platformDetails: PlatformDetails, ) { if (hasMinCodeVersion(platformDetails, { majorDesktop: 12 })) { return { notifsOlmDataEncryptionKeyDBLabel: getOlmEncryptionKeyDBLabelForCookie( cookie, keyserverID, ), notifsOlmDataContentKey: getOlmDataContentKeyForCookie( cookie, keyserverID, ), }; } else { return { notifsOlmDataEncryptionKeyDBLabel: getOlmEncryptionKeyDBLabelForCookie(cookie), notifsOlmDataContentKey: getOlmDataContentKeyForCookie(cookie), }; } } async function reassignLocalForageItem(source: string, destination: string) { const value = await localforage.getItem(source); if (!value) { return; } const valueAtDestination = await localforage.getItem(destination); if (!valueAtDestination) { await localforage.setItem(destination, value); } await localforage.removeItem(source); } 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 getUserPublicKey(): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount, notificationAccount } = cryptoStore; const { payload, signature } = getSignedIdentityKeysBlob(); return { primaryIdentityPublicKeys: JSON.parse(contentAccount.identity_keys()), notificationIdentityPublicKeys: JSON.parse( notificationAccount.identity_keys(), ), blobPayload: payload, signature, }; }, 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 encryptedContent = session.encrypt(content); persistCryptoStore(); return { message: encryptedContent.body, messageType: encryptedContent.type, }; }, async decrypt( encryptedData: EncryptedData, 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( encryptedData.messageType, encryptedData.message, ); persistCryptoStore(); return result; }, async decryptSequentialAndPersist( encryptedData: EncryptedData, deviceID: string, messageID: 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_sequential( encryptedData.messageType, encryptedData.message, ); const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { throw new Error( "Couldn't persist crypto store because database is not initialized", ); } const receivedMessage: ReceivedMessageToDevice = { messageID, senderDeviceID: deviceID, plaintext: result, status: 'decrypted', }; sqliteQueryExecutor.beginTransaction(); try { sqliteQueryExecutor.addReceivedMessageToDevice(receivedMessage); persistCryptoStore(true); sqliteQueryExecutor.commitTransaction(); } catch (e) { sqliteQueryExecutor.rollbackTransaction(); throw e; } return result; }, async contentInboundSessionCreator( contentIdentityKeys: OLMIdentityKeys, initialEncryptedData: EncryptedData, sessionVersion: number, overwrite: boolean, ): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount, contentSessions } = cryptoStore; const existingSession = contentSessions[contentIdentityKeys.ed25519]; if (existingSession) { if (!overwrite && existingSession.version > sessionVersion) { throw new Error(olmSessionErrors.alreadyCreated); } else if (!overwrite && existingSession.version === sessionVersion) { throw new Error(olmSessionErrors.raceCondition); } } const session = new olm.Session(); session.create_inbound_from( contentAccount, contentIdentityKeys.curve25519, initialEncryptedData.message, ); contentAccount.remove_one_time_keys(session); const initialEncryptedMessage = session.decrypt( initialEncryptedData.messageType, initialEncryptedData.message, ); contentSessions[contentIdentityKeys.ed25519] = { session, version: sessionVersion, }; persistCryptoStore(); return initialEncryptedMessage; }, async contentOutboundSessionCreator( contentIdentityKeys: OLMIdentityKeys, contentInitializationInfo: OlmSessionInitializationInfo, ): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount, contentSessions } = cryptoStore; const existingSession = contentSessions[contentIdentityKeys.ed25519]; const session = new olm.Session(); session.create_outbound( contentAccount, contentIdentityKeys.curve25519, contentIdentityKeys.ed25519, contentInitializationInfo.prekey, contentInitializationInfo.prekeySignature, contentInitializationInfo.oneTimeKey, ); const initialEncryptedData = session.encrypt( JSON.stringify(initialEncryptedMessageContent), ); const newSessionVersion = existingSession ? existingSession.version + 1 : 1; contentSessions[contentIdentityKeys.ed25519] = { session, version: newSessionVersion, }; persistCryptoStore(); const encryptedData: EncryptedData = { message: initialEncryptedData.body, messageType: initialEncryptedData.type, }; return { encryptedData, sessionVersion: newSessionVersion }; }, async notificationsSessionCreator( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ): Promise { const platformDetails = getPlatformDetails(); if (!platformDetails) { throw new Error('Worker not initialized'); } if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { notificationAccountPickleKey, notificationAccount } = cryptoStore; const encryptionKey = await generateCryptoKey({ extractable: isDesktopSafari, }); const notificationsPrekey = notificationsInitializationInfo.prekey; const session = new olm.Session(); session.create_outbound( notificationAccount, notificationsIdentityKeys.curve25519, notificationsIdentityKeys.ed25519, notificationsPrekey, notificationsInitializationInfo.prekeySignature, notificationsInitializationInfo.oneTimeKey, ); const { body: initialNotificationsEncryptedMessage } = session.encrypt( JSON.stringify(initialEncryptedMessageContent), ); const mainSession = session.pickle(notificationAccountPickleKey); const notificationsOlmData: NotificationsOlmDataType = { mainSession, pendingSessionUpdate: mainSession, updateCreationTimestamp: Date.now(), picklingKey: notificationAccountPickleKey, }; const encryptedOlmData = await encryptData( new TextEncoder().encode(JSON.stringify(notificationsOlmData)), encryptionKey, ); const { notifsOlmDataContentKey, notifsOlmDataEncryptionKeyDBLabel } = getNotifsPersistenceKeys(cookie, keyserverID, platformDetails); const persistEncryptionKeyPromise = (async () => { let cryptoKeyPersistentForm; if (isDesktopSafari) { // Safari doesn't support structured clone algorithm in service // worker context so we have to store CryptoKey as JSON cryptoKeyPersistentForm = await exportKeyToJWK(encryptionKey); } else { cryptoKeyPersistentForm = encryptionKey; } await localforage.setItem( notifsOlmDataEncryptionKeyDBLabel, cryptoKeyPersistentForm, ); })(); await Promise.all([ localforage.setItem(notifsOlmDataContentKey, encryptedOlmData), persistEncryptionKeyPromise, ]); return initialNotificationsEncryptedMessage; }, async reassignNotificationsSession( prevCookie: ?string, newCookie: ?string, keyserverID: string, ): Promise { const platformDetails = getPlatformDetails(); if (!platformDetails) { throw new Error('Worker not initialized'); } const prevPersistenceKeys = getNotifsPersistenceKeys( prevCookie, keyserverID, platformDetails, ); const newPersistenceKeys = getNotifsPersistenceKeys( newCookie, keyserverID, platformDetails, ); await Promise.all([ reassignLocalForageItem( prevPersistenceKeys.notifsOlmDataContentKey, newPersistenceKeys.notifsOlmDataContentKey, ), reassignLocalForageItem( prevPersistenceKeys.notifsOlmDataEncryptionKeyDBLabel, newPersistenceKeys.notifsOlmDataEncryptionKeyDBLabel, ), ]); }, 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(); }, async signMessage(message: string): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount } = cryptoStore; return contentAccount.sign(message); }, + async verifyMessage( + message: string, + signature: string, + signingPublicKey: string, + ): Promise { + if (!olmUtility) { + throw new Error('Crypto account not initialized'); + } + try { + olmUtility.ed25519_verify(signingPublicKey, message, signature); + return true; + } catch (err) { + const isSignatureInvalid = + getMessageForException(err)?.includes('BAD_MESSAGE_MAC'); + if (isSignatureInvalid) { + return false; + } + throw err; + } + }, }; export { clearCryptoStore, processAppOlmApiRequest, getSignedIdentityKeysBlob, getNewDeviceKeyUpload, getExistingDeviceKeyUpload, };