diff --git a/lib/tunnelbroker/peer-to-peer-context.js b/lib/tunnelbroker/peer-to-peer-context.js index 308b8c405..bf7eea5b3 100644 --- a/lib/tunnelbroker/peer-to-peer-context.js +++ b/lib/tunnelbroker/peer-to-peer-context.js @@ -1,398 +1,398 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import uuid from 'uuid'; import { type TunnelbrokerClientMessageToDevice, useTunnelbroker, } from './tunnelbroker-context.js'; import { usePeerOlmSessionsCreatorContext } from '../components/peer-olm-session-creator-provider.react.js'; import { useSendPushNotifs } from '../push/send-hooks.react.js'; import { type AuthMetadata, IdentityClientContext, type IdentityClientContextType, } from '../shared/identity-client-context.js'; import type { NotificationsCreationData } from '../types/notif-types.js'; import { type OutboundP2PMessage, outboundP2PMessageStatuses, } from '../types/sqlite-types.js'; import { type EncryptedMessage, peerToPeerMessageTypes, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import { getConfig } from '../utils/config.js'; import { getMessageForException } from '../utils/errors.js'; import { entries } from '../utils/objects.js'; import { olmSessionErrors } from '../utils/olm-utils.js'; type PeerToPeerContextType = { +processOutboundMessages: ( outboundMessageIDs: ?$ReadOnlyArray, dmOpID: ?string, notificationsCreationData: ?NotificationsCreationData, ) => void, +getDMOpsSendingPromise: () => { +promise: Promise<$ReadOnlyArray>, +dmOpID: string, }, +broadcastEphemeralMessage: ( contentPayload: string, recipients: $ReadOnlyArray<{ +userID: string, +deviceID: string }>, authMetadata: AuthMetadata, ) => Promise, }; const PeerToPeerContext: React.Context = React.createContext(); type Props = { +children: React.Node, }; async function processOutboundP2PMessages( sendMessage: ( message: TunnelbrokerClientMessageToDevice, messageID: ?string, ) => Promise, identityContext: IdentityClientContextType, peerOlmSessionsCreator: (userID: string, deviceID: string) => Promise, messageIDs: ?$ReadOnlyArray, ): Promise<$ReadOnlyArray> { let authMetadata; try { authMetadata = await identityContext.getAuthMetadata(); } catch (e) { return []; } if ( !authMetadata.deviceID || !authMetadata.userID || !authMetadata.accessToken ) { return []; } const { olmAPI, sqliteAPI } = getConfig(); await olmAPI.initializeCryptoAccount(); let messages; if (messageIDs) { messages = await sqliteAPI.getOutboundP2PMessagesByID(messageIDs); } else { const allMessages = await sqliteAPI.getAllOutboundP2PMessages(); messages = allMessages.filter(message => message.supportsAutoRetry); } const devicesMap: { [deviceID: string]: OutboundP2PMessage[] } = {}; for (const message: OutboundP2PMessage of messages) { if (!devicesMap[message.deviceID]) { devicesMap[message.deviceID] = [message]; } else { devicesMap[message.deviceID].push(message); } } const sentMessagesMap: { [messageID: string]: boolean } = {}; const sendMessageToPeer = async ( message: OutboundP2PMessage, ): Promise => { if (!authMetadata.deviceID || !authMetadata.userID) { return; } try { const encryptedMessage: EncryptedMessage = { type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, senderInfo: { deviceID: authMetadata.deviceID, userID: authMetadata.userID, }, encryptedData: JSON.parse(message.ciphertext), }; await sendMessage( { deviceID: message.deviceID, payload: JSON.stringify(encryptedMessage), }, message.messageID, ); await sqliteAPI.markOutboundP2PMessageAsSent( message.messageID, message.deviceID, ); sentMessagesMap[message.messageID] = true; } catch (e) { console.error(e); } }; const devicePromises = entries(devicesMap).map( async ([peerDeviceID, deviceMessages]) => { for (const message of deviceMessages) { if (message.status === outboundP2PMessageStatuses.persisted) { try { const result = await olmAPI.encryptAndPersist( message.plaintext, message.deviceID, message.messageID, ); const encryptedMessage: OutboundP2PMessage = { ...message, ciphertext: JSON.stringify(result), }; await sendMessageToPeer(encryptedMessage); } catch (e) { - if (!e.message?.includes(olmSessionErrors.sessionDoesNotExists)) { + if (!e.message?.includes(olmSessionErrors.sessionDoesNotExist)) { console.log(`Error sending messages to peer ${peerDeviceID}`, e); break; } try { await peerOlmSessionsCreator(message.userID, peerDeviceID); const result = await olmAPI.encryptAndPersist( message.plaintext, message.deviceID, message.messageID, ); const encryptedMessage: OutboundP2PMessage = { ...message, ciphertext: JSON.stringify(result), }; await sendMessageToPeer(encryptedMessage); } catch (err) { console.log( `Error sending messages to peer ${peerDeviceID}`, err, ); break; } } } else if (message.status === outboundP2PMessageStatuses.encrypted) { await sendMessageToPeer(message); } else if (message.status === outboundP2PMessageStatuses.sent) { // Handle edge-case when message was sent, but it wasn't updated // in the message store. sentMessagesMap[message.messageID] = true; } } }, ); await Promise.all(devicePromises); return Object.keys(sentMessagesMap); } const AUTOMATIC_RETRY_FREQUENCY = 30 * 1000; function PeerToPeerProvider(props: Props): React.Node { const { children } = props; const { sendMessageToDevice } = useTunnelbroker(); const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); const dmOpsSendingPromiseResolvers = React.useRef< Map< string, { +resolve: (messageIDs: $ReadOnlyArray) => mixed, +reject: Error => mixed, }, >, >(new Map()); // This returns a promise that will be resolved with arrays of successfully // sent messages, so in case of failing all messages (e.g. no internet // connection) it will still resolve but with an empty array. const getDMOpsSendingPromise = React.useCallback(() => { const dmOpID = uuid.v4(); const promise = new Promise<$ReadOnlyArray>((resolve, reject) => { dmOpsSendingPromiseResolvers.current.set(dmOpID, { resolve, reject }); }); return { promise, dmOpID }; }, []); const processingQueue = React.useRef< Array<{ +outboundMessageIDs: ?$ReadOnlyArray, +dmOpID: ?string, +notificationsCreationData: ?NotificationsCreationData, }>, >([]); const promiseRunning = React.useRef(false); const { createOlmSessionsWithPeer: peerOlmSessionsCreator } = usePeerOlmSessionsCreatorContext(); const sendPushNotifs = useSendPushNotifs(); const processOutboundMessages = React.useCallback( ( outboundMessageIDs: ?$ReadOnlyArray, dmOpID: ?string, notificationsCreationData: ?NotificationsCreationData, ) => { processingQueue.current.push({ outboundMessageIDs, dmOpID, notificationsCreationData, }); if (!promiseRunning.current) { promiseRunning.current = true; void (async () => { do { const queueFront = processingQueue.current.shift(); try { const [sentMessagesIDs] = await Promise.all([ processOutboundP2PMessages( sendMessageToDevice, identityContext, peerOlmSessionsCreator, queueFront?.outboundMessageIDs, ), sendPushNotifs(queueFront.notificationsCreationData), ]); if (queueFront.dmOpID) { dmOpsSendingPromiseResolvers.current .get(queueFront.dmOpID) ?.resolve?.(sentMessagesIDs); } } catch (e) { console.log( `Error processing outbound P2P messages: ${ getMessageForException(e) ?? 'unknown' }`, ); if (queueFront.dmOpID) { dmOpsSendingPromiseResolvers.current .get(queueFront.dmOpID) ?.reject?.(e); } } finally { if (queueFront.dmOpID) { dmOpsSendingPromiseResolvers.current.delete(queueFront.dmOpID); } } } while (processingQueue.current.length > 0); promiseRunning.current = false; })(); } }, [ sendPushNotifs, peerOlmSessionsCreator, identityContext, sendMessageToDevice, ], ); const broadcastEphemeralMessage = React.useCallback( async ( contentPayload: string, recipients: $ReadOnlyArray<{ +userID: string, +deviceID: string }>, authMetadata: AuthMetadata, ) => { const { userID: thisUserID, deviceID: thisDeviceID } = authMetadata; if (!thisDeviceID || !thisUserID) { throw new Error('No auth metadata'); } const { olmAPI } = getConfig(); await olmAPI.initializeCryptoAccount(); // We want it distinct by device ID to avoid potentially creating // multiple Olm sessions with the same device simultaneously. const recipientsDistinctByDeviceID = [ ...new Map(recipients.map(item => [item.deviceID, item])).values(), ]; const senderInfo = { deviceID: thisDeviceID, userID: thisUserID }; const promises = recipientsDistinctByDeviceID.map(async recipient => { try { const encryptedData = await olmAPI.encrypt( contentPayload, recipient.deviceID, ); const encryptedMessage: EncryptedMessage = { type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, senderInfo, encryptedData, }; await sendMessageToDevice({ deviceID: recipient.deviceID, payload: JSON.stringify(encryptedMessage), }); } catch (e) { - if (!e.message?.includes(olmSessionErrors.sessionDoesNotExists)) { + if (!e.message?.includes(olmSessionErrors.sessionDoesNotExist)) { console.log( `Error sending messages to peer ${recipient.deviceID}`, e, ); return; } try { await peerOlmSessionsCreator(recipient.userID, recipient.deviceID); const encryptedData = await olmAPI.encrypt( contentPayload, recipient.deviceID, ); const encryptedMessage: EncryptedMessage = { type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, senderInfo, encryptedData, }; await sendMessageToDevice({ deviceID: recipient.deviceID, payload: JSON.stringify(encryptedMessage), }); } catch (err) { console.warn( `Error sending Olm-encrypted message to device ${recipient.deviceID}:`, err, ); } } }); await Promise.all(promises); }, [peerOlmSessionsCreator, sendMessageToDevice], ); React.useEffect(() => { const intervalID = setInterval( processOutboundMessages, AUTOMATIC_RETRY_FREQUENCY, ); return () => clearInterval(intervalID); }, [processOutboundMessages]); const value: PeerToPeerContextType = React.useMemo( () => ({ processOutboundMessages, getDMOpsSendingPromise, broadcastEphemeralMessage, }), [ broadcastEphemeralMessage, processOutboundMessages, getDMOpsSendingPromise, ], ); return ( {children} ); } function usePeerToPeerCommunication(): PeerToPeerContextType { const context = React.useContext(PeerToPeerContext); invariant(context, 'PeerToPeerContext not found'); return context; } export { PeerToPeerProvider, usePeerToPeerCommunication }; diff --git a/lib/utils/olm-utils.js b/lib/utils/olm-utils.js index 544d7c354..50e34ca18 100644 --- a/lib/utils/olm-utils.js +++ b/lib/utils/olm-utils.js @@ -1,161 +1,161 @@ // @flow import type { Account as OlmAccount } from '@commapp/olm'; import { getOneTimeKeyValuesFromBlob, getPrekeyValueFromBlob, } from '../shared/crypto-utils.js'; import { ONE_TIME_KEYS_NUMBER } from '../types/identity-service-types.js'; const maxPublishedPrekeyAge = 30 * 24 * 60 * 60 * 1000; const maxOldPrekeyAge = 24 * 60 * 60 * 1000; type AccountKeysSet = { +identityKeys: string, +prekey: string, +prekeySignature: string, +oneTimeKeys: $ReadOnlyArray, }; type IdentityKeysAndPrekeys = { +identityKeys: string, +prekey: string, +prekeySignature: string, }; function validateAccountPrekey(account: OlmAccount) { if (shouldRotatePrekey(account)) { account.generate_prekey(); } if (shouldForgetPrekey(account)) { account.forget_old_prekey(); } } function shouldRotatePrekey(account: OlmAccount): boolean { // Our fork of Olm only remembers two prekeys at a time. // If the new one hasn't been published, then the old one is still active. // In that scenario, we need to avoid rotating the prekey because it will // result in the old active prekey being discarded. if (account.unpublished_prekey()) { return false; } const currentDate = new Date(); const lastPrekeyPublishDate = getLastPrekeyPublishTime(account); return ( currentDate.getTime() - lastPrekeyPublishDate.getTime() >= maxPublishedPrekeyAge ); } function shouldForgetPrekey(account: OlmAccount): boolean { // Our fork of Olm only remembers two prekeys at a time. // We have to hold onto the old one until the new one is published. if (account.unpublished_prekey()) { return false; } const currentDate = new Date(); const lastPrekeyPublishDate = getLastPrekeyPublishTime(account); return ( currentDate.getTime() - lastPrekeyPublishDate.getTime() >= maxOldPrekeyAge ); } function getLastPrekeyPublishTime(account: OlmAccount): Date { const olmLastPrekeyPublishTime = account.last_prekey_publish_time(); // Olm uses seconds, while the Date() constructor expects milliseconds. return new Date(olmLastPrekeyPublishTime * 1000); } function getAccountPrekeysSet(account: OlmAccount): { +prekey: string, +prekeySignature: ?string, } { const prekey = getPrekeyValueFromBlob(account.prekey()); const prekeySignature = account.prekey_signature(); return { prekey, prekeySignature }; } function getAccountOneTimeKeys( account: OlmAccount, numberOfKeys: number = ONE_TIME_KEYS_NUMBER, ): $ReadOnlyArray { let oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); if (oneTimeKeys.length < numberOfKeys) { account.generate_one_time_keys(numberOfKeys - oneTimeKeys.length); oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); } return oneTimeKeys; } function retrieveAccountKeysSet(account: OlmAccount): AccountKeysSet { const { identityKeys, prekey, prekeySignature } = retrieveIdentityKeysAndPrekeys(account); const oneTimeKeys = getAccountOneTimeKeys(account, ONE_TIME_KEYS_NUMBER); return { identityKeys, oneTimeKeys, prekey, prekeySignature }; } function retrieveIdentityKeysAndPrekeys( account: OlmAccount, ): IdentityKeysAndPrekeys { const identityKeys = account.identity_keys(); validateAccountPrekey(account); const { prekey, prekeySignature } = getAccountPrekeysSet(account); if (!prekeySignature || !prekey) { throw new Error('invalid_prekey'); } return { identityKeys, prekey, prekeySignature }; } export const OLM_SESSION_ERROR_PREFIX = 'OLM_'; const olmSessionErrors = Object.freeze({ // Two clients send the session request to each other at the same time, // we choose which session to keep based on `deviceID`. raceCondition: `${OLM_SESSION_ERROR_PREFIX}SESSION_CREATION_RACE_CONDITION`, // The client received a session request with a lower session version, // this request can be ignored. alreadyCreated: `${OLM_SESSION_ERROR_PREFIX}SESSION_ALREADY_CREATED`, // Error thrown when attempting to encrypt/decrypt, indicating that // the session for a given deviceID is not created. // This definition should remain in sync with the value defined in // the corresponding .cpp file // at `native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp`. - sessionDoesNotExists: 'SESSION_DOES_NOT_EXISTS', + sessionDoesNotExist: 'SESSION_DOES_NOT_EXIST', // Error thrown when attempting to decrypt a message encrypted // with an already replaced old session. // This definition should remain in sync with the value defined in // the corresponding .cpp file // at `native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp`. invalidSessionVersion: 'INVALID_SESSION_VERSION', }); function hasHigherDeviceID( currenDeviceID: string, otherDeviceID: string, ): boolean { const compareResult = currenDeviceID.localeCompare(otherDeviceID); if (compareResult === 0) { throw new Error('Comparing the same deviceIDs'); } return compareResult === 1; } export { retrieveAccountKeysSet, getAccountPrekeysSet, shouldForgetPrekey, shouldRotatePrekey, getAccountOneTimeKeys, retrieveIdentityKeysAndPrekeys, hasHigherDeviceID, olmSessionErrors, }; diff --git a/native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp b/native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp index 698f4ef74..53e429127 100644 --- a/native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp +++ b/native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp @@ -1,466 +1,466 @@ #include "CryptoModule.h" #include "Logger.h" #include "PlatformSpecificTools.h" #include "olm/account.hh" #include "olm/session.hh" #include #include #include #include namespace comm { namespace crypto { // This definition should remain in sync with the value defined in // the corresponding JavaScript file at `lib/utils/olm-utils.js`. -const std::string SESSION_DOES_NOT_EXISTS_ERROR{"SESSION_DOES_NOT_EXISTS"}; +const std::string SESSION_DOES_NOT_EXIST_ERROR{"SESSION_DOES_NOT_EXIST"}; const std::string INVALID_SESSION_VERSION_ERROR{"INVALID_SESSION_VERSION"}; CryptoModule::CryptoModule(std::string id) : id{id} { this->createAccount(); } CryptoModule::CryptoModule( std::string id, std::string secretKey, Persist persist) : id{id} { if (persist.isEmpty()) { this->createAccount(); } else { this->restoreFromB64(secretKey, persist); } } OlmAccount *CryptoModule::getOlmAccount() { return reinterpret_cast(this->accountBuffer.data()); } void CryptoModule::createAccount() { this->accountBuffer.resize(::olm_account_size()); ::olm_account(this->accountBuffer.data()); size_t randomSize = ::olm_create_account_random_length(this->getOlmAccount()); OlmBuffer randomBuffer; PlatformSpecificTools::generateSecureRandomBytes(randomBuffer, randomSize); if (-1 == ::olm_create_account( this->getOlmAccount(), randomBuffer.data(), randomSize)) { throw std::runtime_error{ "error createAccount => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; }; } void CryptoModule::exposePublicIdentityKeys() { size_t identityKeysSize = ::olm_account_identity_keys_length(this->getOlmAccount()); if (this->keys.identityKeys.size() == identityKeysSize) { return; } this->keys.identityKeys.resize( ::olm_account_identity_keys_length(this->getOlmAccount())); if (-1 == ::olm_account_identity_keys( this->getOlmAccount(), this->keys.identityKeys.data(), this->keys.identityKeys.size())) { throw std::runtime_error{ "error generateIdentityKeys => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } } void CryptoModule::generateOneTimeKeys(size_t oneTimeKeysAmount) { size_t numRandomBytesRequired = ::olm_account_generate_one_time_keys_random_length( this->getOlmAccount(), oneTimeKeysAmount); OlmBuffer random; PlatformSpecificTools::generateSecureRandomBytes( random, numRandomBytesRequired); if (-1 == ::olm_account_generate_one_time_keys( this->getOlmAccount(), oneTimeKeysAmount, random.data(), random.size())) { throw std::runtime_error{ "error generateOneTimeKeys => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } } // returns number of published keys size_t CryptoModule::publishOneTimeKeys() { this->keys.oneTimeKeys.resize( ::olm_account_one_time_keys_length(this->getOlmAccount())); if (-1 == ::olm_account_one_time_keys( this->getOlmAccount(), this->keys.oneTimeKeys.data(), this->keys.oneTimeKeys.size())) { throw std::runtime_error{ "error publishOneTimeKeys => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } return ::olm_account_mark_keys_as_published(this->getOlmAccount()); } bool CryptoModule::prekeyExistsAndOlderThan(uint64_t threshold) { // Our fork of Olm only remembers two prekeys at a time. // If the new one hasn't been published, then the old one is still active. // In that scenario, we need to avoid rotating the prekey because it will // result in the old active prekey being discarded. if (this->getUnpublishedPrekey().has_value()) { return false; } uint64_t currentTime = std::time(nullptr); uint64_t lastPrekeyPublishTime = ::olm_account_get_last_prekey_publish_time(this->getOlmAccount()); return currentTime - lastPrekeyPublishTime >= threshold; } Keys CryptoModule::keysFromStrings( const std::string &identityKeys, const std::string &oneTimeKeys) { return { OlmBuffer(identityKeys.begin(), identityKeys.end()), OlmBuffer(oneTimeKeys.begin(), oneTimeKeys.end())}; } std::string CryptoModule::getIdentityKeys() { this->exposePublicIdentityKeys(); return std::string{ this->keys.identityKeys.begin(), this->keys.identityKeys.end()}; } std::string CryptoModule::getOneTimeKeysForPublishing(size_t oneTimeKeysAmount) { OlmBuffer unpublishedOneTimeKeys; unpublishedOneTimeKeys.resize( ::olm_account_one_time_keys_length(this->getOlmAccount())); if (-1 == ::olm_account_one_time_keys( this->getOlmAccount(), unpublishedOneTimeKeys.data(), unpublishedOneTimeKeys.size())) { throw std::runtime_error{ "error getOneTimeKeysForPublishing => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } std::string unpublishedKeysString = std::string{unpublishedOneTimeKeys.begin(), unpublishedOneTimeKeys.end()}; folly::dynamic parsedUnpublishedKeys = folly::parseJson(unpublishedKeysString); size_t numUnpublishedKeys = parsedUnpublishedKeys["curve25519"].size(); if (numUnpublishedKeys < oneTimeKeysAmount) { this->generateOneTimeKeys(oneTimeKeysAmount - numUnpublishedKeys); } this->publishOneTimeKeys(); return std::string{ this->keys.oneTimeKeys.begin(), this->keys.oneTimeKeys.end()}; } std::uint8_t CryptoModule::getNumPrekeys() { return reinterpret_cast(this->getOlmAccount())->num_prekeys; } std::string CryptoModule::getPrekey() { OlmBuffer prekey; prekey.resize(::olm_account_prekey_length(this->getOlmAccount())); if (-1 == ::olm_account_prekey( this->getOlmAccount(), prekey.data(), prekey.size())) { throw std::runtime_error{ "error getPrekey => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } return std::string{std::string{prekey.begin(), prekey.end()}}; } std::string CryptoModule::getPrekeySignature() { size_t signatureSize = ::olm_account_signature_length(this->getOlmAccount()); OlmBuffer signatureBuffer; signatureBuffer.resize(signatureSize); if (-1 == ::olm_account_prekey_signature( this->getOlmAccount(), signatureBuffer.data())) { throw std::runtime_error{ "error getPrekeySignature => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } return std::string{signatureBuffer.begin(), signatureBuffer.end()}; } std::optional CryptoModule::getUnpublishedPrekey() { OlmBuffer prekey; prekey.resize(::olm_account_prekey_length(this->getOlmAccount())); std::size_t retval = ::olm_account_unpublished_prekey( this->getOlmAccount(), prekey.data(), prekey.size()); if (0 == retval) { return std::nullopt; } else if (-1 == retval) { throw std::runtime_error{ "error getUnpublishedPrekey => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } return std::string{prekey.begin(), prekey.end()}; } std::string CryptoModule::generateAndGetPrekey() { size_t prekeySize = ::olm_account_generate_prekey_random_length(this->getOlmAccount()); OlmBuffer random; PlatformSpecificTools::generateSecureRandomBytes(random, prekeySize); if (-1 == ::olm_account_generate_prekey( this->getOlmAccount(), random.data(), random.size())) { throw std::runtime_error{ "error generateAndGetPrekey => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } OlmBuffer prekey; prekey.resize(::olm_account_prekey_length(this->getOlmAccount())); if (-1 == ::olm_account_prekey( this->getOlmAccount(), prekey.data(), prekey.size())) { throw std::runtime_error{ "error generateAndGetPrekey => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } return std::string{prekey.begin(), prekey.end()}; } void CryptoModule::markPrekeyAsPublished() { ::olm_account_mark_prekey_as_published(this->getOlmAccount()); } void CryptoModule::forgetOldPrekey() { ::olm_account_forget_old_prekey(this->getOlmAccount()); } void CryptoModule::initializeInboundForReceivingSession( const std::string &targetDeviceId, const OlmBuffer &encryptedMessage, const OlmBuffer &idKeys, int sessionVersion, const bool overwrite) { if (this->hasSessionFor(targetDeviceId)) { std::shared_ptr existingSession = getSessionByDeviceId(targetDeviceId); if (existingSession->getVersion() > sessionVersion) { throw std::runtime_error{"OLM_SESSION_ALREADY_CREATED"}; } else if (existingSession->getVersion() == sessionVersion) { throw std::runtime_error{"OLM_SESSION_CREATION_RACE_CONDITION"}; } this->sessions.erase(this->sessions.find(targetDeviceId)); } std::unique_ptr newSession = Session::createSessionAsResponder( this->getOlmAccount(), this->keys.identityKeys.data(), encryptedMessage, idKeys); newSession->setVersion(sessionVersion); this->sessions.insert(make_pair(targetDeviceId, std::move(newSession))); } int CryptoModule::initializeOutboundForSendingSession( const std::string &targetDeviceId, const OlmBuffer &idKeys, const OlmBuffer &preKeys, const OlmBuffer &preKeySignature, const std::optional &oneTimeKey) { int newSessionVersion = 1; if (this->hasSessionFor(targetDeviceId)) { std::shared_ptr existingSession = getSessionByDeviceId(targetDeviceId); newSessionVersion = existingSession->getVersion() + 1; Logger::log( "olm session overwritten for the device with id: " + targetDeviceId); this->sessions.erase(this->sessions.find(targetDeviceId)); } std::unique_ptr newSession = Session::createSessionAsInitializer( this->getOlmAccount(), this->keys.identityKeys.data(), idKeys, preKeys, preKeySignature, oneTimeKey); newSession->setVersion(newSessionVersion); this->sessions.insert(make_pair(targetDeviceId, std::move(newSession))); return newSessionVersion; } bool CryptoModule::hasSessionFor(const std::string &targetDeviceId) { return (this->sessions.find(targetDeviceId) != this->sessions.end()); } std::shared_ptr CryptoModule::getSessionByDeviceId(const std::string &deviceId) { return this->sessions.at(deviceId); } void CryptoModule::removeSessionByDeviceId(const std::string &deviceId) { this->sessions.erase(deviceId); } Persist CryptoModule::storeAsB64(const std::string &secretKey) { Persist persist; size_t accountPickleLength = ::olm_pickle_account_length(this->getOlmAccount()); OlmBuffer accountPickleBuffer(accountPickleLength); if (accountPickleLength != ::olm_pickle_account( this->getOlmAccount(), secretKey.data(), secretKey.size(), accountPickleBuffer.data(), accountPickleLength)) { throw std::runtime_error{ "error storeAsB64 => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } persist.account = accountPickleBuffer; std::unordered_map>::iterator it; for (it = this->sessions.begin(); it != this->sessions.end(); ++it) { OlmBuffer buffer = it->second->storeAsB64(secretKey); SessionPersist sessionPersist{buffer, it->second->getVersion()}; persist.sessions.insert(make_pair(it->first, sessionPersist)); } return persist; } void CryptoModule::restoreFromB64( const std::string &secretKey, Persist persist) { this->accountBuffer.resize(::olm_account_size()); ::olm_account(this->accountBuffer.data()); if (-1 == ::olm_unpickle_account( this->getOlmAccount(), secretKey.data(), secretKey.size(), persist.account.data(), persist.account.size())) { throw std::runtime_error{ "error restoreFromB64 => " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } std::unordered_map::iterator it; for (it = persist.sessions.begin(); it != persist.sessions.end(); ++it) { std::unique_ptr session = session->restoreFromB64(secretKey, it->second.buffer); session->setVersion(it->second.version); this->sessions.insert(make_pair(it->first, move(session))); } } EncryptedData CryptoModule::encrypt( const std::string &targetDeviceId, const std::string &content) { if (!this->hasSessionFor(targetDeviceId)) { - throw std::runtime_error{SESSION_DOES_NOT_EXISTS_ERROR}; + throw std::runtime_error{SESSION_DOES_NOT_EXIST_ERROR}; } return this->sessions.at(targetDeviceId)->encrypt(content); } std::string CryptoModule::decrypt( const std::string &targetDeviceId, EncryptedData &encryptedData) { if (!this->hasSessionFor(targetDeviceId)) { - throw std::runtime_error{SESSION_DOES_NOT_EXISTS_ERROR}; + throw std::runtime_error{SESSION_DOES_NOT_EXIST_ERROR}; } auto session = this->sessions.at(targetDeviceId); if (encryptedData.sessionVersion.has_value() && encryptedData.sessionVersion.value() < session->getVersion()) { throw std::runtime_error{INVALID_SESSION_VERSION_ERROR}; } return session->decrypt(encryptedData); } std::string CryptoModule::signMessage(const std::string &message) { OlmBuffer signature; signature.resize(::olm_account_signature_length(this->getOlmAccount())); size_t signatureLength = ::olm_account_sign( this->getOlmAccount(), (uint8_t *)message.data(), message.length(), signature.data(), signature.size()); if (signatureLength == -1) { throw std::runtime_error{ "olm error: " + std::string{::olm_account_last_error(this->getOlmAccount())}}; } return std::string{(char *)signature.data(), signatureLength}; } void CryptoModule::verifySignature( const std::string &publicKey, const std::string &message, const std::string &signature) { OlmBuffer utilityBuffer; utilityBuffer.resize(::olm_utility_size()); OlmUtility *olmUtility = ::olm_utility(utilityBuffer.data()); ssize_t verificationResult = ::olm_ed25519_verify( olmUtility, (uint8_t *)publicKey.data(), publicKey.length(), (uint8_t *)message.data(), message.length(), (uint8_t *)signature.data(), signature.length()); if (verificationResult == -1) { throw std::runtime_error{ "olm error: " + std::string{::olm_utility_last_error(olmUtility)}}; } } std::optional CryptoModule::validatePrekey() { static const uint64_t maxPrekeyPublishTime = 10 * 60; static const uint64_t maxOldPrekeyAge = 2 * 60; std::optional maybeNewPrekey; bool shouldRotatePrekey = this->prekeyExistsAndOlderThan(maxPrekeyPublishTime); if (shouldRotatePrekey) { maybeNewPrekey = this->generateAndGetPrekey(); } bool shouldForgetPrekey = this->prekeyExistsAndOlderThan(maxOldPrekeyAge); if (shouldForgetPrekey) { this->forgetOldPrekey(); } return maybeNewPrekey; } } // namespace crypto } // namespace comm diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js index 46dd948fc..7775deb13 100644 --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -1,1030 +1,1030 @@ // @flow 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/olm-session-types.js'; import type { InboundP2PMessage } 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 { getOlmDataKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, getOlmDataKeyForDeviceID, getOlmEncryptionKeyDBLabelForDeviceID, encryptNotification, type NotificationAccountWithPicklingKey, getNotifsCryptoAccount, persistNotifsAccountWithOlmData, } 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'; type OlmSession = { +session: olm.Session, +version: number }; type OlmSessions = { [deviceID: string]: OlmSession, }; type WorkerCryptoStore = { +contentAccountPickleKey: string, +contentAccount: olm.Account, +contentSessions: OlmSessions, }; let cryptoStore: ?WorkerCryptoStore = null; let olmUtility: ?Utility = null; function clearCryptoStore() { cryptoStore = null; } async function persistCryptoStore( notifsCryptoAccount?: NotificationAccountWithPicklingKey, 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 } = 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, })); try { if (!withoutTransaction) { sqliteQueryExecutor.beginTransaction(); } sqliteQueryExecutor.storeOlmPersistAccount( sqliteQueryExecutor.getContentAccountID(), JSON.stringify(pickledContentAccount), ); for (const pickledSession of pickledContentSessions) { sqliteQueryExecutor.storeOlmPersistSession(pickledSession); } if (notifsCryptoAccount) { const { notificationAccount, picklingKey, synchronizationValue, accountEncryptionKey, } = notifsCryptoAccount; const pickledAccount = notificationAccount.pickle(picklingKey); const accountWithPicklingKey: PickledOLMAccount = { pickledAccount, picklingKey, }; await persistNotifsAccountWithOlmData({ accountEncryptionKey, accountWithPicklingKey, synchronizationValue, forceWrite: true, }); } if (!withoutTransaction) { sqliteQueryExecutor.commitTransaction(); } } catch (err) { if (!withoutTransaction) { sqliteQueryExecutor.rollbackTransaction(); } throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); } } async function createAndPersistNotificationsOutboundSession( notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, dataPersistenceKey: string, dataEncryptionKeyDBLabel: string, ): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const notificationAccountWithPicklingKey = await getNotifsCryptoAccount(); const { notificationAccount, picklingKey, synchronizationValue, accountEncryptionKey, } = notificationAccountWithPicklingKey; const notificationsPrekey = notificationsInitializationInfo.prekey; const session = new olm.Session(); if (notificationsInitializationInfo.oneTimeKey) { session.create_outbound( notificationAccount, notificationsIdentityKeys.curve25519, notificationsIdentityKeys.ed25519, notificationsPrekey, notificationsInitializationInfo.prekeySignature, notificationsInitializationInfo.oneTimeKey, ); } else { session.create_outbound_without_otk( notificationAccount, notificationsIdentityKeys.curve25519, notificationsIdentityKeys.ed25519, notificationsPrekey, notificationsInitializationInfo.prekeySignature, ); } const { body: message, type: messageType } = session.encrypt( JSON.stringify(initialEncryptedMessageContent), ); const mainSession = session.pickle( notificationAccountWithPicklingKey.picklingKey, ); const notificationsOlmData: NotificationsOlmDataType = { mainSession, pendingSessionUpdate: mainSession, updateCreationTimestamp: Date.now(), picklingKey, }; const pickledAccount = notificationAccount.pickle(picklingKey); const accountWithPicklingKey: PickledOLMAccount = { pickledAccount, picklingKey, }; await persistNotifsAccountWithOlmData({ accountEncryptionKey, accountWithPicklingKey, olmDataKey: dataPersistenceKey, olmData: notificationsOlmData, olmEncryptionKeyDBLabel: dataEncryptionKeyDBLabel, synchronizationValue, forceWrite: true, }); return { message, messageType }; } async function getOrCreateOlmAccount(accountIDInDB: number): Promise<{ +picklingKey: string, +account: olm.Account, +synchronizationValue?: ?string, }> { 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)); } const maybeNotifsCryptoAccount: ?NotificationAccountWithPicklingKey = await (async () => { if (accountIDInDB !== sqliteQueryExecutor.getNotifsAccountID()) { return undefined; } try { return await getNotifsCryptoAccount(); } catch (e) { return undefined; } })(); if (maybeNotifsCryptoAccount) { const { notificationAccount, picklingKey: notificationAccountPicklingKey, synchronizationValue, } = maybeNotifsCryptoAccount; return { account: notificationAccount, picklingKey: notificationAccountPicklingKey, synchronizationValue, }; } if (accountDBString.isNull) { picklingKey = uuid.v4(); account.create(); } else { const dbAccount: PickledOLMAccount = JSON.parse(accountDBString.value); picklingKey = dbAccount.picklingKey; account.unpickle(picklingKey, dbAccount.pickledAccount); } if (accountIDInDB === sqliteQueryExecutor.getNotifsAccountID()) { return { picklingKey, account, synchronizationValue: uuid.v4() }; } 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: {}, }; const notifsCryptoAccount = { picklingKey: initialCryptoStore.notificationAccount.picklingKey, notificationAccount: unpickleInitialCryptoStoreAccount( initialCryptoStore.notificationAccount, ), synchronizationValue: uuid.v4(), }; await persistCryptoStore(notifsCryptoAccount); 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; } async function getSignedIdentityKeysBlob(): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount } = cryptoStore; const { notificationAccount } = await getNotifsCryptoAccount(); 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; } async function getNewDeviceKeyUpload(): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount } = cryptoStore; const [notifsCryptoAccount, signedIdentityKeysBlob] = await Promise.all([ getNotifsCryptoAccount(), getSignedIdentityKeysBlob(), ]); const primaryAccountKeysSet = retrieveAccountKeysSet(contentAccount); const notificationAccountKeysSet = retrieveAccountKeysSet( notifsCryptoAccount.notificationAccount, ); contentAccount.mark_keys_as_published(); notifsCryptoAccount.notificationAccount.mark_keys_as_published(); await persistCryptoStore(notifsCryptoAccount); 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, }; } async function getExistingDeviceKeyUpload(): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount } = cryptoStore; const [notifsCryptoAccount, signedIdentityKeysBlob] = await Promise.all([ getNotifsCryptoAccount(), getSignedIdentityKeysBlob(), ]); const { prekey: contentPrekey, prekeySignature: contentPrekeySignature } = retrieveIdentityKeysAndPrekeys(contentAccount); const { prekey: notifPrekey, prekeySignature: notifPrekeySignature } = retrieveIdentityKeysAndPrekeys(notifsCryptoAccount.notificationAccount); await persistCryptoStore(notifsCryptoAccount); 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: getOlmDataKeyForCookie(cookie, keyserverID), }; } else { return { notifsOlmDataEncryptionKeyDBLabel: getOlmEncryptionKeyDBLabelForCookie(cookie), notifsOlmDataContentKey: getOlmDataKeyForCookie(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, notificationAccountResult] = await Promise.all( [ getOrCreateOlmAccount(sqliteQueryExecutor.getContentAccountID()), getOrCreateOlmAccount(sqliteQueryExecutor.getNotifsAccountID()), ], ); const contentSessions = getOlmSessions(contentAccountResult.picklingKey); cryptoStore = { contentAccountPickleKey: contentAccountResult.picklingKey, contentAccount: contentAccountResult.account, contentSessions, }; const notifsCryptoAccount = { picklingKey: notificationAccountResult.picklingKey, notificationAccount: notificationAccountResult.account, synchronizationValue: notificationAccountResult.synchronizationValue, }; await persistCryptoStore(notifsCryptoAccount); }, async getUserPublicKey(): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount } = cryptoStore; const [{ notificationAccount }, { payload, signature }] = await Promise.all( [getNotifsCryptoAccount(), 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 olmSession = cryptoStore.contentSessions[deviceID]; if (!olmSession) { - throw new Error(olmSessionErrors.sessionDoesNotExists); + throw new Error(olmSessionErrors.sessionDoesNotExist); } const encryptedContent = olmSession.session.encrypt(content); await persistCryptoStore(); return { message: encryptedContent.body, messageType: encryptedContent.type, sessionVersion: olmSession.version, }; }, async encryptAndPersist( content: string, deviceID: string, messageID: string, ): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const olmSession = cryptoStore.contentSessions[deviceID]; if (!olmSession) { - throw new Error(olmSessionErrors.sessionDoesNotExists); + throw new Error(olmSessionErrors.sessionDoesNotExist); } const encryptedContent = olmSession.session.encrypt(content); const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { throw new Error( "Couldn't persist crypto store because database is not initialized", ); } const result: EncryptedData = { message: encryptedContent.body, messageType: encryptedContent.type, sessionVersion: olmSession.version, }; sqliteQueryExecutor.beginTransaction(); try { sqliteQueryExecutor.setCiphertextForOutboundP2PMessage( messageID, deviceID, JSON.stringify(result), ); await persistCryptoStore(undefined, true); sqliteQueryExecutor.commitTransaction(); } catch (e) { sqliteQueryExecutor.rollbackTransaction(); throw e; } return result; }, async encryptNotification( payload: string, deviceID: string, ): Promise { const { body: message, type: messageType } = await encryptNotification( payload, deviceID, ); return { message, messageType }; }, async decrypt( encryptedData: EncryptedData, deviceID: string, ): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const olmSession = cryptoStore.contentSessions[deviceID]; if (!olmSession) { - throw new Error(olmSessionErrors.sessionDoesNotExists); + throw new Error(olmSessionErrors.sessionDoesNotExist); } if ( encryptedData.sessionVersion && encryptedData.sessionVersion < olmSession.version ) { throw new Error(olmSessionErrors.invalidSessionVersion); } const result = olmSession.session.decrypt( encryptedData.messageType, encryptedData.message, ); await persistCryptoStore(); return result; }, async decryptAndPersist( encryptedData: EncryptedData, deviceID: string, userID: string, messageID: string, ): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const olmSession = cryptoStore.contentSessions[deviceID]; if (!olmSession) { - throw new Error(olmSessionErrors.sessionDoesNotExists); + throw new Error(olmSessionErrors.sessionDoesNotExist); } if ( encryptedData.sessionVersion && encryptedData.sessionVersion < olmSession.version ) { throw new Error(olmSessionErrors.invalidSessionVersion); } const result = olmSession.session.decrypt( 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: InboundP2PMessage = { messageID, senderDeviceID: deviceID, senderUserID: userID, plaintext: result, status: 'decrypted', }; sqliteQueryExecutor.beginTransaction(); try { sqliteQueryExecutor.addInboundP2PMessage(receivedMessage); await persistCryptoStore(undefined, 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, }; await 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(); if (contentInitializationInfo.oneTimeKey) { session.create_outbound( contentAccount, contentIdentityKeys.curve25519, contentIdentityKeys.ed25519, contentInitializationInfo.prekey, contentInitializationInfo.prekeySignature, contentInitializationInfo.oneTimeKey, ); } else { session.create_outbound_without_otk( contentAccount, contentIdentityKeys.curve25519, contentIdentityKeys.ed25519, contentInitializationInfo.prekey, contentInitializationInfo.prekeySignature, ); } const initialEncryptedData = session.encrypt( JSON.stringify(initialEncryptedMessageContent), ); const newSessionVersion = existingSession ? existingSession.version + 1 : 1; contentSessions[contentIdentityKeys.ed25519] = { session, version: newSessionVersion, }; await persistCryptoStore(); const encryptedData: EncryptedData = { message: initialEncryptedData.body, messageType: initialEncryptedData.type, sessionVersion: newSessionVersion, }; return { encryptedData, sessionVersion: newSessionVersion }; }, async isContentSessionInitialized(deviceID: string) { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } return !!cryptoStore.contentSessions[deviceID]; }, async notificationsOutboundSessionCreator( deviceID: string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, ): Promise { const dataPersistenceKey = getOlmDataKeyForDeviceID(deviceID); const dataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForDeviceID(deviceID); return createAndPersistNotificationsOutboundSession( notificationsIdentityKeys, notificationsInitializationInfo, dataPersistenceKey, dataEncryptionKeyDBLabel, ); }, async isDeviceNotificationsSessionInitialized(deviceID: string) { const dataPersistenceKey = getOlmDataKeyForDeviceID(deviceID); const dataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForDeviceID(deviceID); const allKeys = await localforage.keys(); const allKeysSet = new Set(allKeys); return ( allKeysSet.has(dataPersistenceKey) && allKeysSet.has(dataEncryptionKeyDBLabel) ); }, async isNotificationsSessionInitializedWithDevices( deviceIDs: $ReadOnlyArray, ) { const allKeys = await localforage.keys(); const allKeysSet = new Set(allKeys); const deviceInfoPairs = deviceIDs.map(deviceID => { const dataPersistenceKey = getOlmDataKeyForDeviceID(deviceID); const dataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForDeviceID(deviceID); return [ deviceID, allKeysSet.has(dataPersistenceKey) && allKeysSet.has(dataEncryptionKeyDBLabel), ]; }); return Object.fromEntries(deviceInfoPairs); }, async keyserverNotificationsSessionCreator( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ): Promise { const platformDetails = getPlatformDetails(); if (!platformDetails) { throw new Error('Worker not initialized'); } const { notifsOlmDataContentKey, notifsOlmDataEncryptionKeyDBLabel } = getNotifsPersistenceKeys(cookie, keyserverID, platformDetails); const { message } = await createAndPersistNotificationsOutboundSession( notificationsIdentityKeys, notificationsInitializationInfo, notifsOlmDataContentKey, notifsOlmDataEncryptionKeyDBLabel, ); return message; }, 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 } = cryptoStore; const notifsCryptoAccount = await getNotifsCryptoAccount(); const contentOneTimeKeys = getAccountOneTimeKeys( contentAccount, numberOfKeys, ); contentAccount.mark_keys_as_published(); const notificationsOneTimeKeys = getAccountOneTimeKeys( notifsCryptoAccount.notificationAccount, numberOfKeys, ); notifsCryptoAccount.notificationAccount.mark_keys_as_published(); await persistCryptoStore(notifsCryptoAccount); 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 } = cryptoStore; const notifsCryptoAccount = await getNotifsCryptoAccount(); // 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(); notifsCryptoAccount.notificationAccount.generate_prekey(); } if (shouldForgetPrekey(contentAccount)) { contentAccount.forget_old_prekey(); notifsCryptoAccount.notificationAccount.forget_old_prekey(); } await persistCryptoStore(notifsCryptoAccount); if (!contentAccount.unpublished_prekey()) { return; } const { prekey: notifPrekey, prekeySignature: notifPrekeySignature } = getAccountPrekeysSet(notifsCryptoAccount.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(); notifsCryptoAccount.notificationAccount.mark_prekey_as_published(); await persistCryptoStore(notifsCryptoAccount); }, 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; } }, async markPrekeysAsPublished(): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount } = cryptoStore; const notifsCryptoAccount = await getNotifsCryptoAccount(); contentAccount.mark_prekey_as_published(); notifsCryptoAccount.notificationAccount.mark_prekey_as_published(); await persistCryptoStore(notifsCryptoAccount); }, }; export { clearCryptoStore, processAppOlmApiRequest, getSignedIdentityKeysBlob, getNewDeviceKeyUpload, getExistingDeviceKeyUpload, };