From 2927795f2993ebf1f4c1181c1c58b25fde4b2a58 Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Tue, 26 Mar 2024 18:07:34 +0100 Subject: [PATCH 01/10] Fix MM 56723 (iOS) --- app/init/push_notifications.ts | 10 +- ios/Gekidou/Package.swift | 6 +- .../PushNotification+Signature.swift | 133 ++++++++++++++++++ .../Gekidou/Storage/Database+System.swift | 13 ++ .../Sources/Gekidou/Storage/Database.swift | 1 + ios/GekidouWrapper.swift | 4 + ios/Mattermost.xcodeproj/project.pbxproj | 84 +++++------ .../xcshareddata/swiftpm/Package.resolved | 63 +++++++++ ios/Mattermost/AppDelegate.mm | 7 + .../NotificationService.swift | 42 ++++++ 10 files changed, 313 insertions(+), 50 deletions(-) create mode 100644 ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index 517224c55ad..f252593282f 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -31,7 +31,7 @@ import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; import {isBetaApp} from '@utils/general'; import {isMainActivity, isTablet} from '@utils/helpers'; -import {logInfo} from '@utils/log'; +import {logDebug, logInfo} from '@utils/log'; import {convertToNotificationData} from '@utils/notification'; class PushNotifications { @@ -232,6 +232,10 @@ class PushNotifications { // This triggers when the app was in the background (iOS) onNotificationReceivedBackground = async (incoming: Notification, completion: (response: NotificationBackgroundFetchResult) => void) => { + if (incoming.payload.verified === 'false') { + logDebug('not handling background notification because it was not verified, ackId=', incoming.payload.ackId); + return; + } const notification = convertToNotificationData(incoming, false); this.processNotification(notification); @@ -241,6 +245,10 @@ class PushNotifications { // This triggers when the app was in the foreground (Android and iOS) // Also triggers when the app was in the background (Android) onNotificationReceivedForeground = (incoming: Notification, completion: (response: NotificationCompletion) => void) => { + if (incoming.payload.verified === 'false') { + logDebug('not handling foreground notification because it was not verified, ackId=', incoming.payload.ackId); + return; + } const notification = convertToNotificationData(incoming, false); if (AppState.currentState !== 'inactive') { notification.foreground = AppState.currentState === 'active' && isMainActivity(); diff --git a/ios/Gekidou/Package.swift b/ios/Gekidou/Package.swift index 3b3aac2979c..004fcf474b1 100644 --- a/ios/Gekidou/Package.swift +++ b/ios/Gekidou/Package.swift @@ -14,7 +14,8 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1") + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1"), + .package(url: "https://github.com/Kitura/Swift-JWT.git", from:"3.6.1") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -22,7 +23,8 @@ let package = Package( .target( name: "Gekidou", dependencies: [ - .product(name: "SQLite", package: "SQLite.swift") + .product(name: "SQLite", package: "SQLite.swift"), + .product(name: "SwiftJWT", package: "Swift-JWT"), ] ), .testTarget( diff --git a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift new file mode 100644 index 00000000000..192a1c20e55 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift @@ -0,0 +1,133 @@ +import Foundation +import UserNotifications +import os.log +import SwiftJWT + +struct NotificationClaims : Claims { + var ack_id: String; + var device_id: String; +} + +extension PushNotification { + public func verifySignatureFromNotification(_ notification: UNMutableNotificationContent) -> Bool { + return self.verifySignature(notification.userInfo) + } + public func verifySignature(_ userInfo: [AnyHashable : Any]) -> Bool { + guard let signature = userInfo["signature"] as? String + else { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: No signature in the notification" + ) + return true + } + + guard let serverId = userInfo["server_id"] as? String + else { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: No server_id in the notification" + ) + return false + } + + guard let serverUrl = try? Database.default.getServerUrlForServer(serverId) + else { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: No server_url for server_id" + ) + return false + } + + if signature == "NO_SIGNATURE" { + guard let version = Database.default.getConfig(serverUrl, "Version") + else { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: No server version" + ) + return false + } + + // Verify server version + } + + guard let signingKey = Database.default.getConfig(serverUrl, "AsymmetricSigningPublicKey") + else { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: No signing key" + ) + return false + } + + let keyPEM = """ +-----BEGIN PUBLIC KEY----- +\(signingKey) +-----END PUBLIC KEY----- +""" + let jwtVerifier = JWTVerifier.es256(publicKey: keyPEM.data(using: .utf8)!) + guard let newJWT = try? JWT(jwtString: signature, verifier: jwtVerifier) + else { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: Cannot verify the signature" + ) + return false + } + + guard let ackId = userInfo["ack_id"] as? String + else { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: No ack_id in the notification" + ) + return false + } + + if (ackId != newJWT.claims.ack_id) { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: ackId is different" + ) + return false + } + + guard let storedDeviceToken = Database.default.getDeviceToken() + else { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: No device token" + ) + return false + } + + let tokenParts = storedDeviceToken.components(separatedBy: ":") + if (tokenParts.count != 2) { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: Wrong stored deviced token format" + ) + return false + } + let deviceToken = tokenParts[1].dropLast(1) + if (deviceToken.isEmpty) { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: Empty stored deviced token" + ) + return false + } + + if (deviceToken != newJWT.claims.device_id) { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: Device token is different" + ) + return false + } + + return true + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+System.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+System.swift index 9608bf4dc55..0956ea2d59e 100644 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database+System.swift +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+System.swift @@ -10,6 +10,19 @@ import Foundation import SQLite extension Database { + public func getDeviceToken() -> String? { + if let db = try? Connection(DEFAULT_DB_PATH) { + let idCol = Expression("id") + let valueCol = Expression("value") + let query = globalTable.select(valueCol).filter(idCol == "deviceToken") + if let result = try? db.pluck(query) { + return try? result.get(valueCol) + } + } + + return nil + } + public func getConfig(_ serverUrl: String, _ key: String) -> String? { if let db = try? getDatabaseForServer(serverUrl) { let id = Expression("id") diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database.swift index 3f64c42470f..1caf68c4a7c 100644 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database.swift +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database.swift @@ -40,6 +40,7 @@ public class Database: NSObject { internal var defaultDB: OpaquePointer? = nil internal var serversTable = Table("Servers") + internal var globalTable = Table("Global") internal var systemTable = Table("System") internal var teamTable = Table("Team") internal var myTeamTable = Table("MyTeam") diff --git a/ios/GekidouWrapper.swift b/ios/GekidouWrapper.swift index 8a82055a62e..32bedf951dd 100644 --- a/ios/GekidouWrapper.swift +++ b/ios/GekidouWrapper.swift @@ -23,6 +23,10 @@ import Gekidou }) } + @objc func verifySignature(_ notification: [AnyHashable:Any]) -> Bool { + return PushNotification.default.verifySignature(notification) + } + @objc func attachSession(_ id: String, completionHandler: @escaping () -> Void) { let shareExtension = ShareExtension() shareExtension.attachSession( diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 558dff93a9c..c2eab7eaba9 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -85,7 +85,6 @@ 67FEAE292A1261A000DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE262A1261A000DDF4AE /* Localizable.strings */; }; 67FEAE2A2A1261EA00DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADFA2A1260B900DDF4AE /* Localizable.strings */; }; 67FEAE2C2A127C3600DDF4AE /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67FEAE2B2A127C3600DDF4AE /* NumberFormatter.swift */; }; - 6C9B1EFD6561083917AF06CF /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DEEFB3ED6175724A2653247 /* libPods-Mattermost.a */; }; 7F0F4B0A24BA173900E14C60 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F0F4B0924BA173900E14C60 /* LaunchScreen.storyboard */; }; 7F151D3E221B062700FAD8F3 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F151D3D221B062700FAD8F3 /* RuntimeUtils.swift */; }; 7F1EB88527FDE361002E7EEC /* GekidouWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1EB88427FDE361002E7EEC /* GekidouWrapper.swift */; }; @@ -173,6 +172,7 @@ 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; 7FEC870128A4325D00DE96CB /* NotificationsModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEC870028A4325D00DE96CB /* NotificationsModule.m */; }; 7FF9C03D2983E7C6005CDCF5 /* ErrorSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF9C03C2983E7C6005CDCF5 /* ErrorSharingView.swift */; }; + 8E061B69E78C5B8B7593F849 /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B6259F6E6406711451252DE1 /* libPods-Mattermost.a */; }; A94508A396424B2DB778AFE9 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */; }; /* End PBXBuildFile section */ @@ -228,10 +228,7 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Mattermost/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Mattermost/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Mattermost/main.m; sourceTree = ""; }; - 182D203F539AF68F1647EFAF /* Pods-Mattermost-MattermostTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost-MattermostTests.release.xcconfig"; path = "Target Support Files/Pods-Mattermost-MattermostTests/Pods-Mattermost-MattermostTests.release.xcconfig"; sourceTree = ""; }; - 25BF2BACE89201DE6E585B7E /* Pods-Mattermost.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.release.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.release.xcconfig"; sourceTree = ""; }; 27C667A8295241B600E590D5 /* Sentry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentry.swift; sourceTree = ""; }; - 297AAFCCF0BD99FC109DA2BC /* Pods-MattermostTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MattermostTests.release.xcconfig"; path = "Target Support Files/Pods-MattermostTests/Pods-MattermostTests.release.xcconfig"; sourceTree = ""; }; 32AC3D4EA79E44738A6E9766 /* OpenSans-BoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-BoldItalic.ttf"; path = "../assets/fonts/OpenSans-BoldItalic.ttf"; sourceTree = ""; }; 3647DF63D6764CF093375861 /* OpenSans-ExtraBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBold.ttf"; path = "../assets/fonts/OpenSans-ExtraBold.ttf"; sourceTree = ""; }; 41F3AFE83AAF4B74878AB78A /* OpenSans-Italic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Italic.ttf"; path = "../assets/fonts/OpenSans-Italic.ttf"; sourceTree = ""; }; @@ -245,7 +242,6 @@ 536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RNNotificationEventHandler+HandleReplyAction.m"; path = "Mattermost/RNNotificationEventHandler+HandleReplyAction.m"; sourceTree = ""; }; 536CC6C223E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "RNNotificationEventHandler+HandleReplyAction.h"; path = "Mattermost/RNNotificationEventHandler+HandleReplyAction.h"; sourceTree = ""; }; 54956DEFEBB74EF78C3A6AE5 /* Metropolis-SemiBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Metropolis-SemiBold.ttf"; path = "../assets/fonts/Metropolis-SemiBold.ttf"; sourceTree = ""; }; - 57CB4735B7E57B50D0B50E16 /* Pods-MattermostTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MattermostTests.debug.xcconfig"; path = "Target Support Files/Pods-MattermostTests/Pods-MattermostTests.debug.xcconfig"; sourceTree = ""; }; 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Light.ttf"; path = "../assets/fonts/OpenSans-Light.ttf"; sourceTree = ""; }; 672D984729F1927E004228D6 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = InfoPlist.strings; sourceTree = ""; }; 672D984A29F1927E004228D6 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = InfoPlist.strings; sourceTree = ""; }; @@ -302,7 +298,6 @@ 7F25B626270F666D00F32373 /* Metropolis-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Metropolis-Light.ttf"; path = "../assets/fonts/Metropolis-Light.ttf"; sourceTree = ""; }; 7F25B628270F666D00F32373 /* Metropolis-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Metropolis-Regular.ttf"; path = "../assets/fonts/Metropolis-Regular.ttf"; sourceTree = ""; }; 7F292A701E8AB73400A450A3 /* SplashScreenResource */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SplashScreenResource; sourceTree = ""; }; - 7F325D6DAAF1047EB948EFF7 /* Pods-Mattermost-MattermostTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost-MattermostTests.debug.xcconfig"; path = "Target Support Files/Pods-Mattermost-MattermostTests/Pods-Mattermost-MattermostTests.debug.xcconfig"; sourceTree = ""; }; 7F428809286672F6006B48E1 /* ServerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerService.swift; sourceTree = ""; }; 7F42880B2866A9C0006B48E1 /* ChannelService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelService.swift; sourceTree = ""; }; 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-Mattermost.a"; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libPods-Mattermost.a"; sourceTree = ""; }; @@ -377,13 +372,13 @@ 7FFE32BE1FD9CCAA0038C7A0 /* SDWebImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SDWebImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7FFE32BF1FD9CCAA0038C7A0 /* Sentry.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Sentry.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 81061F4CBB31484A94D5A8EE /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; - 8DEEFB3ED6175724A2653247 /* libPods-Mattermost.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Mattermost.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9847F127B1B5382CA73AC863 /* Pods-Mattermost.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.release.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.release.xcconfig"; sourceTree = ""; }; + B6259F6E6406711451252DE1 /* libPods-Mattermost.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Mattermost.a"; sourceTree = BUILT_PRODUCTS_DIR; }; BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Regular.ttf"; path = "../assets/fonts/OpenSans-Regular.ttf"; sourceTree = ""; }; BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-LightItalic.ttf"; path = "../assets/fonts/OpenSans-LightItalic.ttf"; sourceTree = ""; }; D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Bold.ttf"; path = "../assets/fonts/OpenSans-Bold.ttf"; sourceTree = ""; }; + DAC611E794843938817D96FD /* Pods-Mattermost.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.debug.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.debug.xcconfig"; sourceTree = ""; }; E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-SemiBold.ttf"; path = "../assets/fonts/OpenSans-SemiBold.ttf"; sourceTree = ""; }; - EB4F0DF36537B0B21BE962FB /* Pods-Mattermost.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.debug.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.debug.xcconfig"; sourceTree = ""; }; - F41672974C2907F74BB59B16 /* libPods-Mattermost-MattermostTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Mattermost-MattermostTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBoldItalic.ttf"; path = "../assets/fonts/OpenSans-ExtraBoldItalic.ttf"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -393,7 +388,7 @@ buildActionMask = 2147483647; files = ( 7F98836227FD46A9001C9BFC /* Gekidou in Frameworks */, - 6C9B1EFD6561083917AF06CF /* libPods-Mattermost.a in Frameworks */, + 8E061B69E78C5B8B7593F849 /* libPods-Mattermost.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -510,12 +505,8 @@ 33E107B4DC21A5C48B09F800 /* Pods */ = { isa = PBXGroup; children = ( - EB4F0DF36537B0B21BE962FB /* Pods-Mattermost.debug.xcconfig */, - 25BF2BACE89201DE6E585B7E /* Pods-Mattermost.release.xcconfig */, - 57CB4735B7E57B50D0B50E16 /* Pods-MattermostTests.debug.xcconfig */, - 297AAFCCF0BD99FC109DA2BC /* Pods-MattermostTests.release.xcconfig */, - 7F325D6DAAF1047EB948EFF7 /* Pods-Mattermost-MattermostTests.debug.xcconfig */, - 182D203F539AF68F1647EFAF /* Pods-Mattermost-MattermostTests.release.xcconfig */, + DAC611E794843938817D96FD /* Pods-Mattermost.debug.xcconfig */, + 9847F127B1B5382CA73AC863 /* Pods-Mattermost.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -549,8 +540,7 @@ 7FFE32BF1FD9CCAA0038C7A0 /* Sentry.framework */, 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */, 81061F4CBB31484A94D5A8EE /* libz.tbd */, - 8DEEFB3ED6175724A2653247 /* libPods-Mattermost.a */, - F41672974C2907F74BB59B16 /* libPods-Mattermost-MattermostTests.a */, + B6259F6E6406711451252DE1 /* libPods-Mattermost.a */, ); name = Frameworks; sourceTree = ""; @@ -1038,7 +1028,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Mattermost" */; buildPhases = ( - 484322B86D8320281D95970F /* [CP] Check Pods Manifest.lock */, + 8503FB740C671C04DC5FB9DD /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, @@ -1046,8 +1036,8 @@ 37DA4BA41E6F55AD002B058E /* Embed Frameworks */, AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */, 7FFE32A91FD9CB650038C7A0 /* Embed App Extensions */, - ED4C644925C525E30315E09E /* [CP] Copy Pods Resources */, - 791C6C6593EFE251279CC4E9 /* [CP] Embed Pods Frameworks */, + 7C90C661437FD59A4C33AFC3 /* [CP] Embed Pods Frameworks */, + E803C65CE5C895F063C6D8BB /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1339,29 +1329,7 @@ shellPath = /bin/sh; shellScript = "./bundleReactNative.sh\n"; }; - 484322B86D8320281D95970F /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Mattermost-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 791C6C6593EFE251279CC4E9 /* [CP] Embed Pods Frameworks */ = { + 7C90C661437FD59A4C33AFC3 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1387,6 +1355,28 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Mattermost/Pods-Mattermost-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + 8503FB740C671C04DC5FB9DD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Mattermost-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1401,7 +1391,7 @@ shellPath = /bin/sh; shellScript = "./uploadDebugSymbols.sh\n"; }; - ED4C644925C525E30315E09E /* [CP] Copy Pods Resources */ = { + E803C65CE5C895F063C6D8BB /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1926,7 +1916,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EB4F0DF36537B0B21BE962FB /* Pods-Mattermost.debug.xcconfig */; + baseConfigurationReference = DAC611E794843938817D96FD /* Pods-Mattermost.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -1970,7 +1960,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 25BF2BACE89201DE6E585B7E /* Pods-Mattermost.release.xcconfig */; + baseConfigurationReference = 9847F127B1B5382CA73AC863 /* Pods-Mattermost.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; diff --git a/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5b005ea4707..12fa8c31332 100644 --- a/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,51 @@ { "object": { "pins": [ + { + "package": "Cryptor", + "repositoryURL": "https://github.com/Kitura/BlueCryptor.git", + "state": { + "branch": null, + "revision": "cec97c24b111351e70e448972a7d3fe68a756d6d", + "version": "2.0.2" + } + }, + { + "package": "CryptorECC", + "repositoryURL": "https://github.com/Kitura/BlueECC.git", + "state": { + "branch": null, + "revision": "1485268a54f8135435a825a855e733f026fa6cc8", + "version": "1.2.201" + } + }, + { + "package": "CryptorRSA", + "repositoryURL": "https://github.com/Kitura/BlueRSA.git", + "state": { + "branch": null, + "revision": "440f78db26d8bb073f29590f1c7bd31004da09ae", + "version": "1.0.201" + } + }, + { + "package": "KituraContracts", + "repositoryURL": "https://github.com/Kitura/KituraContracts.git", + "state": { + "branch": null, + "revision": "8a4778c3aa7833e9e1af884e8819d436c237cd70", + "version": "1.2.201" + } + }, + { + "package": "LoggerAPI", + "repositoryURL": "https://github.com/Kitura/LoggerAPI.git", + "state": { + "branch": null, + "revision": "e82d34eab3f0b05391082b11ea07d3b70d2f65bb", + "version": "1.9.200" + } + }, { "package": "OpenGraph", "repositoryURL": "https://github.com/satoshi-takano/OpenGraph.git", @@ -27,6 +72,24 @@ "revision": "7a2e3cd27de56f6d396e84f63beefd0267b55ccb", "version": "0.14.1" } + }, + { + "package": "SwiftJWT", + "repositoryURL": "https://github.com/Kitura/Swift-JWT.git", + "state": { + "branch": null, + "revision": "47c6384b6923e9bb1f214d2ba4bd52af39440588", + "version": "3.6.201" + } + }, + { + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", + "state": { + "branch": null, + "revision": "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version": "1.5.4" + } } ] }, diff --git a/ios/Mattermost/AppDelegate.mm b/ios/Mattermost/AppDelegate.mm index a318db4bb82..104569fcc51 100644 --- a/ios/Mattermost/AppDelegate.mm +++ b/ios/Mattermost/AppDelegate.mm @@ -84,6 +84,13 @@ -(void)application:(UIApplication *)application didReceiveRemoteNotification:(no return; } + if (![[GekidouWrapper default] verifySignature:userInfo]) { + NSMutableDictionary *notification = [userInfo mutableCopy]; + [notification setValue:@"false" forKey:@"verified"]; + [RNNotifications didReceiveBackgroundNotification:notification withCompletionHandler:completionHandler]; + return; + } + if (isClearAction) { // When CRT is OFF: // If received a notification that a channel was read, remove all notifications from that channel (only with app in foreground/background) diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index abb06f6f4d3..a66be0a248a 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -20,6 +20,10 @@ class NotificationService: UNNotificationServiceExtension { PushNotification.default.postNotificationReceipt(bestAttemptContent, completionHandler: {[weak self] notification in if let notification = notification { self?.bestAttemptContent = notification + if (!PushNotification.default.verifySignatureFromNotification(notification)) { + self?.sendInvalidNotificationIntent() + return + } if (Gekidou.Preferences.default.object(forKey: "ApplicationIsRunning") as? String != "true") { PushNotification.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: {notification in os_log(OSLogType.default, "Mattermost Notifications: processed data for db. Will call sendMessageIntent") @@ -75,6 +79,44 @@ class NotificationService: UNNotificationServiceExtension { } } + private func sendInvalidNotificationIntent() { + guard let notification = bestAttemptContent else { return } + if #available(iOSApplicationExtension 15.0, *) { + os_log(OSLogType.default, "Mattermost Notifications: creating invalid intent") + + bestAttemptContent?.body = "We could not verify this notification with the server" + bestAttemptContent?.userInfo.updateValue("false", forKey: "verified") + let intent = INSendMessageIntent(recipients: nil, + outgoingMessageType: .outgoingMessageText, + content: "We could not verify this notification with the server", + speakableGroupName: nil, + conversationIdentifier: "NOT_VERIFIED", + serviceName: nil, + sender: nil, + attachments: nil) + + let interaction = INInteraction(intent: intent, response: nil) + interaction.direction = .incoming + interaction.donate { error in + if error != nil { + self.contentHandler?(notification) + os_log(OSLogType.default, "Mattermost Notifications: sendMessageIntent intent error %{public}@", error! as CVarArg) + } + + do { + let updatedContent = try notification.updating(from: intent) + os_log(OSLogType.default, "Mattermost Notifications: present updated notification") + self.contentHandler?(updatedContent) + } catch { + os_log(OSLogType.default, "Mattermost Notifications: something failed updating the notification %{public}@", error as CVarArg) + self.contentHandler?(notification) + } + } + } else { + self.contentHandler?(notification) + } + } + private func sendMessageIntentCompletion(_ avatarData: Data?) { guard let notification = bestAttemptContent else { return } if #available(iOSApplicationExtension 15.0, *), From 811f579b10fd40fa8cf483d30f710db1fb2ac77d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 27 Mar 2024 18:02:33 +0100 Subject: [PATCH 02/10] Add android --- android/app/build.gradle | 6 ++ android/app/proguard-rules.pro | 5 + .../helpers/CustomPushNotificationHelper.java | 94 +++++++++++++++++++ .../helpers/database_extension/General.kt | 18 +++- .../helpers/database_extension/System.kt | 8 ++ .../rnbeta/CustomPushNotification.java | 6 ++ .../PushNotification+Signature.swift | 4 +- 7 files changed, 138 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 0428010df70..73368eef0ae 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -217,6 +217,12 @@ dependencies { androidTestImplementation('com.wix:detox:+') implementation project(':reactnativenotifications') implementation project(':watermelondb-jsi') + + api('io.jsonwebtoken:jjwt-api:0.12.5') + runtimeOnly('io.jsonwebtoken:jjwt-impl:0.12.5') + runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.12.5') { + exclude(group: 'org.json', module: 'json') //provided by Android natively + } } configurations.all { diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 11b025724a3..2f86106f0f6 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -8,3 +8,8 @@ # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: +-keepattributes InnerClasses + +-keep class io.jsonwebtoken.** { *; } +-keepnames class io.jsonwebtoken.* { *; } +-keepnames interface io.jsonwebtoken.* { *; } diff --git a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java index 392f5f1b799..e34aebd7a2e 100644 --- a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java +++ b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java @@ -32,14 +32,23 @@ import com.nozbe.watermelondb.WMDatabase; import java.io.IOException; +import java.security.PublicKey; import java.util.Date; import java.util.Objects; +import io.jsonwebtoken.IncorrectClaimException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MissingClaimException; +import io.jsonwebtoken.security.Jwks; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import static com.mattermost.helpers.database_extension.GeneralKt.getDatabaseForServer; +import static com.mattermost.helpers.database_extension.GeneralKt.getDeviceToken; +import static com.mattermost.helpers.database_extension.SystemKt.queryConfigServerVersion; +import static com.mattermost.helpers.database_extension.SystemKt.queryConfigSigningKey; import static com.mattermost.helpers.database_extension.UserKt.getLastPictureUpdate; public class CustomPushNotificationHelper { @@ -227,6 +236,91 @@ public static void createNotificationChannels(Context context) { } } + public static boolean verifySignature(final Context context, String signature, String serverUrl, String ackId) { + if (signature == null) { + Log.i("Mattermost Notifications Signature verification", "No signature in the notification"); + return true; + } + + signature = "NO_SIGNATURE"; + if (serverUrl == null) { + Log.i("Mattermost Notifications Signature verification", "No server_url for server_id"); + return false; + } + + DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance(); + if (dbHelper == null) { + Log.i("Mattermost Notifications Signature verification", "Cannot access the database"); + return false; + } + + WMDatabase db = getDatabaseForServer(dbHelper, context, serverUrl); + if (db == null) { + Log.i("Mattermost Notifications Signature verification", "Cannot access the server database"); + return false; + } + + if (signature == "NO_SIGNATURE") { + String version = queryConfigServerVersion(db); + if (version == null) { + Log.i("Mattermost Notifications Signature verification", "No server version"); + return false; + } + + // TODO: Verify version + } + + String signingKey = queryConfigSigningKey(db); + if (signingKey == null) { + Log.i("Mattermost Notifications Signature verification", "No signing key"); + return false; + } + + try { + String storedDeviceToken = getDeviceToken(dbHelper); + if (storedDeviceToken == null) { + Log.i("Mattermost Notifications Signature verification", "No device token stored"); + return false; + } + String[] tokenParts = storedDeviceToken.split(":", 2); + if (tokenParts.length != 2) { + Log.i("Mattermost Notifications Signature verification", "Wrong stored device token format"); + return false; + } + String deviceToken = tokenParts[1].substring(0, tokenParts[1].length() -1 ); + if (deviceToken.isEmpty()) { + Log.i("Mattermost Notifications Signature verification", "Empty stored device token"); + return false; + } + + PublicKey parsed = (PublicKey) Jwks.parser().build().parse(signingKey); + Jwts.parser() + .require("ack_id", ackId) + .require("device_id", deviceToken) + .verifyWith((PublicKey) parsed) + .build() + .parseSignedClaims(signature); + } catch (MissingClaimException e) { + Log.i("Mattermost Notifications Signature verification", String.format("Missing claim: %s", e.getMessage())); + e.printStackTrace(); + return false; + } catch (IncorrectClaimException e) { + Log.i("Mattermost Notifications Signature verification", String.format("Incorrect claim: %s", e.getMessage())); + e.printStackTrace(); + return false; + } catch (JwtException e) { + Log.i("Mattermost Notifications Signature verification", String.format("Cannot verify JWT: %s", e.getMessage())); + e.printStackTrace(); + return false; + } catch (Exception e) { + Log.i("Mattermost Notifications Signature verification", String.format("Exception while parsing JWT: %s", e.getMessage())); + e.printStackTrace(); + return false; + } + + return true; + } + private static Bitmap getCircleBitmap(Bitmap bitmap) { final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt index 64c31b8cffe..a3789cbabdf 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt @@ -56,7 +56,7 @@ fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): W defaultDatabase!!.rawQuery(query, arrayOf(serverUrl)).use { cursor -> if (cursor.count == 1) { cursor.moveToFirst() - val databasePath = cursor.getString(0) + val databasePath = String.format("file://%s", cursor.getString(0)) return WMDatabase.getInstance(databasePath, context!!) } } @@ -67,6 +67,22 @@ fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): W return null } +fun DatabaseHelper.getDeviceToken(): String? { + try { + val query = "SELECT value FROM Global WHERE id=?" + defaultDatabase!!.rawQuery(query, arrayOf("deviceToken")).use { cursor -> + if (cursor.count == 1) { + cursor.moveToFirst() + return cursor.getString(0); + } + } + } catch (e: Exception) { + e.printStackTrace() + } + + return null +} + fun find(db: WMDatabase, tableName: String, id: String?): ReadableMap? { try { db.rawQuery( diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/System.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/System.kt index efbf4844936..147448b45c7 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/System.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/System.kt @@ -30,3 +30,11 @@ fun queryConfigDisplayNameSetting(db: WMDatabase): String? { return null } + +fun queryConfigSigningKey(db: WMDatabase): String? { + return find(db, "Config", "AsymmetricSigningPublicKey")?.getString("value") +} + +fun queryConfigServerVersion(db: WMDatabase): String? { + return find(db, "Config", "Version")?.getString("value") +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java index 04d77fb7e0c..b0054eb23bb 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java @@ -53,6 +53,7 @@ public void onReceived() { final String ackId = initialData.getString("ack_id"); final String postId = initialData.getString("post_id"); final String channelId = initialData.getString("channel_id"); + final String signature = initialData.getString("signature"); final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true"); int notificationId = NotificationHelper.getNotificationId(initialData); @@ -70,6 +71,11 @@ public void onReceived() { } } + if (!CustomPushNotificationHelper.verifySignature(mContext, signature, serverUrl, ackId)) { + Log.i("Mattermost Notifications Signature verification", "Notification skipped because we could not verify it."); + return; + } + finishProcessingNotification(serverUrl, type, channelId, notificationId); } diff --git a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift index 192a1c20e55..1179f3d2787 100644 --- a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift @@ -107,7 +107,7 @@ extension PushNotification { if (tokenParts.count != 2) { os_log( OSLogType.default, - "Mattermost Notifications: Signature verification: Wrong stored deviced token format" + "Mattermost Notifications: Signature verification: Wrong stored device token format" ) return false } @@ -115,7 +115,7 @@ extension PushNotification { if (deviceToken.isEmpty) { os_log( OSLogType.default, - "Mattermost Notifications: Signature verification: Empty stored deviced token" + "Mattermost Notifications: Signature verification: Empty stored device token" ) return false } From 57b6ee52666c0b9cc6712874d2b00f1fa70f07ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 2 Apr 2024 12:09:19 +0200 Subject: [PATCH 03/10] Android fixes and version checking --- .../helpers/CustomPushNotificationHelper.java | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java index e34aebd7a2e..89592ba1d59 100644 --- a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java +++ b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java @@ -19,6 +19,7 @@ import android.os.Build; import android.os.Bundle; import android.text.TextUtils; +import android.util.Base64; import android.util.Log; import androidx.annotation.NonNull; @@ -32,7 +33,9 @@ import com.nozbe.watermelondb.WMDatabase; import java.io.IOException; +import java.security.KeyFactory; import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; import java.util.Date; import java.util.Objects; @@ -40,7 +43,6 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MissingClaimException; -import io.jsonwebtoken.security.Jwks; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -242,7 +244,6 @@ public static boolean verifySignature(final Context context, String signature, S return true; } - signature = "NO_SIGNATURE"; if (serverUrl == null) { Log.i("Mattermost Notifications Signature verification", "No server_url for server_id"); return false; @@ -267,7 +268,31 @@ public static boolean verifySignature(final Context context, String signature, S return false; } - // TODO: Verify version + if (!version.matches("[0-9]+(\\.[0-9]+)*")) { + Log.i("Mattermost Notifications Signature verification", "Invalid server version"); + return false; + } + + String[] parts = version.split("\\."); + int mayor = parts.length > 0 ? Integer.parseInt(parts[0]) : 0; + int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + int patch = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + int mayorTarget = 9; + int minorTarget = 7; + int patchTarget = 0; + + if (mayor < mayorTarget) { + return true; + } + if (mayor == mayorTarget && minor < minorTarget) { + return true; + } + if (mayor == mayorTarget && minor == minorTarget && patch < patchTarget) { + return true; + } + + Log.i("Mattermost Notifications Signature verification", "Server version should send signature"); + return false; } String signingKey = queryConfigSigningKey(db); @@ -277,6 +302,10 @@ public static boolean verifySignature(final Context context, String signature, S } try { + byte[] encoded = Base64.decode(signingKey, 0); + KeyFactory kf = KeyFactory.getInstance("EC"); + PublicKey pubKey = (PublicKey) kf.generatePublic(new X509EncodedKeySpec(encoded)); + String storedDeviceToken = getDeviceToken(dbHelper); if (storedDeviceToken == null) { Log.i("Mattermost Notifications Signature verification", "No device token stored"); @@ -293,11 +322,10 @@ public static boolean verifySignature(final Context context, String signature, S return false; } - PublicKey parsed = (PublicKey) Jwks.parser().build().parse(signingKey); Jwts.parser() .require("ack_id", ackId) .require("device_id", deviceToken) - .verifyWith((PublicKey) parsed) + .verifyWith((PublicKey) pubKey) .build() .parseSignedClaims(signature); } catch (MissingClaimException e) { From 707c1ce42f8960726da6735b22cfe7e081f035f4 Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Tue, 2 Apr 2024 12:11:55 +0200 Subject: [PATCH 04/10] Add version check to ios --- .../PushNotification/PushNotification+Signature.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift index 1179f3d2787..baad62e17e9 100644 --- a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift @@ -50,7 +50,15 @@ extension PushNotification { return false } - // Verify server version + let versionTarget = "9.7.0" + let versionOrder = version.compare(versionTarget, options: .numeric) + if (versionOrder == .orderedSame || versionOrder == .orderedDescending) { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: Server version should send signature" + ) + return false + } } guard let signingKey = Database.default.getConfig(serverUrl, "AsymmetricSigningPublicKey") From 402a5925ea9792221273194e56f2789d0859354f Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Fri, 12 Apr 2024 18:17:11 +0200 Subject: [PATCH 05/10] Address feedback --- app/constants/push_notification.ts | 8 ++ assets/base/i18n/en.json | 1 + ios/Mattermost.xcodeproj/project.pbxproj | 84 +++++++++++-------- .../NotificationService.swift | 11 ++- 4 files changed, 63 insertions(+), 41 deletions(-) diff --git a/app/constants/push_notification.ts b/app/constants/push_notification.ts index 37ed85d3b17..c1b6031123e 100644 --- a/app/constants/push_notification.ts +++ b/app/constants/push_notification.ts @@ -1,6 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {defineMessage} from 'react-intl'; + +// Needed for localization on iOS native side +export const notVerifiedErrorMessage = defineMessage({ + id: 'native.ios.notifications.not_verified', + defaultMessage: 'We could not verify this notification with the server', +}); + export const CATEGORY = 'CAN_REPLY'; export const REPLY_ACTION = 'REPLY_ACTION'; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index d097253c740..5aca7fbd686 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -766,6 +766,7 @@ "more_messages.text": "{count} new {count, plural, one {message} other {messages}}", "msg_typing.areTyping": "{users} and {last} are typing...", "msg_typing.isTyping": "{user} is typing...", + "native.ios.notifications.not_verified": "We could not verify this notification with the server", "notification_settings.auto_responder": "Automatic Replies", "notification_settings.auto_responder.default_message": "Hello, I am out of office and unable to respond to messages.", "notification_settings.auto_responder.footer.message": "Set a custom message that is automatically sent in response to direct messages, such as an out of office or vacation reply. Enabling this setting changes your status to Out of Office and disables notifications.", diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index abc3f731eab..0a6fe0948d7 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 67FEAE292A1261A000DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE262A1261A000DDF4AE /* Localizable.strings */; }; 67FEAE2A2A1261EA00DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADFA2A1260B900DDF4AE /* Localizable.strings */; }; 67FEAE2C2A127C3600DDF4AE /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67FEAE2B2A127C3600DDF4AE /* NumberFormatter.swift */; }; + 6C9B1EFD6561083917AF06CF /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DEEFB3ED6175724A2653247 /* libPods-Mattermost.a */; }; 7F0F4B0A24BA173900E14C60 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F0F4B0924BA173900E14C60 /* LaunchScreen.storyboard */; }; 7F151D3E221B062700FAD8F3 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F151D3D221B062700FAD8F3 /* RuntimeUtils.swift */; }; 7F1EB88527FDE361002E7EEC /* GekidouWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F1EB88427FDE361002E7EEC /* GekidouWrapper.swift */; }; @@ -172,7 +173,6 @@ 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; 7FEC870128A4325D00DE96CB /* NotificationsModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEC870028A4325D00DE96CB /* NotificationsModule.m */; }; 7FF9C03D2983E7C6005CDCF5 /* ErrorSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF9C03C2983E7C6005CDCF5 /* ErrorSharingView.swift */; }; - 8E061B69E78C5B8B7593F849 /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B6259F6E6406711451252DE1 /* libPods-Mattermost.a */; }; A94508A396424B2DB778AFE9 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */; }; C9A107102BBD7C8700753CDC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = C9A1070F2BBD7C8700753CDC /* PrivacyInfo.xcprivacy */; }; C9A107122BBDA00200753CDC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = C9A107112BBDA00200753CDC /* PrivacyInfo.xcprivacy */; }; @@ -231,7 +231,10 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Mattermost/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Mattermost/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Mattermost/main.m; sourceTree = ""; }; + 182D203F539AF68F1647EFAF /* Pods-Mattermost-MattermostTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost-MattermostTests.release.xcconfig"; path = "Target Support Files/Pods-Mattermost-MattermostTests/Pods-Mattermost-MattermostTests.release.xcconfig"; sourceTree = ""; }; + 25BF2BACE89201DE6E585B7E /* Pods-Mattermost.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.release.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.release.xcconfig"; sourceTree = ""; }; 27C667A8295241B600E590D5 /* Sentry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentry.swift; sourceTree = ""; }; + 297AAFCCF0BD99FC109DA2BC /* Pods-MattermostTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MattermostTests.release.xcconfig"; path = "Target Support Files/Pods-MattermostTests/Pods-MattermostTests.release.xcconfig"; sourceTree = ""; }; 32AC3D4EA79E44738A6E9766 /* OpenSans-BoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-BoldItalic.ttf"; path = "../assets/fonts/OpenSans-BoldItalic.ttf"; sourceTree = ""; }; 3647DF63D6764CF093375861 /* OpenSans-ExtraBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBold.ttf"; path = "../assets/fonts/OpenSans-ExtraBold.ttf"; sourceTree = ""; }; 41F3AFE83AAF4B74878AB78A /* OpenSans-Italic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Italic.ttf"; path = "../assets/fonts/OpenSans-Italic.ttf"; sourceTree = ""; }; @@ -245,6 +248,7 @@ 536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RNNotificationEventHandler+HandleReplyAction.m"; path = "Mattermost/RNNotificationEventHandler+HandleReplyAction.m"; sourceTree = ""; }; 536CC6C223E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "RNNotificationEventHandler+HandleReplyAction.h"; path = "Mattermost/RNNotificationEventHandler+HandleReplyAction.h"; sourceTree = ""; }; 54956DEFEBB74EF78C3A6AE5 /* Metropolis-SemiBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Metropolis-SemiBold.ttf"; path = "../assets/fonts/Metropolis-SemiBold.ttf"; sourceTree = ""; }; + 57CB4735B7E57B50D0B50E16 /* Pods-MattermostTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MattermostTests.debug.xcconfig"; path = "Target Support Files/Pods-MattermostTests/Pods-MattermostTests.debug.xcconfig"; sourceTree = ""; }; 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Light.ttf"; path = "../assets/fonts/OpenSans-Light.ttf"; sourceTree = ""; }; 672D984729F1927E004228D6 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = InfoPlist.strings; sourceTree = ""; }; 672D984A29F1927E004228D6 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = InfoPlist.strings; sourceTree = ""; }; @@ -301,6 +305,7 @@ 7F25B626270F666D00F32373 /* Metropolis-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Metropolis-Light.ttf"; path = "../assets/fonts/Metropolis-Light.ttf"; sourceTree = ""; }; 7F25B628270F666D00F32373 /* Metropolis-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Metropolis-Regular.ttf"; path = "../assets/fonts/Metropolis-Regular.ttf"; sourceTree = ""; }; 7F292A701E8AB73400A450A3 /* SplashScreenResource */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SplashScreenResource; sourceTree = ""; }; + 7F325D6DAAF1047EB948EFF7 /* Pods-Mattermost-MattermostTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost-MattermostTests.debug.xcconfig"; path = "Target Support Files/Pods-Mattermost-MattermostTests/Pods-Mattermost-MattermostTests.debug.xcconfig"; sourceTree = ""; }; 7F428809286672F6006B48E1 /* ServerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerService.swift; sourceTree = ""; }; 7F42880B2866A9C0006B48E1 /* ChannelService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelService.swift; sourceTree = ""; }; 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-Mattermost.a"; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libPods-Mattermost.a"; sourceTree = ""; }; @@ -375,16 +380,16 @@ 7FFE32BE1FD9CCAA0038C7A0 /* SDWebImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SDWebImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7FFE32BF1FD9CCAA0038C7A0 /* Sentry.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Sentry.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 81061F4CBB31484A94D5A8EE /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; - 9847F127B1B5382CA73AC863 /* Pods-Mattermost.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.release.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.release.xcconfig"; sourceTree = ""; }; - B6259F6E6406711451252DE1 /* libPods-Mattermost.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Mattermost.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 8DEEFB3ED6175724A2653247 /* libPods-Mattermost.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Mattermost.a"; sourceTree = BUILT_PRODUCTS_DIR; }; BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Regular.ttf"; path = "../assets/fonts/OpenSans-Regular.ttf"; sourceTree = ""; }; BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-LightItalic.ttf"; path = "../assets/fonts/OpenSans-LightItalic.ttf"; sourceTree = ""; }; C9A1070F2BBD7C8700753CDC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; C9A107112BBDA00200753CDC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; C9A107132BBDBC8F00753CDC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Bold.ttf"; path = "../assets/fonts/OpenSans-Bold.ttf"; sourceTree = ""; }; - DAC611E794843938817D96FD /* Pods-Mattermost.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.debug.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.debug.xcconfig"; sourceTree = ""; }; E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-SemiBold.ttf"; path = "../assets/fonts/OpenSans-SemiBold.ttf"; sourceTree = ""; }; + EB4F0DF36537B0B21BE962FB /* Pods-Mattermost.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.debug.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.debug.xcconfig"; sourceTree = ""; }; + F41672974C2907F74BB59B16 /* libPods-Mattermost-MattermostTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Mattermost-MattermostTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBoldItalic.ttf"; path = "../assets/fonts/OpenSans-ExtraBoldItalic.ttf"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -394,7 +399,7 @@ buildActionMask = 2147483647; files = ( 7F98836227FD46A9001C9BFC /* Gekidou in Frameworks */, - 8E061B69E78C5B8B7593F849 /* libPods-Mattermost.a in Frameworks */, + 6C9B1EFD6561083917AF06CF /* libPods-Mattermost.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -512,8 +517,12 @@ 33E107B4DC21A5C48B09F800 /* Pods */ = { isa = PBXGroup; children = ( - DAC611E794843938817D96FD /* Pods-Mattermost.debug.xcconfig */, - 9847F127B1B5382CA73AC863 /* Pods-Mattermost.release.xcconfig */, + EB4F0DF36537B0B21BE962FB /* Pods-Mattermost.debug.xcconfig */, + 25BF2BACE89201DE6E585B7E /* Pods-Mattermost.release.xcconfig */, + 57CB4735B7E57B50D0B50E16 /* Pods-MattermostTests.debug.xcconfig */, + 297AAFCCF0BD99FC109DA2BC /* Pods-MattermostTests.release.xcconfig */, + 7F325D6DAAF1047EB948EFF7 /* Pods-Mattermost-MattermostTests.debug.xcconfig */, + 182D203F539AF68F1647EFAF /* Pods-Mattermost-MattermostTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -547,7 +556,8 @@ 7FFE32BF1FD9CCAA0038C7A0 /* Sentry.framework */, 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */, 81061F4CBB31484A94D5A8EE /* libz.tbd */, - B6259F6E6406711451252DE1 /* libPods-Mattermost.a */, + 8DEEFB3ED6175724A2653247 /* libPods-Mattermost.a */, + F41672974C2907F74BB59B16 /* libPods-Mattermost-MattermostTests.a */, ); name = Frameworks; sourceTree = ""; @@ -1037,7 +1047,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Mattermost" */; buildPhases = ( - 8503FB740C671C04DC5FB9DD /* [CP] Check Pods Manifest.lock */, + 484322B86D8320281D95970F /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, @@ -1045,8 +1055,8 @@ 37DA4BA41E6F55AD002B058E /* Embed Frameworks */, AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */, 7FFE32A91FD9CB650038C7A0 /* Embed App Extensions */, - 7C90C661437FD59A4C33AFC3 /* [CP] Embed Pods Frameworks */, - E803C65CE5C895F063C6D8BB /* [CP] Copy Pods Resources */, + ED4C644925C525E30315E09E /* [CP] Copy Pods Resources */, + 791C6C6593EFE251279CC4E9 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -1341,7 +1351,29 @@ shellPath = /bin/sh; shellScript = "./bundleReactNative.sh\n"; }; - 7C90C661437FD59A4C33AFC3 /* [CP] Embed Pods Frameworks */ = { + 484322B86D8320281D95970F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Mattermost-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 791C6C6593EFE251279CC4E9 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1367,28 +1399,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Mattermost/Pods-Mattermost-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 8503FB740C671C04DC5FB9DD /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Mattermost-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1403,7 +1413,7 @@ shellPath = /bin/sh; shellScript = "./uploadDebugSymbols.sh\n"; }; - E803C65CE5C895F063C6D8BB /* [CP] Copy Pods Resources */ = { + ED4C644925C525E30315E09E /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1929,7 +1939,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DAC611E794843938817D96FD /* Pods-Mattermost.debug.xcconfig */; + baseConfigurationReference = EB4F0DF36537B0B21BE962FB /* Pods-Mattermost.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -1973,7 +1983,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9847F127B1B5382CA73AC863 /* Pods-Mattermost.release.xcconfig */; + baseConfigurationReference = 25BF2BACE89201DE6E585B7E /* Pods-Mattermost.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index a66be0a248a..22942dd4096 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -81,11 +81,14 @@ class NotificationService: UNNotificationServiceExtension { private func sendInvalidNotificationIntent() { guard let notification = bestAttemptContent else { return } + os_log(OSLogType.default, "Mattermost Notifications: creating invalid intent") + + bestAttemptContent?.body = NSLocalizedString( "native.ios.notifications.not_verified", + value: "We could not verify this notification with the server", + comment: "") + bestAttemptContent?.userInfo.updateValue("false", forKey: "verified") + if #available(iOSApplicationExtension 15.0, *) { - os_log(OSLogType.default, "Mattermost Notifications: creating invalid intent") - - bestAttemptContent?.body = "We could not verify this notification with the server" - bestAttemptContent?.userInfo.updateValue("false", forKey: "verified") let intent = INSendMessageIntent(recipients: nil, outgoingMessageType: .outgoingMessageText, content: "We could not verify this notification with the server", From 1c9603ac0c0602695fb5833b4a921f21139b35df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Mon, 15 Apr 2024 11:26:39 +0200 Subject: [PATCH 06/10] Add all versions to android --- .../helpers/CustomPushNotificationHelper.java | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java index 89592ba1d59..ba69b1a1794 100644 --- a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java +++ b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java @@ -261,7 +261,7 @@ public static boolean verifySignature(final Context context, String signature, S return false; } - if (signature == "NO_SIGNATURE") { + if (signature.equals("NO_SIGNATURE")) { String version = queryConfigServerVersion(db); if (version == null) { Log.i("Mattermost Notifications Signature verification", "No server version"); @@ -274,25 +274,55 @@ public static boolean verifySignature(final Context context, String signature, S } String[] parts = version.split("\\."); - int mayor = parts.length > 0 ? Integer.parseInt(parts[0]) : 0; + int major = parts.length > 0 ? Integer.parseInt(parts[0]) : 0; int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; int patch = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; - int mayorTarget = 9; - int minorTarget = 7; - int patchTarget = 0; - if (mayor < mayorTarget) { - return true; - } - if (mayor == mayorTarget && minor < minorTarget) { + int[][] targets = {{9,8,0},{9,7,2},{9,6,2},{9,5,4},{8,1,13}}; + boolean rejected = false; + for (int[] targetVersion: targets) { + int majorTarget = targetVersion[0]; + int minorTarget = targetVersion[1]; + int patchTarget = targetVersion[2]; + + if (major > majorTarget) { + rejected = true; + break; + } + + if (major < majorTarget) { + // Continue to see if it complies with a smaller target + continue; + } + + // Same major + if (minor > minorTarget) { + rejected = true; + break; + } + + if (minor < minorTarget) { + // Continue to see if it complies with a smaller target + continue; + } + + // Same major and same minor + if (patch >= patchTarget) { + rejected = true; + break; + } + + // Patch is lower than target return true; } - if (mayor == mayorTarget && minor == minorTarget && patch < patchTarget) { - return true; + + if (rejected) { + Log.i("Mattermost Notifications Signature verification", "Server version should send signature"); + return false; } - Log.i("Mattermost Notifications Signature verification", "Server version should send signature"); - return false; + // Version number is below any of the targets, so it should not send the signature + return true; } String signingKey = queryConfigSigningKey(db); From 63ef8458e88271d9ba432f0f4280e984a2f3d3f5 Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Mon, 15 Apr 2024 12:10:42 +0200 Subject: [PATCH 07/10] Check all versions on iOS --- .../PushNotification+Signature.swift | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift index baad62e17e9..dc1d42fdb3a 100644 --- a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift @@ -50,15 +50,73 @@ extension PushNotification { return false } - let versionTarget = "9.7.0" - let versionOrder = version.compare(versionTarget, options: .numeric) - if (versionOrder == .orderedSame || versionOrder == .orderedDescending) { + let parts = version.components(separatedBy: "."); + if (parts.count < 3) { os_log( OSLogType.default, - "Mattermost Notifications: Signature verification: Server version should send signature" + "Mattermost Notifications: Signature verification: Invalid server version" ) return false } + guard let major = Int(parts[0]), + let minor = Int(parts[1]), + let patch = Int(parts[2]) + else { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: Invalid server version" + ) + return false + } + + let versionTargets = [[9,8,0], [9,7,2], [9,6,2], [9,5,4], [8,1,13]] + var rejected = false + for versionTarget in versionTargets { + let majorTarget = versionTarget[0] + let minorTarget = versionTarget[1] + let patchTarget = versionTarget[2] + + if (major > majorTarget) { + rejected = true; + break; + } + + if (major < majorTarget) { + // Continue to see if it complies with a smaller target + continue; + } + + // Same major + if (minor > minorTarget) { + rejected = true; + break; + } + + if (minor < minorTarget) { + // Continue to see if it complies with a smaller target + continue; + } + + // Same major and same minor + if (patch >= patchTarget) { + rejected = true; + break; + } + + // Patch is lower than target + return true; + } + + if (rejected) { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: Server version should send signature" + ) + return false; + } + + // Version number is below any of the targets, so it should not send the signature + return true } guard let signingKey = Database.default.getConfig(serverUrl, "AsymmetricSigningPublicKey") From ec2642e5910585940156f301ce72b89d2eed7e72 Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Mon, 15 Apr 2024 14:23:57 +0200 Subject: [PATCH 08/10] Fix unhandled version case --- .../helpers/CustomPushNotificationHelper.java | 12 +++++++++--- .../PushNotification+Signature.swift | 11 ++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java index ba69b1a1794..2ba50f64db9 100644 --- a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java +++ b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java @@ -280,13 +280,17 @@ public static boolean verifySignature(final Context context, String signature, S int[][] targets = {{9,8,0},{9,7,2},{9,6,2},{9,5,4},{8,1,13}}; boolean rejected = false; - for (int[] targetVersion: targets) { + for (int i = 0; i < targets.length; i++) { + boolean first = i == 0; + int[] targetVersion = targets[i]; int majorTarget = targetVersion[0]; int minorTarget = targetVersion[1]; int patchTarget = targetVersion[2]; if (major > majorTarget) { - rejected = true; + // Only reject if we are considering the first (highest) version. + // Any version in between should be acceptable. + rejected = first; break; } @@ -297,7 +301,9 @@ public static boolean verifySignature(final Context context, String signature, S // Same major if (minor > minorTarget) { - rejected = true; + // Only reject if we are considering the first (highest) version. + // Any version in between should be acceptable. + rejected = first; break; } diff --git a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift index dc1d42fdb3a..9f8b37ccb5b 100644 --- a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift @@ -71,13 +71,16 @@ extension PushNotification { let versionTargets = [[9,8,0], [9,7,2], [9,6,2], [9,5,4], [8,1,13]] var rejected = false - for versionTarget in versionTargets { + for (index, versionTarget) in versionTargets.enumerated() { + let first = index == 0; let majorTarget = versionTarget[0] let minorTarget = versionTarget[1] let patchTarget = versionTarget[2] if (major > majorTarget) { - rejected = true; + // Only reject if we are considering the first (highest) version. + // Any version in between should be acceptable. + rejected = first; break; } @@ -88,7 +91,9 @@ extension PushNotification { // Same major if (minor > minorTarget) { - rejected = true; + // Only reject if we are considering the first (highest) version. + // Any version in between should be acceptable. + rejected = first; break; } From 94d2754826be9fbfe5051ea72b3bd545341b7977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 16 Apr 2024 09:28:07 +0200 Subject: [PATCH 09/10] Add comments --- .../com/mattermost/helpers/CustomPushNotificationHelper.java | 1 + .../Gekidou/PushNotification/PushNotification+Signature.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java index 2ba50f64db9..15e2a19a1e5 100644 --- a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java +++ b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java @@ -240,6 +240,7 @@ public static void createNotificationChannels(Context context) { public static boolean verifySignature(final Context context, String signature, String serverUrl, String ackId) { if (signature == null) { + // Backward compatibility with old push proxies Log.i("Mattermost Notifications Signature verification", "No signature in the notification"); return true; } diff --git a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift index 9f8b37ccb5b..05fb0ea0530 100644 --- a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift @@ -15,6 +15,7 @@ extension PushNotification { public func verifySignature(_ userInfo: [AnyHashable : Any]) -> Bool { guard let signature = userInfo["signature"] as? String else { + // Backward compatibility with old push proxies os_log( OSLogType.default, "Mattermost Notifications: Signature verification: No signature in the notification" From 6fca57b2d89f1cf6a76c62acea1cf81b32ceebb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 24 Apr 2024 17:08:03 +0200 Subject: [PATCH 10/10] Add final version numbers --- .../com/mattermost/helpers/CustomPushNotificationHelper.java | 2 +- .../Gekidou/PushNotification/PushNotification+Signature.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java index 15e2a19a1e5..b2172509c2e 100644 --- a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java +++ b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java @@ -279,7 +279,7 @@ public static boolean verifySignature(final Context context, String signature, S int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; int patch = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; - int[][] targets = {{9,8,0},{9,7,2},{9,6,2},{9,5,4},{8,1,13}}; + int[][] targets = {{9,8,0},{9,7,3},{9,6,3},{9,5,5},{8,1,14}}; boolean rejected = false; for (int i = 0; i < targets.length; i++) { boolean first = i == 0; diff --git a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift index 05fb0ea0530..0fe1da18c7f 100644 --- a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift @@ -70,7 +70,7 @@ extension PushNotification { return false } - let versionTargets = [[9,8,0], [9,7,2], [9,6,2], [9,5,4], [8,1,13]] + let versionTargets = [[9,8,0], [9,7,3], [9,6,3], [9,5,5], [8,1,14]] var rejected = false for (index, versionTarget) in versionTargets.enumerated() { let first = index == 0;