Jonas Björkert преди 11 месеца
родител
ревизия
330e3abe5d

+ 22 - 2
LoopFollow.xcodeproj/project.pbxproj

@@ -10,9 +10,9 @@
 		3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; };
 		654132E72E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */; };
 		654132EA2E19F24800BDBE08 /* TOTPGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654132E92E19F24800BDBE08 /* TOTPGenerator.swift */; };
-		6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */; };
 		654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134172E1DC09700BDBE08 /* OverridePresetsView.swift */; };
 		6541341A2E1DC27900BDBE08 /* OverridePresetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134192E1DC27900BDBE08 /* OverridePresetData.swift */; };
+		6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */; };
 		DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; };
 		DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; };
 		DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */; };
@@ -52,6 +52,8 @@
 		DD2C2E512D3B8B0C006413A5 /* NightscoutSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */; };
 		DD2C2E542D3C37DC006413A5 /* DexcomSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */; };
 		DD2C2E562D3C3917006413A5 /* DexcomSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E552D3C3913006413A5 /* DexcomSettingsView.swift */; };
+		DD485F142E454B2600CE8CBF /* SecureMessenger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD485F132E454B2600CE8CBF /* SecureMessenger.swift */; };
+		DD485F162E46631000CE8CBF /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = DD485F152E46631000CE8CBF /* CryptoSwift */; };
 		DD4878032C7B297E0048F05C /* StorageValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878022C7B297E0048F05C /* StorageValue.swift */; };
 		DD4878052C7B2C970048F05C /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878042C7B2C970048F05C /* Storage.swift */; };
 		DD4878082C7B30BF0048F05C /* RemoteSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878072C7B30BF0048F05C /* RemoteSettingsView.swift */; };
@@ -393,9 +395,9 @@
 		059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = "<group>"; };
 		654132E62E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleQRCodeScannerView.swift; sourceTree = "<group>"; };
 		654132E92E19F24800BDBE08 /* TOTPGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPGenerator.swift; sourceTree = "<group>"; };
-		6541341B2E1DC28000BDBE08 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = "<group>"; };
 		654134172E1DC09700BDBE08 /* OverridePresetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetsView.swift; sourceTree = "<group>"; };
 		654134192E1DC27900BDBE08 /* OverridePresetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetData.swift; sourceTree = "<group>"; };
+		6541341B2E1DC28000BDBE08 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = "<group>"; };
 		A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = "<group>"; };
 		DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = "<group>"; };
@@ -436,6 +438,7 @@
 		DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsViewModel.swift; sourceTree = "<group>"; };
 		DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSettingsViewModel.swift; sourceTree = "<group>"; };
 		DD2C2E552D3C3913006413A5 /* DexcomSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSettingsView.swift; sourceTree = "<group>"; };
+		DD485F132E454B2600CE8CBF /* SecureMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureMessenger.swift; sourceTree = "<group>"; };
 		DD4878022C7B297E0048F05C /* StorageValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageValue.swift; sourceTree = "<group>"; };
 		DD4878042C7B2C970048F05C /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
 		DD4878072C7B30BF0048F05C /* RemoteSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSettingsView.swift; sourceTree = "<group>"; };
@@ -787,6 +790,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */,
+				DD485F162E46631000CE8CBF /* CryptoSwift in Frameworks */,
 				3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */,
 				DD48781C2C7DAF140048F05C /* SwiftJWT in Frameworks */,
 			);
@@ -919,6 +923,7 @@
 		DD4878112C7B74F90048F05C /* TRC */ = {
 			isa = PBXGroup;
 			children = (
+				DD485F132E454B2600CE8CBF /* SecureMessenger.swift */,
 				DD48781F2C7DAF890048F05C /* PushMessage.swift */,
 				DD4878122C7B750D0048F05C /* TempTargetView.swift */,
 				DD48781D2C7DAF2F0048F05C /* PushNotificationManager.swift */,
@@ -1569,6 +1574,7 @@
 			name = LoopFollow;
 			packageProductDependencies = (
 				DD48781B2C7DAF140048F05C /* SwiftJWT */,
+				DD485F152E46631000CE8CBF /* CryptoSwift */,
 			);
 			productName = LoopFollow;
 			productReference = FC9788142485969B00A7906C /* Loop Follow.app */;
@@ -1605,6 +1611,7 @@
 			packageReferences = (
 				DD48781A2C7DAF140048F05C /* XCRemoteSwiftPackageReference "Swift-JWT" */,
 				654132E82E19F0B800BDBE08 /* XCRemoteSwiftPackageReference "swift-crypto" */,
+				DD485F0B2E4547C800CE8CBF /* XCRemoteSwiftPackageReference "CryptoSwift" */,
 			);
 			productRefGroup = FC9788152485969B00A7906C /* Products */;
 			projectDirPath = "";
@@ -2092,6 +2099,7 @@
 				FC3CAB022493B6220068A152 /* BackgroundTaskAudio.swift in Sources */,
 				DDCC3A582DDC9655006F1C10 /* MissedBolusAlarmEditor.swift in Sources */,
 				DDEF50402D479B8A00884336 /* LoopAPNSService.swift in Sources */,
+				DD485F142E454B2600CE8CBF /* SecureMessenger.swift in Sources */,
 				DDEF50422D479BAA00884336 /* LoopAPNSCarbsView.swift in Sources */,
 				DDEF50432D479BBA00884336 /* LoopAPNSBolusView.swift in Sources */,
 				DDEF50452D479BDA00884336 /* LoopAPNSRemoteView.swift in Sources */,
@@ -2388,6 +2396,14 @@
 				minimumVersion = 3.12.3;
 			};
 		};
+		DD485F0B2E4547C800CE8CBF /* XCRemoteSwiftPackageReference "CryptoSwift" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 1.9.0;
+			};
+		};
 		DD48781A2C7DAF140048F05C /* XCRemoteSwiftPackageReference "Swift-JWT" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/Kitura/Swift-JWT.git";
@@ -2399,6 +2415,10 @@
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
+		DD485F152E46631000CE8CBF /* CryptoSwift */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = CryptoSwift;
+		};
 		DD48781B2C7DAF140048F05C /* SwiftJWT */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = DD48781A2C7DAF140048F05C /* XCRemoteSwiftPackageReference "Swift-JWT" */;

+ 10 - 1
LoopFollow.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "7da8de315eadc567d44eb98fc0ead2ee437ac62830b8c28200a84636eb14e2f3",
+  "originHash" : "94a024be279d128a7e82f3c76785db1e4cf7c9380d0c4aa59dfdf54952403b8d",
   "pins" : [
     {
       "identity" : "bluecryptor",
@@ -29,6 +29,15 @@
       }
     },
     {
+      "identity" : "cryptoswift",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
+      "state" : {
+        "revision" : "e45a26384239e028ec87fbcc788f513b67e10d8f",
+        "version" : "1.9.0"
+      }
+    },
+    {
       "identity" : "kituracontracts",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/Kitura/KituraContracts.git",

+ 14 - 24
LoopFollow/Remote/TRC/PushMessage.swift

@@ -4,18 +4,28 @@
 
 import Foundation
 
-struct PushMessage: Encodable {
+struct EncryptedPushMessage: Encodable {
     let aps: [String: Int] = ["content-available": 1]
+
+    let encryptedData: String
+
+    enum CodingKeys: String, CodingKey {
+        case aps
+        case encryptedData = "encrypted_data"
+    }
+}
+
+struct CommandPayload: Encodable {
     var user: String
     var commandType: TRCCommandType
+    var timestamp: TimeInterval
+
     var bolusAmount: Decimal?
     var target: Int?
     var duration: Int?
     var carbs: Int?
     var protein: Int?
     var fat: Int?
-    var sharedSecret: String
-    var timestamp: TimeInterval
     var overrideName: String?
     var scheduledTime: TimeInterval?
     var returnNotification: ReturnNotificationInfo?
@@ -39,37 +49,17 @@ struct PushMessage: Encodable {
     }
 
     enum CodingKeys: String, CodingKey {
-        case aps
         case user
         case commandType = "command_type"
+        case timestamp
         case bolusAmount = "bolus_amount"
         case target
         case duration
         case carbs
         case protein
         case fat
-        case sharedSecret = "shared_secret"
-        case timestamp
         case overrideName
         case scheduledTime = "scheduled_time"
         case returnNotification = "return_notification"
     }
-
-    func encode(to encoder: Encoder) throws {
-        var container = encoder.container(keyedBy: CodingKeys.self)
-        try container.encode(aps, forKey: .aps)
-        try container.encode(user, forKey: .user)
-        try container.encode(commandType.rawValue, forKey: .commandType)
-        try container.encodeIfPresent(bolusAmount, forKey: .bolusAmount)
-        try container.encodeIfPresent(target, forKey: .target)
-        try container.encodeIfPresent(duration, forKey: .duration)
-        try container.encodeIfPresent(carbs, forKey: .carbs)
-        try container.encodeIfPresent(protein, forKey: .protein)
-        try container.encodeIfPresent(fat, forKey: .fat)
-        try container.encode(sharedSecret, forKey: .sharedSecret)
-        try container.encode(timestamp, forKey: .timestamp)
-        try container.encodeIfPresent(overrideName, forKey: .overrideName)
-        try container.encodeIfPresent(scheduledTime, forKey: .scheduledTime)
-        try container.encodeIfPresent(returnNotification, forKey: .returnNotification)
-    }
 }

+ 43 - 85
LoopFollow/Remote/TRC/PushNotificationManager.swift

@@ -27,42 +27,36 @@ class PushNotificationManager {
         bundleId = Storage.shared.bundleId.value
     }
 
-    private func createReturnNotificationInfo() -> PushMessage.ReturnNotificationInfo? {
+    private func createReturnNotificationInfo() -> CommandPayload.ReturnNotificationInfo? {
         let loopFollowDeviceToken = Observable.shared.loopFollowDeviceToken.value
 
         guard !loopFollowDeviceToken.isEmpty else {
             return nil
         }
 
-        // Get the LoopFollow Team ID from BuildDetails.
         guard let loopFollowTeamID = BuildDetails.default.teamID, !loopFollowTeamID.isEmpty else {
             LogManager.shared.log(category: .apns, message: "LoopFollow Team ID not found in BuildDetails.plist. Cannot create return notification info.")
             return nil
         }
 
         let teamIdsAreDifferent = loopFollowTeamID != teamId
-
         let keyIdForReturn: String
         let apnsKeyForReturn: String
 
         if teamIdsAreDifferent {
-            // Team IDs differ, so we MUST use the separate return credentials from storage.
             keyIdForReturn = Storage.shared.returnKeyId.value
             apnsKeyForReturn = Storage.shared.returnApnsKey.value
         } else {
-            // Team IDs are the same, so we can use the primary credentials.
             keyIdForReturn = keyId
             apnsKeyForReturn = apnsKey
         }
 
-        // Ensure we have the necessary credentials before proceeding
-        // Note: The teamId for the return message is ALWAYS the loopFollowTeamID.
         guard !keyIdForReturn.isEmpty, !apnsKeyForReturn.isEmpty else {
             LogManager.shared.log(category: .apns, message: "Missing required return APNS credentials. Check Remote Settings.")
             return nil
         }
 
-        return PushMessage.ReturnNotificationInfo(
+        return CommandPayload.ReturnNotificationInfo(
             productionEnvironment: BuildDetails.default.isTestFlightBuild(),
             deviceToken: loopFollowDeviceToken,
             bundleId: Bundle.main.bundleIdentifier ?? "",
@@ -73,74 +67,61 @@ class PushNotificationManager {
     }
 
     func sendOverridePushNotification(override: ProfileManager.TrioOverride, completion: @escaping (Bool, String?) -> Void) {
-        let message = PushMessage(
+        let payload = CommandPayload(
             user: user,
             commandType: .startOverride,
-            sharedSecret: sharedSecret,
             timestamp: Date().timeIntervalSince1970,
             overrideName: override.name,
             returnNotification: createReturnNotificationInfo()
         )
-
-        sendPushNotification(message: message, completion: completion)
+        sendEncryptedCommand(payload: payload, completion: completion)
     }
 
     func sendCancelOverridePushNotification(completion: @escaping (Bool, String?) -> Void) {
-        let message = PushMessage(
+        let payload = CommandPayload(
             user: user,
             commandType: .cancelOverride,
-            sharedSecret: sharedSecret,
             timestamp: Date().timeIntervalSince1970,
             overrideName: nil,
             returnNotification: createReturnNotificationInfo()
         )
-
-        sendPushNotification(message: message, completion: completion)
+        sendEncryptedCommand(payload: payload, completion: completion)
     }
 
     func sendBolusPushNotification(bolusAmount: HKQuantity, completion: @escaping (Bool, String?) -> Void) {
-        let bolusAmount = Decimal(bolusAmount.doubleValue(for: .internationalUnit()))
-
-        let message = PushMessage(
+        let bolusAmountDecimal = Decimal(bolusAmount.doubleValue(for: .internationalUnit()))
+        let payload = CommandPayload(
             user: user,
             commandType: .bolus,
-            bolusAmount: bolusAmount,
-            sharedSecret: sharedSecret,
             timestamp: Date().timeIntervalSince1970,
+            bolusAmount: bolusAmountDecimal,
             returnNotification: createReturnNotificationInfo()
         )
-
-        sendPushNotification(message: message, completion: completion)
+        sendEncryptedCommand(payload: payload, completion: completion)
     }
 
     func sendTempTargetPushNotification(target: HKQuantity, duration: HKQuantity, completion: @escaping (Bool, String?) -> Void) {
         let targetValue = Int(target.doubleValue(for: HKUnit.milligramsPerDeciliter))
         let durationValue = Int(duration.doubleValue(for: HKUnit.minute()))
-
-        let message = PushMessage(
+        let payload = CommandPayload(
             user: user,
             commandType: .tempTarget,
-            bolusAmount: nil,
+            timestamp: Date().timeIntervalSince1970,
             target: targetValue,
             duration: durationValue,
-            sharedSecret: sharedSecret,
-            timestamp: Date().timeIntervalSince1970,
             returnNotification: createReturnNotificationInfo()
         )
-
-        sendPushNotification(message: message, completion: completion)
+        sendEncryptedCommand(payload: payload, completion: completion)
     }
 
     func sendCancelTempTargetPushNotification(completion: @escaping (Bool, String?) -> Void) {
-        let message = PushMessage(
+        let payload = CommandPayload(
             user: user,
             commandType: .cancelTempTarget,
-            sharedSecret: sharedSecret,
             timestamp: Date().timeIntervalSince1970,
             returnNotification: createReturnNotificationInfo()
         )
-
-        sendPushNotification(message: message, completion: completion)
+        sendEncryptedCommand(payload: payload, completion: completion)
     }
 
     func sendMealPushNotification(
@@ -155,60 +136,47 @@ class PushNotificationManager {
             let valueInGrams = quantity.doubleValue(for: .gram())
             return valueInGrams > 0 ? Int(valueInGrams) : nil
         }
-
         func convertToOptionalDecimal(_ quantity: HKQuantity?) -> Decimal? {
             guard let quantity = quantity else { return nil }
             let value = quantity.doubleValue(for: .internationalUnit())
             return value > 0 ? Decimal(value) : nil
         }
-
         let carbsValue = convertToOptionalInt(carbs)
         let proteinValue = convertToOptionalInt(protein)
         let fatValue = convertToOptionalInt(fat)
         let scheduledTimeInterval: TimeInterval? = scheduledTime?.timeIntervalSince1970
         let bolusAmountValue = convertToOptionalDecimal(bolusAmount)
-
         guard carbsValue != nil || proteinValue != nil || fatValue != nil else {
             completion(false, "No nutrient data provided. At least one of carbs, fat, or protein must be greater than 0.")
             return
         }
-
-        let message = PushMessage(
+        let payload = CommandPayload(
             user: user,
             commandType: .meal,
+            timestamp: Date().timeIntervalSince1970,
             bolusAmount: bolusAmountValue,
             carbs: carbsValue,
             protein: proteinValue,
             fat: fatValue,
-            sharedSecret: sharedSecret,
-            timestamp: Date().timeIntervalSince1970,
             scheduledTime: scheduledTimeInterval,
             returnNotification: createReturnNotificationInfo()
         )
-
-        sendPushNotification(message: message, completion: completion)
+        sendEncryptedCommand(payload: payload, completion: completion)
     }
 
     private func validateCredentials() -> [String]? {
         var errors = [String]()
-
-        // Validate keyId (should be 10 alphanumeric characters)
         let keyIdPattern = "^[A-Z0-9]{10}$"
         if !matchesRegex(keyId, pattern: keyIdPattern) {
             errors.append("APNS Key ID (\(keyId)) must be 10 uppercase alphanumeric characters.")
         }
-
-        // Validate teamId (should be 10 alphanumeric characters)
         let teamIdPattern = "^[A-Z0-9]{10}$"
         if !matchesRegex(teamId, pattern: teamIdPattern) {
             errors.append("Team ID (\(teamId)) must be 10 uppercase alphanumeric characters.")
         }
-
-        // Validate apnsKey (should contain the BEGIN and END PRIVATE KEY markers)
         if !apnsKey.contains("-----BEGIN PRIVATE KEY-----") || !apnsKey.contains("-----END PRIVATE KEY-----") {
             errors.append("APNS Key must be a valid PEM-formatted private key.")
         } else {
-            // Validate that the key data between the markers is valid Base64
             if let keyData = extractKeyData(from: apnsKey) {
                 if Data(base64Encoded: keyData) == nil {
                     errors.append("APNS Key contains invalid Base64 key data.")
@@ -217,7 +185,6 @@ class PushNotificationManager {
                 errors.append("APNS Key has invalid formatting.")
             }
         }
-
         return errors.isEmpty ? nil : errors
     }
 
@@ -239,28 +206,18 @@ class PushNotificationManager {
         return keyLines.joined()
     }
 
-    private func sendPushNotification(message: PushMessage, completion: @escaping (Bool, String?) -> Void) {
-        print("Push message to send: \(message)")
-
+    private func sendEncryptedCommand(payload: CommandPayload, completion: @escaping (Bool, String?) -> Void) {
         var missingFields = [String]()
         if sharedSecret.isEmpty { missingFields.append("sharedSecret") }
-        if apnsKey.isEmpty { missingFields.append("token") }
+        if apnsKey.isEmpty { missingFields.append("apnsKey") }
         if keyId.isEmpty { missingFields.append("keyId") }
         if user.isEmpty { missingFields.append("user") }
-
-        if !missingFields.isEmpty {
-            let errorMessage = "Missing required fields, check your remote settings: \(missingFields.joined(separator: ", "))"
-            LogManager.shared.log(category: .apns, message: errorMessage)
-            completion(false, errorMessage)
-            return
-        }
-
         if deviceToken.isEmpty { missingFields.append("deviceToken") }
         if bundleId.isEmpty { missingFields.append("bundleId") }
         if teamId.isEmpty { missingFields.append("teamId") }
 
         if !missingFields.isEmpty {
-            let errorMessage = "Missing required data, verify that you are using the latest version of Trio: \(missingFields.joined(separator: ", "))"
+            let errorMessage = "Missing required fields for command: \(missingFields.joined(separator: ", "))"
             LogManager.shared.log(category: .apns, message: errorMessage)
             completion(false, errorMessage)
             return
@@ -287,18 +244,27 @@ class PushNotificationManager {
             return
         }
 
-        var request = URLRequest(url: url)
-        request.httpMethod = "POST"
-        request.setValue("bearer \(jwt)", forHTTPHeaderField: "authorization")
-        request.setValue("application/json", forHTTPHeaderField: "content-type")
-        request.setValue("10", forHTTPHeaderField: "apns-priority")
-        request.setValue("0", forHTTPHeaderField: "apns-expiration")
-        request.setValue(bundleId, forHTTPHeaderField: "apns-topic")
-        request.setValue("background", forHTTPHeaderField: "apns-push-type")
-
         do {
-            let jsonData = try JSONEncoder().encode(message)
-            request.httpBody = jsonData
+            guard let messenger = SecureMessenger(sharedSecret: sharedSecret) else {
+                let errorMessage = "Failed to initialize security module. Check shared secret."
+                LogManager.shared.log(category: .apns, message: errorMessage)
+                completion(false, errorMessage)
+                return
+            }
+
+            let encryptedDataString = try messenger.encrypt(payload)
+            let finalMessage = EncryptedPushMessage(encryptedData: encryptedDataString)
+
+            var request = URLRequest(url: url)
+            request.httpMethod = "POST"
+            request.setValue("bearer \(jwt)", forHTTPHeaderField: "authorization")
+            request.setValue("application/json", forHTTPHeaderField: "content-type")
+            request.setValue("10", forHTTPHeaderField: "apns-priority")
+            request.setValue("0", forHTTPHeaderField: "apns-expiration")
+            request.setValue(bundleId, forHTTPHeaderField: "apns-topic")
+            request.setValue("background", forHTTPHeaderField: "apns-push-type")
+
+            request.httpBody = try JSONEncoder().encode(finalMessage)
 
             let task = URLSession.shared.dataTask(with: request) { data, response, error in
                 if let error = error {
@@ -312,22 +278,14 @@ class PushNotificationManager {
                     print("Push notification sent.")
                     print("Status code: \(httpResponse.statusCode)")
 
-                    print("Response headers:")
-                    for (key, value) in httpResponse.allHeaderFields {
-                        print("\(key): \(value)")
-                    }
-
                     var responseBodyMessage = ""
                     if let data = data, let responseBody = String(data: data, encoding: .utf8) {
                         print("Response body: \(responseBody)")
-
                         if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
                            let reason = json["reason"] as? String
                         {
                             responseBodyMessage = reason
                         }
-                    } else {
-                        print("No response body")
                     }
 
                     switch httpResponse.statusCode {
@@ -361,8 +319,8 @@ class PushNotificationManager {
             task.resume()
 
         } catch {
-            let errorMessage = "Failed to encode push message: \(error.localizedDescription)"
-            print(errorMessage)
+            let errorMessage = "Failed to encode or encrypt push message: \(error.localizedDescription)"
+            LogManager.shared.log(category: .apns, message: errorMessage)
             completion(false, errorMessage)
         }
     }

+ 39 - 0
LoopFollow/Remote/TRC/SecureMessenger.swift

@@ -0,0 +1,39 @@
+// LoopFollow
+// SecureMessenger.swift
+// Created by Jonas Björkert.
+
+import CryptoSwift
+import Foundation
+import Security
+
+struct SecureMessenger {
+    private let sharedKey: [UInt8]
+
+    init?(sharedSecret: String) {
+        guard let secretData = sharedSecret.data(using: .utf8) else {
+            return nil
+        }
+        sharedKey = Array(secretData.sha256())
+    }
+
+    private func generateSecureRandomBytes(count: Int) -> [UInt8]? {
+        var bytes = [UInt8](repeating: 0, count: count)
+        let status = SecRandomCopyBytes(kSecRandomDefault, count, &bytes)
+        return status == errSecSuccess ? bytes : nil
+    }
+
+    func encrypt<T: Encodable>(_ object: T) throws -> String {
+        let dataToEncrypt = try JSONEncoder().encode(object)
+
+        guard let nonce = generateSecureRandomBytes(count: 12) else {
+            throw NSError(domain: "SecureMessenger", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to generate secure random nonce."])
+        }
+
+        let gcm = GCM(iv: nonce, mode: .combined)
+        let aes = try AES(key: sharedKey, blockMode: gcm, padding: .noPadding)
+        let encryptedBytes = try aes.encrypt(Array(dataToEncrypt))
+        let finalData = Data(nonce + encryptedBytes)
+
+        return finalData.base64EncodedString()
+    }
+}

+ 1 - 1
LoopFollow/Remote/TRC/TRCCommandType.swift

@@ -4,7 +4,7 @@
 
 import Foundation
 
-enum TRCCommandType: String {
+enum TRCCommandType: String, Encodable {
     case bolus
     case tempTarget = "temp_target"
     case cancelTempTarget = "cancel_temp_target"