diff --git a/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.h b/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.h --- a/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.h +++ b/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.h @@ -9,10 +9,18 @@ class NotificationsCryptoModule { const static std::string notificationsCryptoAccountID; - // Used for migration of legacy notifications sessions + // Used for handling of legacy notifications sessions + const static std::string secureStoreNotificationsAccountDataKey; + const static std::string keyserverHostedNotificationsID; + static std::unique_ptr deserializeCryptoModule( const std::string &path, const std::string &picklingKey); + static void serializeAndFlushCryptoModule( + std::unique_ptr cryptoModule, + const std::string &path, + const std::string &picklingKey); + static std::string getKeyserverNotificationsSessionKey(const std::string &keyserverID); static std::string serializeNotificationsSession( @@ -24,7 +32,7 @@ const std::string &keyserverID, const std::string &picklingKey, std::shared_ptr session); - static std::pair, std::string> + static std::optional, std::string>> fetchNotificationsSession(const std::string &keyserverID); public: @@ -38,34 +46,69 @@ const std::string &keyserverID, std::shared_ptr keyserverNotificationsSession); static bool isNotificationsSessionInitialized(const std::string &keyserverID); - static std::string decrypt( - const std::string &keyserverID, - const std::string &data, - const size_t messageType); - class StatefulDecryptResult { + class BaseStatefulDecryptResult { + BaseStatefulDecryptResult( + std::string picklingKey, + std::string decryptedData); + + std::string picklingKey; + std::string decryptedData; + friend NotificationsCryptoModule; + + public: + std::string getDecryptedData(); + virtual void flushState() = 0; + virtual ~BaseStatefulDecryptResult() = default; + }; + + class StatefulDecryptResult : public BaseStatefulDecryptResult { StatefulDecryptResult( std::unique_ptr session, std::string keyserverID, std::string picklingKey, std::string decryptedData); + std::unique_ptr sessionState; std::string keyserverID; - std::string picklingKey; - std::string decryptedData; friend NotificationsCryptoModule; public: - std::string getDecryptedData(); - std::string getKeyserverID(); - std::string getPicklingKey(); + void flushState() override; + }; + + class LegacyStatefulDecryptResult : public BaseStatefulDecryptResult { + LegacyStatefulDecryptResult( + std::unique_ptr cryptoModule, + std::string path, + std::string picklingKey, + std::string decryptedData); + std::unique_ptr cryptoModule; + std::string path; + friend NotificationsCryptoModule; + + public: + void flushState() override; }; - static std::unique_ptr statefulDecrypt( +private: + static std::unique_ptr + prepareLegacyDecryptedState( + const std::string &data, + const size_t messageType); + +public: + static std::string decrypt( const std::string &keyserverID, const std::string &data, const size_t messageType); + + static std::unique_ptr statefulDecrypt( + const std::string &keyserverID, + const std::string &data, + const size_t messageType); + static void - flushState(std::unique_ptr statefulDecryptResult); + flushState(std::unique_ptr statefulDecryptResult); }; } // namespace comm diff --git a/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.cpp b/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.cpp --- a/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.cpp +++ b/native/cpp/CommonCpp/Notifications/BackgroundDataStorage/NotificationsCryptoModule.cpp @@ -6,17 +6,23 @@ #include "../../Tools/Logger.h" #include "../../Tools/PlatformSpecificTools.h" +#include #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; @@ -29,6 +35,7 @@ // app version. Do not introduce new usages of this constant in the code!!! const std::string ashoatKeyserverIDUsedOnlyForMigrationFromLegacyNotifStorage = "256"; +const int temporaryFilePathRandomSuffixLength = 32; std::unique_ptr NotificationsCryptoModule::deserializeCryptoModule( @@ -77,6 +84,68 @@ crypto::Persist({account, sessions})); } +void NotificationsCryptoModule::serializeAndFlushCryptoModule( + std::unique_ptr cryptoModule, + const std::string &path, + const std::string &picklingKey) { + 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 + 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()); +} + std::string NotificationsCryptoModule::getKeyserverNotificationsSessionKey( const std::string &keyserverID) { return "KEYSERVER." + keyserverID + ".NOTIFS_SESSION"; @@ -181,18 +250,27 @@ } } -std::pair, std::string> +std::optional, std::string>> NotificationsCryptoModule::fetchNotificationsSession( const std::string &keyserverID) { std::string keyserverNotificationsSessionKey = NotificationsCryptoModule::getKeyserverNotificationsSessionKey( keyserverID); - std::optional serializedSession = - CommMMKV::getString(keyserverNotificationsSessionKey); - if (!serializedSession.has_value()) { + std::optional serializedSession; + try { + serializedSession = CommMMKV::getString(keyserverNotificationsSessionKey); + } catch (const CommMMKV::InitFromNSEForbiddenError &e) { + serializedSession = std::nullopt; + } + + if (!serializedSession.has_value() && + keyserverID != + ashoatKeyserverIDUsedOnlyForMigrationFromLegacyNotifStorage) { throw std::runtime_error( "Missing notifications session for keyserver: " + keyserverID); + } else if (!serializedSession.has_value()) { + return std::nullopt; } return NotificationsCryptoModule::deserializeNotificationsSession( @@ -214,21 +292,15 @@ return CommMMKV::getString(keyserverNotificationsSessionKey).has_value(); } -std::string NotificationsCryptoModule::decrypt( - const std::string &keyserverID, - const std::string &data, - const size_t messageType) { - std::unique_ptr session; - std::string picklingKey; - std::tie(session, picklingKey) = - NotificationsCryptoModule::fetchNotificationsSession(keyserverID); +NotificationsCryptoModule::BaseStatefulDecryptResult::BaseStatefulDecryptResult( + std::string picklingKey, + std::string decryptedData) + : picklingKey(picklingKey), decryptedData(decryptedData) { +} - crypto::EncryptedData encryptedData{ - std::vector(data.begin(), data.end()), messageType}; - std::string decryptedData = session->decrypt(encryptedData); - NotificationsCryptoModule::persistNotificationsSessionInternal( - keyserverID, picklingKey, std::move(session)); - return decryptedData; +std::string +NotificationsCryptoModule::BaseStatefulDecryptResult::getDecryptedData() { + return this->decryptedData; } NotificationsCryptoModule::StatefulDecryptResult::StatefulDecryptResult( @@ -236,50 +308,123 @@ std::string keyserverID, std::string picklingKey, std::string decryptedData) - : sessionState(std::move(session)), - keyserverID(keyserverID), - picklingKey(picklingKey), - decryptedData(decryptedData) { + : NotificationsCryptoModule::BaseStatefulDecryptResult:: + BaseStatefulDecryptResult(picklingKey, decryptedData), + sessionState(std::move(session)), + keyserverID(keyserverID) { } -std::string -NotificationsCryptoModule::StatefulDecryptResult::getDecryptedData() { - return this->decryptedData; +void NotificationsCryptoModule::StatefulDecryptResult::flushState() { + NotificationsCryptoModule::persistNotificationsSessionInternal( + this->keyserverID, this->picklingKey, std::move(this->sessionState)); } -std::string NotificationsCryptoModule::StatefulDecryptResult::getKeyserverID() { - return this->keyserverID; +NotificationsCryptoModule::LegacyStatefulDecryptResult:: + LegacyStatefulDecryptResult( + std::unique_ptr cryptoModule, + std::string path, + std::string picklingKey, + std::string decryptedData) + : NotificationsCryptoModule::BaseStatefulDecryptResult:: + BaseStatefulDecryptResult(picklingKey, decryptedData), + path(path), + cryptoModule(std::move(cryptoModule)) { } -std::string NotificationsCryptoModule::StatefulDecryptResult::getPicklingKey() { - return this->picklingKey; +void NotificationsCryptoModule::LegacyStatefulDecryptResult::flushState() { + NotificationsCryptoModule::serializeAndFlushCryptoModule( + std::move(this->cryptoModule), this->path, this->picklingKey); } -std::unique_ptr -NotificationsCryptoModule::statefulDecrypt( +std::unique_ptr +NotificationsCryptoModule::prepareLegacyDecryptedState( + const std::string &data, + const size_t messageType) { + folly::Optional picklingKey = comm::CommSecureStore::get( + NotificationsCryptoModule::secureStoreNotificationsAccountDataKey); + + if (!picklingKey.hasValue()) { + throw std::runtime_error( + "Legacy notifications session pickling key missing."); + } + + std::string legacyNotificationsAccountPath = + comm::PlatformSpecificTools::getNotificationsCryptoAccountPath(); + + crypto::EncryptedData encryptedData{ + std::vector(data.begin(), data.end()), messageType}; + + auto cryptoModule = NotificationsCryptoModule::deserializeCryptoModule( + legacyNotificationsAccountPath, picklingKey.value()); + + std::string decryptedData = cryptoModule->decrypt( + NotificationsCryptoModule::keyserverHostedNotificationsID, encryptedData); + + LegacyStatefulDecryptResult statefulDecryptResult( + std::move(cryptoModule), + legacyNotificationsAccountPath, + picklingKey.value(), + decryptedData); + + return std::make_unique( + std::move(statefulDecryptResult)); +} + +std::string NotificationsCryptoModule::decrypt( const std::string &keyserverID, const std::string &data, const size_t messageType) { - std::unique_ptr session; - std::string picklingKey; - std::tie(session, picklingKey) = + + auto sessionWithPicklingKey = NotificationsCryptoModule::fetchNotificationsSession(keyserverID); + if (!sessionWithPicklingKey.has_value()) { + auto statefulDecryptResult = + NotificationsCryptoModule::prepareLegacyDecryptedState( + data, messageType); + statefulDecryptResult->flushState(); + return statefulDecryptResult->getDecryptedData(); + } + std::unique_ptr session = + std::move(sessionWithPicklingKey.value().first); + std::string picklingKey = sessionWithPicklingKey.value().second; crypto::EncryptedData encryptedData{ std::vector(data.begin(), data.end()), messageType}; + std::string decryptedData = session->decrypt(encryptedData); + NotificationsCryptoModule::persistNotificationsSessionInternal( + keyserverID, picklingKey, std::move(session)); + return decryptedData; +} + +std::unique_ptr +NotificationsCryptoModule::statefulDecrypt( + const std::string &keyserverID, + const std::string &data, + const size_t messageType) { + auto sessionWithPicklingKey = + NotificationsCryptoModule::fetchNotificationsSession(keyserverID); + if (!sessionWithPicklingKey.has_value()) { + return NotificationsCryptoModule::prepareLegacyDecryptedState( + data, messageType); + } + + std::unique_ptr session = + std::move(sessionWithPicklingKey.value().first); + std::string picklingKey = sessionWithPicklingKey.value().second; + crypto::EncryptedData encryptedData{ + std::vector(data.begin(), data.end()), messageType}; + std::string decryptedData = session->decrypt(encryptedData); StatefulDecryptResult statefulDecryptResult( std::move(session), keyserverID, picklingKey, decryptedData); + return std::make_unique( std::move(statefulDecryptResult)); } void NotificationsCryptoModule::flushState( - std::unique_ptr statefulDecryptResult) { - NotificationsCryptoModule::persistNotificationsSessionInternal( - statefulDecryptResult->getKeyserverID(), - statefulDecryptResult->getPicklingKey(), - std::move(statefulDecryptResult->sessionState)); + std::unique_ptr baseStatefulDecryptResult) { + baseStatefulDecryptResult->flushState(); } } // namespace comm diff --git a/native/cpp/CommonCpp/Tools/CommMMKV.h b/native/cpp/CommonCpp/Tools/CommMMKV.h --- a/native/cpp/CommonCpp/Tools/CommMMKV.h +++ b/native/cpp/CommonCpp/Tools/CommMMKV.h @@ -26,5 +26,10 @@ static std::vector getAllKeys(); static void removeKeys(const std::vector &keys); + + class InitFromNSEForbiddenError : public std::runtime_error { + public: + using std::runtime_error::runtime_error; + }; }; } // namespace comm diff --git a/native/ios/Comm/CommMMKV.mm b/native/ios/Comm/CommMMKV.mm --- a/native/ios/Comm/CommMMKV.mm +++ b/native/ios/Comm/CommMMKV.mm @@ -63,7 +63,8 @@ } else if (!isRunningInAppExtension) { assignInitializationData(); } else { - throw std::runtime_error("NSE can't initialize MMKV encryption key."); + throw CommMMKV::InitFromNSEForbiddenError( + std::string("NSE can't initialize MMKV encryption key.")); } [MMKV initializeMMKV:nil diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm --- a/native/ios/NotificationService/NotificationService.mm +++ b/native/ios/NotificationService/NotificationService.mm @@ -85,7 +85,7 @@ UNNotificationContent *publicUserContent = content; // Step 1: notification decryption. - std::unique_ptr + std::unique_ptr statefulDecryptResultPtr; BOOL decryptionExecuted = NO; @@ -178,7 +178,8 @@ " during unread count calculation."; } - if (unreadCountCalculationError.size()) { + if (unreadCountCalculationError.size() && + comm::StaffUtils::isStaffRelease()) { [errorMessages addObject:[NSString stringWithUTF8String:unreadCountCalculationError .c_str()]]; @@ -544,7 +545,7 @@ [payload[encryptionFailureKey] isEqualToNumber:@(1)]; } -- (std::unique_ptr) +- (std::unique_ptr) decryptContentInPlace:(UNMutableNotificationContent *)content { std::string encryptedData = std::string([content.userInfo[encryptedPayloadKey] UTF8String]);