diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js index 258c7e1e9..8981e787e 100644 --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -1,206 +1,208 @@ // @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, + +sessionVersion?: number, }; export const encryptedDataValidator: TInterface = tShape({ message: t.String, messageType: t.Number, + sessionVersion: t.maybe(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, +encryptAndPersist: ( content: string, deviceID: string, messageID: string, ) => Promise, +encryptNotification: ( payload: string, deviceID: string, ) => Promise, +decrypt: (encryptedData: EncryptedData, deviceID: string) => Promise, +decryptAndPersist: ( encryptedData: EncryptedData, deviceID: string, messageID: string, ) => Promise, +contentInboundSessionCreator: ( contentIdentityKeys: OLMIdentityKeys, initialEncryptedData: EncryptedData, sessionVersion: number, overwrite: boolean, ) => Promise, +contentOutboundSessionCreator: ( contentIdentityKeys: OLMIdentityKeys, contentInitializationInfo: OlmSessionInitializationInfo, ) => Promise, +isContentSessionInitialized: (deviceID: string) => Promise, +keyserverNotificationsSessionCreator: ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => Promise, +notificationsOutboundSessionCreator: ( deviceID: string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, ) => Promise, +isDeviceNotificationsSessionInitialized: ( deviceID: string, ) => Promise, +isNotificationsSessionInitializedWithDevices: ( deviceIDs: $ReadOnlyArray, ) => Promise<{ +[deviceID: string]: boolean }>, +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, +markPrekeysAsPublished: () => Promise, }; diff --git a/lib/utils/olm-utils.js b/lib/utils/olm-utils.js index b4e4588d2..f9919bd69 100644 --- a/lib/utils/olm-utils.js +++ b/lib/utils/olm-utils.js @@ -1,158 +1,164 @@ // @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 }; } 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_CREATION_RACE_CONDITION', // The client received a session request with a lower session version, // this request can be ignored. alreadyCreated: 'OLM_SESSION_ALREADY_CREATED', // If using sequential decrypt this error means that message was decrypted. // Otherwise, it could mean that the receiver chain advance beyond and the key // to decrypt that message was discarded. messageAlreadyDecrypted: 'OLM_ALREADY_DECRYPTED_OR_KEYS_SKIPPED', // 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', + // 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 8fe5fcfbb..698f4ef74 100644 --- a/native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp +++ b/native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp @@ -1,460 +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 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}; } 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}; } - return this->sessions.at(targetDeviceId)->decrypt(encryptedData); + 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/native/cpp/CommonCpp/CryptoTools/Session.cpp b/native/cpp/CommonCpp/CryptoTools/Session.cpp index 3b39543ad..c56e62815 100644 --- a/native/cpp/CommonCpp/CryptoTools/Session.cpp +++ b/native/cpp/CommonCpp/CryptoTools/Session.cpp @@ -1,216 +1,216 @@ #include "Session.h" #include "PlatformSpecificTools.h" #include #include namespace comm { namespace crypto { OlmSession *Session::getOlmSession() { return reinterpret_cast(this->olmSessionBuffer.data()); } std::unique_ptr Session::createSessionAsInitializer( OlmAccount *account, std::uint8_t *ownerIdentityKeys, const OlmBuffer &idKeys, const OlmBuffer &preKeys, const OlmBuffer &preKeySignature, const std::optional &oneTimeKey) { std::unique_ptr session(new Session()); session->olmSessionBuffer.resize(::olm_session_size()); ::olm_session(session->olmSessionBuffer.data()); OlmBuffer randomBuffer; PlatformSpecificTools::generateSecureRandomBytes( randomBuffer, ::olm_create_outbound_session_random_length(session->getOlmSession())); if (oneTimeKey) { if (-1 == ::olm_create_outbound_session( session->getOlmSession(), account, idKeys.data() + ID_KEYS_PREFIX_OFFSET, KEYSIZE, idKeys.data() + SIGNING_KEYS_PREFIX_OFFSET, KEYSIZE, preKeys.data(), KEYSIZE, preKeySignature.data(), SIGNATURESIZE, oneTimeKey->data(), KEYSIZE, randomBuffer.data(), randomBuffer.size())) { throw std::runtime_error( "error createOutbound => " + std::string{::olm_session_last_error(session->getOlmSession())}); } return session; } if (-1 == ::olm_create_outbound_session_without_otk( session->getOlmSession(), account, idKeys.data() + ID_KEYS_PREFIX_OFFSET, KEYSIZE, idKeys.data() + SIGNING_KEYS_PREFIX_OFFSET, KEYSIZE, preKeys.data(), KEYSIZE, preKeySignature.data(), SIGNATURESIZE, randomBuffer.data(), randomBuffer.size())) { throw std::runtime_error( "error createOutbound => " + std::string{::olm_session_last_error(session->getOlmSession())}); } return session; } std::unique_ptr Session::createSessionAsResponder( OlmAccount *account, std::uint8_t *ownerIdentityKeys, const OlmBuffer &encryptedMessage, const OlmBuffer &idKeys) { std::unique_ptr session(new Session()); OlmBuffer tmpEncryptedMessage(encryptedMessage); session->olmSessionBuffer.resize(::olm_session_size()); ::olm_session(session->olmSessionBuffer.data()); if (-1 == ::olm_create_inbound_session_from( session->getOlmSession(), account, idKeys.data() + ID_KEYS_PREFIX_OFFSET, KEYSIZE, tmpEncryptedMessage.data(), encryptedMessage.size())) { throw std::runtime_error( "error createInbound => " + std::string{::olm_session_last_error(session->getOlmSession())}); } if (-1 == ::olm_remove_one_time_keys(account, session->getOlmSession())) { throw std::runtime_error( "error createInbound (remove oneTimeKey) => " + std::string{::olm_session_last_error(session->getOlmSession())}); } return session; } OlmBuffer Session::storeAsB64(const std::string &secretKey) { size_t pickleLength = ::olm_pickle_session_length(this->getOlmSession()); OlmBuffer pickle(pickleLength); size_t res = ::olm_pickle_session( this->getOlmSession(), secretKey.data(), secretKey.size(), pickle.data(), pickleLength); if (pickleLength != res) { throw std::runtime_error("error pickleSession => ::olm_pickle_session"); } return pickle; } std::unique_ptr Session::restoreFromB64(const std::string &secretKey, OlmBuffer &b64) { std::unique_ptr session(new Session()); session->olmSessionBuffer.resize(::olm_session_size()); ::olm_session(session->olmSessionBuffer.data()); if (-1 == ::olm_unpickle_session( session->getOlmSession(), secretKey.data(), secretKey.size(), b64.data(), b64.size())) { throw std::runtime_error("error pickleSession => ::olm_unpickle_session"); } return session; } std::string Session::decrypt(EncryptedData &encryptedData) { OlmSession *session = this->getOlmSession(); OlmBuffer utilityBuffer(::olm_utility_size()); OlmUtility *olmUtility = ::olm_utility(utilityBuffer.data()); OlmBuffer messageHashBuffer(::olm_sha256_length(olmUtility)); ::olm_sha256( olmUtility, encryptedData.message.data(), encryptedData.message.size(), messageHashBuffer.data(), messageHashBuffer.size()); OlmBuffer tmpEncryptedMessage(encryptedData.message); size_t maxSize = ::olm_decrypt_max_plaintext_length( session, encryptedData.messageType, tmpEncryptedMessage.data(), tmpEncryptedMessage.size()); if (maxSize == -1) { throw std::runtime_error{ "error decrypt_max_plaintext_length => " + std::string{::olm_session_last_error(session)} + ". Hash: " + std::string{messageHashBuffer.begin(), messageHashBuffer.end()}}; } OlmBuffer decryptedMessage(maxSize); size_t decryptedSize = ::olm_decrypt( session, encryptedData.messageType, encryptedData.message.data(), encryptedData.message.size(), decryptedMessage.data(), decryptedMessage.size()); if (decryptedSize == -1) { throw std::runtime_error{ "error decrypt => " + std::string{::olm_session_last_error(session)} + ". Hash: " + std::string{messageHashBuffer.begin(), messageHashBuffer.end()}}; } return std::string{(char *)decryptedMessage.data(), decryptedSize}; } EncryptedData Session::encrypt(const std::string &content) { OlmSession *session = this->getOlmSession(); OlmBuffer encryptedMessage( ::olm_encrypt_message_length(session, content.size())); OlmBuffer messageRandom; PlatformSpecificTools::generateSecureRandomBytes( messageRandom, ::olm_encrypt_random_length(session)); size_t messageType = ::olm_encrypt_message_type(session); if (-1 == ::olm_encrypt( session, (uint8_t *)content.data(), content.size(), messageRandom.data(), messageRandom.size(), encryptedMessage.data(), encryptedMessage.size())) { throw std::runtime_error{ "error encrypt => " + std::string{::olm_session_last_error(session)}}; } - return {encryptedMessage, messageType}; + return {encryptedMessage, messageType, this->getVersion()}; } int Session::getVersion() { return this->version; } void Session::setVersion(int newVersion) { this->version = newVersion; } } // namespace crypto } // namespace comm diff --git a/native/cpp/CommonCpp/CryptoTools/Tools.h b/native/cpp/CommonCpp/CryptoTools/Tools.h index be2afb6a3..cd26db852 100644 --- a/native/cpp/CommonCpp/CryptoTools/Tools.h +++ b/native/cpp/CommonCpp/CryptoTools/Tools.h @@ -1,40 +1,42 @@ #pragma once #include +#include #include #include // base64-encoded #define KEYSIZE 43 #define SIGNATURESIZE 86 #define ID_KEYS_PREFIX_OFFSET 15 #define SIGNING_KEYS_PREFIX_OFFSET 71 namespace comm { namespace crypto { typedef std::vector OlmBuffer; struct Keys { OlmBuffer identityKeys; // size = 116 OlmBuffer oneTimeKeys; // size = 43 each }; struct EncryptedData { OlmBuffer message; size_t messageType; + std::optional sessionVersion; }; class Tools { private: static std::string generateRandomString(size_t size, const std::string &availableSigns); public: static std::string generateRandomString(size_t size); static std::string generateRandomHexString(size_t size); }; } // namespace crypto } // namespace comm diff --git a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp index 78ec42f38..8a39c0975 100644 --- a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp +++ b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp @@ -1,3073 +1,3095 @@ #include "CommCoreModule.h" #include "../Notifications/BackgroundDataStorage/NotificationsCryptoModule.h" #include "BaseDataStore.h" #include "CommServicesAuthMetadataEmitter.h" #include "DatabaseManager.h" #include "InternalModules/GlobalDBSingleton.h" #include "InternalModules/RustPromiseManager.h" #include "NativeModuleUtils.h" #include "TerminateApp.h" #include #include #include #include #include "JSIRust.h" #include "lib.rs.h" #include #include namespace comm { using namespace facebook::react; jsi::Value CommCoreModule::getDraft(jsi::Runtime &rt, jsi::String key) { std::string keyStr = key.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string draftStr; try { draftStr = DatabaseManager::getQueryExecutor().getDraft(keyStr); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } jsi::String draft = jsi::String::createFromUtf8(innerRt, draftStr); promise->resolve(std::move(draft)); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::updateDraft( jsi::Runtime &rt, jsi::String key, jsi::String text) { std::string keyStr = key.utf8(rt); std::string textStr = text.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { std::string error; try { DatabaseManager::getQueryExecutor().updateDraft(keyStr, textStr); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); } else { promise->resolve(true); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::moveDraft( jsi::Runtime &rt, jsi::String oldKey, jsi::String newKey) { std::string oldKeyStr = oldKey.utf8(rt); std::string newKeyStr = newKey.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { std::string error; bool result = false; try { result = DatabaseManager::getQueryExecutor().moveDraft( oldKeyStr, newKeyStr); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); } else { promise->resolve(result); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::getClientDBStore(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::vector draftsVector; std::vector threadsVector; std::vector messagesVector; std::vector messageStoreThreadsVector; std::vector reportStoreVector; std::vector userStoreVector; std::vector keyserverStoreVector; std::vector communityStoreVector; std::vector integrityStoreVector; std::vector syncedMetadataStoreVector; std::vector auxUserStoreVector; std::vector threadActivityStoreVector; std::vector entryStoreVector; std::vector messageStoreLocalMessageInfosVector; try { draftsVector = DatabaseManager::getQueryExecutor().getAllDrafts(); messagesVector = DatabaseManager::getQueryExecutor().getAllMessages(); threadsVector = DatabaseManager::getQueryExecutor().getAllThreads(); messageStoreThreadsVector = DatabaseManager::getQueryExecutor().getAllMessageStoreThreads(); reportStoreVector = DatabaseManager::getQueryExecutor().getAllReports(); userStoreVector = DatabaseManager::getQueryExecutor().getAllUsers(); keyserverStoreVector = DatabaseManager::getQueryExecutor().getAllKeyservers(); communityStoreVector = DatabaseManager::getQueryExecutor().getAllCommunities(); integrityStoreVector = DatabaseManager::getQueryExecutor() .getAllIntegrityThreadHashes(); syncedMetadataStoreVector = DatabaseManager::getQueryExecutor().getAllSyncedMetadata(); auxUserStoreVector = DatabaseManager::getQueryExecutor().getAllAuxUserInfos(); threadActivityStoreVector = DatabaseManager::getQueryExecutor() .getAllThreadActivityEntries(); entryStoreVector = DatabaseManager::getQueryExecutor().getAllEntries(); messageStoreLocalMessageInfosVector = DatabaseManager::getQueryExecutor() .getAllMessageStoreLocalMessageInfos(); } catch (std::system_error &e) { error = e.what(); } auto draftsVectorPtr = std::make_shared>(std::move(draftsVector)); auto messagesVectorPtr = std::make_shared>( std::move(messagesVector)); auto threadsVectorPtr = std::make_shared>(std::move(threadsVector)); auto messageStoreThreadsVectorPtr = std::make_shared>( std::move(messageStoreThreadsVector)); auto reportStoreVectorPtr = std::make_shared>( std::move(reportStoreVector)); auto userStoreVectorPtr = std::make_shared>( std::move(userStoreVector)); auto keyserveStoreVectorPtr = std::make_shared>( std::move(keyserverStoreVector)); auto communityStoreVectorPtr = std::make_shared>( std::move(communityStoreVector)); auto integrityStoreVectorPtr = std::make_shared>( std::move(integrityStoreVector)); auto syncedMetadataStoreVectorPtr = std::make_shared>( std::move(syncedMetadataStoreVector)); auto auxUserStoreVectorPtr = std::make_shared>( std::move(auxUserStoreVector)); auto threadActivityStoreVectorPtr = std::make_shared>( std::move(threadActivityStoreVector)); auto entryStoreVectorPtr = std::make_shared>( std::move(entryStoreVector)); auto messageStoreLocalMessageInfosVectorPtr = std::make_shared>( std::move(messageStoreLocalMessageInfosVector)); this->jsInvoker_->invokeAsync([&innerRt, draftsVectorPtr, messagesVectorPtr, threadsVectorPtr, messageStoreThreadsVectorPtr, reportStoreVectorPtr, userStoreVectorPtr, keyserveStoreVectorPtr, communityStoreVectorPtr, integrityStoreVectorPtr, syncedMetadataStoreVectorPtr, auxUserStoreVectorPtr, threadActivityStoreVectorPtr, entryStoreVectorPtr, messageStoreLocalMessageInfosVectorPtr, error, promise, draftStore = this->draftStore, threadStore = this->threadStore, messageStore = this->messageStore, reportStore = this->reportStore, userStore = this->userStore, keyserverStore = this->keyserverStore, communityStore = this->communityStore, integrityStore = this->integrityStore, syncedMetadataStore = this->syncedMetadataStore, auxUserStore = this->auxUserStore, threadActivityStore = this->threadActivityStore, entryStore = this->entryStore]() { if (error.size()) { promise->reject(error); return; } jsi::Array jsiDrafts = draftStore.parseDBDataStore(innerRt, draftsVectorPtr); jsi::Array jsiMessages = messageStore.parseDBDataStore(innerRt, messagesVectorPtr); jsi::Array jsiThreads = threadStore.parseDBDataStore(innerRt, threadsVectorPtr); jsi::Array jsiMessageStoreThreads = messageStore.parseDBMessageStoreThreads( innerRt, messageStoreThreadsVectorPtr); jsi::Array jsiReportStore = reportStore.parseDBDataStore(innerRt, reportStoreVectorPtr); jsi::Array jsiUserStore = userStore.parseDBDataStore(innerRt, userStoreVectorPtr); jsi::Array jsiKeyserverStore = keyserverStore.parseDBDataStore( innerRt, keyserveStoreVectorPtr); jsi::Array jsiCommunityStore = communityStore.parseDBDataStore( innerRt, communityStoreVectorPtr); jsi::Array jsiIntegrityStore = integrityStore.parseDBDataStore( innerRt, integrityStoreVectorPtr); jsi::Array jsiSyncedMetadataStore = syncedMetadataStore.parseDBDataStore( innerRt, syncedMetadataStoreVectorPtr); jsi::Array jsiAuxUserStore = auxUserStore.parseDBDataStore(innerRt, auxUserStoreVectorPtr); jsi::Array jsiThreadActivityStore = threadActivityStore.parseDBDataStore( innerRt, threadActivityStoreVectorPtr); jsi::Array jsiEntryStore = entryStore.parseDBDataStore(innerRt, entryStoreVectorPtr); jsi::Array jsiMessageStoreLocalMessageInfos = messageStore.parseDBMessageStoreLocalMessageInfos( innerRt, messageStoreLocalMessageInfosVectorPtr); auto jsiClientDBStore = jsi::Object(innerRt); jsiClientDBStore.setProperty(innerRt, "messages", jsiMessages); jsiClientDBStore.setProperty(innerRt, "threads", jsiThreads); jsiClientDBStore.setProperty(innerRt, "drafts", jsiDrafts); jsiClientDBStore.setProperty( innerRt, "messageStoreThreads", jsiMessageStoreThreads); jsiClientDBStore.setProperty(innerRt, "reports", jsiReportStore); jsiClientDBStore.setProperty(innerRt, "users", jsiUserStore); jsiClientDBStore.setProperty( innerRt, "keyservers", jsiKeyserverStore); jsiClientDBStore.setProperty( innerRt, "communities", jsiCommunityStore); jsiClientDBStore.setProperty( innerRt, "integrityThreadHashes", jsiIntegrityStore); jsiClientDBStore.setProperty( innerRt, "syncedMetadata", jsiSyncedMetadataStore); jsiClientDBStore.setProperty( innerRt, "auxUserInfos", jsiAuxUserStore); jsiClientDBStore.setProperty( innerRt, "threadActivityEntries", jsiThreadActivityStore); jsiClientDBStore.setProperty(innerRt, "entries", jsiEntryStore); jsiClientDBStore.setProperty( innerRt, "messageStoreLocalMessageInfos", jsiMessageStoreLocalMessageInfos); promise->resolve(std::move(jsiClientDBStore)); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::removeAllDrafts(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { std::string error; try { DatabaseManager::getQueryExecutor().removeAllDrafts(); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::Value::undefined()); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Array CommCoreModule::getAllMessagesSync(jsi::Runtime &rt) { auto messagesVector = NativeModuleUtils::runSyncOrThrowJSError>( rt, []() { return DatabaseManager::getQueryExecutor().getAllMessages(); }); auto messagesVectorPtr = std::make_shared>(std::move(messagesVector)); jsi::Array jsiMessages = this->messageStore.parseDBDataStore(rt, messagesVectorPtr); return jsiMessages; } void CommCoreModule::processMessageStoreOperationsSync( jsi::Runtime &rt, jsi::Array operations) { return this->messageStore.processStoreOperationsSync( rt, std::move(operations)); } jsi::Array CommCoreModule::getAllThreadsSync(jsi::Runtime &rt) { auto threadsVector = NativeModuleUtils::runSyncOrThrowJSError>(rt, []() { return DatabaseManager::getQueryExecutor().getAllThreads(); }); auto threadsVectorPtr = std::make_shared>(std::move(threadsVector)); jsi::Array jsiThreads = this->threadStore.parseDBDataStore(rt, threadsVectorPtr); return jsiThreads; } void CommCoreModule::processThreadStoreOperationsSync( jsi::Runtime &rt, jsi::Array operations) { this->threadStore.processStoreOperationsSync(rt, std::move(operations)); } void CommCoreModule::processReportStoreOperationsSync( jsi::Runtime &rt, jsi::Array operations) { this->reportStore.processStoreOperationsSync(rt, std::move(operations)); } template void CommCoreModule::appendDBStoreOps( jsi::Runtime &rt, jsi::Object &operations, const char *key, T &store, std::shared_ptr>> &destination) { auto opsObject = operations.getProperty(rt, key); if (opsObject.isObject()) { auto ops = store.createOperations(rt, opsObject.asObject(rt).asArray(rt)); std::move( std::make_move_iterator(ops.begin()), std::make_move_iterator(ops.end()), std::back_inserter(*destination)); } } jsi::Value CommCoreModule::processDBStoreOperations( jsi::Runtime &rt, jsi::Object operations) { std::string createOperationsError; auto storeOpsPtr = std::make_shared>>(); try { this->appendDBStoreOps( rt, operations, "draftStoreOperations", this->draftStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "threadStoreOperations", this->threadStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "messageStoreOperations", this->messageStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "reportStoreOperations", this->reportStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "userStoreOperations", this->userStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "keyserverStoreOperations", this->keyserverStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "communityStoreOperations", this->communityStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "integrityStoreOperations", this->integrityStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "syncedMetadataStoreOperations", this->syncedMetadataStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "auxUserStoreOperations", this->auxUserStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "threadActivityStoreOperations", this->threadActivityStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "entryStoreOperations", this->entryStore, storeOpsPtr); this->appendDBStoreOps( rt, operations, "messageSearchStoreOperations", this->messageSearchStore, storeOpsPtr); } catch (std::runtime_error &e) { createOperationsError = e.what(); } std::vector messages; try { auto messagesJSIObj = operations.getProperty(rt, "outboundP2PMessages"); if (messagesJSIObj.isObject()) { auto messagesJSI = messagesJSIObj.asObject(rt).asArray(rt); for (size_t idx = 0; idx < messagesJSI.size(rt); idx++) { jsi::Object msgObj = messagesJSI.getValueAtIndex(rt, idx).asObject(rt); std::string messageID = msgObj.getProperty(rt, "messageID").asString(rt).utf8(rt); std::string deviceID = msgObj.getProperty(rt, "deviceID").asString(rt).utf8(rt); std::string userID = msgObj.getProperty(rt, "userID").asString(rt).utf8(rt); std::string timestamp = msgObj.getProperty(rt, "timestamp").asString(rt).utf8(rt); std::string plaintext = msgObj.getProperty(rt, "plaintext").asString(rt).utf8(rt); std::string ciphertext = msgObj.getProperty(rt, "ciphertext").asString(rt).utf8(rt); std::string status = msgObj.getProperty(rt, "status").asString(rt).utf8(rt); bool supports_auto_retry = msgObj.getProperty(rt, "supportsAutoRetry").asBool(); OutboundP2PMessage outboundMessage{ messageID, deviceID, userID, timestamp, plaintext, ciphertext, status, supports_auto_retry}; messages.push_back(outboundMessage); } } } catch (std::runtime_error &e) { createOperationsError = e.what(); } return facebook::react::createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { std::string error = createOperationsError; if (!error.size()) { try { DatabaseManager::getQueryExecutor().beginTransaction(); for (const auto &operation : *storeOpsPtr) { operation->execute(); } if (messages.size() > 0) { DatabaseManager::getQueryExecutor().addOutboundP2PMessages( messages); } DatabaseManager::getQueryExecutor().captureBackupLogs(); DatabaseManager::getQueryExecutor().commitTransaction(); } catch (std::system_error &e) { error = e.what(); DatabaseManager::getQueryExecutor().rollbackTransaction(); } } if (!error.size()) { ::triggerBackupFileUpload(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } void CommCoreModule::terminate(jsi::Runtime &rt) { TerminateApp::terminate(); } const std::string getAccountDataKey(const std::string secureStoreAccountDataKey) { folly::Optional storedSecretKey = CommSecureStore::get(secureStoreAccountDataKey); if (!storedSecretKey.hasValue()) { storedSecretKey = crypto::Tools::generateRandomString(64); CommSecureStore::set(secureStoreAccountDataKey, storedSecretKey.value()); } return storedSecretKey.value(); } void CommCoreModule::persistCryptoModules( bool persistContentModule, std::optional, std::string>> maybeUpdatedNotifsCryptoModule) { std::string storedSecretKey = getAccountDataKey(secureStoreAccountDataKey); if (!persistContentModule && !maybeUpdatedNotifsCryptoModule.has_value()) { return; } crypto::Persist newContentPersist; if (persistContentModule) { newContentPersist = this->contentCryptoModule->storeAsB64(storedSecretKey); } std::promise persistencePromise; std::future persistenceFuture = persistencePromise.get_future(); GlobalDBSingleton::instance.scheduleOrRunCancellable( [=, &persistencePromise]() { try { DatabaseManager::getQueryExecutor().beginTransaction(); if (persistContentModule) { DatabaseManager::getQueryExecutor().storeOlmPersistData( DatabaseManager::getQueryExecutor().getContentAccountID(), newContentPersist); } if (maybeUpdatedNotifsCryptoModule.has_value()) { NotificationsCryptoModule::persistNotificationsAccount( maybeUpdatedNotifsCryptoModule.value().first, maybeUpdatedNotifsCryptoModule.value().second, true); } DatabaseManager::getQueryExecutor().commitTransaction(); persistencePromise.set_value(); } catch (std::system_error &e) { DatabaseManager::getQueryExecutor().rollbackTransaction(); persistencePromise.set_exception(std::make_exception_ptr(e)); } }); persistenceFuture.get(); } jsi::Value CommCoreModule::initializeCryptoAccount(jsi::Runtime &rt) { folly::Optional storedSecretKey = CommSecureStore::get(this->secureStoreAccountDataKey); if (!storedSecretKey.hasValue()) { storedSecretKey = crypto::Tools::generateRandomString(64); CommSecureStore::set( this->secureStoreAccountDataKey, storedSecretKey.value()); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { crypto::Persist contentPersist; crypto::Persist notifsPersist; std::string error; try { std::optional contentAccountData = DatabaseManager::getQueryExecutor().getOlmPersistAccountData( DatabaseManager::getQueryExecutor().getContentAccountID()); if (contentAccountData.has_value()) { contentPersist.account = crypto::OlmBuffer( contentAccountData->begin(), contentAccountData->end()); // handle sessions data std::vector sessionsData = DatabaseManager::getQueryExecutor() .getOlmPersistSessionsData(); for (OlmPersistSession &sessionsDataItem : sessionsData) { crypto::OlmBuffer sessionDataBuffer( sessionsDataItem.session_data.begin(), sessionsDataItem.session_data.end()); crypto::SessionPersist sessionPersist{ sessionDataBuffer, sessionsDataItem.version}; contentPersist.sessions.insert(std::make_pair( sessionsDataItem.target_device_id, sessionPersist)); } } std::optional notifsAccountData = DatabaseManager::getQueryExecutor().getOlmPersistAccountData( DatabaseManager::getQueryExecutor().getNotifsAccountID()); if (notifsAccountData.has_value()) { notifsPersist.account = crypto::OlmBuffer( notifsAccountData->begin(), notifsAccountData->end()); } } catch (std::system_error &e) { error = e.what(); } this->cryptoThread->scheduleTask([=]() { std::string error; this->contentCryptoModule.reset(new crypto::CryptoModule( this->publicCryptoAccountID, storedSecretKey.value(), contentPersist)); std::optional< std::pair, std::string>> maybeNotifsCryptoAccountToPersist; if (!NotificationsCryptoModule:: isNotificationsAccountInitialized()) { maybeNotifsCryptoAccountToPersist = { std::make_shared( this->notifsCryptoAccountID, storedSecretKey.value(), notifsPersist), storedSecretKey.value()}; } try { this->persistCryptoModules( contentPersist.isEmpty(), maybeNotifsCryptoAccountToPersist); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); return; } }); this->jsInvoker_->invokeAsync( [=]() { promise->resolve(jsi::Value::undefined()); }); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::getUserPublicKey(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string primaryKeysResult; std::string notificationsKeysResult; if (this->contentCryptoModule == nullptr || !NotificationsCryptoModule::isNotificationsAccountInitialized()) { error = "user has not been initialized"; } else { primaryKeysResult = this->contentCryptoModule->getIdentityKeys(); notificationsKeysResult = NotificationsCryptoModule::getIdentityKeys(); } std::string notificationsCurve25519Cpp, notificationsEd25519Cpp, blobPayloadCpp, signatureCpp, primaryCurve25519Cpp, primaryEd25519Cpp; if (!error.size()) { folly::dynamic parsedPrimary; try { parsedPrimary = folly::parseJson(primaryKeysResult); } catch (const folly::json::parse_error &e) { error = "parsing identity keys failed with: " + std::string(e.what()); } if (!error.size()) { primaryCurve25519Cpp = parsedPrimary["curve25519"].asString(); primaryEd25519Cpp = parsedPrimary["ed25519"].asString(); folly::dynamic parsedNotifications; try { parsedNotifications = folly::parseJson(notificationsKeysResult); } catch (const folly::json::parse_error &e) { error = "parsing notifications keys failed with: " + std::string(e.what()); } if (!error.size()) { notificationsCurve25519Cpp = parsedNotifications["curve25519"].asString(); notificationsEd25519Cpp = parsedNotifications["ed25519"].asString(); folly::dynamic blobPayloadJSON = folly::dynamic::object( "primaryIdentityPublicKeys", folly::dynamic::object("ed25519", primaryEd25519Cpp)( "curve25519", primaryCurve25519Cpp))( "notificationIdentityPublicKeys", folly::dynamic::object("ed25519", notificationsEd25519Cpp)( "curve25519", notificationsCurve25519Cpp)); blobPayloadCpp = folly::toJson(blobPayloadJSON); signatureCpp = this->contentCryptoModule->signMessage(blobPayloadCpp); } } } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } auto primaryCurve25519{ jsi::String::createFromUtf8(innerRt, primaryCurve25519Cpp)}; auto primaryEd25519{ jsi::String::createFromUtf8(innerRt, primaryEd25519Cpp)}; auto jsiPrimaryIdentityPublicKeys = jsi::Object(innerRt); jsiPrimaryIdentityPublicKeys.setProperty( innerRt, "ed25519", primaryEd25519); jsiPrimaryIdentityPublicKeys.setProperty( innerRt, "curve25519", primaryCurve25519); auto notificationsCurve25519{jsi::String::createFromUtf8( innerRt, notificationsCurve25519Cpp)}; auto notificationsEd25519{ jsi::String::createFromUtf8(innerRt, notificationsEd25519Cpp)}; auto jsiNotificationIdentityPublicKeys = jsi::Object(innerRt); jsiNotificationIdentityPublicKeys.setProperty( innerRt, "ed25519", notificationsEd25519); jsiNotificationIdentityPublicKeys.setProperty( innerRt, "curve25519", notificationsCurve25519); auto blobPayload{ jsi::String::createFromUtf8(innerRt, blobPayloadCpp)}; auto signature{jsi::String::createFromUtf8(innerRt, signatureCpp)}; auto jsiClientPublicKeys = jsi::Object(innerRt); jsiClientPublicKeys.setProperty( innerRt, "primaryIdentityPublicKeys", jsiPrimaryIdentityPublicKeys); jsiClientPublicKeys.setProperty( innerRt, "notificationIdentityPublicKeys", jsiNotificationIdentityPublicKeys); jsiClientPublicKeys.setProperty( innerRt, "blobPayload", blobPayload); jsiClientPublicKeys.setProperty(innerRt, "signature", signature); promise->resolve(std::move(jsiClientPublicKeys)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Object parseOLMOneTimeKeys(jsi::Runtime &rt, std::string oneTimeKeysBlob) { folly::dynamic parsedOneTimeKeys = folly::parseJson(oneTimeKeysBlob); auto jsiOneTimeKeysInner = jsi::Object(rt); for (auto &kvPair : parsedOneTimeKeys["curve25519"].items()) { jsiOneTimeKeysInner.setProperty( rt, kvPair.first.asString().c_str(), jsi::String::createFromUtf8(rt, kvPair.second.asString())); } auto jsiOneTimeKeys = jsi::Object(rt); jsiOneTimeKeys.setProperty(rt, "curve25519", jsiOneTimeKeysInner); return jsiOneTimeKeys; } std::string parseOLMPrekey(std::string prekeyBlob) { folly::dynamic parsedPrekey; try { parsedPrekey = folly::parseJson(prekeyBlob); } catch (const folly::json::parse_error &e) { throw std::runtime_error( "parsing prekey failed with: " + std::string(e.what())); } folly::dynamic innerObject = parsedPrekey["curve25519"]; if (!innerObject.isObject()) { throw std::runtime_error("parsing prekey failed: inner object malformed"); } if (innerObject.values().begin() == innerObject.values().end()) { throw std::runtime_error("parsing prekey failed: prekey missing"); } return parsedPrekey["curve25519"].values().begin()->asString(); } jsi::Object parseOneTimeKeysResult( jsi::Runtime &rt, std::string contentOneTimeKeysBlob, std::string notifOneTimeKeysBlob) { auto contentOneTimeKeys = parseOLMOneTimeKeys(rt, contentOneTimeKeysBlob); auto notifOneTimeKeys = parseOLMOneTimeKeys(rt, notifOneTimeKeysBlob); auto jsiOneTimeKeysResult = jsi::Object(rt); jsiOneTimeKeysResult.setProperty( rt, "contentOneTimeKeys", contentOneTimeKeys); jsiOneTimeKeysResult.setProperty( rt, "notificationsOneTimeKeys", notifOneTimeKeys); return jsiOneTimeKeysResult; } jsi::Object parseEncryptedData( jsi::Runtime &rt, const crypto::EncryptedData &encryptedData) { auto encryptedDataJSI = jsi::Object(rt); auto message = std::string{encryptedData.message.begin(), encryptedData.message.end()}; auto messageJSI = jsi::String::createFromUtf8(rt, message); encryptedDataJSI.setProperty(rt, "message", messageJSI); encryptedDataJSI.setProperty( rt, "messageType", static_cast(encryptedData.messageType)); + if (encryptedData.sessionVersion.has_value()) { + encryptedDataJSI.setProperty( + rt, + "sessionVersion", + static_cast(encryptedData.sessionVersion.value())); + } return encryptedDataJSI; } jsi::Value CommCoreModule::getOneTimeKeys(jsi::Runtime &rt, double oneTimeKeysAmount) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string contentResult; std::string notifResult; if (this->contentCryptoModule == nullptr || !NotificationsCryptoModule::isNotificationsAccountInitialized()) { this->jsInvoker_->invokeAsync([=, &innerRt]() { promise->reject("user has not been initialized"); }); return; } try { contentResult = this->contentCryptoModule->getOneTimeKeysForPublishing( oneTimeKeysAmount); std::pair, std::string> notifsCryptoModuleWithPicklingKey = NotificationsCryptoModule::fetchNotificationsAccount() .value(); notifResult = notifsCryptoModuleWithPicklingKey.first ->getOneTimeKeysForPublishing(oneTimeKeysAmount); this->persistCryptoModules(true, notifsCryptoModuleWithPicklingKey); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve( parseOneTimeKeysResult(innerRt, contentResult, notifResult)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::validateAndUploadPrekeys( jsi::Runtime &rt, jsi::String authUserID, jsi::String authDeviceID, jsi::String authAccessToken) { auto authUserIDRust = jsiStringToRustString(authUserID, rt); auto authDeviceIDRust = jsiStringToRustString(authDeviceID, rt); auto authAccessTokenRust = jsiStringToRustString(authAccessToken, rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::optional maybeContentPrekeyToUpload; std::optional maybeNotifsPrekeyToUpload; if (this->contentCryptoModule == nullptr || !NotificationsCryptoModule::isNotificationsAccountInitialized()) { this->jsInvoker_->invokeAsync([=, &innerRt]() { promise->reject("user has not been initialized"); }); return; } std::optional< std::pair, std::string>> notifsCryptoModuleWithPicklingKey; try { notifsCryptoModuleWithPicklingKey = NotificationsCryptoModule::fetchNotificationsAccount(); maybeContentPrekeyToUpload = this->contentCryptoModule->validatePrekey(); maybeNotifsPrekeyToUpload = notifsCryptoModuleWithPicklingKey.value() .first->validatePrekey(); this->persistCryptoModules(true, notifsCryptoModuleWithPicklingKey); if (!maybeContentPrekeyToUpload.has_value()) { maybeContentPrekeyToUpload = this->contentCryptoModule->getUnpublishedPrekey(); } if (!maybeNotifsPrekeyToUpload.has_value()) { maybeNotifsPrekeyToUpload = notifsCryptoModuleWithPicklingKey.value() .first->getUnpublishedPrekey(); } } catch (const std::exception &e) { error = e.what(); } if (error.size()) { this->jsInvoker_->invokeAsync( [=, &innerRt]() { promise->reject(error); }); return; } if (!maybeContentPrekeyToUpload.has_value() && !maybeNotifsPrekeyToUpload.has_value()) { this->jsInvoker_->invokeAsync( [=]() { promise->resolve(jsi::Value::undefined()); }); return; } std::string contentPrekeyToUpload; if (maybeContentPrekeyToUpload.has_value()) { contentPrekeyToUpload = maybeContentPrekeyToUpload.value(); } else { contentPrekeyToUpload = this->contentCryptoModule->getPrekey(); } std::string notifsPrekeyToUpload; if (maybeNotifsPrekeyToUpload.has_value()) { notifsPrekeyToUpload = maybeNotifsPrekeyToUpload.value(); } else { notifsPrekeyToUpload = notifsCryptoModuleWithPicklingKey.value().first->getPrekey(); } std::string prekeyUploadError; try { std::string contentPrekeySignature = this->contentCryptoModule->getPrekeySignature(); std::string notifsPrekeySignature = notifsCryptoModuleWithPicklingKey.value() .first->getPrekeySignature(); try { std::promise prekeyPromise; std::future prekeyFuture = prekeyPromise.get_future(); RustPromiseManager::CPPPromiseInfo promiseInfo = { std::move(prekeyPromise)}; auto currentID = RustPromiseManager::instance.addPromise( std::move(promiseInfo)); auto contentPrekeyToUploadRust = rust::String(parseOLMPrekey(contentPrekeyToUpload)); auto prekeySignatureRust = rust::string(contentPrekeySignature); auto notifsPrekeyToUploadRust = rust::String(parseOLMPrekey(notifsPrekeyToUpload)); auto notificationsPrekeySignatureRust = rust::string(notifsPrekeySignature); ::identityRefreshUserPrekeys( authUserIDRust, authDeviceIDRust, authAccessTokenRust, contentPrekeyToUploadRust, prekeySignatureRust, notifsPrekeyToUploadRust, notificationsPrekeySignatureRust, currentID); prekeyFuture.get(); } catch (const std::exception &e) { prekeyUploadError = e.what(); } if (!prekeyUploadError.size()) { this->contentCryptoModule->markPrekeyAsPublished(); notifsCryptoModuleWithPicklingKey.value() .first->markPrekeyAsPublished(); this->persistCryptoModules( true, notifsCryptoModuleWithPicklingKey); } } catch (std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); return; } if (prekeyUploadError.size()) { promise->reject(prekeyUploadError); return; } promise->resolve(jsi::Value::undefined()); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::validateAndGetPrekeys(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string contentPrekey, notifPrekey, contentPrekeySignature, notifPrekeySignature; std::optional contentPrekeyBlob; std::optional notifPrekeyBlob; if (this->contentCryptoModule == nullptr || !NotificationsCryptoModule::isNotificationsAccountInitialized()) { this->jsInvoker_->invokeAsync([=, &innerRt]() { promise->reject("user has not been initialized"); }); return; } std::optional< std::pair, std::string>> notifsCryptoModuleWithPicklingKey; try { notifsCryptoModuleWithPicklingKey = NotificationsCryptoModule::fetchNotificationsAccount(); contentPrekeyBlob = this->contentCryptoModule->validatePrekey(); if (!contentPrekeyBlob) { contentPrekeyBlob = this->contentCryptoModule->getUnpublishedPrekey(); } if (!contentPrekeyBlob) { contentPrekeyBlob = this->contentCryptoModule->getPrekey(); } notifPrekeyBlob = notifsCryptoModuleWithPicklingKey.value() .first->validatePrekey(); if (!notifPrekeyBlob) { notifPrekeyBlob = notifsCryptoModuleWithPicklingKey.value() .first->getUnpublishedPrekey(); } if (!notifPrekeyBlob) { notifPrekeyBlob = notifsCryptoModuleWithPicklingKey.value().first->getPrekey(); } this->persistCryptoModules(true, notifsCryptoModuleWithPicklingKey); contentPrekeySignature = this->contentCryptoModule->getPrekeySignature(); notifPrekeySignature = notifsCryptoModuleWithPicklingKey.value() .first->getPrekeySignature(); contentPrekey = parseOLMPrekey(contentPrekeyBlob.value()); notifPrekey = parseOLMPrekey(notifPrekeyBlob.value()); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } auto contentPrekeyJSI = jsi::String::createFromUtf8(innerRt, contentPrekey); auto contentPrekeySignatureJSI = jsi::String::createFromUtf8(innerRt, contentPrekeySignature); auto notifPrekeyJSI = jsi::String::createFromUtf8(innerRt, notifPrekey); auto notifPrekeySignatureJSI = jsi::String::createFromUtf8(innerRt, notifPrekeySignature); auto signedPrekeysJSI = jsi::Object(innerRt); signedPrekeysJSI.setProperty( innerRt, "contentPrekey", contentPrekeyJSI); signedPrekeysJSI.setProperty( innerRt, "contentPrekeySignature", contentPrekeySignatureJSI); signedPrekeysJSI.setProperty( innerRt, "notifPrekey", notifPrekeyJSI); signedPrekeysJSI.setProperty( innerRt, "notifPrekeySignature", notifPrekeySignatureJSI); promise->resolve(std::move(signedPrekeysJSI)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::initializeNotificationsSession( jsi::Runtime &rt, jsi::String identityKeys, jsi::String prekey, jsi::String prekeySignature, std::optional oneTimeKey, jsi::String keyserverID) { auto identityKeysCpp{identityKeys.utf8(rt)}; auto prekeyCpp{prekey.utf8(rt)}; auto prekeySignatureCpp{prekeySignature.utf8(rt)}; auto keyserverIDCpp{keyserverID.utf8(rt)}; std::optional oneTimeKeyCpp; if (oneTimeKey) { oneTimeKeyCpp = oneTimeKey->utf8(rt); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; crypto::EncryptedData result; std::optional< std::pair, std::string>> notifsCryptoModuleWithPicklingKey; try { notifsCryptoModuleWithPicklingKey = NotificationsCryptoModule::fetchNotificationsAccount(); std::optional oneTimeKeyBuffer; if (oneTimeKeyCpp) { oneTimeKeyBuffer = crypto::OlmBuffer( oneTimeKeyCpp->begin(), oneTimeKeyCpp->end()); } notifsCryptoModuleWithPicklingKey.value() .first->initializeOutboundForSendingSession( keyserverIDCpp, std::vector( identityKeysCpp.begin(), identityKeysCpp.end()), std::vector(prekeyCpp.begin(), prekeyCpp.end()), std::vector( prekeySignatureCpp.begin(), prekeySignatureCpp.end()), oneTimeKeyBuffer); result = notifsCryptoModuleWithPicklingKey.value().first->encrypt( keyserverIDCpp, NotificationsCryptoModule::initialEncryptedMessageContent); std::shared_ptr keyserverNotificationsSession = notifsCryptoModuleWithPicklingKey.value() .first->getSessionByDeviceId(keyserverIDCpp); NotificationsCryptoModule::persistNotificationsSession( keyserverIDCpp, keyserverNotificationsSession); // Session is removed from the account since it is persisted // at different location that the account after serialization notifsCryptoModuleWithPicklingKey.value() .first->removeSessionByDeviceId(keyserverIDCpp); this->persistCryptoModules( false, notifsCryptoModuleWithPicklingKey); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::String::createFromUtf8( innerRt, std::string{result.message.begin(), result.message.end()})); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::isNotificationsSessionInitialized(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; bool result; try { result = NotificationsCryptoModule::isNotificationsSessionInitialized( "Comm"); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve(result); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::isDeviceNotificationsSessionInitialized( jsi::Runtime &rt, jsi::String deviceID) { auto deviceIDCpp{deviceID.utf8(rt)}; return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { if (this->contentCryptoModule == nullptr || !NotificationsCryptoModule::isNotificationsAccountInitialized()) { this->jsInvoker_->invokeAsync([=, &innerRt]() { promise->reject("user has not been initialized"); }); return; } std::string error; bool result; try { result = NotificationsCryptoModule:: isDeviceNotificationsSessionInitialized(deviceIDCpp); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve(result); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::isNotificationsSessionInitializedWithDevices( jsi::Runtime &rt, jsi::Array deviceIDs) { std::vector deviceIDsCpp; for (auto idx = 0; idx < deviceIDs.size(rt); idx++) { std::string deviceIDCpp = deviceIDs.getValueAtIndex(rt, idx).asString(rt).utf8(rt); deviceIDsCpp.push_back(deviceIDCpp); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { if (this->contentCryptoModule == nullptr || !NotificationsCryptoModule::isNotificationsAccountInitialized()) { this->jsInvoker_->invokeAsync([=, &innerRt]() { promise->reject("user has not been initialized"); }); return; } std::string error; std::vector> result; try { result = NotificationsCryptoModule:: isNotificationsSessionInitializedWithDevices(deviceIDsCpp); } catch (const std::exception &e) { error = e.what(); } auto resultPtr = std::make_shared>>( std::move(result)); this->jsInvoker_->invokeAsync( [&innerRt, resultPtr, error, promise]() { if (error.size()) { promise->reject(error); return; } jsi::Object jsiResult = jsi::Object(innerRt); for (const auto &deviceResult : *resultPtr) { jsiResult.setProperty( innerRt, deviceResult.first.c_str(), deviceResult.second); } promise->resolve(std::move(jsiResult)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::updateKeyserverDataInNotifStorage( jsi::Runtime &rt, jsi::Array keyserversData) { std::vector> keyserversDataCpp; for (auto idx = 0; idx < keyserversData.size(rt); idx++) { auto data = keyserversData.getValueAtIndex(rt, idx).asObject(rt); std::string keyserverID = data.getProperty(rt, "id").asString(rt).utf8(rt); std::string keyserverUnreadCountKey = "KEYSERVER." + keyserverID + ".UNREAD_COUNT"; int unreadCount = data.getProperty(rt, "unreadCount").asNumber(); keyserversDataCpp.push_back({keyserverUnreadCountKey, unreadCount}); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { std::string error; try { for (const auto &keyserverData : keyserversDataCpp) { CommMMKV::setInt(keyserverData.first, keyserverData.second); } } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::Value::undefined()); }); }); } jsi::Value CommCoreModule::removeKeyserverDataFromNotifStorage( jsi::Runtime &rt, jsi::Array keyserverIDsToDelete) { std::vector keyserverIDsToDeleteCpp{}; for (auto idx = 0; idx < keyserverIDsToDelete.size(rt); idx++) { std::string keyserverID = keyserverIDsToDelete.getValueAtIndex(rt, idx).asString(rt).utf8(rt); std::string keyserverUnreadCountKey = "KEYSERVER." + keyserverID + ".UNREAD_COUNT"; keyserverIDsToDeleteCpp.push_back(keyserverUnreadCountKey); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { std::string error; try { CommMMKV::removeKeys(keyserverIDsToDeleteCpp); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::Value::undefined()); }); }); } jsi::Value CommCoreModule::getKeyserverDataFromNotifStorage( jsi::Runtime &rt, jsi::Array keyserverIDs) { std::vector keyserverIDsCpp{}; for (auto idx = 0; idx < keyserverIDs.size(rt); idx++) { std::string keyserverID = keyserverIDs.getValueAtIndex(rt, idx).asString(rt).utf8(rt); keyserverIDsCpp.push_back(keyserverID); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { std::string error; std::vector> keyserversDataVector{}; try { for (const auto &keyserverID : keyserverIDsCpp) { std::string keyserverUnreadCountKey = "KEYSERVER." + keyserverID + ".UNREAD_COUNT"; std::optional unreadCount = CommMMKV::getInt(keyserverUnreadCountKey, -1); if (!unreadCount.has_value()) { continue; } keyserversDataVector.push_back({keyserverID, unreadCount.value()}); } } catch (const std::exception &e) { error = e.what(); } auto keyserversDataVectorPtr = std::make_shared>>( std::move(keyserversDataVector)); this->jsInvoker_->invokeAsync( [&innerRt, keyserversDataVectorPtr, error, promise]() { if (error.size()) { promise->reject(error); return; } size_t numKeyserversData = keyserversDataVectorPtr->size(); jsi::Array jsiKeyserversData = jsi::Array(innerRt, numKeyserversData); size_t writeIdx = 0; for (const auto &keyserverData : *keyserversDataVectorPtr) { jsi::Object jsiKeyserverData = jsi::Object(innerRt); jsiKeyserverData.setProperty( innerRt, "id", keyserverData.first); jsiKeyserverData.setProperty( innerRt, "unreadCount", keyserverData.second); jsiKeyserversData.setValueAtIndex( innerRt, writeIdx++, jsiKeyserverData); } promise->resolve(std::move(jsiKeyserversData)); }); }); } jsi::Value CommCoreModule::initializeContentOutboundSession( jsi::Runtime &rt, jsi::String identityKeys, jsi::String prekey, jsi::String prekeySignature, std::optional oneTimeKey, jsi::String deviceID) { auto identityKeysCpp{identityKeys.utf8(rt)}; auto prekeyCpp{prekey.utf8(rt)}; auto prekeySignatureCpp{prekeySignature.utf8(rt)}; auto deviceIDCpp{deviceID.utf8(rt)}; std::optional oneTimeKeyCpp; if (oneTimeKey) { oneTimeKeyCpp = oneTimeKey->utf8(rt); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; crypto::EncryptedData initialEncryptedData; int sessionVersion; try { std::optional oneTimeKeyBuffer; if (oneTimeKeyCpp) { oneTimeKeyBuffer = crypto::OlmBuffer( oneTimeKeyCpp->begin(), oneTimeKeyCpp->end()); } sessionVersion = this->contentCryptoModule->initializeOutboundForSendingSession( deviceIDCpp, std::vector( identityKeysCpp.begin(), identityKeysCpp.end()), std::vector(prekeyCpp.begin(), prekeyCpp.end()), std::vector( prekeySignatureCpp.begin(), prekeySignatureCpp.end()), oneTimeKeyBuffer); const std::string initMessage = "{\"type\": \"init\"}"; initialEncryptedData = contentCryptoModule->encrypt(deviceIDCpp, initMessage); this->persistCryptoModules(true, std::nullopt); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } auto initialEncryptedDataJSI = parseEncryptedData(innerRt, initialEncryptedData); auto outboundSessionCreationResultJSI = jsi::Object(innerRt); outboundSessionCreationResultJSI.setProperty( innerRt, "encryptedData", initialEncryptedDataJSI); outboundSessionCreationResultJSI.setProperty( innerRt, "sessionVersion", sessionVersion); promise->resolve(std::move(outboundSessionCreationResultJSI)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::initializeContentInboundSession( jsi::Runtime &rt, jsi::String identityKeys, jsi::Object encryptedDataJSI, jsi::String deviceID, double sessionVersion, bool overwrite) { auto identityKeysCpp{identityKeys.utf8(rt)}; size_t messageType = std::lround(encryptedDataJSI.getProperty(rt, "messageType").asNumber()); std::string encryptedMessageCpp = encryptedDataJSI.getProperty(rt, "message").asString(rt).utf8(rt); auto deviceIDCpp{deviceID.utf8(rt)}; return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string decryptedMessage; try { this->contentCryptoModule->initializeInboundForReceivingSession( deviceIDCpp, std::vector( encryptedMessageCpp.begin(), encryptedMessageCpp.end()), std::vector( identityKeysCpp.begin(), identityKeysCpp.end()), static_cast(sessionVersion), overwrite); crypto::EncryptedData encryptedData{ std::vector( encryptedMessageCpp.begin(), encryptedMessageCpp.end()), messageType}; decryptedMessage = this->contentCryptoModule->decrypt(deviceIDCpp, encryptedData); this->persistCryptoModules(true, std::nullopt); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve( jsi::String::createFromUtf8(innerRt, decryptedMessage)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::isContentSessionInitialized( jsi::Runtime &rt, jsi::String deviceID) { auto deviceIDCpp{deviceID.utf8(rt)}; return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; bool result; if (this->contentCryptoModule == nullptr || !NotificationsCryptoModule::isNotificationsAccountInitialized()) { this->jsInvoker_->invokeAsync([=, &innerRt]() { promise->reject("user has not been initialized"); }); return; } try { result = this->contentCryptoModule->hasSessionFor(deviceIDCpp); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve(result); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::initializeNotificationsOutboundSession( jsi::Runtime &rt, jsi::String identityKeys, jsi::String prekey, jsi::String prekeySignature, std::optional oneTimeKey, jsi::String deviceID) { auto identityKeysCpp{identityKeys.utf8(rt)}; auto prekeyCpp{prekey.utf8(rt)}; auto prekeySignatureCpp{prekeySignature.utf8(rt)}; auto deviceIDCpp{deviceID.utf8(rt)}; std::optional oneTimeKeyCpp; if (oneTimeKey) { oneTimeKeyCpp = oneTimeKey->utf8(rt); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; crypto::EncryptedData result; std::optional< std::pair, std::string>> notifsCryptoModuleWithPicklingKey; try { notifsCryptoModuleWithPicklingKey = NotificationsCryptoModule::fetchNotificationsAccount(); std::optional oneTimeKeyBuffer; if (oneTimeKeyCpp) { oneTimeKeyBuffer = crypto::OlmBuffer( oneTimeKeyCpp->begin(), oneTimeKeyCpp->end()); } notifsCryptoModuleWithPicklingKey.value() .first->initializeOutboundForSendingSession( deviceIDCpp, std::vector( identityKeysCpp.begin(), identityKeysCpp.end()), std::vector(prekeyCpp.begin(), prekeyCpp.end()), std::vector( prekeySignatureCpp.begin(), prekeySignatureCpp.end()), oneTimeKeyBuffer); result = notifsCryptoModuleWithPicklingKey.value().first->encrypt( deviceIDCpp, NotificationsCryptoModule::initialEncryptedMessageContent); std::shared_ptr peerNotificationsSession = notifsCryptoModuleWithPicklingKey.value() .first->getSessionByDeviceId(deviceIDCpp); NotificationsCryptoModule::persistDeviceNotificationsSession( deviceIDCpp, peerNotificationsSession); // Session is removed from the account since it is persisted // at different location that the account after serialization notifsCryptoModuleWithPicklingKey.value() .first->removeSessionByDeviceId(deviceIDCpp); this->persistCryptoModules( false, notifsCryptoModuleWithPicklingKey); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } auto initialEncryptedDataJSI = parseEncryptedData(innerRt, result); promise->resolve(std::move(initialEncryptedDataJSI)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::encrypt( jsi::Runtime &rt, jsi::String message, jsi::String deviceID) { auto messageCpp{message.utf8(rt)}; auto deviceIDCpp{deviceID.utf8(rt)}; return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; crypto::EncryptedData encryptedMessage; try { encryptedMessage = contentCryptoModule->encrypt(deviceIDCpp, messageCpp); this->persistCryptoModules(true, std::nullopt); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } auto encryptedDataJSI = parseEncryptedData(innerRt, encryptedMessage); promise->resolve(std::move(encryptedDataJSI)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::encryptNotification( jsi::Runtime &rt, jsi::String payload, jsi::String deviceID) { auto payloadCpp{payload.utf8(rt)}; auto deviceIDCpp{deviceID.utf8(rt)}; return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; crypto::EncryptedData result; try { result = NotificationsCryptoModule::encrypt(deviceIDCpp, payloadCpp); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } auto encryptedDataJSI = parseEncryptedData(innerRt, result); promise->resolve(std::move(encryptedDataJSI)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::encryptAndPersist( jsi::Runtime &rt, jsi::String message, jsi::String deviceID, jsi::String messageID) { auto messageCpp{message.utf8(rt)}; auto deviceIDCpp{deviceID.utf8(rt)}; auto messageIDCpp{messageID.utf8(rt)}; return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; crypto::EncryptedData encryptedMessage; try { encryptedMessage = contentCryptoModule->encrypt(deviceIDCpp, messageCpp); std::string storedSecretKey = getAccountDataKey(secureStoreAccountDataKey); crypto::Persist newContentPersist = this->contentCryptoModule->storeAsB64(storedSecretKey); std::promise persistencePromise; std::future persistenceFuture = persistencePromise.get_future(); GlobalDBSingleton::instance.scheduleOrRunCancellable( [=, &persistencePromise]() { try { folly::dynamic jsonObject = folly::dynamic::object; std::string messageStr( encryptedMessage.message.begin(), encryptedMessage.message.end()); jsonObject["message"] = messageStr; jsonObject["messageType"] = encryptedMessage.messageType; std::string ciphertext = folly::toJson(jsonObject); DatabaseManager::getQueryExecutor().beginTransaction(); DatabaseManager::getQueryExecutor() .setCiphertextForOutboundP2PMessage( messageIDCpp, deviceIDCpp, ciphertext); DatabaseManager::getQueryExecutor().storeOlmPersistData( DatabaseManager::getQueryExecutor() .getContentAccountID(), newContentPersist); DatabaseManager::getQueryExecutor().commitTransaction(); persistencePromise.set_value(); } catch (std::system_error &e) { DatabaseManager::getQueryExecutor().rollbackTransaction(); persistencePromise.set_exception( std::make_exception_ptr(e)); } }); persistenceFuture.get(); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } auto encryptedDataJSI = parseEncryptedData(innerRt, encryptedMessage); promise->resolve(std::move(encryptedDataJSI)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::decrypt( jsi::Runtime &rt, jsi::Object encryptedDataJSI, jsi::String deviceID) { size_t messageType = std::lround(encryptedDataJSI.getProperty(rt, "messageType").asNumber()); std::string message = encryptedDataJSI.getProperty(rt, "message").asString(rt).utf8(rt); auto deviceIDCpp{deviceID.utf8(rt)}; + + std::optional sessionVersion; + if (encryptedDataJSI.hasProperty(rt, "sessionVersion")) { + sessionVersion = std::lround( + encryptedDataJSI.getProperty(rt, "sessionVersion").asNumber()); + } + return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string decryptedMessage; try { crypto::EncryptedData encryptedData{ std::vector(message.begin(), message.end()), - messageType}; + messageType, + sessionVersion}; decryptedMessage = this->contentCryptoModule->decrypt(deviceIDCpp, encryptedData); this->persistCryptoModules(true, std::nullopt); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve( jsi::String::createFromUtf8(innerRt, decryptedMessage)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::decryptAndPersist( jsi::Runtime &rt, jsi::Object encryptedDataJSI, jsi::String deviceID, jsi::String messageID) { size_t messageType = std::lround(encryptedDataJSI.getProperty(rt, "messageType").asNumber()); std::string message = encryptedDataJSI.getProperty(rt, "message").asString(rt).utf8(rt); + + std::optional sessionVersion; + if (encryptedDataJSI.hasProperty(rt, "sessionVersion")) { + sessionVersion = std::lround( + encryptedDataJSI.getProperty(rt, "sessionVersion").asNumber()); + } + auto deviceIDCpp{deviceID.utf8(rt)}; auto messageIDCpp{messageID.utf8(rt)}; return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string decryptedMessage; try { crypto::EncryptedData encryptedData{ std::vector(message.begin(), message.end()), - messageType}; + messageType, + sessionVersion}; decryptedMessage = this->contentCryptoModule->decrypt(deviceIDCpp, encryptedData); std::string storedSecretKey = getAccountDataKey(secureStoreAccountDataKey); crypto::Persist newContentPersist = this->contentCryptoModule->storeAsB64(storedSecretKey); std::promise persistencePromise; std::future persistenceFuture = persistencePromise.get_future(); GlobalDBSingleton::instance.scheduleOrRunCancellable( [=, &persistencePromise]() { try { InboundP2PMessage message{ messageIDCpp, deviceIDCpp, decryptedMessage, "decrypted"}; DatabaseManager::getQueryExecutor().beginTransaction(); DatabaseManager::getQueryExecutor().addInboundP2PMessage( message); DatabaseManager::getQueryExecutor().storeOlmPersistData( DatabaseManager::getQueryExecutor() .getContentAccountID(), newContentPersist); DatabaseManager::getQueryExecutor().commitTransaction(); persistencePromise.set_value(); } catch (std::system_error &e) { DatabaseManager::getQueryExecutor().rollbackTransaction(); persistencePromise.set_exception( std::make_exception_ptr(e)); } }); persistenceFuture.get(); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve( jsi::String::createFromUtf8(innerRt, decryptedMessage)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::signMessage(jsi::Runtime &rt, jsi::String message) { std::string messageStr = message.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string signature; try { signature = this->contentCryptoModule->signMessage(messageStr); } catch (const std::exception &e) { error = "signing message failed with: " + std::string(e.what()); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } auto jsiSignature{jsi::String::createFromUtf8(innerRt, signature)}; promise->resolve(std::move(jsiSignature)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::verifySignature( jsi::Runtime &rt, jsi::String publicKey, jsi::String message, jsi::String signature) { std::string keyStr = publicKey.utf8(rt); std::string messageStr = message.utf8(rt); std::string signatureStr = signature.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; try { crypto::CryptoModule::verifySignature( keyStr, messageStr, signatureStr); } catch (const std::exception &e) { error = "verifying signature failed with: " + std::string(e.what()); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::Value::undefined()); }); }; this->cryptoThread->scheduleTask(job); }); } CommCoreModule::CommCoreModule( std::shared_ptr jsInvoker) : facebook::react::CommCoreModuleSchemaCxxSpecJSI(jsInvoker), cryptoThread(std::make_unique("crypto")), draftStore(jsInvoker), threadStore(jsInvoker), messageStore(jsInvoker), reportStore(jsInvoker), userStore(jsInvoker), keyserverStore(jsInvoker), communityStore(jsInvoker), integrityStore(jsInvoker), syncedMetadataStore(jsInvoker), auxUserStore(jsInvoker), threadActivityStore(jsInvoker), entryStore(jsInvoker), messageSearchStore(jsInvoker) { GlobalDBSingleton::instance.enableMultithreading(); } double CommCoreModule::getCodeVersion(jsi::Runtime &rt) { return this->codeVersion; } jsi::Value CommCoreModule::setNotifyToken(jsi::Runtime &rt, jsi::String token) { auto notifyToken{token.utf8(rt)}; return createPromiseAsJSIValue( rt, [this, notifyToken](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [this, notifyToken, promise]() { std::string error; try { DatabaseManager::getQueryExecutor().setNotifyToken(notifyToken); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::clearNotifyToken(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [this](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [this, promise]() { std::string error; try { DatabaseManager::getQueryExecutor().clearNotifyToken(); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); }; jsi::Value CommCoreModule::stampSQLiteDBUserID(jsi::Runtime &rt, jsi::String userID) { auto currentUserID{userID.utf8(rt)}; return createPromiseAsJSIValue( rt, [this, currentUserID](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [this, promise, currentUserID]() { std::string error; try { DatabaseManager::getQueryExecutor().stampSQLiteDBUserID( currentUserID); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::getSQLiteStampedUserID(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [this](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [this, &innerRt, promise]() { std::string error; std::string result; try { result = DatabaseManager::getQueryExecutor().getSQLiteStampedUserID(); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([&innerRt, error, result, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::String::createFromUtf8(innerRt, result)); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::clearSensitiveData(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [this](jsi::Runtime &innerRt, std::shared_ptr promise) { GlobalDBSingleton::instance.setTasksCancelled(true); taskType job = [this, promise]() { std::string error; try { this->innerClearCommServicesAuthMetadata(); DatabaseManager::clearSensitiveData(); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); GlobalDBSingleton::instance.scheduleOrRun( []() { GlobalDBSingleton::instance.setTasksCancelled(false); }); }; GlobalDBSingleton::instance.scheduleOrRun(job); }); } bool CommCoreModule::checkIfDatabaseNeedsDeletion(jsi::Runtime &rt) { return DatabaseManager::checkIfDatabaseNeedsDeletion(); } void CommCoreModule::reportDBOperationsFailure(jsi::Runtime &rt) { DatabaseManager::reportDBOperationsFailure(); } jsi::Value CommCoreModule::computeBackupKey( jsi::Runtime &rt, jsi::String password, jsi::String backupID) { std::string passwordStr = password.utf8(rt); std::string backupIDStr = backupID.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::array<::std::uint8_t, 32> backupKey; try { backupKey = compute_backup_key(passwordStr, backupIDStr); } catch (const std::exception &e) { error = std::string{"Failed to compute backup key: "} + e.what(); } this->jsInvoker_->invokeAsync([=, &innerRt]() { if (error.size()) { promise->reject(error); return; } auto size = backupKey.size(); auto arrayBuffer = innerRt.global() .getPropertyAsFunction(innerRt, "ArrayBuffer") .callAsConstructor(innerRt, {static_cast(size)}) .asObject(innerRt) .getArrayBuffer(innerRt); auto bufferPtr = arrayBuffer.data(innerRt); memcpy(bufferPtr, backupKey.data(), size); promise->resolve(std::move(arrayBuffer)); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::generateRandomString(jsi::Runtime &rt, double size) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::string randomString; try { randomString = crypto::Tools::generateRandomString(static_cast(size)); } catch (const std::exception &e) { error = "Failed to generate random string for size " + std::to_string(size) + ": " + e.what(); } this->jsInvoker_->invokeAsync( [&innerRt, error, randomString, promise]() { if (error.size()) { promise->reject(error); } else { jsi::String jsiRandomString = jsi::String::createFromUtf8(innerRt, randomString); promise->resolve(std::move(jsiRandomString)); } }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::setCommServicesAuthMetadata( jsi::Runtime &rt, jsi::String userID, jsi::String deviceID, jsi::String accessToken) { auto userIDStr{userID.utf8(rt)}; auto deviceIDStr{deviceID.utf8(rt)}; auto accessTokenStr{accessToken.utf8(rt)}; return createPromiseAsJSIValue( rt, [this, userIDStr, deviceIDStr, accessTokenStr]( jsi::Runtime &innerRt, std::shared_ptr promise) { std::string error; try { this->innerSetCommServicesAuthMetadata( userIDStr, deviceIDStr, accessTokenStr); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }); } void CommCoreModule::innerSetCommServicesAuthMetadata( std::string userID, std::string deviceID, std::string accessToken) { CommSecureStore::set(CommSecureStore::userID, userID); CommSecureStore::set(CommSecureStore::deviceID, deviceID); CommSecureStore::set(CommSecureStore::commServicesAccessToken, accessToken); CommServicesAuthMetadataEmitter::sendAuthMetadataToJS(accessToken, userID); } jsi::Value CommCoreModule::getCommServicesAuthMetadata(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [this](jsi::Runtime &innerRt, std::shared_ptr promise) { std::string error; std::string userID; std::string deviceID; std::string accessToken; try { folly::Optional userIDOpt = CommSecureStore::get(CommSecureStore::userID); if (userIDOpt.hasValue()) { userID = userIDOpt.value(); } folly::Optional deviceIDOpt = CommSecureStore::get(CommSecureStore::deviceID); if (deviceIDOpt.hasValue()) { deviceID = deviceIDOpt.value(); } folly::Optional accessTokenOpt = CommSecureStore::get(CommSecureStore::commServicesAccessToken); if (accessTokenOpt.hasValue()) { accessToken = accessTokenOpt.value(); } } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync( [&innerRt, error, userID, deviceID, accessToken, promise]() { if (error.size()) { promise->reject(error); } else { auto authMetadata = jsi::Object(innerRt); if (!userID.empty()) { authMetadata.setProperty( innerRt, "userID", jsi::String::createFromUtf8(innerRt, userID)); } if (!deviceID.empty()) { authMetadata.setProperty( innerRt, "deviceID", jsi::String::createFromUtf8(innerRt, deviceID)); } if (!accessToken.empty()) { authMetadata.setProperty( innerRt, "accessToken", jsi::String::createFromUtf8(innerRt, accessToken)); } promise->resolve(std::move(authMetadata)); } }); }); } jsi::Value CommCoreModule::clearCommServicesAuthMetadata(jsi::Runtime &rt) { return this->setCommServicesAuthMetadata( rt, jsi::String::createFromUtf8(rt, ""), jsi::String::createFromUtf8(rt, ""), jsi::String::createFromUtf8(rt, "")); } void CommCoreModule::innerClearCommServicesAuthMetadata() { return this->innerSetCommServicesAuthMetadata("", "", ""); } jsi::Value CommCoreModule::setCommServicesAccessToken( jsi::Runtime &rt, jsi::String accessToken) { auto accessTokenStr{accessToken.utf8(rt)}; return createPromiseAsJSIValue( rt, [this, accessTokenStr]( jsi::Runtime &innerRt, std::shared_ptr promise) { std::string error; try { CommSecureStore::set( CommSecureStore::commServicesAccessToken, accessTokenStr); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }); } jsi::Value CommCoreModule::clearCommServicesAccessToken(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [this](jsi::Runtime &innerRt, std::shared_ptr promise) { std::string error; try { CommSecureStore::set(CommSecureStore::commServicesAccessToken, ""); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }); } void CommCoreModule::startBackupHandler(jsi::Runtime &rt) { try { ::startBackupHandler(); } catch (const std::exception &e) { throw jsi::JSError(rt, e.what()); } } void CommCoreModule::stopBackupHandler(jsi::Runtime &rt) { try { ::stopBackupHandler(); } catch (const std::exception &e) { throw jsi::JSError(rt, e.what()); } } jsi::Value CommCoreModule::createNewBackupInternal( jsi::Runtime &rt, std::string backupSecret, std::string backupMessage) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { this->cryptoThread->scheduleTask([=, &innerRt]() { std::string error; std::string backupID; try { backupID = crypto::Tools::generateRandomString(32); } catch (const std::exception &e) { error = "Failed to generate backupID"; } std::string pickleKey; std::string pickledAccount; if (!error.size()) { try { pickleKey = crypto::Tools::generateRandomString(64); crypto::Persist persist = this->contentCryptoModule->storeAsB64(pickleKey); pickledAccount = std::string(persist.account.begin(), persist.account.end()); } catch (const std::exception &e) { error = "Failed to pickle crypto account"; } } if (!error.size()) { auto currentID = RustPromiseManager::instance.addPromise( {promise, this->jsInvoker_, innerRt}); ::createBackup( rust::string(backupID), rust::string(backupSecret), rust::string(pickleKey), rust::string(pickledAccount), rust::string(backupMessage), currentID); } else { this->jsInvoker_->invokeAsync( [=, &innerRt]() { promise->reject(error); }); } }); }); } jsi::Value CommCoreModule::createNewBackup(jsi::Runtime &rt, jsi::String backupSecret) { std::string backupSecretStr = backupSecret.utf8(rt); return createNewBackupInternal(rt, backupSecretStr, ""); } jsi::Value CommCoreModule::createNewSIWEBackup( jsi::Runtime &rt, jsi::String backupSecret, jsi::String siweBackupMsg) { std::string backupSecretStr = backupSecret.utf8(rt); std::string siweBackupMsgStr = siweBackupMsg.utf8(rt); return createNewBackupInternal(rt, backupSecretStr, siweBackupMsgStr); } jsi::Value CommCoreModule::restoreBackupInternal( jsi::Runtime &rt, std::string backupSecret, std::string backupID, std::string maxVersion) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { auto currentID = RustPromiseManager::instance.addPromise( {promise, this->jsInvoker_, innerRt}); ::restoreBackup( rust::string(backupSecret), rust::string(backupID), rust::string(maxVersion), currentID); }); } jsi::Value CommCoreModule::restoreBackup( jsi::Runtime &rt, jsi::String backupSecret, jsi::String maxVersion) { std::string backupSecretStr = backupSecret.utf8(rt); std::string maxVersionStr = maxVersion.utf8(rt); return restoreBackupInternal(rt, backupSecretStr, "", maxVersionStr); } jsi::Value CommCoreModule::restoreSIWEBackup( jsi::Runtime &rt, jsi::String backupSecret, jsi::String backupID, jsi::String maxVersion) { std::string backupSecretStr = backupSecret.utf8(rt); std::string backupIDStr = backupID.utf8(rt); std::string maxVersionStr = maxVersion.utf8(rt); return restoreBackupInternal(rt, backupSecretStr, backupIDStr, maxVersionStr); } jsi::Value CommCoreModule::restoreBackupData( jsi::Runtime &rt, jsi::String backupID, jsi::String backupDataKey, jsi::String backupLogDataKey, jsi::String maxVersion) { std::string backupIDStr = backupID.utf8(rt); std::string backupDataKeyStr = backupDataKey.utf8(rt); std::string backupLogDataKeyStr = backupLogDataKey.utf8(rt); std::string maxVersionStr = maxVersion.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { auto currentID = RustPromiseManager::instance.addPromise( {promise, this->jsInvoker_, innerRt}); ::restoreBackupData( rust::string(backupIDStr), rust::string(backupDataKeyStr), rust::string(backupLogDataKeyStr), rust::string(maxVersionStr), currentID); }); } jsi::Value CommCoreModule::retrieveBackupKeys(jsi::Runtime &rt, jsi::String backupSecret) { std::string backupSecretStr = backupSecret.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { auto currentID = RustPromiseManager::instance.addPromise( {promise, this->jsInvoker_, innerRt}); ::retrieveBackupKeys(rust::string(backupSecretStr), currentID); }); } jsi::Value CommCoreModule::retrieveLatestSIWEBackupData(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { auto currentID = RustPromiseManager::instance.addPromise( {promise, this->jsInvoker_, innerRt}); ::retrieveLatestSIWEBackupData(currentID); }); } jsi::Value CommCoreModule::setSIWEBackupSecrets( jsi::Runtime &rt, jsi::Object siweBackupSecrets) { std::string message = siweBackupSecrets.getProperty(rt, "message").asString(rt).utf8(rt); std::string signature = siweBackupSecrets.getProperty(rt, "signature").asString(rt).utf8(rt); folly::dynamic backupSecretsJSON = folly::dynamic::object("message", message)("signature", signature); std::string backupSecrets = folly::toJson(backupSecretsJSON); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [this, promise, backupSecrets]() { std::string error; try { DatabaseManager::getQueryExecutor().setMetadata( "siweBackupSecrets", backupSecrets); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::getSIWEBackupSecrets(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [this](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [this, &innerRt, promise]() { std::string error; std::string backupSecrets; try { backupSecrets = DatabaseManager::getQueryExecutor().getMetadata( "siweBackupSecrets"); } catch (const std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync( [&innerRt, error, backupSecrets, promise]() { if (error.size()) { promise->reject(error); } else if (!backupSecrets.size()) { promise->resolve(jsi::Value::undefined()); } else { folly::dynamic backupSecretsJSON = folly::parseJson(backupSecrets); std::string message = backupSecretsJSON["message"].asString(); std::string signature = backupSecretsJSON["signature"].asString(); auto siweBackupSecrets = jsi::Object(innerRt); siweBackupSecrets.setProperty( innerRt, "message", jsi::String::createFromUtf8(innerRt, message)); siweBackupSecrets.setProperty( innerRt, "signature", jsi::String::createFromUtf8(innerRt, signature)); promise->resolve(std::move(siweBackupSecrets)); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::getAllInboundP2PMessages(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::vector messages; try { messages = DatabaseManager::getQueryExecutor().getAllInboundP2PMessage(); } catch (std::system_error &e) { error = e.what(); } auto messagesPtr = std::make_shared>( std::move(messages)); this->jsInvoker_->invokeAsync( [&innerRt, messagesPtr, error, promise]() { if (error.size()) { promise->reject(error); return; } jsi::Array jsiMessages = jsi::Array(innerRt, messagesPtr->size()); size_t writeIdx = 0; for (const InboundP2PMessage &msg : *messagesPtr) { jsi::Object jsiMsg = jsi::Object(innerRt); jsiMsg.setProperty(innerRt, "messageID", msg.message_id); jsiMsg.setProperty( innerRt, "senderDeviceID", msg.sender_device_id); jsiMsg.setProperty(innerRt, "plaintext", msg.plaintext); jsiMsg.setProperty(innerRt, "status", msg.status); jsiMessages.setValueAtIndex(innerRt, writeIdx++, jsiMsg); } promise->resolve(std::move(jsiMessages)); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::removeInboundP2PMessages(jsi::Runtime &rt, jsi::Array ids) { std::vector msgIDsCPP{}; for (auto idx = 0; idx < ids.size(rt); idx++) { std::string msgID = ids.getValueAtIndex(rt, idx).asString(rt).utf8(rt); msgIDsCPP.push_back(msgID); } return createPromiseAsJSIValue( rt, [this, msgIDsCPP](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [this, promise, msgIDsCPP]() { std::string error; try { DatabaseManager::getQueryExecutor().removeInboundP2PMessages( msgIDsCPP); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::getOutboundP2PMessagesByID(jsi::Runtime &rt, jsi::Array ids) { std::vector msgIDsCPP{}; for (auto idx = 0; idx < ids.size(rt); idx++) { std::string msgID = ids.getValueAtIndex(rt, idx).asString(rt).utf8(rt); msgIDsCPP.push_back(msgID); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::vector messages; try { messages = DatabaseManager::getQueryExecutor().getOutboundP2PMessagesByID( msgIDsCPP); } catch (std::system_error &e) { error = e.what(); } auto messagesPtr = std::make_shared>( std::move(messages)); this->jsInvoker_->invokeAsync( [&innerRt, messagesPtr, error, promise]() { if (error.size()) { promise->reject(error); return; } jsi::Array jsiMessages = jsi::Array(innerRt, messagesPtr->size()); size_t writeIdx = 0; for (const OutboundP2PMessage &msg : *messagesPtr) { jsi::Object jsiMsg = jsi::Object(innerRt); jsiMsg.setProperty(innerRt, "messageID", msg.message_id); jsiMsg.setProperty(innerRt, "deviceID", msg.device_id); jsiMsg.setProperty(innerRt, "userID", msg.user_id); jsiMsg.setProperty(innerRt, "timestamp", msg.timestamp); jsiMsg.setProperty(innerRt, "plaintext", msg.plaintext); jsiMsg.setProperty(innerRt, "ciphertext", msg.ciphertext); jsiMsg.setProperty(innerRt, "status", msg.status); jsiMsg.setProperty( innerRt, "supportsAutoRetry", msg.supports_auto_retry); jsiMessages.setValueAtIndex(innerRt, writeIdx++, jsiMsg); } promise->resolve(std::move(jsiMessages)); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::getAllOutboundP2PMessages(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::vector messages; try { messages = DatabaseManager::getQueryExecutor().getAllOutboundP2PMessages(); } catch (std::system_error &e) { error = e.what(); } auto messagesPtr = std::make_shared>( std::move(messages)); this->jsInvoker_->invokeAsync( [&innerRt, messagesPtr, error, promise]() { if (error.size()) { promise->reject(error); return; } jsi::Array jsiMessages = jsi::Array(innerRt, messagesPtr->size()); size_t writeIdx = 0; for (const OutboundP2PMessage &msg : *messagesPtr) { jsi::Object jsiMsg = jsi::Object(innerRt); jsiMsg.setProperty(innerRt, "messageID", msg.message_id); jsiMsg.setProperty(innerRt, "deviceID", msg.device_id); jsiMsg.setProperty(innerRt, "userID", msg.user_id); jsiMsg.setProperty(innerRt, "timestamp", msg.timestamp); jsiMsg.setProperty(innerRt, "plaintext", msg.plaintext); jsiMsg.setProperty(innerRt, "ciphertext", msg.ciphertext); jsiMsg.setProperty(innerRt, "status", msg.status); jsiMsg.setProperty( innerRt, "supportsAutoRetry", msg.supports_auto_retry); jsiMessages.setValueAtIndex(innerRt, writeIdx++, jsiMsg); } promise->resolve(std::move(jsiMessages)); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::markOutboundP2PMessageAsSent( jsi::Runtime &rt, jsi::String messageID, jsi::String deviceID) { auto messageIDCpp{messageID.utf8(rt)}; auto deviceIDCpp{deviceID.utf8(rt)}; return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { std::string error; try { DatabaseManager::getQueryExecutor().markOutboundP2PMessageAsSent( messageIDCpp, deviceIDCpp); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::removeOutboundP2PMessagesOlderThan( jsi::Runtime &rt, jsi::String messageID, jsi::String deviceID) { auto messageIDCpp{messageID.utf8(rt)}; auto deviceIDCpp{deviceID.utf8(rt)}; return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=]() { std::string error; try { DatabaseManager::getQueryExecutor() .removeOutboundP2PMessagesOlderThan(messageIDCpp, deviceIDCpp); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([error, promise]() { if (error.size()) { promise->reject(error); } else { promise->resolve(jsi::Value::undefined()); } }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::resetOutboundP2PMessagesForDevice( jsi::Runtime &rt, jsi::String deviceID) { std::string deviceIDCpp{deviceID.utf8(rt)}; return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::vector messageIDs; try { DatabaseManager::getQueryExecutor().beginTransaction(); messageIDs = DatabaseManager::getQueryExecutor() .resetOutboundP2PMessagesForDevice(deviceIDCpp); DatabaseManager::getQueryExecutor().commitTransaction(); } catch (std::system_error &e) { error = e.what(); DatabaseManager::getQueryExecutor().rollbackTransaction(); } auto messageIDsPtr = std::make_shared>(std::move(messageIDs)); this->jsInvoker_->invokeAsync( [&innerRt, messageIDsPtr, error, promise]() { if (error.size()) { promise->reject(error); return; } jsi::Array jsiMessageIDs = jsi::Array(innerRt, messageIDsPtr->size()); size_t writeIdx = 0; for (const std::string &id : *messageIDsPtr) { jsi::String jsiString = jsi::String::createFromUtf8(innerRt, id); jsiMessageIDs.setValueAtIndex(innerRt, writeIdx++, jsiString); } promise->resolve(std::move(jsiMessageIDs)); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::getSyncedDatabaseVersion(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::vector syncedMetadataStoreVector; try { syncedMetadataStoreVector = DatabaseManager::getQueryExecutor().getAllSyncedMetadata(); } catch (std::system_error &e) { error = e.what(); } std::string version; for (auto &entry : syncedMetadataStoreVector) { if (entry.name == "db_version") { version = entry.data; } } this->jsInvoker_->invokeAsync([&innerRt, error, promise, version]() { if (error.size()) { promise->reject(error); return; } jsi::String jsiVersion = jsi::String::createFromUtf8(innerRt, version); promise->resolve(std::move(jsiVersion)); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::markPrekeysAsPublished(jsi::Runtime &rt) { return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; if (this->contentCryptoModule == nullptr || !NotificationsCryptoModule::isNotificationsAccountInitialized()) { this->jsInvoker_->invokeAsync([=, &innerRt]() { promise->reject("user has not been initialized"); }); return; } std::optional< std::pair, std::string>> notifsCryptoModuleWithPicklingKey; try { notifsCryptoModuleWithPicklingKey = NotificationsCryptoModule::fetchNotificationsAccount(); this->contentCryptoModule->markPrekeyAsPublished(); notifsCryptoModuleWithPicklingKey.value() .first->markPrekeyAsPublished(); this->persistCryptoModules(true, notifsCryptoModuleWithPicklingKey); } catch (std::exception &e) { error = e.what(); } this->jsInvoker_->invokeAsync([=]() { if (error.size()) { promise->reject(error); return; } promise->resolve(jsi::Value::undefined()); }); }; this->cryptoThread->scheduleTask(job); }); } jsi::Value CommCoreModule::getRelatedMessages(jsi::Runtime &rt, jsi::String messageID) { std::string messageIDStr = messageID.utf8(rt); return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::shared_ptr> messages; try { messages = std::make_shared>( DatabaseManager::getQueryExecutor().getRelatedMessages( messageIDStr)); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([&innerRt, error, promise, messages, messageStore = this->messageStore]() { if (error.size()) { promise->reject(error); return; } jsi::Array jsiMessages = messageStore.parseDBDataStore(innerRt, messages); promise->resolve(std::move(jsiMessages)); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); } jsi::Value CommCoreModule::searchMessages( jsi::Runtime &rt, jsi::String query, jsi::String threadID, std::optional timestampCursor, std::optional messageIDCursor) { std::string queryStr = query.utf8(rt); std::string threadIDStr = threadID.utf8(rt); std::optional timestampCursorCpp; if (timestampCursor) { timestampCursorCpp = timestampCursor->utf8(rt); } std::optional messageIDCursorCpp; if (messageIDCursor) { messageIDCursorCpp = messageIDCursor->utf8(rt); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { taskType job = [=, &innerRt]() { std::string error; std::shared_ptr> messages; try { messages = std::make_shared>( DatabaseManager::getQueryExecutor().searchMessages( queryStr, threadIDStr, timestampCursorCpp, messageIDCursorCpp)); } catch (std::system_error &e) { error = e.what(); } this->jsInvoker_->invokeAsync([&innerRt, error, promise, messages, messageStore = this->messageStore]() { if (error.size()) { promise->reject(error); return; } jsi::Array jsiMessages = messageStore.parseDBDataStore(innerRt, messages); promise->resolve(std::move(jsiMessages)); }); }; GlobalDBSingleton::instance.scheduleOrRunCancellable( job, promise, this->jsInvoker_); }); }; } // namespace comm diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js index 9bdee7df3..7d3ed5e2d 100644 --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -1,1011 +1,1028 @@ // @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/request-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); } 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); } 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); } + 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, messageID: string, ): Promise { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const olmSession = cryptoStore.contentSessions[deviceID]; if (!olmSession) { throw new Error(olmSessionErrors.sessionDoesNotExists); } + 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, 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, };