From c5e6e34fea61a4c717afaa143bf2df87f4e04caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 24 Apr 2024 17:12:56 +0200 Subject: [PATCH] Fix MM 56723 (#7883) * Fix MM 56723 (iOS) * Add android * Android fixes and version checking * Add version check to ios * Address feedback * Add all versions to android * Check all versions on iOS * Fix unhandled version case * Add comments * Add final version numbers --- android/app/build.gradle | 6 + android/app/proguard-rules.pro | 5 + .../helpers/CustomPushNotificationHelper.java | 159 ++++++++++++++ .../helpers/database_extension/General.kt | 18 +- .../helpers/database_extension/System.kt | 8 + .../rnbeta/CustomPushNotification.java | 6 + app/constants/push_notification.ts | 8 + app/init/push_notifications.ts | 10 +- assets/base/i18n/en.json | 1 + ios/Gekidou/Package.swift | 6 +- .../PushNotification+Signature.swift | 205 ++++++++++++++++++ .../Gekidou/Storage/Database+System.swift | 13 ++ .../Sources/Gekidou/Storage/Database.swift | 1 + ios/GekidouWrapper.swift | 4 + .../xcshareddata/swiftpm/Package.resolved | 63 ++++++ ios/Mattermost/AppDelegate.mm | 7 + .../NotificationService.swift | 45 ++++ 17 files changed, 561 insertions(+), 4 deletions(-) create mode 100644 ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift diff --git a/android/app/build.gradle b/android/app/build.gradle index 204fb02a5d8..1319a603809 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -212,6 +212,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..b2172509c2e 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,14 +33,24 @@ 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; +import io.jsonwebtoken.IncorrectClaimException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MissingClaimException; 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 +238,154 @@ 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; + } + + 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.equals("NO_SIGNATURE")) { + String version = queryConfigServerVersion(db); + if (version == null) { + Log.i("Mattermost Notifications Signature verification", "No server version"); + return false; + } + + if (!version.matches("[0-9]+(\\.[0-9]+)*")) { + Log.i("Mattermost Notifications Signature verification", "Invalid server version"); + return false; + } + + String[] parts = version.split("\\."); + 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[][] 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; + int[] targetVersion = targets[i]; + int majorTarget = targetVersion[0]; + int minorTarget = targetVersion[1]; + int patchTarget = targetVersion[2]; + + if (major > majorTarget) { + // Only reject if we are considering the first (highest) version. + // Any version in between should be acceptable. + rejected = first; + break; + } + + if (major < majorTarget) { + // Continue to see if it complies with a smaller target + continue; + } + + // Same major + if (minor > minorTarget) { + // Only reject if we are considering the first (highest) version. + // Any version in between should be acceptable. + rejected = first; + 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) { + 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); + if (signingKey == null) { + Log.i("Mattermost Notifications Signature verification", "No signing key"); + return false; + } + + 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"); + 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; + } + + Jwts.parser() + .require("ack_id", ackId) + .require("device_id", deviceToken) + .verifyWith((PublicKey) pubKey) + .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/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/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/assets/base/i18n/en.json b/assets/base/i18n/en.json index 4a4ec368d5a..8ca8536058a 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -768,6 +768,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/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..0fe1da18c7f --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+Signature.swift @@ -0,0 +1,205 @@ +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 { + // Backward compatibility with old push proxies + 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 + } + + let parts = version.components(separatedBy: "."); + if (parts.count < 3) { + os_log( + OSLogType.default, + "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,3], [9,6,3], [9,5,5], [8,1,14]] + var rejected = false + 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) { + // Only reject if we are considering the first (highest) version. + // Any version in between should be acceptable. + rejected = first; + break; + } + + if (major < majorTarget) { + // Continue to see if it complies with a smaller target + continue; + } + + // Same major + if (minor > minorTarget) { + // Only reject if we are considering the first (highest) version. + // Any version in between should be acceptable. + rejected = first; + 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") + 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 device token format" + ) + return false + } + let deviceToken = tokenParts[1].dropLast(1) + if (deviceToken.isEmpty) { + os_log( + OSLogType.default, + "Mattermost Notifications: Signature verification: Empty stored device 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.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9e215be82a1..8d3d7c36f0b 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 97ec2bd54dd..0315aa57fca 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..22942dd4096 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,47 @@ 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, *) { + 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, *),