From 09ab977861c7d07214bbf874d014a97662ca5757 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 26 May 2023 14:27:14 +1000 Subject: [PATCH] Updated the code to decode and use updated notifications Made the JobQueue execution type explicit Fixed a bug where legacy group's might not be unsubscribed from --- Session.xcodeproj/project.pbxproj | 28 ++ .../Open Groups/Types/SodiumProtocols.swift | 1 + .../Models/NotificationMetadata.swift | 47 ++++ .../Notifications/PushNotificationAPI.swift | 77 ++++- .../Notifications/Types/ProcessResult.swift | 13 + .../Notifications/Types/Service.swift | 1 + .../MockAeadXChaCha20Poly1305Ietf.swift | 1 + .../NotificationServiceExtension.swift | 18 +- .../Networking/OnionRequestAPI.swift | 43 +-- SessionUtilitiesKit/JobRunner/JobRunner.swift | 5 +- SessionUtilitiesKit/Utilities/Bencode.swift | 263 ++++++++++++++++++ .../Utilities/BencodeSpec.swift | 97 +++++++ 12 files changed, 537 insertions(+), 57 deletions(-) create mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift create mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift create mode 100644 SessionUtilitiesKit/Utilities/Bencode.swift create mode 100644 SessionUtilitiesKitTests/Utilities/BencodeSpec.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index cbe919ba5..69124a8f7 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -654,6 +654,7 @@ FD6A7A692818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */; }; FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */; }; FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; }; + FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */; }; FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */; }; @@ -817,6 +818,7 @@ FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; }; FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; + FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */; }; FDDCBDA829E776BF00303C38 /* seed2-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */; }; FDDCBDA929E776BF00303C38 /* seed1-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */; }; @@ -910,6 +912,9 @@ FDF848F329413DB0007DCAE5 /* ImagePickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */; }; FDF848F529413EEC007DCAE5 /* SessionCell+Styling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */; }; FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */; }; + FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */; }; + FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */; }; + FDFBB7542A2023EB00CA7350 /* BencodeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB7532A2023EB00CA7350 /* BencodeSpec.swift */; }; FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; }; FDFC4E1929F1F9A600992FB6 /* libsession-util.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDFC4E1829F1F9A600992FB6 /* libsession-util.xcframework */; }; FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Utilities.swift */; }; @@ -1802,6 +1807,7 @@ FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrieveDefaultOpenGroupRoomsJob.swift; sourceTree = ""; }; FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = ""; }; FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModel.swift; sourceTree = ""; }; FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = ""; }; @@ -1958,6 +1964,7 @@ FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = ""; }; FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = ""; }; FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = ""; }; + FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = ""; }; FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionTypeConversionUtilitiesSpec.swift; sourceTree = ""; }; FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed2-2023-2y.crt"; sourceTree = ""; }; FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed1-2023-2y.crt"; sourceTree = ""; }; @@ -2054,6 +2061,9 @@ FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerHandler.swift; sourceTree = ""; }; FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SessionCell+Styling.swift"; sourceTree = ""; }; FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrentUserPoller.swift; sourceTree = ""; }; + FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bencode.swift; sourceTree = ""; }; + FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMetadata.swift; sourceTree = ""; }; + FDFBB7532A2023EB00CA7350 /* BencodeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeSpec.swift; sourceTree = ""; }; FDFC4E1829F1F9A600992FB6 /* libsession-util.xcframework */ = {isa = PBXFileReference; explicitFileType = wrapper.xcframework; includeInIndex = 0; path = "libsession-util.xcframework"; sourceTree = BUILD_DIR; }; FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGeneralCache.swift; sourceTree = ""; }; @@ -3612,6 +3622,7 @@ FD09796527F6B0A800936362 /* Utilities */ = { isa = PBXGroup; children = ( + FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */, FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */, FD09796A27F6C67500936362 /* Failable.swift */, FD09797127FAA2F500936362 /* Optional+Utilities.swift */, @@ -4045,6 +4056,7 @@ FD37EA1228AB3F60003AE748 /* Database */, FD83B9B927CF20A5005E1583 /* General */, FD9B30F1293EA0AF008DEE3E /* Networking */, + FDFBB7522A2023DE00CA7350 /* Utilities */, ); path = SessionUtilitiesKitTests; sourceTree = ""; @@ -4163,6 +4175,7 @@ isa = PBXGroup; children = ( FDC13D482A16EC20007267C7 /* Service.swift */, + FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */, FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */, ); path = Types; @@ -4225,9 +4238,11 @@ FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */, FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */, FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */, + FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */, FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */, FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */, FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */, + FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */, ); path = Models; sourceTree = ""; @@ -4425,6 +4440,14 @@ path = Models; sourceTree = ""; }; + FDFBB7522A2023DE00CA7350 /* Utilities */ = { + isa = PBXGroup; + children = ( + FDFBB7532A2023EB00CA7350 /* BencodeSpec.swift */, + ); + path = Utilities; + sourceTree = ""; + }; FDFDE122282D04E30098B17F /* Transitions */ = { isa = PBXGroup; children = ( @@ -5644,6 +5667,7 @@ C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */, C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */, FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */, + FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */, FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, @@ -5758,6 +5782,7 @@ FD245C5A2850660100B966DD /* LinkPreviewDraft.swift in Sources */, FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */, FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */, + FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */, FD716E6A2850327900C96BF4 /* EndCallMode.swift in Sources */, FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */, 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */, @@ -5771,6 +5796,7 @@ FD245C57285065F100B966DD /* Poller.swift in Sources */, FDA8EAFE280E8B78002B68E5 /* FailedMessageSendsJob.swift in Sources */, FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */, + FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */, FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, FD8ECF9029381FC200C0D1BB /* SessionUtil+UserProfile.swift in Sources */, @@ -5808,6 +5834,7 @@ FDC6D6F32860607300B04575 /* Environment.swift in Sources */, C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */, FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */, + FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */, B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */, FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */, FD8ECF892935AB7200C0D1BB /* SessionUtilError.swift in Sources */, @@ -6155,6 +6182,7 @@ FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */, FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */, FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, + FDFBB7542A2023EB00CA7350 /* BencodeSpec.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */, diff --git a/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift b/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift index 223a42e44..cbbc7e66a 100644 --- a/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift +++ b/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift @@ -24,6 +24,7 @@ public protocol SodiumType { public protocol AeadXChaCha20Poly1305IetfType { var KeyBytes: Int { get } var ABytes: Int { get } + var NonceBytes: Int { get } func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes? func decrypt(authenticatedCipherText: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes? diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift new file mode 100644 index 000000000..9a3633d85 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift @@ -0,0 +1,47 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension PushNotificationAPI { + struct NotificationMetadata: Codable { + private enum CodingKeys: String, CodingKey { + case accountId = "@" + case hash = "#" + case namespace = "n" + case dataLength = "l" + case dataTooLong = "B" + } + + /// Account ID (such as Session ID or closed group ID) where the message arrived. + let accountId: String + + /// The hash of the message in the swarm. + let hash: String + + /// The swarm namespace in which this message arrived. + let namespace: Int + + /// The length of the message data. This is always included, even if the message content + /// itself was too large to fit into the push notification. + let dataLength: Int + + /// This will be `true` if the data was omitted because it was too long to fit in a push + /// notification (around 2.5kB of raw data), in which case the push notification includes + /// only this metadata but not the message content itself. + let dataTooLong: Bool + } +} + +extension PushNotificationAPI.NotificationMetadata { + init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self = PushNotificationAPI.NotificationMetadata( + accountId: try container.decode(String.self, forKey: .accountId), + hash: try container.decode(String.self, forKey: .hash), + namespace: try container.decode(Int.self, forKey: .namespace), + dataLength: try container.decode(Int.self, forKey: .dataLength), + dataTooLong: ((try? container.decode(Bool.self, forKey: .dataTooLong)) ?? false) + ) + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index bb5d6ce8d..66e28ae9a 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -54,13 +54,13 @@ public enum PushNotificationAPI { } let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) - let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] - .defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType) - let request: SubscribeRequest = SubscribeRequest( pubkey: currentUserPublicKey, namespaces: [.default], - includeMessageData: (previewType == .nameAndPreview), // TODO: Test resubscribing when changing the type + // Note: Unfortunately we always need the message content because without the content + // control messages can't be distinguished from visible messages which results in the + // 'generic' notification being shown when receiving things like typing indicator updates + includeMessageData: true, serviceInfo: SubscribeRequest.ServiceInfo( token: hexEncodedToken ), @@ -349,14 +349,6 @@ public enum PushNotificationAPI { ) -> AnyPublisher { let isUsingFullAPNs = UserDefaults.standard[.isUsingFullAPNs] - // TODO: Need to validate if this is actually desired behaviour - would this check prevent the app from unsubscribing if the user switches off fast mode??? (this is what the app is currently doing) - // TODO: This flag seems like it might actually be buggy... should double check it - guard isUsingFullAPNs else { - return Just(()) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - return PushNotificationAPI .send( request: PushNotificationAPIRequest( @@ -385,11 +377,70 @@ public enum PushNotificationAPI { .map { _ in () } .eraseToAnyPublisher() } + + // MARK: - Notification Handling + + public static func processNotification( + notificationContent: UNNotificationContent, + dependencies: SMKDependencies = SMKDependencies() + ) -> (envelope: SNProtoEnvelope?, result: ProcessResult) { + // Make sure the notification is from the updated push server + guard notificationContent.userInfo["spns"] != nil else { + guard + let base64EncodedData: String = notificationContent.userInfo["ENCRYPTED_DATA"] as? String, + let data: Data = Data(base64Encoded: base64EncodedData), + let envelope: SNProtoEnvelope = try? MessageWrapper.unwrap(data: data) + else { return (nil, .legacyFailure) } + + // We only support legacy notifications for legacy group conversations + guard envelope.type == .closedGroupMessage else { return (envelope, .legacyForceSilent) } + + return (envelope, .legacySuccess) + } + + guard + let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String, + let encData: Data = Data(base64Encoded: base64EncodedEncString), + let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(), + encData.count > dependencies.aeadXChaCha20Poly1305Ietf.NonceBytes + else { return (nil, .failure) } + + let nonce: Data = encData[0.. = try? Bencode.decodeResponse(from: decryptedData) else { + return (nil, .failure) + } + + // If the metadata says that the message was too large then we should show the generic + // notification (this is a valid case) + guard !notification.info.dataTooLong else { return (nil, .success) } + + // Check that the body we were given is valid + guard + let notificationData: Data = notification.data, + notification.info.dataLength == notificationData.count, + let envelope = try? MessageWrapper.unwrap(data: notificationData) + else { return (nil, .failure) } + + // Success, we have the notification content + return (envelope, .success) + } // MARK: - Security @discardableResult private static func getOrGenerateEncryptionKey() throws -> Data { - // TODO: May want to work this differently (will break after a phone restart if the device hasn't been unlocked yet) do { var encryptionKey: Data = try SSKDefaultKeychainStorage.shared.data( forService: keychainService, diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift new file mode 100644 index 000000000..1c72b1629 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift @@ -0,0 +1,13 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension PushNotificationAPI { + enum ProcessResult { + case success + case failure + case legacySuccess + case legacyFailure + case legacyForceSilent + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift index cc2e5157f..b9aeb904b 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift @@ -5,5 +5,6 @@ import Foundation extension PushNotificationAPI { enum Service: String, Codable { case apns + case sandbox = "apns-sandbox" // Use for push notifications in Testnet } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockAeadXChaCha20Poly1305Ietf.swift b/SessionMessagingKitTests/_TestUtilities/MockAeadXChaCha20Poly1305Ietf.swift index cb3888b59..a90f118ca 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockAeadXChaCha20Poly1305Ietf.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockAeadXChaCha20Poly1305Ietf.swift @@ -8,6 +8,7 @@ import Sodium class MockAeadXChaCha20Poly1305Ietf: Mock, AeadXChaCha20Poly1305IetfType { var KeyBytes: Int = 32 var ABytes: Int = 16 + var NonceBytes: Int = 24 func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes? { return accept(args: [message, secretKey, nonce, additionalData]) as? Bytes diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 3a61aa8b3..741f17f27 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -57,12 +57,22 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension ) } + let (maybeEnvelope, result) = PushNotificationAPI.processNotification( + notificationContent: notificationContent + ) + guard - let base64EncodedData: String = notificationContent.userInfo["ENCRYPTED_DATA"] as? String, - let data: Data = Data(base64Encoded: base64EncodedData), - let envelope = try? MessageWrapper.unwrap(data: data) + (result == .success || result == .legacySuccess), + let envelope: SNProtoEnvelope = maybeEnvelope else { - return self.handleFailure(for: notificationContent) + switch result { + // If we got an explicit failure, or we got a success but no content then show + // the fallback notification + case .success, .legacySuccess, .failure, .legacyFailure: + return self.handleFailure(for: notificationContent) + + case .legacyForceSilent: return + } } // HACK: It is important to use write synchronously here to avoid a race condition diff --git a/SessionSnodeKit/Networking/OnionRequestAPI.swift b/SessionSnodeKit/Networking/OnionRequestAPI.swift index 538733eea..100a8d380 100644 --- a/SessionSnodeKit/Networking/OnionRequestAPI.swift +++ b/SessionSnodeKit/Networking/OnionRequestAPI.swift @@ -784,50 +784,15 @@ public enum OnionRequestAPI: OnionRequestAPIType { } public static func process(bencodedData data: Data) -> (info: ResponseInfoType, body: Data?)? { - // The data will be in the form of `l123:jsone` or `l123:json456:bodye` so we need to break - // the data into parts to properly process it - guard let responseString: String = String(data: data, encoding: .ascii), responseString.starts(with: "l") else { + guard let response: BencodeResponse = try? Bencode.decodeResponse(from: data) else { return nil } - let stringParts: [String.SubSequence] = responseString.split(separator: ":") - - guard stringParts.count > 1, let infoLength: Int = Int(stringParts[0].suffix(from: stringParts[0].index(stringParts[0].startIndex, offsetBy: 1))) else { - return nil - } - - let infoStringStartIndex: String.Index = responseString.index(responseString.startIndex, offsetBy: "l\(infoLength):".count) - let infoStringEndIndex: String.Index = responseString.index(infoStringStartIndex, offsetBy: infoLength) - let infoString: String = String(responseString[infoStringStartIndex.. "l\(infoLength)\(infoString)e".count else { - return (responseInfo, nil) - } - - // Extract the response data as well - let dataString: String = String(responseString.suffix(from: infoStringEndIndex)) - let dataStringParts: [String.SubSequence] = dataString.split(separator: ":") - - guard dataStringParts.count > 1, let finalDataLength: Int = Int(dataStringParts[0]), let suffixData: Data = "e".data(using: .utf8) else { - return nil - } - - let dataBytes: Array = Array(data) - let dataEndIndex: Int = (dataBytes.count - suffixData.count) - let dataStartIndex: Int = (dataEndIndex - finalDataLength) - let finalDataBytes: ArraySlice = dataBytes[dataStartIndex.. = Atomic( JobQueue( type: .blocking, + executionType: .serial, qos: .default, jobVariants: [], onQueueDrained: { @@ -85,6 +86,7 @@ public final class JobRunner { ) let attachmentDownloadQueue: JobQueue = JobQueue( type: .attachmentDownload, + executionType: .serial, qos: .utility, jobVariants: [ jobVariants.remove(.attachmentDownload) @@ -92,6 +94,7 @@ public final class JobRunner { ) let generalQueue: JobQueue = JobQueue( type: .general(number: 0), + executionType: .serial, qos: .utility, jobVariants: Array(jobVariants) ) @@ -509,7 +512,7 @@ private final class JobQueue { init( type: QueueType, - executionType: ExecutionType = .serial, + executionType: ExecutionType, qos: DispatchQoS, jobVariants: [Job.Variant], onQueueDrained: (() -> ())? = nil diff --git a/SessionUtilitiesKit/Utilities/Bencode.swift b/SessionUtilitiesKit/Utilities/Bencode.swift new file mode 100644 index 000000000..1138208cc --- /dev/null +++ b/SessionUtilitiesKit/Utilities/Bencode.swift @@ -0,0 +1,263 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol BencodableType { + associatedtype ValueType: BencodableType + + static var isCollection: Bool { get } + static var isDictionary: Bool { get } +} + +public struct BencodeResponse { + public let info: T + public let data: Data? +} + +extension BencodeResponse: Equatable where T: Equatable {} + +public enum Bencode { + private enum Element: Character { + case number0 = "0" + case number1 = "1" + case number2 = "2" + case number3 = "3" + case number4 = "4" + case number5 = "5" + case number6 = "6" + case number7 = "7" + case number8 = "8" + case number9 = "9" + case intIndicator = "i" + case listIndicator = "l" + case dictIndicator = "d" + case endIndicator = "e" + case separator = ":" + + init?(_ byte: UInt8?) { + guard + let byte: UInt8 = byte, + let byteString: String = String(data: Data([byte]), encoding: .utf8), + let character: Character = byteString.first, + let result: Element = Element(rawValue: character) + else { return nil } + + self = result + } + } + + private struct BencodeString { + let value: String? + let rawValue: Data + } + + // MARK: - Functions + + public static func decodeResponse( + from data: Data, + using dependencies: Dependencies = Dependencies() + ) throws -> BencodeResponse where T: Decodable { + guard + let result: [Data] = try? decode([Data].self, from: data), + let responseData: Data = result.first + else { throw HTTPError.parsingFailed } + + return BencodeResponse( + info: try responseData.decoded(as: T.self, using: dependencies), + data: (result.count > 1 ? result.last : nil) + ) + } + + public static func decode(_ type: T.Type, from data: Data) throws -> T { + guard + let decodedData: (value: Any, remainingData: Data) = decodeData(data), + decodedData.remainingData.isEmpty == true // Ensure there is no left over data + else { throw HTTPError.parsingFailed } + + return try recursiveCast(type, from: decodedData.value) + } + + // MARK: - Logic + + private static func decodeData(_ data: Data) -> (value: Any, remainingData: Data)? { + switch Element(data.first) { + case .number0, .number1, .number2, .number3, .number4, + .number5, .number6, .number7, .number8, .number9: + return decodeString(data) + + case .intIndicator: return decodeInt(data) + case .listIndicator: return decodeList(data) + case .dictIndicator: return decodeDict(data) + default: return nil + } + } + + /// Decode a string element from iterator assumed to have structure `{length}:{data}` + private static func decodeString(_ data: Data) -> (value: BencodeString, remainingData: Data)? { + var mutableData: Data = data + var lengthData: [UInt8] = [] + + // Remove bytes until we hit the separator + while let next: UInt8 = mutableData.popFirst(), Element(next) != .separator { + lengthData.append(next) + } + + // Need to reset the index of the data (it maintains the index after popping/slicing) + // See https://forums.swift.org/t/data-subscript/57195 for more info + mutableData = Data(mutableData) + + guard + let lengthString: String = String(data: Data(lengthData), encoding: .ascii), + let length: Int = Int(lengthString, radix: 10), + mutableData.count >= length + else { return nil } + + // Need to reset the index of the data (it maintains the index after popping/slicing) + // See https://forums.swift.org/t/data-subscript/57195 for more info + return ( + BencodeString( + value: String(data: mutableData[0.. (value: Int, remainingData: Data)? { + var mutableData: Data = data + var intData: [UInt8] = [] + _ = mutableData.popFirst() // drop `i` + + // Pop until after `e` + while let next: UInt8 = mutableData.popFirst(), Element(next) != .endIndicator { + intData.append(next) + } + + guard + let intString: String = String(data: Data(intData), encoding: .ascii), + let result: Int = Int(intString, radix: 10) + else { return nil } + + // Need to reset the index of the data (it maintains the index after popping/slicing) + // See https://forums.swift.org/t/data-subscript/57195 for more info + return (result, Data(mutableData)) + } + + /// Decode a list element from iterator assumed to have structure `l{data}e` + private static func decodeList(_ data: Data) -> ([Any], Data)? { + var mutableData: Data = data + var listElements: [Any] = [] + _ = mutableData.popFirst() // drop `l` + + while !mutableData.isEmpty, let next: UInt8 = mutableData.first, Element(next) != .endIndicator { + guard let result = decodeData(mutableData) else { break } + + listElements.append(result.value) + mutableData = result.remainingData + } + + _ = mutableData.popFirst() // drop `e` + + // Need to reset the index of the data (it maintains the index after popping/slicing) + // See https://forums.swift.org/t/data-subscript/57195 for more info + return (listElements, Data(mutableData)) + } + + /// Decode a dict element from iterator assumed to have structure `d{data}e` + private static func decodeDict(_ data: Data) -> ([String: Any], Data)? { + var mutableData: Data = data + var dictElements: [String: Any] = [:] + _ = mutableData.popFirst() // drop `d` + + while !mutableData.isEmpty, let next: UInt8 = mutableData.first, Element(next) != .endIndicator { + guard + let keyResult = decodeString(mutableData), + let key: String = keyResult.value.value, + let valueResult = decodeData(keyResult.remainingData) + else { return nil } + + dictElements[key] = valueResult.value + mutableData = valueResult.remainingData + } + + _ = mutableData.popFirst() // drop `e` + + // Need to reset the index of the data (it maintains the index after popping/slicing) + // See https://forums.swift.org/t/data-subscript/57195 for more info + return (dictElements, Data(mutableData)) + } + + // MARK: - Internal Functions + + private static func recursiveCast(_ type: T.Type, from value: Any) throws -> T { + switch (type.isCollection, type.isDictionary) { + case (_, true): + guard let dictValue: [String: Any] = value as? [String: Any] else { throw HTTPError.parsingFailed } + + return try ( + dictValue.mapValues { try recursiveCast(type.ValueType.self, from: $0) } as? T ?? + { throw HTTPError.parsingFailed }() + ) + + case (true, _): + guard let arrayValue: [Any] = value as? [Any] else { throw HTTPError.parsingFailed } + + return try ( + arrayValue.map { try recursiveCast(type.ValueType.self, from: $0) } as? T ?? + { throw HTTPError.parsingFailed }() + ) + + default: + switch (value, type) { + case (let bencodeString as BencodeString, is String.Type): + return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }()) + + case (let bencodeString as BencodeString, is Optional.Type): + return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }()) + + case (let bencodeString as BencodeString, _): + return try (bencodeString.rawValue as? T ?? { throw HTTPError.parsingFailed }()) + + default: return try (value as? T ?? { throw HTTPError.parsingFailed }()) + } + } + } +} + +// MARK: - BencodableType Extensions + +extension Data: BencodableType { + public typealias ValueType = Data + + public static var isCollection: Bool { false } + public static var isDictionary: Bool { false } +} + +extension Int: BencodableType { + public typealias ValueType = Int + + public static var isCollection: Bool { false } + public static var isDictionary: Bool { false } +} + +extension String: BencodableType { + public typealias ValueType = String + + public static var isCollection: Bool { false } + public static var isDictionary: Bool { false } +} + +extension Array: BencodableType where Element: BencodableType { + public typealias ValueType = Element + + public static var isCollection: Bool { true } + public static var isDictionary: Bool { false } +} + +extension Dictionary: BencodableType where Key == String, Value: BencodableType { + public typealias ValueType = Value + + public static var isCollection: Bool { false } + public static var isDictionary: Bool { true } +} diff --git a/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift b/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift new file mode 100644 index 000000000..08f00df10 --- /dev/null +++ b/SessionUtilitiesKitTests/Utilities/BencodeSpec.swift @@ -0,0 +1,97 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionUtilitiesKit + +class BencodeSpec: QuickSpec { + struct TestType: Codable, Equatable { + let intValue: Int + let stringValue: String + } + + // MARK: - Spec + + override func spec() { + describe("Bencode") { + context("when decoding") { + it("should decode a basic string") { + let basicStringData: Data = "5:howdy".data(using: .utf8)! + let result = try? Bencode.decode(String.self, from: basicStringData) + + expect(result).to(equal("howdy")) + } + + it("should decode a basic integer") { + let basicIntegerData: Data = "i3e".data(using: .utf8)! + let result = try? Bencode.decode(Int.self, from: basicIntegerData) + + expect(result).to(equal(3)) + } + + it("should decode a list of integers") { + let basicIntListData: Data = "li1ei2ee".data(using: .utf8)! + let result = try? Bencode.decode([Int].self, from: basicIntListData) + + expect(result).to(equal([1, 2])) + } + + it("should decode a basic dict") { + let basicDictData: Data = "d4:spaml1:a1:bee".data(using: .utf8)! + let result = try? Bencode.decode([String: [String]].self, from: basicDictData) + + expect(result).to(equal(["spam": ["a", "b"]])) + } + } + + context("when decoding a response") { + it("decodes successfully") { + let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e" + .data(using: .utf8)! + let result: BencodeResponse? = try? Bencode.decodeResponse(from: data) + + expect(result) + .to(equal( + BencodeResponse( + info: TestType( + intValue: 100, + stringValue: "Test" + ), + data: Data([1, 2, 3, 4, 5]) + ) + )) + } + + it("decodes successfully with no body") { + let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e" + .data(using: .utf8)! + let result: BencodeResponse? = try? Bencode.decodeResponse(from: data) + + expect(result) + .to(equal( + BencodeResponse( + info: TestType( + intValue: 100, + stringValue: "Test" + ), + data: nil + ) + )) + } + + it("throws a parsing error when invalid") { + let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e" + .data(using: .utf8)! + + expect { + let result: BencodeResponse = try Bencode.decodeResponse(from: data) + _ = result + }.to(throwError(HTTPError.parsingFailed)) + } + } + } + } +}