diff --git a/native/ios/Comm.xcodeproj/project.pbxproj b/native/ios/Comm.xcodeproj/project.pbxproj --- a/native/ios/Comm.xcodeproj/project.pbxproj +++ b/native/ios/Comm.xcodeproj/project.pbxproj @@ -71,6 +71,7 @@ CB7EF17E295C674300B17035 /* CommIOSNotifications.mm in Sources */ = {isa = PBXBuildFile; fileRef = CB7EF17D295C5D1800B17035 /* CommIOSNotifications.mm */; }; CB7EF180295C674300B17035 /* CommIOSNotificationsBridgeQueue.mm in Sources */ = {isa = PBXBuildFile; fileRef = CB7EF17B295C580500B17035 /* CommIOSNotificationsBridgeQueue.mm */; }; CB90951F29534B32002F2A7F /* CommSecureStore.mm in Sources */ = {isa = PBXBuildFile; fileRef = 71D4D7CB26C50B1000FCDBCD /* CommSecureStore.mm */; }; + CB9A156E2A6AC79600ED9D2F /* NSEBlobServiceClient.mm in Sources */ = {isa = PBXBuildFile; fileRef = CB9A156D2A6AC79600ED9D2F /* NSEBlobServiceClient.mm */; }; CBDEC69B28ED867000C17588 /* GlobalDBSingleton.mm in Sources */ = {isa = PBXBuildFile; fileRef = CBDEC69A28ED867000C17588 /* GlobalDBSingleton.mm */; }; CBFE58292885852B003B94C9 /* ThreadOperations.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CBFE58282885852B003B94C9 /* ThreadOperations.cpp */; }; D7DB6E0F85B2DBE15B01EC21 /* libPods-Comm.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 994BEBDD4E4959F69CEA0BC3 /* libPods-Comm.a */; }; @@ -241,6 +242,8 @@ CB7EF17C295C580500B17035 /* CommIOSNotificationsBridgeQueue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = CommIOSNotificationsBridgeQueue.h; path = Comm/CommIOSNotifications/CommIOSNotificationsBridgeQueue.h; sourceTree = ""; }; CB7EF17D295C5D1800B17035 /* CommIOSNotifications.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = CommIOSNotifications.mm; path = Comm/CommIOSNotifications/CommIOSNotifications.mm; sourceTree = ""; }; CB90951929531663002F2A7F /* CommIOSNotifications.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CommIOSNotifications.h; path = Comm/CommIOSNotifications/CommIOSNotifications.h; sourceTree = ""; }; + CB9A156C2A6AC79500ED9D2F /* NSEBlobServiceClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSEBlobServiceClient.h; sourceTree = ""; }; + CB9A156D2A6AC79600ED9D2F /* NSEBlobServiceClient.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = NSEBlobServiceClient.mm; sourceTree = ""; }; CBDEC69928ED859600C17588 /* GlobalDBSingleton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GlobalDBSingleton.h; sourceTree = ""; }; CBDEC69A28ED867000C17588 /* GlobalDBSingleton.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = GlobalDBSingleton.mm; path = Comm/GlobalDBSingleton.mm; sourceTree = ""; }; CBFE58272885852B003B94C9 /* ThreadOperations.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ThreadOperations.h; path = PersistentStorageUtilities/ThreadOperationsUtilities/ThreadOperations.h; sourceTree = ""; }; @@ -479,6 +482,8 @@ 724995D227B4103A00323FCE /* NotificationService */ = { isa = PBXGroup; children = ( + CB9A156C2A6AC79500ED9D2F /* NSEBlobServiceClient.h */, + CB9A156D2A6AC79600ED9D2F /* NSEBlobServiceClient.mm */, CB30C12327D0ACF700FBE8DE /* NotificationService.entitlements */, 724995D327B4103A00323FCE /* NotificationService.h */, 724995D427B4103A00323FCE /* NotificationService.mm */, @@ -1049,6 +1054,7 @@ CB90951F29534B32002F2A7F /* CommSecureStore.mm in Sources */, CB38B48728771CE500171182 /* TemporaryMessageStorage.mm in Sources */, CB38B48528771CB800171182 /* EncryptedFileUtils.mm in Sources */, + CB9A156E2A6AC79600ED9D2F /* NSEBlobServiceClient.mm in Sources */, CB38B48328771C8300171182 /* NonBlockingLock.mm in Sources */, CB1648AF27CFBE6A00394D9D /* CryptoModule.cpp in Sources */, CB4821AE27CFB187001AB7E1 /* Tools.cpp in Sources */, diff --git a/native/ios/NotificationService/NSEBlobServiceClient.h b/native/ios/NotificationService/NSEBlobServiceClient.h new file mode 100644 --- /dev/null +++ b/native/ios/NotificationService/NSEBlobServiceClient.h @@ -0,0 +1,9 @@ +#pragma once + +#import + +@interface NSEBlobServiceClient : NSObject ++ (id)sharedInstance; +- (void)getAndConsumeSync:(NSString *)blobHash + withSuccessConsumer:(void (^)(NSData *))successConsumer; +@end diff --git a/native/ios/NotificationService/NSEBlobServiceClient.mm b/native/ios/NotificationService/NSEBlobServiceClient.mm new file mode 100644 --- /dev/null +++ b/native/ios/NotificationService/NSEBlobServiceClient.mm @@ -0,0 +1,66 @@ +#import "NSEBlobServiceClient.h" +#import "Logger.h" + +NSString const *blobServiceAddress = @"https://blob.commtechnologies.org"; +int const blobServiceQueryTimeLimit = 15; + +@interface NSEBlobServiceClient () +@property(nonatomic, strong) NSURLSession *sharedBlobServiceSession; +@end + +@implementation NSEBlobServiceClient + ++ (id)sharedInstance { + static NSEBlobServiceClient *sharedBlobServiceClient = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + NSURLSessionConfiguration *config = + [NSURLSessionConfiguration ephemeralSessionConfiguration]; + // TODO: put necessary authentication into session config + [config setTimeoutIntervalForRequest:blobServiceQueryTimeLimit]; + NSURLSession *session = + [NSURLSession sessionWithConfiguration:config + delegate:nil + delegateQueue:[NSOperationQueue mainQueue]]; + sharedBlobServiceClient = [[self alloc] init]; + sharedBlobServiceClient.sharedBlobServiceSession = session; + }); + return sharedBlobServiceClient; +} + +- (void)getAndConsumeSync:(NSString *)blobHash + withSuccessConsumer:(void (^)(NSData *))successConsumer { + NSString *blobUrlStr = [blobServiceAddress + stringByAppendingString:[@"/blob/" stringByAppendingString:blobHash]]; + NSURL *blobUrl = [NSURL URLWithString:blobUrlStr]; + NSURLRequest *blobRequest = [NSURLRequest requestWithURL:blobUrl]; + + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + NSURLSessionDataTask *task = [self.sharedBlobServiceSession + dataTaskWithRequest:blobRequest + completionHandler:^( + NSData *_Nullable data, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + comm::Logger::log( + "NSE: Failed to download blob from blob service. Reason: " + + std::string([error.localizedDescription UTF8String])); + dispatch_semaphore_signal(semaphore); + return; + } + + successConsumer(data); + dispatch_semaphore_signal(semaphore); + }]; + + [task resume]; + dispatch_semaphore_wait( + semaphore, + dispatch_time( + DISPATCH_TIME_NOW, + (int64_t)(blobServiceQueryTimeLimit * NSEC_PER_SEC))); +} + +@end 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 @@ -1,13 +1,16 @@ #import "NotificationService.h" #import "Logger.h" +#import "NSEBlobServiceClient.h" #import "NotificationsCryptoModule.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"; +const int aesTagAndIVCombinedLength = 28; // 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); @@ -49,16 +52,43 @@ comm::Logger::log("NSE: Received erroneously unencrypted notitication."); } - [self persistMessagePayload:self.bestAttemptContent.userInfo]; + if (self.bestAttemptContent.userInfo[@"blobHash"]) { + NSString *blobHash = self.bestAttemptContent.userInfo[@"blobHash"]; + NSData *encryptionKey = [[NSData alloc] + initWithBase64EncodedString:self.bestAttemptContent + .userInfo[@"encryptionKey"] + options:0]; + [[NSEBlobServiceClient sharedInstance] + getAndConsumeSync:blobHash + withSuccessConsumer:^(NSData *data) { + @try { + NSDictionary *largePayload = + [NotificationService aesDecryptAndParse:data + withKey:encryptionKey]; + [NotificationService persistMessagePayload:largePayload]; + [NotificationService sendNewMessageInfosNotification]; + } @catch (NSException *e) { + comm::Logger::log( + "NSE: Received exception: " + std::string([e.name UTF8String]) + + " with reason: " + std::string([e.reason UTF8String]) + + " during large notification payload processing"); + } + }]; + + self.contentHandler(self.bestAttemptContent); + return; + } + + [NotificationService persistMessagePayload:self.bestAttemptContent.userInfo]; // Message payload persistence is a higher priority task, so it has // to happen prior to potential notification center clearing. - if ([self isRescind:self.bestAttemptContent.userInfo]) { + if ([NotificationService isRescind:self.bestAttemptContent.userInfo]) { [self removeNotificationWithIdentifier:self.bestAttemptContent .userInfo[@"notificationId"]]; self.contentHandler([[UNNotificationContent alloc] init]); return; } - [self sendNewMessageInfosNotification]; + [NotificationService sendNewMessageInfosNotification]; // TODO modify self.bestAttemptContent here self.contentHandler(self.bestAttemptContent); @@ -68,7 +98,7 @@ // 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. - if ([self isRescind:self.bestAttemptContent.userInfo]) { + if ([NotificationService isRescind:self.bestAttemptContent.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. @@ -117,7 +147,7 @@ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } -- (void)persistMessagePayload:(NSDictionary *)payload { ++ (void)persistMessagePayload:(NSDictionary *)payload { if (payload[messageInfosKey]) { TemporaryMessageStorage *temporaryStorage = [[TemporaryMessageStorage alloc] init]; @@ -125,7 +155,7 @@ return; } - if (![self isRescind:payload]) { + if (![NotificationService isRescind:payload]) { return; } @@ -150,16 +180,16 @@ [temporaryRescindsStorage writeMessage:serializedRescindPayload]; } -- (BOOL)isRescind:(NSDictionary *)payload { ++ (BOOL)isRescind:(NSDictionary *)payload { return payload[backgroundNotificationTypeKey] && [payload[backgroundNotificationTypeKey] isEqualToString:@"CLEAR"]; } -- (void)sendNewMessageInfosNotification { ++ (void)sendNewMessageInfosNotification { CFNotificationCenterPostNotification( CFNotificationCenterGetDarwinNotifyCenter(), newMessageInfosDarwinNotification, - (__bridge const void *)(self), + nil, nil, TRUE); } @@ -245,4 +275,32 @@ self.bestAttemptContent.userInfo = mutableUserInfo; } ++ (NSDictionary *)aesDecryptAndParse:(NSData *)sealedData + withKey:(NSData *)key { + AESCryptoModuleObjCCompat *obj = [[AESCryptoModuleObjCCompat alloc] init]; + NSError *decryptError = nil; + NSMutableData *destination = [NSMutableData + dataWithLength:sealedData.length - aesTagAndIVCombinedLength]; + [obj decryptWithKey:key + sealedData:sealedData + destination:destination + withError:&decryptError]; + + if (decryptError) { + comm::Logger::log( + "NSE: Notification aes decryption failure. Details: " + + std::string([decryptError.localizedDescription UTF8String])); + return nil; + } + + NSString *decryptedSerializedPayload = + [[NSString alloc] initWithData:destination encoding:NSUTF8StringEncoding]; + + return [NSJSONSerialization + JSONObjectWithData:[decryptedSerializedPayload + dataUsingEncoding:NSUTF8StringEncoding] + options:0 + error:nil]; +} + @end