// // NotificationService.swift // NotificationService // // Created by Tobi on 03.04.25. // import UserNotifications import CryptoKit import Foundation class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent = bestAttemptContent { guard let _ = bestAttemptContent.userInfo as? [String: Any], let push_data = bestAttemptContent.userInfo["push_data"] as? String else { return contentHandler(bestAttemptContent); } let data = getPushNotificationData(pushDataJson: push_data) if data != nil { bestAttemptContent.title = data!.title; bestAttemptContent.body = data!.body; bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId) } else { bestAttemptContent.title = "\(bestAttemptContent.title) [10]" } contentHandler(bestAttemptContent) } } 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) } } } enum PushKind: String, Codable { case text case twonly case video case image case contactRequest case acceptRequest case storedMediaFile case reaction case testNotification case reopenedMedia } import CryptoKit import Foundation import Security func getPushNotificationData(pushDataJson: String) -> (title: String, body: String, notificationId: Int)? { // Decode the pushDataJson guard let pushData = decodePushData(pushDataJson) else { NSLog("Failed to decode push data") return nil } var pushKind: PushKind? var displayName: String? var fromUserId: Int? // Check the keyId if pushData.keyId == 0 { let key = "InsecureOnlyUsedForAddingContact".data(using: .utf8)!.map { Int($0) } pushKind = tryDecryptMessage(key: key, pushData: pushData) } else { let pushKeys = getPushKey() if pushKeys != nil { for (userId, userKeys) in pushKeys! { for key in userKeys.keys { if key.id == pushData.keyId { pushKind = tryDecryptMessage(key: key.key, pushData: pushData) if pushKind != nil { displayName = userKeys.displayName fromUserId = userId break } } } // Found correct key and user if displayName != nil { break } } } else { NSLog("pushKeys are empty") } } // Handle the push notification based on the pushKind if let pushKind = pushKind { if pushKind == .testNotification { return ("Test Notification", "This is a test notification.", 0) } else if displayName != nil && fromUserId != nil { return (displayName!, getPushNotificationText(pushKind: pushKind), fromUserId!) } else { return ("", getPushNotificationTextWithoutUserId(pushKind: pushKind), 1) } } else { NSLog("Failed to decrypt message or pushKind is nil") } return nil } func tryDecryptMessage(key: [Int], pushData: PushNotification) -> PushKind? { // Convert the key from [Int] to Data let keyData = Data(key.map { UInt8($0) }) // Convert Int to UInt8 guard let nonceData = Data(base64Encoded: pushData.nonce), let cipherTextData = Data(base64Encoded: pushData.cipherText), let macData = Data(base64Encoded: pushData.mac) else { NSLog("Failed to decode base64 strings") return nil } do { // Create a nonce for ChaChaPoly let nonce = try ChaChaPoly.Nonce(data: nonceData) // Create a sealed box for ChaChaPoly let sealedBox = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: cipherTextData, tag: macData) // Decrypt the data using the key let decryptedData = try ChaChaPoly.open(sealedBox, using: SymmetricKey(data: keyData)) // Convert decrypted data to a string if let decryptedMessage = String(data: decryptedData, encoding: .utf8) { NSLog("Decrypted message: \(decryptedMessage)") // Here you can determine the PushKind based on the decrypted message return determinePushKind(from: decryptedMessage) } } catch { NSLog("Decryption failed: \(error)") } return nil } // Placeholder function to determine PushKind from the decrypted message func determinePushKind(from message: String) -> PushKind? { // Implement your logic to determine the PushKind based on the message content // For example, you might check for specific keywords or formats in the message // This is just a placeholder implementation if message.contains("text") { return .text } else if message.contains("video") { return .video } else if message.contains("image") { return .image } else if message.contains("twonly") { return .twonly } else if message.contains("contactRequest") { return .contactRequest } else if message.contains("acceptRequest") { return .acceptRequest } else if message.contains("storedMediaFile") { return .storedMediaFile } else if message.contains("reaction") { return .reaction } else if message.contains("testNotification") { return .testNotification } else if message.contains("reopenedMedia") { return .reopenedMedia } else { return nil // Unknown PushKind } } func decodePushData(_ json: String) -> PushNotification? { // First, decode the base64 string guard let base64Data = Data(base64Encoded: json) else { NSLog("Failed to decode base64 string") return nil } // Convert the base64 decoded data to a JSON string guard let jsonString = String(data: base64Data, encoding: .utf8) else { NSLog("Failed to convert base64 data to JSON string") return nil } // Convert the JSON string to Data guard let jsonData = jsonString.data(using: .utf8) else { NSLog("Failed to convert JSON string to Data") return nil } do { // Use JSONDecoder to decode the JSON data into a PushNotification instance let decoder = JSONDecoder() let pushNotification = try decoder.decode(PushNotification.self, from: jsonData) return pushNotification } catch { NSLog("Error decoding JSON: \(error)") return nil } } struct PushNotification: Codable { let keyId: Int let nonce: String let cipherText: String let mac: String // You can add custom coding keys if the JSON keys differ from the property names enum CodingKeys: String, CodingKey { case keyId case nonce case cipherText case mac } } struct PushKeyMeta: Codable { let id: Int let key: [Int] let createdAt: Date enum CodingKeys: String, CodingKey { case id case key case createdAt } } struct PushUser: Codable { let displayName: String let keys: [PushKeyMeta] enum CodingKeys: String, CodingKey { case displayName case keys } } func getPushKey() -> [Int: PushUser]? { // Retrieve the data from secure storage (Keychain) guard let data = readFromKeychain(key: "receivingPushKeys") else { print("No data found for key: receivingPushKeys") return nil } do { // Decode the JSON data into a dictionary let jsonData = data.data(using: .utf8)! let jsonMap = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] var pushKeys: [Int: PushUser] = [:] // Iterate through the JSON map and decode each PushUser for (key, value) in jsonMap ?? [:] { if let userData = try? JSONSerialization.data(withJSONObject: value, options: []), let pushUser = try? JSONDecoder().decode(PushUser.self, from: userData) { pushKeys[Int(key)!] = pushUser } } return pushKeys } catch { print("Error decoding JSON: \(error)") return nil } } // Helper function to read from Keychain func readFromKeychain(key: String) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: kCFBooleanTrue!, kSecMatchLimit as String: kSecMatchLimitOne, kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared" // Use your access group ] var dataTypeRef: AnyObject? = nil let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) if status == errSecSuccess { if let data = dataTypeRef as? Data { return String(data: data, encoding: .utf8) } } return nil } func getPushNotificationText(pushKind: PushKind) -> String { let systemLanguage = Locale.current.languageCode ?? "en" // Get the current system language var pushNotificationText: [PushKind: String] = [:] // Define the messages based on the system language if systemLanguage.contains("de") { // German pushNotificationText = [ .text: "hat dir eine Nachricht gesendet.", .twonly: "hat dir ein twonly gesendet.", .video: "hat dir ein Video gesendet.", .image: "hat dir ein Bild gesendet.", .contactRequest: "möchte sich mit dir vernetzen.", .acceptRequest: "ist jetzt mit dir vernetzt.", .storedMediaFile: "hat dein Bild gespeichert.", .reaction: "hat auf dein Bild reagiert.", .reopenedMedia: "Dein Bild wurde erneut geöffnet." ] } else { // Default to English pushNotificationText = [ .text: "has sent you a message.", .twonly: "has sent you a twonly.", .video: "has sent you a video.", .image: "has sent you an image.", .contactRequest: "wants to connect with you.", .acceptRequest: "is now connected with you.", .storedMediaFile: "has stored your image.", .reaction: "has reacted to your image.", .reopenedMedia: "Your image was reopened." ] } // Return the corresponding message or an empty string if not found return pushNotificationText[pushKind] ?? "" } func getPushNotificationTextWithoutUserId(pushKind: PushKind) -> String { let systemLanguage = Locale.current.languageCode ?? "en" // Get the current system language var pushNotificationText: [PushKind: String] = [:] // Define the messages based on the system language if systemLanguage.contains("de") { // German pushNotificationText = [ .text: "Du hast eine Nachricht erhalten.", .twonly: "Du hast ein twonly erhalten.", .video: "Du hast ein Video erhalten.", .image: "Du hast ein Bild erhalten.", .contactRequest: "Du hast eine Kontaktanfrage erhalten.", .acceptRequest: "Deine Kontaktanfrage wurde angenommen.", .storedMediaFile: "Dein Bild wurde gespeichert.", .reaction: "Du hast eine Reaktion auf dein Bild erhalten.", .reopenedMedia: "hat dein Bild erneut geöffnet." ] } else { // Default to English pushNotificationText = [ .text: "You got a message.", .twonly: "You got a twonly.", .video: "You got a video.", .image: "You got an image.", .contactRequest: "You got a contact request.", .acceptRequest: "Your contact request has been accepted.", .storedMediaFile: "Your image has been saved.", .reaction: "You got a reaction to your image.", .reopenedMedia: "has reopened your image." ] } // Return the corresponding message or an empty string if not found return pushNotificationText[pushKind] ?? "" }