diff --git a/native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp b/native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp index 4fc3563fe..a7594ec5d 100644 --- a/native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp +++ b/native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp @@ -1,409 +1,429 @@ #include "CryptoModule.h" #include "Logger.h" #include "PlatformSpecificTools.h" #include "olm/account.hh" #include "olm/session.hh" #include namespace comm { namespace crypto { 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); } } void CryptoModule::createAccount() { this->accountBuffer.resize(::olm_account_size()); this->account = ::olm_account(this->accountBuffer.data()); size_t randomSize = ::olm_create_account_random_length(this->account); OlmBuffer randomBuffer; PlatformSpecificTools::generateSecureRandomBytes(randomBuffer, randomSize); if (-1 == ::olm_create_account(this->account, randomBuffer.data(), randomSize)) { throw std::runtime_error{ "error createAccount => " + std::string{::olm_account_last_error(this->account)}}; }; } void CryptoModule::exposePublicIdentityKeys() { size_t identityKeysSize = ::olm_account_identity_keys_length(this->account); if (this->keys.identityKeys.size() == identityKeysSize) { return; } this->keys.identityKeys.resize( ::olm_account_identity_keys_length(this->account)); if (-1 == ::olm_account_identity_keys( this->account, this->keys.identityKeys.data(), this->keys.identityKeys.size())) { throw std::runtime_error{ "error generateIdentityKeys => " + std::string{::olm_account_last_error(this->account)}}; } } void CryptoModule::generateOneTimeKeys(size_t oneTimeKeysAmount) { size_t oneTimeKeysSize = ::olm_account_generate_one_time_keys_random_length( this->account, oneTimeKeysAmount); if (this->keys.oneTimeKeys.size() == oneTimeKeysSize) { return; } OlmBuffer random; PlatformSpecificTools::generateSecureRandomBytes(random, oneTimeKeysSize); if (-1 == ::olm_account_generate_one_time_keys( this->account, oneTimeKeysAmount, random.data(), random.size())) { throw std::runtime_error{ "error generateOneTimeKeys => " + std::string{::olm_account_last_error(this->account)}}; } } // returns number of published keys size_t CryptoModule::publishOneTimeKeys() { this->keys.oneTimeKeys.resize( ::olm_account_one_time_keys_length(this->account)); if (-1 == ::olm_account_one_time_keys( this->account, this->keys.oneTimeKeys.data(), this->keys.oneTimeKeys.size())) { throw std::runtime_error{ "error publishOneTimeKeys => " + std::string{::olm_account_last_error(this->account)}}; } return ::olm_account_mark_keys_as_published(this->account); } 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::getOneTimeKeys(size_t oneTimeKeysAmount) { this->generateOneTimeKeys(oneTimeKeysAmount); size_t publishedOneTimeKeys = this->publishOneTimeKeys(); if (publishedOneTimeKeys != oneTimeKeysAmount) { throw std::runtime_error{ "error generateKeys => invalid amount of one-time keys published. " "Expected " + std::to_string(oneTimeKeysAmount) + ", got " + std::to_string(publishedOneTimeKeys)}; } return std::string{ this->keys.oneTimeKeys.begin(), this->keys.oneTimeKeys.end()}; } std::uint8_t CryptoModule::getNumPrekeys() { return reinterpret_cast(this->account)->num_prekeys; } std::string CryptoModule::getPrekey() { OlmBuffer prekey; prekey.resize(::olm_account_prekey_length(this->account)); if (-1 == ::olm_account_prekey(this->account, prekey.data(), prekey.size())) { throw std::runtime_error{ "error getPrekey => " + std::string{::olm_account_last_error(this->account)}}; } return std::string{std::string{prekey.begin(), prekey.end()}}; } std::string CryptoModule::getPrekeySignature() { size_t signatureSize = ::olm_account_signature_length(this->account); OlmBuffer signatureBuffer; signatureBuffer.resize(signatureSize); if (-1 == ::olm_account_prekey_signature(this->account, signatureBuffer.data())) { throw std::runtime_error{ "error getPrekeySignature => " + std::string{::olm_account_last_error(this->account)}}; } return std::string{signatureBuffer.begin(), signatureBuffer.end()}; } folly::Optional CryptoModule::getUnpublishedPrekey() { OlmBuffer prekey; prekey.resize(::olm_account_prekey_length(this->account)); std::size_t retval = ::olm_account_unpublished_prekey( this->account, prekey.data(), prekey.size()); if (0 == retval) { return folly::none; } else if (-1 == retval) { throw std::runtime_error{ "error getUnpublishedPrekey => " + std::string{::olm_account_last_error(this->account)}}; } return std::string{prekey.begin(), prekey.end()}; } std::string CryptoModule::generateAndGetPrekey() { size_t prekeySize = ::olm_account_generate_prekey_random_length(this->account); OlmBuffer random; PlatformSpecificTools::generateSecureRandomBytes(random, prekeySize); if (-1 == ::olm_account_generate_prekey( this->account, random.data(), random.size())) { throw std::runtime_error{ "error generateAndGetPrekey => " + std::string{::olm_account_last_error(this->account)}}; } OlmBuffer prekey; prekey.resize(::olm_account_prekey_length(this->account)); if (-1 == ::olm_account_prekey(this->account, prekey.data(), prekey.size())) { throw std::runtime_error{ "error generateAndGetPrekey => " + std::string{::olm_account_last_error(this->account)}}; } return std::string{prekey.begin(), prekey.end()}; } void CryptoModule::markPrekeyAsPublished() { ::olm_account_mark_prekey_as_published(this->account); } void CryptoModule::forgetOldPrekey() { ::olm_account_forget_old_prekey(this->account); } void CryptoModule::initializeInboundForReceivingSession( const std::string &targetUserId, const OlmBuffer &encryptedMessage, const OlmBuffer &idKeys, const bool overwrite) { if (this->hasSessionFor(targetUserId)) { if (overwrite) { this->sessions.erase(this->sessions.find(targetUserId)); } else { throw std::runtime_error{ "error initializeInboundForReceivingSession => session already " "initialized"}; } } std::unique_ptr newSession = Session::createSessionAsResponder( this->account, this->keys.identityKeys.data(), encryptedMessage, idKeys); this->sessions.insert(make_pair(targetUserId, std::move(newSession))); } void CryptoModule::initializeOutboundForSendingSession( const std::string &targetUserId, const OlmBuffer &idKeys, const OlmBuffer &preKeys, const OlmBuffer &preKeySignature, const OlmBuffer &oneTimeKeys, size_t keyIndex) { if (this->hasSessionFor(targetUserId)) { Logger::log( "olm session overwritten for the user with id: " + targetUserId); this->sessions.erase(this->sessions.find(targetUserId)); } std::unique_ptr newSession = Session::createSessionAsInitializer( this->account, this->keys.identityKeys.data(), idKeys, preKeys, preKeySignature, oneTimeKeys, keyIndex); this->sessions.insert(make_pair(targetUserId, std::move(newSession))); } bool CryptoModule::hasSessionFor(const std::string &targetUserId) { return (this->sessions.find(targetUserId) != this->sessions.end()); } std::shared_ptr CryptoModule::getSessionByUserId(const std::string &userId) { return this->sessions.at(userId); } Persist CryptoModule::storeAsB64(const std::string &secretKey) { Persist persist; size_t accountPickleLength = ::olm_pickle_account_length(this->account); OlmBuffer accountPickleBuffer(accountPickleLength); if (accountPickleLength != ::olm_pickle_account( this->account, secretKey.data(), secretKey.size(), accountPickleBuffer.data(), accountPickleLength)) { throw std::runtime_error{ "error storeAsB64 => " + std::string{::olm_account_last_error(this->account)}}; } persist.account = accountPickleBuffer; std::unordered_map>::iterator it; for (it = this->sessions.begin(); it != this->sessions.end(); ++it) { OlmBuffer buffer = it->second->storeAsB64(secretKey); persist.sessions.insert(make_pair(it->first, buffer)); } return persist; } void CryptoModule::restoreFromB64( const std::string &secretKey, Persist persist) { this->accountBuffer.resize(::olm_account_size()); this->account = ::olm_account(this->accountBuffer.data()); if (-1 == ::olm_unpickle_account( this->account, secretKey.data(), secretKey.size(), persist.account.data(), persist.account.size())) { throw std::runtime_error{ "error restoreFromB64 => " + std::string{::olm_account_last_error(this->account)}}; } std::unordered_map::iterator it; for (it = persist.sessions.begin(); it != persist.sessions.end(); ++it) { std::unique_ptr session = session->restoreFromB64( this->account, this->keys.identityKeys.data(), secretKey, it->second); this->sessions.insert(make_pair(it->first, move(session))); } } EncryptedData CryptoModule::encrypt( const std::string &targetUserId, const std::string &content) { if (!this->hasSessionFor(targetUserId)) { throw std::runtime_error{"error encrypt => uninitialized session"}; } OlmSession *session = this->sessions.at(targetUserId)->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}; } std::string CryptoModule::decrypt( const std::string &targetUserId, - EncryptedData encryptedData) { + EncryptedData &encryptedData) { if (!this->hasSessionFor(targetUserId)) { throw std::runtime_error{"error decrypt => uninitialized session"}; } OlmSession *session = this->sessions.at(targetUserId)->getOlmSession(); - OlmBuffer tmpEncryptedMessage(encryptedData.message); + 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)}}; + "error decrypt => " + std::string{::olm_session_last_error(session)} + + ". Hash: " + + std::string{messageHashBuffer.begin(), messageHashBuffer.end()}}; } return std::string{(char *)decryptedMessage.data(), decryptedSize}; } std::string CryptoModule::signMessage(const std::string &message) { OlmBuffer signature; signature.resize(::olm_account_signature_length(this->account)); size_t signatureLength = ::olm_account_sign( this->account, (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->account)}}; } 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)}}; } } } // namespace crypto } // namespace comm diff --git a/native/cpp/CommonCpp/CryptoTools/CryptoModule.h b/native/cpp/CommonCpp/CryptoTools/CryptoModule.h index 7df059a40..2d1d0b5a1 100644 --- a/native/cpp/CommonCpp/CryptoTools/CryptoModule.h +++ b/native/cpp/CommonCpp/CryptoTools/CryptoModule.h @@ -1,84 +1,84 @@ #pragma once #include #include #include #include "folly/Optional.h" #include "olm/olm.h" #include "Persist.h" #include "Session.h" #include "Tools.h" namespace comm { namespace crypto { class CryptoModule { OlmAccount *account = nullptr; OlmBuffer accountBuffer; std::unordered_map> sessions = {}; Keys keys; void createAccount(); void exposePublicIdentityKeys(); void generateOneTimeKeys(size_t oneTimeKeysAmount); // returns number of published keys size_t publishOneTimeKeys(); public: const std::string id; CryptoModule(std::string id); CryptoModule(std::string id, std::string secretKey, Persist persist); static Keys keysFromStrings( const std::string &identityKeys, const std::string &oneTimeKeys); std::string getIdentityKeys(); std::string getOneTimeKeys(size_t oneTimeKeysAmount = 50); // Prekey rotation methods for X3DH std::uint8_t getNumPrekeys(); std::string getPrekey(); std::string getPrekeySignature(); folly::Optional getUnpublishedPrekey(); std::string generateAndGetPrekey(); void markPrekeyAsPublished(); void forgetOldPrekey(); void initializeInboundForReceivingSession( const std::string &targetUserId, const OlmBuffer &encryptedMessage, const OlmBuffer &idKeys, const bool overwrite = false); void initializeOutboundForSendingSession( const std::string &targetUserId, const OlmBuffer &idKeys, const OlmBuffer &preKeys, const OlmBuffer &preKeySignature, const OlmBuffer &oneTimeKeys, size_t keyIndex = 0); bool hasSessionFor(const std::string &targetUserId); std::shared_ptr getSessionByUserId(const std::string &userId); Persist storeAsB64(const std::string &secretKey); void restoreFromB64(const std::string &secretKey, Persist persist); EncryptedData encrypt(const std::string &targetUserId, const std::string &content); std::string - decrypt(const std::string &targetUserId, EncryptedData encryptedData); + decrypt(const std::string &targetUserId, EncryptedData &encryptedData); std::string signMessage(const std::string &message); static void verifySignature( const std::string &publicKey, const std::string &message, const std::string &signature); }; } // namespace crypto } // namespace comm diff --git a/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.cpp b/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.cpp index ee0320af3..a53dbf69d 100644 --- a/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.cpp +++ b/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.cpp @@ -1,284 +1,286 @@ #include "NotificationsCryptoModule.h" #include "../../CryptoTools/Persist.h" #include "../../CryptoTools/Tools.h" #include "../../Tools/CommSecureStore.h" #include "../../Tools/PlatformSpecificTools.h" #include #include #include #include #include #include #include namespace comm { const std::string NotificationsCryptoModule::secureStoreNotificationsAccountDataKey = "notificationsCryptoAccountDataKey"; const std::string NotificationsCryptoModule::notificationsCryptoAccountID = "notificationsCryptoAccountDataID"; const std::string NotificationsCryptoModule::keyserverHostedNotificationsID = "keyserverHostedNotificationsID"; const std::string NotificationsCryptoModule::initialEncryptedMessageContent = "{\"type\": \"init\"}"; const int NotificationsCryptoModule::olmEncryptedTypeMessage = 1; const int temporaryFilePathRandomSuffixLength = 32; crypto::CryptoModule NotificationsCryptoModule::deserializeCryptoModule( const std::string &path, const std::string &picklingKey) { std::ifstream pickledPersistStream(path, std::ifstream::in); if (!pickledPersistStream.good()) { throw std::runtime_error( "Attempt to deserialize non-existing notifications crypto account"); } std::stringstream pickledPersistStringStream; pickledPersistStringStream << pickledPersistStream.rdbuf(); pickledPersistStream.close(); std::string pickledPersist = pickledPersistStringStream.str(); folly::dynamic persistJSON; try { persistJSON = folly::parseJson(pickledPersist); } catch (const folly::json::parse_error &e) { throw std::runtime_error( "Notifications crypto account JSON deserialization failed with " "reason: " + std::string(e.what())); } std::string accountString = persistJSON["account"].asString(); crypto::OlmBuffer account = std::vector(accountString.begin(), accountString.end()); std::unordered_map sessions; if (persistJSON["sessions"].isNull()) { return crypto::CryptoModule{ notificationsCryptoAccountID, picklingKey, {account, sessions}}; } for (auto &sessionKeyValuePair : persistJSON["sessions"].items()) { std::string targetUserID = sessionKeyValuePair.first.asString(); std::string sessionData = sessionKeyValuePair.second.asString(); sessions[targetUserID] = std::vector(sessionData.begin(), sessionData.end()); } return crypto::CryptoModule{ notificationsCryptoAccountID, picklingKey, {account, sessions}}; } void NotificationsCryptoModule::serializeAndFlushCryptoModule( crypto::CryptoModule &cryptoModule, const std::string &path, const std::string &picklingKey, const std::string &callingProcessName) { crypto::Persist persist = cryptoModule.storeAsB64(picklingKey); folly::dynamic sessions = folly::dynamic::object; for (auto &sessionKeyValuePair : persist.sessions) { std::string targetUserID = sessionKeyValuePair.first; crypto::OlmBuffer sessionData = sessionKeyValuePair.second; sessions[targetUserID] = std::string(sessionData.begin(), sessionData.end()); } std::string account = std::string(persist.account.begin(), persist.account.end()); folly::dynamic persistJSON = folly::dynamic::object("account", account)("sessions", sessions); std::string pickledPersist = folly::toJson(persistJSON); std::string temporaryFilePathRandomSuffix = crypto::Tools::generateRandomHexString( temporaryFilePathRandomSuffixLength); std::string temporaryPath = path + callingProcessName + temporaryFilePathRandomSuffix; mode_t readWritePermissionsMode = 0666; int temporaryFD = open(temporaryPath.c_str(), O_CREAT | O_WRONLY, readWritePermissionsMode); if (temporaryFD == -1) { throw std::runtime_error( "Failed to create temporary file. Unable to atomically update " "notifications crypto account. Details: " + std::string(strerror(errno))); } ssize_t bytesWritten = write(temporaryFD, pickledPersist.c_str(), pickledPersist.length()); if (bytesWritten == -1 || bytesWritten != pickledPersist.length()) { remove(temporaryPath.c_str()); throw std::runtime_error( "Failed to write all data to temporary file. Unable to atomically " "update notifications crypto account. Details: " + std::string(strerror(errno))); } if (fsync(temporaryFD) == -1) { remove(temporaryPath.c_str()); throw std::runtime_error( "Failed to synchronize temporary file data with hardware storage. " "Unable to atomically update notifications crypto account. Details: " + std::string(strerror(errno))); }; close(temporaryFD); if (rename(temporaryPath.c_str(), path.c_str()) == -1) { remove(temporaryPath.c_str()); throw std::runtime_error( "Failed to replace temporary file content with notifications crypto " "account. Unable to atomically update notifications crypto account. " "Details: " + std::string(strerror(errno))); } remove(temporaryPath.c_str()); } void NotificationsCryptoModule::callCryptoModule( std::function caller, const std::string &callingProcessName) { CommSecureStore secureStore{}; folly::Optional picklingKey = secureStore.get( NotificationsCryptoModule::secureStoreNotificationsAccountDataKey); if (!picklingKey.hasValue()) { throw std::runtime_error( "Attempt to retrieve notifications crypto account before it was " "correctly initialized."); } const std::string path = PlatformSpecificTools::getNotificationsCryptoAccountPath(); crypto::CryptoModule cryptoModule = NotificationsCryptoModule::deserializeCryptoModule( path, picklingKey.value()); caller(cryptoModule); NotificationsCryptoModule::serializeAndFlushCryptoModule( cryptoModule, path, picklingKey.value(), callingProcessName); } void NotificationsCryptoModule::initializeNotificationsCryptoAccount( const std::string &callingProcessName) { const std::string notificationsCryptoAccountPath = PlatformSpecificTools::getNotificationsCryptoAccountPath(); std::ifstream notificationCryptoAccountCheck(notificationsCryptoAccountPath); if (notificationCryptoAccountCheck.good()) { // Implemented in CommmCoreModule semantics regarding public olm account // initialization is idempotent. We should follow the same approach when it // comes to notifications notificationCryptoAccountCheck.close(); return; } // There is no reason to check if the key is already present since if we are // in this place in the code we are about to create new account CommSecureStore secureStore{}; std::string picklingKey = crypto::Tools::generateRandomString(64); secureStore.set( NotificationsCryptoModule::secureStoreNotificationsAccountDataKey, picklingKey); crypto::CryptoModule cryptoModule{ NotificationsCryptoModule::notificationsCryptoAccountID}; NotificationsCryptoModule::serializeAndFlushCryptoModule( cryptoModule, notificationsCryptoAccountPath, picklingKey, callingProcessName); } std::string NotificationsCryptoModule::getNotificationsIdentityKeys( const std::string &callingProcessName) { std::string identityKeys; auto caller = [&identityKeys](crypto::CryptoModule cryptoModule) { identityKeys = cryptoModule.getIdentityKeys(); }; NotificationsCryptoModule::callCryptoModule(caller, callingProcessName); return identityKeys; } std::string NotificationsCryptoModule::generateAndGetNotificationsPrekey( const std::string &callingProcessName) { std::string prekey; auto caller = [&prekey](crypto::CryptoModule cryptoModule) { prekey = cryptoModule.generateAndGetPrekey(); }; NotificationsCryptoModule::callCryptoModule(caller, callingProcessName); return prekey; } std::string NotificationsCryptoModule::getNotificationsPrekeySignature( const std::string &callingProcessName) { std::string prekeySignature; auto caller = [&prekeySignature](crypto::CryptoModule cryptoModule) { prekeySignature = cryptoModule.getPrekeySignature(); }; NotificationsCryptoModule::callCryptoModule(caller, callingProcessName); return prekeySignature; } std::string NotificationsCryptoModule::getNotificationsOneTimeKeys( const size_t oneTimeKeysAmount, const std::string &callingProcessName) { std::string oneTimeKeys; auto caller = [&oneTimeKeys, oneTimeKeysAmount](crypto::CryptoModule cryptoModule) { oneTimeKeys = cryptoModule.getOneTimeKeys(oneTimeKeysAmount); }; NotificationsCryptoModule::callCryptoModule(caller, callingProcessName); return oneTimeKeys; } crypto::EncryptedData NotificationsCryptoModule::initializeNotificationsSession( const std::string &identityKeys, const std::string &prekey, const std::string &prekeySignature, const std::string &oneTimeKeys, const std::string &callingProcessName) { crypto::EncryptedData initialEncryptedMessage; auto caller = [&](crypto::CryptoModule &cryptoModule) { cryptoModule.initializeOutboundForSendingSession( NotificationsCryptoModule::keyserverHostedNotificationsID, std::vector(identityKeys.begin(), identityKeys.end()), std::vector(prekey.begin(), prekey.end()), std::vector(prekeySignature.begin(), prekeySignature.end()), std::vector(oneTimeKeys.begin(), oneTimeKeys.end())); initialEncryptedMessage = cryptoModule.encrypt( NotificationsCryptoModule::keyserverHostedNotificationsID, NotificationsCryptoModule::initialEncryptedMessageContent); }; NotificationsCryptoModule::callCryptoModule(caller, callingProcessName); return initialEncryptedMessage; } bool NotificationsCryptoModule::isNotificationsSessionInitialized( const std::string &callingProcessName) { bool sessionInitialized; auto caller = [&sessionInitialized](crypto::CryptoModule &cryptoModule) { sessionInitialized = cryptoModule.hasSessionFor( NotificationsCryptoModule::keyserverHostedNotificationsID); }; NotificationsCryptoModule::callCryptoModule(caller, callingProcessName); return sessionInitialized; } void NotificationsCryptoModule::clearSensitiveData() { std::string notificationsCryptoAccountPath = PlatformSpecificTools::getNotificationsCryptoAccountPath(); if (remove(notificationsCryptoAccountPath.c_str()) == -1 && errno != ENOENT) { throw std::runtime_error( "Unable to remove notifications crypto account. Security requirements " "might be violated."); } } std::string NotificationsCryptoModule::decrypt( const std::string &data, const size_t messageType, const std::string &callingProcessName) { std::string decryptedData; auto caller = [&](crypto::CryptoModule &cryptoModule) { + crypto::EncryptedData encryptedData{ + std::vector(data.begin(), data.end()), messageType}; decryptedData = cryptoModule.decrypt( NotificationsCryptoModule::keyserverHostedNotificationsID, - {std::vector(data.begin(), data.end()), messageType}); + encryptedData); }; NotificationsCryptoModule::callCryptoModule(caller, callingProcessName); return decryptedData; } } // namespace comm diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm index c04d7ec93..bae7519f6 100644 --- a/native/ios/NotificationService/NotificationService.mm +++ b/native/ios/NotificationService/NotificationService.mm @@ -1,539 +1,540 @@ #import "NotificationService.h" #import "Logger.h" #import "NotificationsCryptoModule.h" #import "StaffUtils.h" #import "TemporaryMessageStorage.h" #import NSString *const backgroundNotificationTypeKey = @"backgroundNotifType"; NSString *const messageInfosKey = @"messageInfos"; NSString *const encryptedPayloadKey = @"encryptedPayload"; NSString *const encryptionFailureKey = @"encryptionFailure"; const std::string callingProcessName = "NSE"; // The context for this constant can be found here: // https://linear.app/comm/issue/ENG-3074#comment-bd2f5e28 int64_t const notificationRemovalDelay = (int64_t)(0.1 * NSEC_PER_SEC); CFStringRef newMessageInfosDarwinNotification = CFSTR("app.comm.darwin_new_message_infos"); // Implementation below was inspired by the // following discussion with Apple staff member: // https://developer.apple.com/forums/thread/105088 size_t getMemoryUsageInBytes() { task_vm_info_data_t vmInfo; mach_msg_type_number_t count = TASK_VM_INFO_COUNT; kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count); if (result != KERN_SUCCESS) { return -1; } size_t memory_usage = static_cast(vmInfo.phys_footprint); return memory_usage; } @interface NotificationService () @property(strong) NSMutableDictionary *contentHandlers; @property(strong) NSMutableDictionary *contents; @end @implementation NotificationService - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler: (void (^)(UNNotificationContent *_Nonnull)) contentHandler { // Set-up methods are idempotent [NotificationService setUpNSEProcess]; [self setUpNSEInstance]; NSString *contentHandlerKey = [request.identifier copy]; UNMutableNotificationContent *content = [request.content mutableCopy]; [self putContent:content withHandler:contentHandler forKey:contentHandlerKey]; UNNotificationContent *publicUserContent = content; // Step 1: notification decryption. if ([self shouldBeDecrypted:content.userInfo]) { std::string decryptErrorMessage; + std::string notifID = std::string([content.userInfo[@"id"] UTF8String]); try { @try { [self decryptContentInPlace:content]; } @catch (NSException *e) { decryptErrorMessage = "NSE: Received Obj-C exception: " + std::string([e.name UTF8String]) + - " during notification decryption."; + " during notification decryption. Notif ID: " + notifID; } } catch (const std::exception &e) { decryptErrorMessage = "NSE: Received C++ exception: " + std::string(e.what()) + - " during notification decryption."; + " during notification decryption. Notif ID: " + notifID; } if (decryptErrorMessage.size()) { NSString *errorMessage = [NSString stringWithUTF8String:decryptErrorMessage.c_str()]; [self callContentHandlerForKey:contentHandlerKey onErrorMessage:errorMessage withPublicUserContent:[[UNNotificationContent alloc] init]]; return; } } else if ([self shouldAlertUnencryptedNotification:content.userInfo]) { // In future this will be replaced by notification content // modification for DEV environment and staff members comm::Logger::log("NSE: Received erroneously unencrypted notitication."); } NSMutableArray *errorMessages = [[NSMutableArray alloc] init]; // Step 2: notification persistence in a temporary storage std::string persistErrorMessage; try { @try { [self persistMessagePayload:content.userInfo]; } @catch (NSException *e) { persistErrorMessage = "Obj-C exception: " + std::string([e.name UTF8String]) + " during notification persistence."; } } catch (const std::exception &e) { persistErrorMessage = "C++ exception: " + std::string(e.what()) + " during notification persistence."; } if (persistErrorMessage.size()) { [errorMessages addObject:[NSString stringWithUTF8String:persistErrorMessage.c_str()]]; } // Step 3: (optional) rescind read notifications // Message payload persistence is a higher priority task, so it has // to happen prior to potential notification center clearing. if ([self isRescind:content.userInfo]) { std::string rescindErrorMessage; try { @try { [self removeNotificationWithIdentifier:content .userInfo[@"notificationId"]]; } @catch (NSException *e) { rescindErrorMessage = "Obj-C exception: " + std::string([e.name UTF8String]) + " during notification rescind."; } } catch (const std::exception &e) { rescindErrorMessage = "C++ exception: " + std::string(e.what()) + " during notification rescind."; } if (rescindErrorMessage.size()) { [errorMessages addObject:[NSString stringWithUTF8String:persistErrorMessage.c_str()]]; } publicUserContent = [[UNNotificationContent alloc] init]; } if ([self isBadgeOnly:content.userInfo]) { UNMutableNotificationContent *badgeOnlyContent = [[UNMutableNotificationContent alloc] init]; badgeOnlyContent.badge = content.badge; content = badgeOnlyContent; publicUserContent = badgeOnlyContent; } // Step 5: notify main app that there is data // to transfer to SQLite and redux. [self sendNewMessageInfosNotification]; if (NSString *currentMemoryEventMessage = [NotificationService getAndSetMemoryEventMessage:nil]) { [errorMessages addObject:currentMemoryEventMessage]; } if (errorMessages.count) { NSString *cumulatedErrorMessage = [@"NSE: Received " stringByAppendingString:[errorMessages componentsJoinedByString:@" "]]; [self callContentHandlerForKey:contentHandlerKey onErrorMessage:cumulatedErrorMessage withPublicUserContent:publicUserContent]; return; } [self callContentHandlerForKey:contentHandlerKey withContent:publicUserContent]; } - (void)serviceExtensionTimeWillExpire { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified // content, otherwise the original push payload will be used. NSMutableArray *allHandlers = [[NSMutableArray alloc] init]; NSMutableArray *allContents = [[NSMutableArray alloc] init]; @synchronized(self.contentHandlers) { for (NSString *key in self.contentHandlers) { [allHandlers addObject:self.contentHandlers[key]]; [allContents addObject:self.contents[key]]; } [self.contentHandlers removeAllObjects]; [self.contents removeAllObjects]; } for (int i = 0; i < allContents.count; i++) { UNNotificationContent *content = allContents[i]; void (^handler)(UNNotificationContent *_Nonnull) = allHandlers[i]; if ([self isRescind:content.userInfo]) { // If we get to this place it means we were unable to // remove relevant notification from notification center in // in time given to NSE to process notification. // It is an extremely unlikely to happen. if (!comm::StaffUtils::isStaffRelease()) { handler([[UNNotificationContent alloc] init]); continue; } NSString *errorMessage = @"NSE: Exceeded time limit to rescind a notification."; UNNotificationContent *errorContent = [self buildContentForError:errorMessage]; handler(errorContent); continue; } if ([self shouldBeDecrypted:content.userInfo] && !content.userInfo[@"succesfullyDecrypted"]) { // If we get to this place it means we were unable to // decrypt encrypted notification content in time // given to NSE to process notification. if (!comm::StaffUtils::isStaffRelease()) { handler([[UNNotificationContent alloc] init]); continue; } NSString *errorMessage = @"NSE: Exceeded time limit to decrypt a notification."; UNNotificationContent *errorContent = [self buildContentForError:errorMessage]; handler(errorContent); continue; } // At this point we know that the content is at least // correctly decrypted so we can display it to the user. // Another operation, like persistence, had failed. if ([self isBadgeOnly:content.userInfo]) { UNNotificationContent *badgeOnlyContent = [self getBadgeOnlyContentFor:content]; handler(badgeOnlyContent); continue; } handler(content); } } - (void)removeNotificationWithIdentifier:(NSString *)identifier { dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); void (^delayedSemaphorePostCallback)() = ^() { dispatch_time_t timeToPostSemaphore = dispatch_time(DISPATCH_TIME_NOW, notificationRemovalDelay); dispatch_after(timeToPostSemaphore, dispatch_get_main_queue(), ^{ dispatch_semaphore_signal(semaphore); }); }; [UNUserNotificationCenter.currentNotificationCenter getDeliveredNotificationsWithCompletionHandler:^( NSArray *_Nonnull notifications) { for (UNNotification *notif in notifications) { if ([identifier isEqual:notif.request.content.userInfo[@"id"]]) { [UNUserNotificationCenter.currentNotificationCenter removeDeliveredNotificationsWithIdentifiers:@[ notif.request.identifier ]]; } } delayedSemaphorePostCallback(); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } - (void)persistMessagePayload:(NSDictionary *)payload { if (payload[messageInfosKey]) { TemporaryMessageStorage *temporaryStorage = [[TemporaryMessageStorage alloc] init]; [temporaryStorage writeMessage:payload[messageInfosKey]]; return; } if (![self isRescind:payload]) { return; } NSError *jsonError = nil; NSData *binarySerializedRescindPayload = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&jsonError]; if (jsonError) { comm::Logger::log( "NSE: Failed to serialize rescind payload. Details: " + std::string([jsonError.localizedDescription UTF8String])); return; } NSString *serializedRescindPayload = [[NSString alloc] initWithData:binarySerializedRescindPayload encoding:NSUTF8StringEncoding]; TemporaryMessageStorage *temporaryRescindsStorage = [[TemporaryMessageStorage alloc] initForRescinds]; [temporaryRescindsStorage writeMessage:serializedRescindPayload]; } - (BOOL)isRescind:(NSDictionary *)payload { return payload[backgroundNotificationTypeKey] && [payload[backgroundNotificationTypeKey] isEqualToString:@"CLEAR"]; } - (BOOL)isBadgeOnly:(NSDictionary *)payload { // TODO: refactor this check by introducing // badgeOnly property in iOS notification payload return !payload[@"threadID"]; } - (UNNotificationContent *)getBadgeOnlyContentFor: (UNNotificationContent *)content { UNMutableNotificationContent *badgeOnlyContent = [[UNMutableNotificationContent alloc] init]; badgeOnlyContent.badge = content.badge; return badgeOnlyContent; } - (void)sendNewMessageInfosNotification { CFNotificationCenterPostNotification( CFNotificationCenterGetDarwinNotifyCenter(), newMessageInfosDarwinNotification, (__bridge const void *)(self), nil, TRUE); } - (BOOL)shouldBeDecrypted:(NSDictionary *)payload { return payload[encryptedPayloadKey]; } - (BOOL)shouldAlertUnencryptedNotification:(NSDictionary *)payload { return payload[encryptionFailureKey] && [payload[encryptionFailureKey] isEqualToNumber:@(1)]; } - (NSString *)singleDecrypt:(NSString *)data { std::string encryptedData = std::string([data UTF8String]); return [NSString stringWithUTF8String: (comm::NotificationsCryptoModule::decrypt( encryptedData, comm::NotificationsCryptoModule::olmEncryptedTypeMessage, callingProcessName)) .c_str()]; } - (void)decryptContentInPlace:(UNMutableNotificationContent *)content { NSString *decryptedSerializedPayload = [self singleDecrypt:content.userInfo[encryptedPayloadKey]]; NSDictionary *decryptedPayload = [NSJSONSerialization JSONObjectWithData:[decryptedSerializedPayload dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; NSMutableDictionary *mutableUserInfo = [content.userInfo mutableCopy]; NSMutableDictionary *mutableAps = nil; if (mutableUserInfo[@"aps"]) { mutableAps = [mutableUserInfo[@"aps"] mutableCopy]; } NSString *body = decryptedPayload[@"merged"]; if (body) { content.body = body; if (mutableAps && mutableAps[@"alert"]) { mutableAps[@"alert"] = body; } } NSString *threadID = decryptedPayload[@"threadID"]; if (threadID) { content.threadIdentifier = threadID; mutableUserInfo[@"threadID"] = threadID; if (mutableAps) { mutableAps[@"thread-id"] = threadID; } } NSString *badgeStr = decryptedPayload[@"badge"]; if (badgeStr) { NSNumber *badge = @([badgeStr intValue]); content.badge = badge; if (mutableAps) { mutableAps[@"badge"] = badge; } } // The rest have been already decrypted and handled. static NSArray *handledKeys = @[ @"merged", @"badge", @"threadID" ]; for (NSString *payloadKey in decryptedPayload) { if ([handledKeys containsObject:payloadKey]) { continue; } mutableUserInfo[payloadKey] = decryptedPayload[payloadKey]; } if (mutableAps) { mutableUserInfo[@"aps"] = mutableAps; } [mutableUserInfo removeObjectForKey:encryptedPayloadKey]; content.userInfo = mutableUserInfo; } // Apple documentation for NSE does not explicitly state // that single NSE instance will be used by only one thread // at a time. Even though UNNotificationServiceExtension API // suggests that it could be the case we don't trust it // and keep a synchronized collection of handlers and contents. // We keep reports of events that strongly suggest there is // parallelism in notifications processing. In particular we // have see notifications not being decrypted when access // to encryption keys had not been correctly implemented. // Similar behaviour is adopted by other apps such as Signal, // Telegram or Element. - (void)setUpNSEInstance { @synchronized(self) { if (self.contentHandlers) { return; } self.contentHandlers = [[NSMutableDictionary alloc] init]; self.contents = [[NSMutableDictionary alloc] init]; } } - (void)putContent:(UNNotificationContent *)content withHandler:(void (^)(UNNotificationContent *_Nonnull))handler forKey:(NSString *)key { @synchronized(self.contentHandlers) { [self.contentHandlers setObject:handler forKey:key]; [self.contents setObject:content forKey:key]; } } - (void)callContentHandlerForKey:(NSString *)key withContent:(UNNotificationContent *)content { void (^handler)(UNNotificationContent *_Nonnull); @synchronized(self.contentHandlers) { handler = [self.contentHandlers objectForKey:key]; [self.contentHandlers removeObjectForKey:key]; [self.contents removeObjectForKey:key]; } if (!handler) { return; } handler(content); } - (UNNotificationContent *)buildContentForError:(NSString *)error { UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.body = error; return content; } - (void)callContentHandlerForKey:(NSString *)key onErrorMessage:(NSString *)errorMessage withPublicUserContent:(UNNotificationContent *)publicUserContent { comm::Logger::log(std::string([errorMessage UTF8String])); if (!comm::StaffUtils::isStaffRelease()) { [self callContentHandlerForKey:key withContent:publicUserContent]; return; } UNNotificationContent *content = [self buildContentForError:errorMessage]; [self callContentHandlerForKey:key withContent:content]; } // Monitor memory usage + (NSString *)getAndSetMemoryEventMessage:(NSString *)message { static NSString *memoryEventMessage = nil; static NSLock *memoryEventLock = [[NSLock alloc] init]; @try { if (![memoryEventLock tryLock]) { return nil; } NSString *currentMemoryEventMessage = memoryEventMessage ? [memoryEventMessage copy] : nil; memoryEventMessage = [message copy]; return currentMemoryEventMessage; } @finally { [memoryEventLock unlock]; } } + (dispatch_source_t)registerForMemoryEvents { dispatch_source_t memorySource = dispatch_source_create( DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0L, DISPATCH_MEMORYPRESSURE_CRITICAL, dispatch_get_main_queue()); dispatch_block_t eventHandler = ^{ NSString *criticalMemoryEventMessage = [NSString stringWithFormat: @"NSE: Received CRITICAL memory event. Memory usage: %ld bytes", getMemoryUsageInBytes()]; comm::Logger::log(std::string([criticalMemoryEventMessage UTF8String])); if (!comm::StaffUtils::isStaffRelease()) { // If it is not a staff release we don't set // memoryEventMessage variable since it will // not be displayed to the client anyway return; } [NotificationService getAndSetMemoryEventMessage:criticalMemoryEventMessage]; }; dispatch_source_set_event_handler(memorySource, eventHandler); dispatch_activate(memorySource); return memorySource; } + (void)setUpNSEProcess { static dispatch_source_t memoryEventSource; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ memoryEventSource = [NotificationService registerForMemoryEvents]; }); } @end