-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[NEW] E2E Encryption push (iOS) (#2463)
* link pods to notification service * push encryption poc * decrypt room key poc * read user key from mmkv and cast into a pkcs * push decrypt poc (iOS) * expose needed watermelon methods * watermelon -> database * indent & simple-crypto update * string extensions * storage * toBase64 -> toData * remove a forced unwrap * remove unused import * database driver * improvement * folder structure & watermelon bridge * more improvement stuff * watermelon -> database * reuse database instance * improvement * database fix: bypass watermelon cache * some code improvements * encryption instances * start api stuff * network layer * improve notification service * improve folder structure * watermelon patch * retry fetch logic * rocketchat class * fix try to decrypt without a roomKey * fallback to original content that is translated * some fixes to rocketchat logic * merge develop * remove unnecessary extension * [CHORE] Improve reply notification code (iOS) * undo sign changes * remove mocked value * import direct from library * send message request * reply notification with encrypted message working properly * revert apple sign * fix api onerror * trick to display sender name on group notifications * revert data.host change * fix some multithread issues * use sendername sent by server * small improvement * Bump crypto lib * Update ios/NotificationService/NotificationService.swift * add experimental string * remove trailing slash * remove trailing slash on reply * fix decrypt messages Co-authored-by: Diego Mello <[email protected]>
- Loading branch information
1 parent
f30c405
commit 60dc128
Showing
68 changed files
with
31,843 additions
and
22,178 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,177 +1,55 @@ | ||
import CoreLocation | ||
import UserNotifications | ||
|
||
struct PushResponse: Decodable { | ||
let success: Bool | ||
let data: Data | ||
|
||
struct Data: Decodable { | ||
let notification: Notification | ||
|
||
struct Notification: Decodable { | ||
let notId: Int | ||
let title: String | ||
let text: String | ||
let payload: Payload | ||
|
||
struct Payload: Decodable, Encodable { | ||
let host: String | ||
let rid: String? | ||
let type: String? | ||
let sender: Sender? | ||
let messageId: String | ||
let notificationType: String? | ||
let name: String? | ||
let messageType: String? | ||
|
||
struct Sender: Decodable, Encodable { | ||
let _id: String | ||
let username: String | ||
let name: String? | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
class NotificationService: UNNotificationServiceExtension { | ||
|
||
var contentHandler: ((UNNotificationContent) -> Void)? | ||
var bestAttemptContent: UNMutableNotificationContent? | ||
|
||
var retryCount = 0 | ||
var retryTimeout = [1.0, 3.0, 5.0, 10.0] | ||
|
||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { | ||
self.contentHandler = contentHandler | ||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) | ||
|
||
if let bestAttemptContent = bestAttemptContent { | ||
let ejson = (bestAttemptContent.userInfo["ejson"] as? String ?? "").data(using: .utf8)! | ||
guard let data = try? (JSONDecoder().decode(PushResponse.Data.Notification.Payload.self, from: ejson)) else { | ||
return | ||
} | ||
|
||
let notificationType = data.notificationType ?? "" | ||
|
||
// If the notification have the content at her payload, show it | ||
if notificationType != "message-id-only" { | ||
contentHandler(bestAttemptContent) | ||
return | ||
} | ||
|
||
let mmapID = "default" | ||
let instanceID = "com.MMKV.\(mmapID)" | ||
let secureStorage = SecureStorage() | ||
var cryptKey: Data = Data() | ||
// get mmkv instance password from keychain | ||
secureStorage.getSecureKey(instanceID.toHex()) { (response) -> () in | ||
guard let password = response?[1] as? String else { | ||
// kill the process and show notification as it came from APN | ||
exit(0) | ||
} | ||
cryptKey = password.data(using: .utf8)! | ||
} | ||
|
||
// Get App Group directory | ||
let suiteName = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String | ||
guard let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: suiteName) else { | ||
return | ||
} | ||
|
||
// Set App Group dir | ||
MMKV.initialize(rootDir: nil, groupDir: directory.path, logLevel: MMKVLogLevel.none) | ||
guard let mmkv = MMKV(mmapID: mmapID, cryptKey: cryptKey, mode: MMKVMode.multiProcess) else { | ||
return | ||
} | ||
|
||
var server = data.host | ||
if (server.last == "/") { | ||
server.removeLast() | ||
} | ||
let msgId = data.messageId | ||
|
||
let userId = mmkv.string(forKey: "reactnativemeteor_usertoken-\(server)") ?? "" | ||
let token = mmkv.string(forKey: "reactnativemeteor_usertoken-\(userId)") ?? "" | ||
|
||
if userId.isEmpty || token.isEmpty { | ||
contentHandler(bestAttemptContent) | ||
return | ||
} | ||
|
||
var urlComponents = URLComponents(string: "\(server)/api/v1/push.get")! | ||
let queryItems = [URLQueryItem(name: "id", value: msgId)] | ||
urlComponents.queryItems = queryItems | ||
|
||
var request = URLRequest(url: urlComponents.url!) | ||
request.httpMethod = "GET" | ||
request.addValue(userId, forHTTPHeaderField: "x-user-id") | ||
request.addValue(token, forHTTPHeaderField: "x-auth-token") | ||
|
||
runRequest(request: request, bestAttemptContent: bestAttemptContent, contentHandler: contentHandler) | ||
|
||
var contentHandler: ((UNNotificationContent) -> Void)? | ||
var bestAttemptContent: UNMutableNotificationContent? | ||
var rocketchat: RocketChat? | ||
|
||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { | ||
self.contentHandler = contentHandler | ||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) | ||
|
||
if let bestAttemptContent = bestAttemptContent { | ||
let ejson = (bestAttemptContent.userInfo["ejson"] as? String ?? "").data(using: .utf8)! | ||
guard let data = try? (JSONDecoder().decode(Payload.self, from: ejson)) else { | ||
return | ||
} | ||
|
||
rocketchat = RocketChat.instanceForServer(server: data.host.removeTrailingSlash()) | ||
|
||
// If the notification has the content on the payload, show it | ||
if data.notificationType != .messageIdOnly { | ||
self.processPayload(payload: data) | ||
return | ||
} | ||
|
||
// Request the content from server | ||
rocketchat?.getPushWithId(data.messageId) { notification in | ||
if let notification = notification { | ||
self.bestAttemptContent?.title = notification.title | ||
self.bestAttemptContent?.body = notification.text | ||
self.processPayload(payload: notification.payload) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func runRequest(request: URLRequest, bestAttemptContent: UNMutableNotificationContent, contentHandler: @escaping (UNNotificationContent) -> Void) { | ||
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in | ||
|
||
func retryRequest() { | ||
// if we can try again | ||
if self.retryCount < self.retryTimeout.count { | ||
// Try again after X seconds | ||
DispatchQueue.main.asyncAfter(deadline: .now() + self.retryTimeout[self.retryCount], execute: { | ||
self.runRequest(request: request, bestAttemptContent: bestAttemptContent, contentHandler: contentHandler) | ||
self.retryCount += 1 | ||
}) | ||
func processPayload(payload: Payload) { | ||
// If is a encrypted message | ||
if payload.messageType == .e2e { | ||
if let message = payload.msg, let rid = payload.rid { | ||
if let decryptedMessage = rocketchat?.decryptMessage(rid: rid, message: message) { | ||
bestAttemptContent?.body = decryptedMessage | ||
if let roomType = payload.type, roomType == .group, let sender = payload.senderName { | ||
bestAttemptContent?.body = "\(sender): \(decryptedMessage)" | ||
} | ||
} | ||
|
||
// If some error happened | ||
if error != nil { | ||
retryRequest() | ||
|
||
// Check if the request did successfully | ||
} else if let response = response as? HTTPURLResponse { | ||
// if it not was successfully | ||
if response.statusCode != 200 { | ||
retryRequest() | ||
|
||
// If the response status is 200 | ||
} else { | ||
// Process data | ||
if let data = data { | ||
// Parse data of response | ||
let push = try? (JSONDecoder().decode(PushResponse.self, from: data)) | ||
if let push = push { | ||
if push.success { | ||
bestAttemptContent.title = push.data.notification.title | ||
bestAttemptContent.body = push.data.notification.text | ||
|
||
let payload = try? (JSONEncoder().encode(push.data.notification.payload)) | ||
if let payload = payload { | ||
bestAttemptContent.userInfo["ejson"] = String(data: payload, encoding: .utf8) ?? "{}" | ||
} | ||
|
||
// Show notification with the content modified | ||
contentHandler(bestAttemptContent) | ||
return | ||
} | ||
} | ||
} | ||
retryRequest() | ||
} | ||
} | ||
} | ||
|
||
task.resume() | ||
} | ||
|
||
override func serviceExtensionTimeWillExpire() { | ||
// Called just before the extension will be terminated by the system. | ||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. | ||
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { | ||
contentHandler(bestAttemptContent) | ||
} | ||
if let bestAttemptContent = bestAttemptContent { | ||
contentHandler?(bestAttemptContent) | ||
} | ||
|
||
} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.