浏览代码

Encrypted payload

Jonas Björkert 9 月之前
父节点
当前提交
2bb41a1c83

+ 8 - 4
Trio.xcodeproj/project.pbxproj

@@ -574,6 +574,7 @@
 		DD3F1F8B2D9E08B600DCE7B3 /* NightscoutLoginStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */; };
 		DD3F1F8D2D9E0E0600DCE7B3 /* NightscoutSetupStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */; };
 		DD3F1F902D9E153F00DCE7B3 /* NightscoutImportStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */; };
+		DD485F182E466F1800CE8CBF /* SecureMessenger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD485F172E466F1800CE8CBF /* SecureMessenger.swift */; };
 		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
@@ -607,7 +608,7 @@
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
 		DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98ACBF2D71013200C0778F /* StatChartUtils.swift */; };
 		DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */; };
-		DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */; };
+		DD9ECB6A2CA99F6C00AA7C45 /* CommandPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */; };
 		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
@@ -1400,6 +1401,7 @@
 		DD3F1F8A2D9E08B200DCE7B3 /* NightscoutLoginStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutLoginStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8C2D9E0E0000DCE7B3 /* NightscoutSetupStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSetupStepView.swift; sourceTree = "<group>"; };
 		DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportStepView.swift; sourceTree = "<group>"; };
+		DD485F172E466F1800CE8CBF /* SecureMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureMessenger.swift; sourceTree = "<group>"; };
 		DD4A00202DAEEEC400AB7387 /* OnboardingView+AlgorithmUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AlgorithmUtil.swift"; sourceTree = "<group>"; };
 		DD4A00232DAEF5DC00AB7387 /* AlgorithmSettingsSubstepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsSubstepView.swift; sourceTree = "<group>"; };
 		DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsContentsStepView.swift; sourceTree = "<group>"; };
@@ -1430,7 +1432,7 @@
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
 		DD98ACBF2D71013200C0778F /* StatChartUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatChartUtils.swift; sourceTree = "<group>"; };
 		DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControl.swift; sourceTree = "<group>"; };
-		DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = "<group>"; };
+		DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPayload.swift; sourceTree = "<group>"; };
 		DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigStateModel.swift; sourceTree = "<group>"; };
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -2398,7 +2400,7 @@
 				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
-				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
+				DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */,
 				DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */,
 			);
 			path = Models;
@@ -3400,6 +3402,7 @@
 		DD9ECB662CA99EFE00AA7C45 /* RemoteControl */ = {
 			isa = PBXGroup;
 			children = (
+				DD485F172E466F1800CE8CBF /* SecureMessenger.swift */,
 				DD17A0312E3FEA1F008E1BF0 /* RemoteNotificationResponseManager.swift */,
 				DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */,
 				DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */,
@@ -4094,7 +4097,7 @@
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
-				DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */,
+				DD9ECB6A2CA99F6C00AA7C45 /* CommandPayload.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				5825A1BE2C97335C0046467E /* EditTempTargetForm.swift in Sources */,
 				BD47FDDD2D8B65B10043966B /* GlucoseTargetStepView.swift in Sources */,
@@ -4253,6 +4256,7 @@
 				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
+				DD485F182E466F1800CE8CBF /* SecureMessenger.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,

+ 3 - 3
Trio/Sources/Application/AppDelegate.swift

@@ -32,11 +32,11 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
 
         do {
             let jsonData = try JSONSerialization.data(withJSONObject: userInfo)
-            let pushMessage = try JSONDecoder().decode(PushMessage.self, from: jsonData)
+            let encryptedMessage = try JSONDecoder().decode(EncryptedPushMessage.self, from: jsonData)
 
             Task {
                 do {
-                    try await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
+                    try await TrioRemoteControl.shared.handleRemoteNotification(encryptedData: encryptedMessage.encryptedData)
                     completionHandler(.newData)
                 } catch {
                     debug(
@@ -47,7 +47,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
                 }
             }
         } catch {
-            debug(.remoteControl, "Error decoding push message: \(error)")
+            debug(.remoteControl, "Error decoding push message shell: \(error)")
             completionHandler(.failed)
         }
     }

+ 132 - 0
Trio/Sources/Models/CommandPayload.swift

@@ -0,0 +1,132 @@
+import Foundation
+
+struct EncryptedPushMessage: Decodable {
+    let encryptedData: String
+
+    enum CodingKeys: String, CodingKey {
+        case encryptedData = "encrypted_data"
+    }
+}
+
+struct CommandPayload: Decodable, Sendable {
+    var user: String
+    var commandType: TrioRemoteControl.CommandType
+    var timestamp: TimeInterval
+    var bolusAmount: Decimal?
+    var target: Int?
+    var duration: Int?
+    var carbs: Int?
+    var protein: Int?
+    var fat: Int?
+    var overrideName: String?
+    var scheduledTime: TimeInterval?
+    var returnNotification: ReturnNotificationInfo?
+
+    struct ReturnNotificationInfo: Decodable, Sendable {
+        let productionEnvironment: Bool
+        let deviceToken: String
+        let bundleId: String
+        let teamId: String
+        let keyId: String
+        let apnsKey: String
+
+        enum CodingKeys: String, CodingKey {
+            case productionEnvironment = "production_environment"
+            case deviceToken = "device_token"
+            case bundleId = "bundle_id"
+            case teamId = "team_id"
+            case keyId = "key_id"
+            case apnsKey = "apns_key"
+        }
+    }
+
+    enum CodingKeys: String, CodingKey {
+        case user
+        case timestamp
+        case target
+        case duration
+        case carbs
+        case protein
+        case fat
+        case overrideName
+        case commandType = "command_type"
+        case bolusAmount = "bolus_amount"
+        case scheduledTime = "scheduled_time"
+        case returnNotification = "return_notification"
+    }
+
+    func humanReadableDescription() -> String {
+        var description = "User: \(user). Command Type: \(commandType.description). "
+
+        if let override = overrideName {
+            description += "Override Name: \(override). "
+        }
+
+        switch commandType {
+        case .bolus:
+            if let amount = bolusAmount {
+                description += "Bolus Amount: \(amount) units."
+            } else {
+                description += "Bolus Amount: unknown."
+            }
+        case .tempTarget:
+            let targetDesc = target != nil ? "\(target!) mg/dL" : "unknown target"
+            let durationDesc = duration != nil ? "\(duration!) minutes" : "unknown duration"
+            description += "Temp Target: \(targetDesc), Duration: \(durationDesc)."
+        case .cancelTempTarget:
+            description += "Cancel Temp Target command."
+        case .meal:
+            let carbsDesc = carbs != nil ? "\(carbs!)g carbs" : "unknown carbs"
+            let fatDesc = fat != nil ? "\(fat!)g fat" : "unknown fat"
+            let proteinDesc = protein != nil ? "\(protein!)g protein" : "unknown protein"
+            description += "Meal with \(carbsDesc), \(fatDesc), \(proteinDesc)."
+        case .startOverride:
+            if let override = overrideName {
+                description += "Start Override: \(override)."
+            } else {
+                description += "Start Override: unknown override name."
+            }
+        case .cancelOverride:
+            description += "Cancel Override command."
+        }
+
+        if let scheduledTime = scheduledTime {
+            let date = Date(timeIntervalSince1970: scheduledTime)
+            let formatter = DateFormatter()
+            formatter.dateStyle = .short
+            formatter.timeStyle = .short
+            let dateString = formatter.string(from: date)
+            description += " Scheduled for: \(dateString)."
+        }
+
+        return description
+    }
+}
+
+extension TrioRemoteControl {
+    enum CommandType: String, Codable {
+        case bolus
+        case tempTarget = "temp_target"
+        case cancelTempTarget = "cancel_temp_target"
+        case meal
+        case startOverride = "start_override"
+        case cancelOverride = "cancel_override"
+
+        var description: String {
+            switch self {
+            case .bolus:
+                return "Bolus"
+            case .tempTarget:
+                return "Temporary Target"
+            case .cancelTempTarget:
+                return "Cancel Temporary Target"
+            case .meal:
+                return "Meal"
+            case .startOverride:
+                return "Start Override"
+            case .cancelOverride:
+                return "Cancel Override"
+            }
+        }
+    }
+}

+ 0 - 163
Trio/Sources/Models/PushMessage.swift

@@ -1,163 +0,0 @@
-import Foundation
-
-struct PushMessage: Codable, Sendable {
-    var user: String
-    var commandType: TrioRemoteControl.CommandType
-    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?
-
-    struct ReturnNotificationInfo: Codable, Sendable {
-        let productionEnvironment: Bool
-        let deviceToken: String
-        let bundleId: String
-        let teamId: String
-        let keyId: String
-        let apnsKey: String
-
-        enum CodingKeys: String, CodingKey {
-            case productionEnvironment = "production_environment"
-            case deviceToken = "device_token"
-            case bundleId = "bundle_id"
-            case teamId = "team_id"
-            case keyId = "key_id"
-            case apnsKey = "apns_key"
-        }
-    }
-
-    enum CodingKeys: String, CodingKey {
-        case aps
-        case user
-        case commandType = "command_type"
-        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(user, forKey: .user)
-        try container.encode(commandType, 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)
-    }
-
-    init(from decoder: Decoder) throws {
-        let container = try decoder.container(keyedBy: CodingKeys.self)
-        user = try container.decode(String.self, forKey: .user)
-        commandType = try container.decode(TrioRemoteControl.CommandType.self, forKey: .commandType)
-        bolusAmount = try container.decodeIfPresent(Decimal.self, forKey: .bolusAmount)
-        target = try container.decodeIfPresent(Int.self, forKey: .target)
-        duration = try container.decodeIfPresent(Int.self, forKey: .duration)
-        carbs = try container.decodeIfPresent(Int.self, forKey: .carbs)
-        protein = try container.decodeIfPresent(Int.self, forKey: .protein)
-        fat = try container.decodeIfPresent(Int.self, forKey: .fat)
-        sharedSecret = try container.decode(String.self, forKey: .sharedSecret)
-        timestamp = try container.decode(TimeInterval.self, forKey: .timestamp)
-        overrideName = try container.decodeIfPresent(String.self, forKey: .overrideName)
-        scheduledTime = try container.decodeIfPresent(TimeInterval.self, forKey: .scheduledTime)
-        returnNotification = try container.decodeIfPresent(ReturnNotificationInfo.self, forKey: .returnNotification)
-    }
-
-    init(
-        user: String,
-        commandType: TrioRemoteControl.CommandType,
-        bolusAmount: Decimal? = nil,
-        target: Int? = nil,
-        duration: Int? = nil,
-        carbs: Int? = nil,
-        protein: Int? = nil,
-        fat: Int? = nil,
-        sharedSecret: String,
-        timestamp: TimeInterval,
-        overrideName: String? = nil,
-        scheduledTime: TimeInterval? = nil,
-        returnNotification: ReturnNotificationInfo? = nil
-    ) {
-        self.user = user
-        self.commandType = commandType
-        self.bolusAmount = bolusAmount
-        self.target = target
-        self.duration = duration
-        self.carbs = carbs
-        self.protein = protein
-        self.fat = fat
-        self.sharedSecret = sharedSecret
-        self.timestamp = timestamp
-        self.overrideName = overrideName
-        self.scheduledTime = scheduledTime
-        self.returnNotification = returnNotification
-    }
-
-    func humanReadableDescription() -> String {
-        var description = "User: \(user). Command Type: \(commandType.description). "
-
-        if let override = overrideName {
-            description += "Override Name: \(override). "
-        }
-
-        switch commandType {
-        case .bolus:
-            if let amount = bolusAmount {
-                description += "Bolus Amount: \(amount) units."
-            } else {
-                description += "Bolus Amount: unknown."
-            }
-        case .tempTarget:
-            let targetDesc = target != nil ? "\(target!) mg/dL" : "unknown target"
-            let durationDesc = duration != nil ? "\(duration!) minutes" : "unknown duration"
-            description += "Temp Target: \(targetDesc), Duration: \(durationDesc)."
-        case .cancelTempTarget:
-            description += "Cancel Temp Target command."
-        case .meal:
-            let carbsDesc = carbs != nil ? "\(carbs!)g carbs" : "unknown carbs"
-            let fatDesc = fat != nil ? "\(fat!)g fat" : "unknown fat"
-            let proteinDesc = protein != nil ? "\(protein!)g protein" : "unknown protein"
-            description += "Meal with \(carbsDesc), \(fatDesc), \(proteinDesc)."
-        case .startOverride:
-            if let override = overrideName {
-                description += "Start Override: \(override)."
-            } else {
-                description += "Start Override: unknown override name."
-            }
-        case .cancelOverride:
-            description += "Cancel Override command."
-        }
-
-        if let scheduledTime = scheduledTime {
-            let date = Date(timeIntervalSince1970: scheduledTime)
-            let formatter = DateFormatter()
-            formatter.dateStyle = .short
-            formatter.timeStyle = .short
-            let dateString = formatter.string(from: date)
-            description += " Scheduled for: \(dateString)."
-        }
-
-        return description
-    }
-}

+ 2 - 3
Trio/Sources/Services/RemoteControl/RemoteNotificationResponseManager.swift

@@ -30,12 +30,11 @@ class RemoteNotificationResponseManager {
     }
 
     func sendResponseNotification(
-        to returnInfo: PushMessage.ReturnNotificationInfo?,
+        to returnInfo: CommandPayload.ReturnNotificationInfo?,
         commandType: TrioRemoteControl.CommandType,
         success: Bool,
         message: String
     ) async {
-        // Don't send notification if no return info provided (old LoopFollow version)
         guard let returnInfo = returnInfo,
               !returnInfo.deviceToken.isEmpty
         else {
@@ -65,7 +64,7 @@ class RemoteNotificationResponseManager {
     private func sendPushNotification(
         payload: NotificationPayload,
         to deviceToken: String,
-        using returnInfo: PushMessage.ReturnNotificationInfo
+        using returnInfo: CommandPayload.ReturnNotificationInfo
     ) async {
         guard let jwt = APNSJWTManager.shared.getOrGenerateJWT(
             keyId: returnInfo.keyId,

+ 38 - 0
Trio/Sources/Services/RemoteControl/SecureMessenger.swift

@@ -0,0 +1,38 @@
+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())
+    }
+
+    func decrypt(base64EncodedString: String) throws -> CommandPayload {
+        guard let combinedData = Data(base64Encoded: base64EncodedString) else {
+            throw NSError(domain: "SecureMessenger", code: 100, userInfo: [NSLocalizedDescriptionKey: "Invalid Base64 string"])
+        }
+
+        let nonceSize = 12
+        guard combinedData.count > nonceSize else {
+            throw NSError(
+                domain: "SecureMessenger",
+                code: 101,
+                userInfo: [NSLocalizedDescriptionKey: "Encrypted data is too short to contain a nonce"]
+            )
+        }
+        let nonce = Array(combinedData.prefix(nonceSize))
+        let ciphertextAndTag = Array(combinedData.suffix(from: nonceSize))
+        let gcm = GCM(iv: nonce, mode: .combined)
+        let aes = try AES(key: sharedKey, blockMode: gcm, padding: .noPadding)
+        let decryptedBytes = try aes.decrypt(ciphertextAndTag)
+        let decryptedData = Data(decryptedBytes)
+        let commandPayload = try JSONDecoder().decode(CommandPayload.self, from: decryptedData)
+
+        return commandPayload
+    }
+}

+ 18 - 36
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift

@@ -1,9 +1,10 @@
 import Foundation
+import HealthKit
 
 extension TrioRemoteControl {
-    internal func handleBolusCommand(_ pushMessage: PushMessage) async throws {
-        guard let bolusAmount = pushMessage.bolusAmount else {
-            await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
+    internal func handleBolusCommand(_ payload: CommandPayload) async throws {
+        guard let bolusAmount = payload.bolusAmount else {
+            await logError("Command rejected: bolus amount is missing or invalid.", payload: payload)
             return
         }
 
@@ -12,7 +13,7 @@ extension TrioRemoteControl {
         if bolusAmount > maxBolus {
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
@@ -22,18 +23,18 @@ extension TrioRemoteControl {
         if (currentIOB + bolusAmount) > maxIOB {
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
 
         let totalRecentBolusAmount =
-            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
+            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: payload.timestamp))
 
         if totalRecentBolusAmount >= bolusAmount * 0.2 {
             await logError(
                 "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
@@ -43,16 +44,15 @@ extension TrioRemoteControl {
         guard let apsManager = await TrioApp.resolver.resolve(APSManager.self) else {
             await logError(
                 "Error: unable to process bolus command because the APS Manager is not available.",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
 
-        // Send "initiating bolus" notification
-        if let returnInfo = pushMessage.returnNotification {
+        if let returnInfo = payload.returnNotification {
             await RemoteNotificationResponseManager.shared.sendResponseNotification(
                 to: returnInfo,
-                commandType: pushMessage.commandType,
+                commandType: payload.commandType,
                 success: true,
                 message: "Initiating bolus..."
             )
@@ -61,18 +61,17 @@ extension TrioRemoteControl {
         await apsManager
             .enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false) { [weak self] success, message in
                 guard let self = self else { return }
-
                 Task {
                     if success {
                         await self.logSuccess(
-                            "Remote command processed successfully. \(pushMessage.humanReadableDescription())",
-                            pushMessage: pushMessage,
+                            "Remote command processed successfully. \(payload.humanReadableDescription())",
+                            payload: payload,
                             customNotificationMessage: "Bolus started"
                         )
                     } else {
                         await self.logError(
                             message,
-                            pushMessage: pushMessage
+                            payload: payload
                         )
                     }
                 }
@@ -81,17 +80,10 @@ extension TrioRemoteControl {
 
     private func fetchCurrentIOB() async throws -> Decimal {
         let predicate = NSPredicate.predicateFor30MinAgoForDetermination
-
         let determinations = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OrefDetermination.self,
-            onContext: pumpHistoryFetchContext,
-            predicate: predicate,
-            key: "timestamp",
-            ascending: false,
-            fetchLimit: 1,
-            propertiesToFetch: ["iob"]
+            ofType: OrefDetermination.self, onContext: pumpHistoryFetchContext, predicate: predicate, key: "timestamp",
+            ascending: false, fetchLimit: 1, propertiesToFetch: ["iob"]
         )
-
         guard let fetchedResults = determinations as? [[String: Any]],
               let firstResult = fetchedResults.first,
               let iob = firstResult["iob"] as? Decimal
@@ -99,7 +91,6 @@ extension TrioRemoteControl {
             await logError("Failed to fetch current IOB.")
             throw CoreDataError.fetchError(function: #function, file: #file)
         }
-
         return iob
     }
 
@@ -109,24 +100,15 @@ extension TrioRemoteControl {
             PumpEventStored.EventType.bolus.rawValue,
             date as NSDate
         )
-
         let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: pumpHistoryFetchContext,
-            predicate: predicate,
-            key: "timestamp",
-            ascending: true,
-            fetchLimit: nil,
-            propertiesToFetch: ["bolus.amount"]
+            ofType: PumpEventStored.self, onContext: pumpHistoryFetchContext, predicate: predicate, key: "timestamp",
+            ascending: true, fetchLimit: nil, propertiesToFetch: ["bolus.amount"]
         )
-
         guard let bolusDictionaries = results as? [[String: Any]] else {
             await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
             throw CoreDataError.fetchError(function: #function, file: #file)
         }
-
         let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
-
         return totalAmount
     }
 }

+ 8 - 10
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Helpers.swift

@@ -1,16 +1,15 @@
 import Foundation
 
 extension TrioRemoteControl {
-    func logError(_ errorMessage: String, pushMessage: PushMessage? = nil) async {
+    func logError(_ errorMessage: String, payload: CommandPayload? = nil) async {
         var note = errorMessage
-        if let pushMessage = pushMessage {
-            note += " Details: \(pushMessage.humanReadableDescription())"
+        if let payload = payload {
+            note += " Details: \(payload.humanReadableDescription())"
 
-            // Send error notification back to LoopFollow if return info exists
-            if let returnInfo = pushMessage.returnNotification {
+            if let returnInfo = payload.returnNotification {
                 await RemoteNotificationResponseManager.shared.sendResponseNotification(
                     to: returnInfo,
-                    commandType: pushMessage.commandType,
+                    commandType: payload.commandType,
                     success: false,
                     message: errorMessage
                 )
@@ -20,14 +19,13 @@ extension TrioRemoteControl {
         await nightscoutManager.uploadNoteTreatment(note: note)
     }
 
-    func logSuccess(_ message: String, pushMessage: PushMessage, customNotificationMessage: String? = nil) async {
+    func logSuccess(_ message: String, payload: CommandPayload, customNotificationMessage: String? = nil) async {
         debug(.remoteControl, message)
 
-        // Send success notification back to LoopFollow if return info exists
-        if let returnInfo = pushMessage.returnNotification {
+        if let returnInfo = payload.returnNotification {
             await RemoteNotificationResponseManager.shared.sendResponseNotification(
                 to: returnInfo,
-                commandType: pushMessage.commandType,
+                commandType: payload.commandType,
                 success: true,
                 message: customNotificationMessage ?? "Command successful"
             )

+ 22 - 41
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift

@@ -1,15 +1,15 @@
 import Foundation
 
 extension TrioRemoteControl {
-    func handleMealCommand(_ pushMessage: PushMessage) async throws {
-        guard pushMessage.carbs != nil || pushMessage.fat != nil || pushMessage.protein != nil else {
-            await logError("Command rejected: meal data is incomplete or invalid.", pushMessage: pushMessage)
+    func handleMealCommand(_ payload: CommandPayload) async throws {
+        guard payload.carbs != nil || payload.fat != nil || payload.protein != nil else {
+            await logError("Command rejected: meal data is incomplete or invalid.", payload: payload)
             return
         }
 
-        let carbsDecimal = pushMessage.carbs != nil ? Decimal(pushMessage.carbs!) : nil
-        let fatDecimal = pushMessage.fat != nil ? Decimal(pushMessage.fat!) : nil
-        let proteinDecimal = pushMessage.protein != nil ? Decimal(pushMessage.protein!) : nil
+        let carbsDecimal = payload.carbs != nil ? Decimal(payload.carbs!) : nil
+        let fatDecimal = payload.fat != nil ? Decimal(payload.fat!) : nil
+        let proteinDecimal = payload.protein != nil ? Decimal(payload.protein!) : nil
 
         let settings = await TrioApp.resolver.resolve(SettingsManager.self)?.settings
         let maxCarbs = settings?.maxCarbs ?? Decimal(0)
@@ -19,35 +19,29 @@ extension TrioRemoteControl {
         if let carbs = carbsDecimal, carbs > maxCarbs {
             await logError(
                 "Command rejected: carbs amount (\(carbs)g) exceeds the maximum allowed (\(maxCarbs)g).",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
-
         if let fat = fatDecimal, fat > maxFat {
-            await logError(
-                "Command rejected: fat amount (\(fat)g) exceeds the maximum allowed (\(maxFat)g).",
-                pushMessage: pushMessage
-            )
+            await logError("Command rejected: fat amount (\(fat)g) exceeds the maximum allowed (\(maxFat)g).", payload: payload)
             return
         }
-
         if let protein = proteinDecimal, protein > maxProtein {
             await logError(
                 "Command rejected: protein amount (\(protein)g) exceeds the maximum allowed (\(maxProtein)g).",
-                pushMessage: pushMessage
+                payload: payload
             )
             return
         }
 
-        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+        let payloadDate = Date(timeIntervalSince1970: payload.timestamp)
         let taskContext = CoreDataStack.shared.newTaskContext()
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: CarbEntryStored.self,
-            onContext: taskContext,
-            predicate: NSPredicate(format: "date > %@", pushMessageDate as NSDate),
-            key: "date",
-            ascending: false
+            ofType: CarbEntryStored.self, onContext: taskContext, predicate: NSPredicate(
+                format: "date > %@",
+                payloadDate as NSDate
+            ), key: "date", ascending: false
         )
 
         await taskContext.perform {
@@ -56,41 +50,28 @@ extension TrioRemoteControl {
                 Task {
                     await self.logError(
                         "Command rejected: newer carb entries have been logged since the command was sent.",
-                        pushMessage: pushMessage
+                        payload: payload
                     )
                     return
                 }
             }
         }
 
-        let actualDate: Date?
-        if let scheduledTime = pushMessage.scheduledTime {
-            actualDate = Date(timeIntervalSince1970: scheduledTime)
-        } else {
-            actualDate = nil
-        }
+        let actualDate = payload.scheduledTime.map { Date(timeIntervalSince1970: $0) }
 
         let mealEntry = CarbsEntry(
-            id: UUID().uuidString,
-            createdAt: Date(),
-            actualDate: actualDate,
-            carbs: carbsDecimal ?? 0,
-            fat: fatDecimal,
-            protein: proteinDecimal,
-            note: "Remote meal command",
-            enteredBy: CarbsEntry.local,
-            isFPU: false,
+            id: UUID().uuidString, createdAt: Date(), actualDate: actualDate,
+            carbs: carbsDecimal ?? 0, fat: fatDecimal, protein: proteinDecimal,
+            note: "Remote meal command", enteredBy: CarbsEntry.local, isFPU: false,
             fpuID: fatDecimal ?? 0 > 0 || proteinDecimal ?? 0 > 0 ? UUID().uuidString : nil
         )
 
         try await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
 
-        // Only send success notification if there's no bolus
-        // If there's a bolus, the bolus handler will send the notification
-        if pushMessage.bolusAmount == nil {
+        if payload.bolusAmount == nil {
             await logSuccess(
-                "Remote command processed successfully. \(pushMessage.humanReadableDescription())",
-                pushMessage: pushMessage,
+                "Remote command processed successfully. \(payload.humanReadableDescription())",
+                payload: payload,
                 customNotificationMessage: "Meal logged"
             )
         }

+ 23 - 58
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Override.swift

@@ -1,107 +1,78 @@
 import CoreData
 import Foundation
+import UIKit
 
 extension TrioRemoteControl {
-    @MainActor internal func handleCancelOverrideCommand(_ pushMessage: PushMessage) async {
+    @MainActor internal func handleCancelOverrideCommand(_ payload: CommandPayload) async {
         await disableAllActiveOverrides()
-
         await logSuccess(
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())",
-            pushMessage: pushMessage,
+            "Remote command processed successfully. \(payload.humanReadableDescription())",
+            payload: payload,
             customNotificationMessage: "Override canceled"
         )
     }
 
-    @MainActor internal func handleStartOverrideCommand(_ pushMessage: PushMessage) async {
+    @MainActor internal func handleStartOverrideCommand(_ payload: CommandPayload) async {
         do {
-            guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
-                await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
+            guard let overrideName = payload.overrideName, !overrideName.isEmpty else {
+                await logError("Command rejected: override name is missing.", payload: payload)
                 return
             }
-
             let presetIDs = try await overrideStorage.fetchForOverridePresets()
-
-            let presets = try presetIDs.compactMap { id in
-                try viewContext.existingObject(with: id) as? OverrideStored
-            }
-
+            let presets = try presetIDs.compactMap { try viewContext.existingObject(with: $0) as? OverrideStored }
             if let preset = presets.first(where: { $0.name == overrideName }) {
-                await enactOverridePreset(preset: preset, pushMessage: pushMessage)
+                await enactOverridePreset(preset: preset, payload: payload)
             } else {
-                await logError(
-                    "Command rejected: override preset '\(overrideName)' not found.",
-                    pushMessage: pushMessage
-                )
+                await logError("Command rejected: override preset '\(overrideName)' not found.", payload: payload)
             }
         } catch {
-            debug(
-                .remoteControl,
-                "\(DebuggingIdentifiers.failed) Failed to handle start override command: \(error)"
-            )
-            await logError(
-                "Command failed: \(error.localizedDescription)",
-                pushMessage: pushMessage
-            )
+            debug(.remoteControl, "\(DebuggingIdentifiers.failed) Failed to handle start override command: \(error)")
+            await logError("Command failed: \(error.localizedDescription)", payload: payload)
         }
     }
 
-    @MainActor private func enactOverridePreset(preset: OverrideStored, pushMessage: PushMessage) async {
+    @MainActor private func enactOverridePreset(preset: OverrideStored, payload: CommandPayload) async {
         preset.enabled = true
         preset.date = Date()
         preset.isUploadedToNS = false
-
         await disableAllActiveOverrides(except: preset.objectID)
-
         do {
             if viewContext.hasChanges {
                 try viewContext.save()
-
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 await awaitNotification(.didUpdateOverrideConfiguration)
-
                 await logSuccess(
-                    "Remote command processed successfully. \(pushMessage.humanReadableDescription())",
-                    pushMessage: pushMessage,
+                    "Remote command processed successfully. \(payload.humanReadableDescription())",
+                    payload: payload,
                     customNotificationMessage: "Override started"
                 )
             }
         } catch {
             debug(.remoteControl, "Failed to enact override preset: \(error)")
-            await logError("Failed to enact override preset: \(error.localizedDescription)", pushMessage: pushMessage)
+            await logError("Failed to enact override preset: \(error.localizedDescription)", payload: payload)
         }
     }
 
     @MainActor private func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil) async {
         do {
-            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
-
+            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0)
             let didPostNotification = try await viewContext.perform { () -> Bool in
-                let results = try ids.compactMap { id in
-                    try self.viewContext.existingObject(with: id) as? OverrideStored
-                }
-
+                let results = try ids.compactMap { try self.viewContext.existingObject(with: $0) as? OverrideStored }
                 guard !results.isEmpty else { return false }
-
                 for canceledOverride in results where canceledOverride.enabled {
-                    if let overrideID = overrideID, canceledOverride.objectID == overrideID {
-                        continue
-                    }
-
+                    if let overrideID = overrideID, canceledOverride.objectID == overrideID { continue }
                     let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
                     newOverrideRunStored.id = UUID()
                     newOverrideRunStored.name = canceledOverride.name
                     newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                     newOverrideRunStored.endDate = Date()
-                    newOverrideRunStored.target = NSDecimalNumber(
-                        decimal: self.overrideStorage.calculateTarget(override: canceledOverride)
-                    )
+                    newOverrideRunStored
+                        .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
                     newOverrideRunStored.override = canceledOverride
                     newOverrideRunStored.isUploadedToNS = false
-
                     canceledOverride.enabled = false
                     canceledOverride.isUploadedToNS = false
                 }
-
                 if self.viewContext.hasChanges {
                     try self.viewContext.save()
                     Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
@@ -110,15 +81,9 @@ extension TrioRemoteControl {
                     return false
                 }
             }
-
-            if didPostNotification {
-                await awaitNotification(.didUpdateOverrideConfiguration)
-            }
+            if didPostNotification { await awaitNotification(.didUpdateOverrideConfiguration) }
         } catch {
-            debug(
-                .remoteControl,
-                "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error)"
-            )
+            debug(.remoteControl, "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error)")
         }
     }
 }

+ 18 - 43
Trio/Sources/Services/RemoteControl/TrioRemoteControl+TempTarget.swift

@@ -1,28 +1,22 @@
 import CoreData
 import Foundation
+import UIKit
 
 extension TrioRemoteControl {
-    @MainActor func handleTempTargetCommand(_ pushMessage: PushMessage) async throws {
-        guard let targetValue = pushMessage.target,
-              let durationValue = pushMessage.duration
-        else {
-            await logError("Command rejected: temp target data is incomplete or invalid.", pushMessage: pushMessage)
+    @MainActor func handleTempTargetCommand(_ payload: CommandPayload) async throws {
+        guard let targetValue = payload.target, let durationValue = payload.duration else {
+            await logError("Command rejected: temp target data is incomplete or invalid.", payload: payload)
             return
         }
 
         let durationInMinutes = Int(durationValue)
-        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+        let payloadDate = Date(timeIntervalSince1970: payload.timestamp)
 
         let tempTarget = TempTarget(
-            name: TempTarget.custom,
-            createdAt: pushMessageDate,
-            targetTop: Decimal(targetValue),
-            targetBottom: Decimal(targetValue),
-            duration: Decimal(durationInMinutes),
-            enteredBy: TempTarget.local,
-            reason: TempTarget.custom,
-            isPreset: false,
-            enabled: true,
+            name: TempTarget.custom, createdAt: payloadDate,
+            targetTop: Decimal(targetValue), targetBottom: Decimal(targetValue),
+            duration: Decimal(durationInMinutes), enteredBy: TempTarget.local,
+            reason: TempTarget.custom, isPreset: false, enabled: true,
             halfBasalTarget: settings.preferences.halfBasalExerciseTarget
         )
 
@@ -30,20 +24,18 @@ extension TrioRemoteControl {
         tempTargetsStorage.saveTempTargetsToStorage([tempTarget])
 
         await logSuccess(
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())",
-            pushMessage: pushMessage,
+            "Remote command processed successfully. \(payload.humanReadableDescription())",
+            payload: payload,
             customNotificationMessage: "Temp target set"
         )
     }
 
-    @MainActor func cancelTempTarget(_ pushMessage: PushMessage) async {
+    @MainActor func cancelTempTarget(_ payload: CommandPayload) async {
         debug(.remoteControl, "Cancelling temp target.")
-
         await disableAllActiveTempTargets()
-
         await logSuccess(
-            "Remote command processed successfully. \(pushMessage.humanReadableDescription())",
-            pushMessage: pushMessage,
+            "Remote command processed successfully. \(payload.humanReadableDescription())",
+            payload: payload,
             customNotificationMessage: "Temp target canceled"
         )
     }
@@ -51,19 +43,12 @@ extension TrioRemoteControl {
     @MainActor func disableAllActiveTempTargets() async {
         do {
             let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
-
             let didPostNotification = try await viewContext.perform { () -> Bool in
-                let results = try ids.compactMap { id in
-                    try self.viewContext.existingObject(with: id) as? TempTargetStored
-                }
-
+                let results = try ids.compactMap { try self.viewContext.existingObject(with: $0) as? TempTargetStored }
                 guard !results.isEmpty else {
-                    Task {
-                        await self.logError("Command rejected: no active temp target to cancel.")
-                    }
+                    Task { await self.logError("Command rejected: no active temp target to cancel.") }
                     return false
                 }
-
                 for canceledTempTarget in results where canceledTempTarget.enabled {
                     let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
                     newTempTargetRunStored.id = UUID()
@@ -73,31 +58,21 @@ extension TrioRemoteControl {
                     newTempTargetRunStored.target = canceledTempTarget.target ?? 0
                     newTempTargetRunStored.tempTarget = canceledTempTarget
                     newTempTargetRunStored.isUploadedToNS = false
-
                     canceledTempTarget.enabled = false
                     canceledTempTarget.isUploadedToNS = false
                 }
-
                 if self.viewContext.hasChanges {
                     try self.viewContext.save()
                     Foundation.NotificationCenter.default.post(name: .willUpdateTempTargetConfiguration, object: nil)
-
-                    // Update the storage so oref can pick up cancellation
                     self.tempTargetsStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
                     return true
                 } else {
                     return false
                 }
             }
-
-            if didPostNotification {
-                await awaitNotification(.didUpdateTempTargetConfiguration)
-            }
+            if didPostNotification { await awaitNotification(.didUpdateTempTargetConfiguration) }
         } catch {
-            debug(
-                .remoteControl,
-                "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error)"
-            )
+            debug(.remoteControl, "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error)")
             await logError("Failed to disable temp targets: \(error.localizedDescription)")
         }
     }

+ 38 - 62
Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift

@@ -11,7 +11,7 @@ class TrioRemoteControl: Injectable {
     @Injected() internal var overrideStorage: OverrideStorage!
     @Injected() internal var settings: SettingsManager!
 
-    private let timeWindow: TimeInterval = 600 // Defines how old messages that are accepted, 10 minutes
+    private let timeWindow: TimeInterval = 600
 
     internal let pumpHistoryFetchContext: NSManagedObjectContext
     internal let viewContext: NSManagedObjectContext
@@ -22,96 +22,72 @@ class TrioRemoteControl: Injectable {
         injectServices(TrioApp.resolver)
     }
 
-    func handleRemoteNotification(pushMessage: PushMessage) async throws {
+    func handleRemoteNotification(encryptedData: String) async throws {
         let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
         guard isTrioRemoteControlEnabled else {
             await logError("Remote command received, but remote control is disabled in settings. Ignoring the command.")
             return
         }
 
-        let currentTime = Date().timeIntervalSince1970
-        let timeDifference = currentTime - pushMessage.timestamp
+        let storedSecret = UserDefaults.standard.string(forKey: "trioRemoteControlSharedSecret") ?? ""
+        guard !storedSecret.isEmpty else {
+            await logError("Command rejected: shared secret is missing in settings. Cannot authenticate the command.")
+            return
+        }
 
-        if timeDifference > timeWindow {
-            await logError(
-                "Command rejected: the message is too old (sent \(Int(timeDifference)) seconds ago, which exceeds the allowed limit).",
-                pushMessage: pushMessage
-            )
+        guard let messenger = SecureMessenger(sharedSecret: storedSecret) else {
+            await logError("Command rejected: Failed to initialize security module. The shared secret might be invalid.")
             return
-        } else if timeDifference < -timeWindow {
+        }
+
+        let commandPayload: CommandPayload
+        do {
+            commandPayload = try messenger.decrypt(base64EncodedString: encryptedData)
+        } catch {
             await logError(
-                "Command rejected: the message has an invalid future timestamp (timestamp is \(Int(-timeDifference)) seconds ahead of the current time).",
-                pushMessage: pushMessage
+                "Command rejected: Decryption failed. Mismatched shared secret or corrupted message. Error: \(error.localizedDescription)"
             )
             return
         }
 
-        debug(.remoteControl, "Command received with acceptable time difference: \(Int(timeDifference)) seconds.")
+        let currentTime = Date().timeIntervalSince1970
+        let timeDifference = currentTime - commandPayload.timestamp
 
-        let storedSecret = UserDefaults.standard.string(forKey: "trioRemoteControlSharedSecret") ?? ""
-        guard !storedSecret.isEmpty else {
+        if timeDifference > timeWindow {
             await logError(
-                "Command rejected: shared secret is missing in settings. Cannot authenticate the command.",
-                pushMessage: pushMessage
+                "Command rejected: the message is too old (sent \(Int(timeDifference)) seconds ago).",
+                payload: commandPayload
             )
             return
-        }
-
-        guard pushMessage.sharedSecret == storedSecret else {
+        } else if timeDifference < -timeWindow {
             await logError(
-                "Command rejected: shared secret does not match. Cannot authenticate the command.",
-                pushMessage: pushMessage
+                "Command rejected: the message has an invalid future timestamp.",
+                payload: commandPayload
             )
             return
         }
 
-        switch pushMessage.commandType {
+        debug(
+            .remoteControl,
+            "Command successfully decrypted and authenticated. Time difference: \(Int(timeDifference)) seconds."
+        )
+
+        switch commandPayload.commandType {
         case .bolus:
-            try await handleBolusCommand(pushMessage)
+            try await handleBolusCommand(commandPayload)
         case .tempTarget:
-            try await handleTempTargetCommand(pushMessage)
+            try await handleTempTargetCommand(commandPayload)
         case .cancelTempTarget:
-            await cancelTempTarget(pushMessage)
+            await cancelTempTarget(commandPayload)
         case .meal:
-            try await handleMealCommand(pushMessage)
-
-            if pushMessage.bolusAmount != nil {
-                try await handleBolusCommand(pushMessage)
+            try await handleMealCommand(commandPayload)
+            if commandPayload.bolusAmount != nil {
+                try await handleBolusCommand(commandPayload)
             }
         case .startOverride:
-            await handleStartOverrideCommand(pushMessage)
+            await handleStartOverrideCommand(commandPayload)
         case .cancelOverride:
-            await handleCancelOverrideCommand(pushMessage)
-        }
-    }
-}
-
-// MARK: - CommandType Enum
-
-extension TrioRemoteControl {
-    enum CommandType: String, Codable {
-        case bolus
-        case tempTarget = "temp_target"
-        case cancelTempTarget = "cancel_temp_target"
-        case meal
-        case startOverride = "start_override"
-        case cancelOverride = "cancel_override"
-
-        var description: String {
-            switch self {
-            case .bolus:
-                return "Bolus"
-            case .tempTarget:
-                return "Temporary Target"
-            case .cancelTempTarget:
-                return "Cancel Temporary Target"
-            case .meal:
-                return "Meal"
-            case .startOverride:
-                return "Start Override"
-            case .cancelOverride:
-                return "Cancel Override"
-            }
+            await handleCancelOverrideCommand(commandPayload)
         }
     }
 }