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 @@ -77,6 +77,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 */; }; @@ -261,6 +262,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 = ""; }; @@ -504,6 +507,8 @@ 724995D227B4103A00323FCE /* NotificationService */ = { isa = PBXGroup; children = ( + CB9A156C2A6AC79500ED9D2F /* NSEBlobServiceClient.h */, + CB9A156D2A6AC79600ED9D2F /* NSEBlobServiceClient.mm */, CB30C12327D0ACF700FBE8DE /* NotificationService.entitlements */, 724995D327B4103A00323FCE /* NotificationService.h */, 724995D427B4103A00323FCE /* NotificationService.mm */, @@ -1097,6 +1102,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,7 +1,9 @@ #import "NotificationService.h" #import "Logger.h" +#import "NSEBlobServiceClient.h" #import "NotificationsCryptoModule.h" #import "TemporaryMessageStorage.h" +#import NSString *const backgroundNotificationTypeKey = @"backgroundNotifType"; NSString *const messageInfosKey = @"messageInfos"; @@ -19,7 +21,8 @@ @property(nonatomic, strong) void (^contentHandler) (UNNotificationContent *contentToDeliver); @property(nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; - +@property(class, nonatomic, strong, readonly) + AESCryptoModuleObjCCompat *processLocalAESCryptoModule; @end @implementation NotificationService @@ -28,6 +31,8 @@ withContentHandler: (void (^)(UNNotificationContent *_Nonnull)) contentHandler { + [NotificationService initializeProcessLocalObjects]; + self.contentHandler = contentHandler; self.bestAttemptContent = [request.content mutableCopy]; @@ -49,16 +54,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 +100,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 +149,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 +157,7 @@ return; } - if (![self isRescind:payload]) { + if (![NotificationService isRescind:payload]) { return; } @@ -150,16 +182,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 +277,57 @@ self.bestAttemptContent.userInfo = mutableUserInfo; } ++ (NSDictionary *)aesDecryptAndParse:(NSData *)sealedData + withKey:(NSData *)key { + NSError *decryptError = nil; + NSInteger destinationLength = + [[NotificationService processLocalAESCryptoModule] + decryptedLength:sealedData]; + + NSMutableData *destination = [NSMutableData dataWithLength:destinationLength]; + [[NotificationService processLocalAESCryptoModule] + 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]; +} + +// Process-local initialization code +// NSE may use different threads and instancess +// of this class to process notifications, but it +// usually keeps the same process for extended +// period of time. Objects that can be initialized +// once and reused on each notification should be +// declared in a method below to avoid unnecessary +// resource usage. + +static AESCryptoModuleObjCCompat *_aesCryptoModule = nil; + ++ (void)initializeProcessLocalObjects { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _aesCryptoModule = [[AESCryptoModuleObjCCompat alloc] init]; + }); +} + ++ (AESCryptoModuleObjCCompat *)processLocalAESCryptoModule { + return _aesCryptoModule; +} + @end