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
This commit is contained in:
parent
205816cb93
commit
c5e6e34fea
17 changed files with 561 additions and 4 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
5
android/app/proguard-rules.pro
vendored
5
android/app/proguard-rules.pro
vendored
|
|
@ -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.* { *; }
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import android.graphics.RectF;
|
|||
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.mattermost.rnbeta.*;
|
|||
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 class CustomPushNotificationHelper {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ public class CustomPushNotification extends PushNotification {
|
|||
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 class CustomPushNotification extends PushNotification {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<NotificationClaims>(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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>("id")
|
||||
let valueCol = Expression<String>("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<String>("id")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -84,6 +84,13 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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, *),
|
||||
|
|
|
|||
Loading…
Reference in a new issue