diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -146,6 +146,11 @@ +initializeCryptoAccount: () => Promise, +getUserPublicKey: () => Promise, +encrypt: (content: string, deviceID: string) => Promise, + +encryptAndPersist: ( + content: string, + deviceID: string, + messageID: string, + ) => Promise, +decrypt: (encryptedData: EncryptedData, deviceID: string) => Promise, +decryptSequentialAndPersist: ( encryptedData: EncryptedData, diff --git a/lib/utils/__mocks__/config.js b/lib/utils/__mocks__/config.js --- a/lib/utils/__mocks__/config.js +++ b/lib/utils/__mocks__/config.js @@ -16,6 +16,7 @@ initializeCryptoAccount: jest.fn(), getUserPublicKey: jest.fn(), encrypt: jest.fn(), + encryptAndPersist: jest.fn(), decrypt: jest.fn(), decryptSequentialAndPersist: jest.fn(), contentInboundSessionCreator: jest.fn(), diff --git a/native/cpp/CommonCpp/NativeModules/CommCoreModule.h b/native/cpp/CommonCpp/NativeModules/CommCoreModule.h --- a/native/cpp/CommonCpp/NativeModules/CommCoreModule.h +++ b/native/cpp/CommonCpp/NativeModules/CommCoreModule.h @@ -129,6 +129,11 @@ bool overwrite) override; virtual jsi::Value encrypt(jsi::Runtime &rt, jsi::String message, jsi::String deviceID) override; + virtual jsi::Value encryptAndPersist( + jsi::Runtime &rt, + jsi::String message, + jsi::String deviceID, + jsi::String messageID) override; virtual jsi::Value decrypt( jsi::Runtime &rt, jsi::Object encryptedDataJSI, diff --git a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp --- a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp +++ b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp @@ -1389,6 +1389,86 @@ }); } +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 = jsi::Object(innerRt); + auto message = std::string{ + encryptedMessage.message.begin(), + encryptedMessage.message.end()}; + auto messageJSI = jsi::String::createFromUtf8(innerRt, message); + encryptedDataJSI.setProperty(innerRt, "message", messageJSI); + encryptedDataJSI.setProperty( + innerRt, + "messageType", + static_cast(encryptedMessage.messageType)); + promise->resolve(std::move(encryptedDataJSI)); + }); + }; + this->cryptoThread->scheduleTask(job); + }); +} + jsi::Value CommCoreModule::decrypt( jsi::Runtime &rt, jsi::Object encryptedDataJSI, diff --git a/native/cpp/CommonCpp/_generated/commJSI-generated.cpp b/native/cpp/CommonCpp/_generated/commJSI-generated.cpp --- a/native/cpp/CommonCpp/_generated/commJSI-generated.cpp +++ b/native/cpp/CommonCpp/_generated/commJSI-generated.cpp @@ -87,6 +87,9 @@ static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_encrypt(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { return static_cast(&turboModule)->encrypt(rt, args[0].asString(rt), args[1].asString(rt)); } +static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_encryptAndPersist(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->encryptAndPersist(rt, args[0].asString(rt), args[1].asString(rt), args[2].asString(rt)); +} static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_decrypt(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { return static_cast(&turboModule)->decrypt(rt, args[0].asObject(rt), args[1].asString(rt)); } @@ -226,6 +229,7 @@ methodMap_["initializeContentOutboundSession"] = MethodMetadata {5, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeContentOutboundSession}; methodMap_["initializeContentInboundSession"] = MethodMetadata {5, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeContentInboundSession}; methodMap_["encrypt"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_encrypt}; + methodMap_["encryptAndPersist"] = MethodMetadata {3, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_encryptAndPersist}; methodMap_["decrypt"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_decrypt}; methodMap_["decryptSequentialAndPersist"] = MethodMetadata {3, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_decryptSequentialAndPersist}; methodMap_["signMessage"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_signMessage}; diff --git a/native/cpp/CommonCpp/_generated/commJSI.h b/native/cpp/CommonCpp/_generated/commJSI.h --- a/native/cpp/CommonCpp/_generated/commJSI.h +++ b/native/cpp/CommonCpp/_generated/commJSI.h @@ -44,6 +44,7 @@ virtual jsi::Value initializeContentOutboundSession(jsi::Runtime &rt, jsi::String identityKeys, jsi::String prekey, jsi::String prekeySignature, jsi::String oneTimeKey, jsi::String deviceID) = 0; virtual jsi::Value initializeContentInboundSession(jsi::Runtime &rt, jsi::String identityKeys, jsi::Object encryptedContent, jsi::String deviceID, double sessionVersion, bool overwrite) = 0; virtual jsi::Value encrypt(jsi::Runtime &rt, jsi::String message, jsi::String deviceID) = 0; + virtual jsi::Value encryptAndPersist(jsi::Runtime &rt, jsi::String message, jsi::String deviceID, jsi::String messageID) = 0; virtual jsi::Value decrypt(jsi::Runtime &rt, jsi::Object encryptedData, jsi::String deviceID) = 0; virtual jsi::Value decryptSequentialAndPersist(jsi::Runtime &rt, jsi::Object encryptedData, jsi::String deviceID, jsi::String messageID) = 0; virtual jsi::Value signMessage(jsi::Runtime &rt, jsi::String message) = 0; @@ -293,6 +294,14 @@ return bridging::callFromJs( rt, &T::encrypt, jsInvoker_, instance_, std::move(message), std::move(deviceID)); } + jsi::Value encryptAndPersist(jsi::Runtime &rt, jsi::String message, jsi::String deviceID, jsi::String messageID) override { + static_assert( + bridging::getParameterCount(&T::encryptAndPersist) == 4, + "Expected encryptAndPersist(...) to have 4 parameters"); + + return bridging::callFromJs( + rt, &T::encryptAndPersist, jsInvoker_, instance_, std::move(message), std::move(deviceID), std::move(messageID)); + } jsi::Value decrypt(jsi::Runtime &rt, jsi::Object encryptedData, jsi::String deviceID) override { static_assert( bridging::getParameterCount(&T::decrypt) == 3, diff --git a/native/crypto/olm-api.js b/native/crypto/olm-api.js --- a/native/crypto/olm-api.js +++ b/native/crypto/olm-api.js @@ -20,6 +20,7 @@ }, getUserPublicKey: commCoreModule.getUserPublicKey, encrypt: commCoreModule.encrypt, + encryptAndPersist: commCoreModule.encryptAndPersist, decrypt: commCoreModule.decrypt, decryptSequentialAndPersist: commCoreModule.decryptSequentialAndPersist, async contentInboundSessionCreator( diff --git a/native/schema/CommCoreModuleSchema.js b/native/schema/CommCoreModuleSchema.js --- a/native/schema/CommCoreModuleSchema.js +++ b/native/schema/CommCoreModuleSchema.js @@ -92,6 +92,11 @@ overwrite: boolean, ) => Promise; +encrypt: (message: string, deviceID: string) => Promise; + +encryptAndPersist: ( + message: string, + deviceID: string, + messageID: string, + ) => Promise; +decrypt: (encryptedData: Object, deviceID: string) => Promise; +decryptSequentialAndPersist: ( encryptedData: Object, diff --git a/web/crypto/olm-api.js b/web/crypto/olm-api.js --- a/web/crypto/olm-api.js +++ b/web/crypto/olm-api.js @@ -45,6 +45,7 @@ }, getUserPublicKey: proxyToWorker('getUserPublicKey'), encrypt: proxyToWorker('encrypt'), + encryptAndPersist: proxyToWorker('encryptAndPersist'), decrypt: proxyToWorker('decrypt'), decryptSequentialAndPersist: proxyToWorker('decryptSequentialAndPersist'), contentInboundSessionCreator: proxyToWorker('contentInboundSessionCreator'), diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -443,6 +443,50 @@ messageType: encryptedContent.type, }; }, + 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(`No session for deviceID: ${deviceID}`); + } + + 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, + }; + + sqliteQueryExecutor.beginTransaction(); + try { + sqliteQueryExecutor.setCiphertextForOutboundP2PMessage( + messageID, + deviceID, + JSON.stringify(result), + ); + persistCryptoStore(true); + sqliteQueryExecutor.commitTransaction(); + } catch (e) { + sqliteQueryExecutor.rollbackTransaction(); + throw e; + } + + return result; + }, async decrypt( encryptedData: EncryptedData, deviceID: string,