diff --git a/StikJIT/Utilities/DeviceLibraryStore.swift b/StikJIT/Utilities/DeviceLibraryStore.swift index b4284ca..ab87c74 100644 --- a/StikJIT/Utilities/DeviceLibraryStore.swift +++ b/StikJIT/Utilities/DeviceLibraryStore.swift @@ -15,6 +15,53 @@ struct DeviceProfileEntry: Identifiable, Codable, Equatable { var pairingFilename: String var dateAdded: Date var lastUpdated: Date + var isTXM: Bool + + init(id: UUID, + name: String, + ipAddress: String, + pairingRelativePath: String, + pairingFilename: String, + dateAdded: Date, + lastUpdated: Date, + isTXM: Bool = false) { + self.id = id + self.name = name + self.ipAddress = ipAddress + self.pairingRelativePath = pairingRelativePath + self.pairingFilename = pairingFilename + self.dateAdded = dateAdded + self.lastUpdated = lastUpdated + self.isTXM = isTXM + } + + enum CodingKeys: String, CodingKey { + case id, name, ipAddress, pairingRelativePath, pairingFilename, dateAdded, lastUpdated, isTXM + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + ipAddress = try container.decode(String.self, forKey: .ipAddress) + pairingRelativePath = try container.decode(String.self, forKey: .pairingRelativePath) + pairingFilename = try container.decode(String.self, forKey: .pairingFilename) + dateAdded = try container.decode(Date.self, forKey: .dateAdded) + lastUpdated = try container.decode(Date.self, forKey: .lastUpdated) + isTXM = try container.decodeIfPresent(Bool.self, forKey: .isTXM) ?? false + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(name, forKey: .name) + try container.encode(ipAddress, forKey: .ipAddress) + try container.encode(pairingRelativePath, forKey: .pairingRelativePath) + try container.encode(pairingFilename, forKey: .pairingFilename) + try container.encode(dateAdded, forKey: .dateAdded) + try container.encode(lastUpdated, forKey: .lastUpdated) + try container.encode(isTXM, forKey: .isTXM) + } } enum DeviceLibraryError: LocalizedError { @@ -95,7 +142,11 @@ final class DeviceLibraryStore: ObservableObject { loadFromDisk() } - func addDevice(name: String, ipAddress: String, pairingData: Data?, originalFilename: String?) throws { + func addDevice(name: String, + ipAddress: String, + pairingData: Data?, + originalFilename: String?, + isTXM: Bool) throws { guard let pairingData else { throw DeviceLibraryError.missingPairingData } @@ -110,7 +161,8 @@ final class DeviceLibraryStore: ObservableObject { pairingRelativePath: relativePath, pairingFilename: originalFilename ?? "pairingFile.plist", dateAdded: now, - lastUpdated: now + lastUpdated: now, + isTXM: isTXM ) devices.append(entry) persistDevices() @@ -120,7 +172,8 @@ final class DeviceLibraryStore: ObservableObject { name: String, ipAddress: String, pairingData: Data?, - originalFilename: String?) throws { + originalFilename: String?, + isTXM: Bool) throws { guard !isDefaultDevice(device) else { return } guard let index = devices.firstIndex(where: { $0.id == device.id }) else { throw DeviceLibraryError.deviceNotFound @@ -129,6 +182,7 @@ final class DeviceLibraryStore: ObservableObject { devices[index].name = name.trimmingCharacters(in: .whitespacesAndNewlines) devices[index].ipAddress = ipAddress.trimmingCharacters(in: .whitespacesAndNewlines) devices[index].lastUpdated = Date() + devices[index].isTXM = isTXM if activeDeviceID == device.id { UserDefaults.standard.set(devices[index].ipAddress, forKey: "TunnelDeviceIP") } diff --git a/StikJIT/Views/DeviceLibraryView.swift b/StikJIT/Views/DeviceLibraryView.swift index f9d0c0d..8a5f27c 100644 --- a/StikJIT/Views/DeviceLibraryView.swift +++ b/StikJIT/Views/DeviceLibraryView.swift @@ -140,14 +140,19 @@ struct DeviceLibraryView: View { private func handleSave(mode: DeviceEditorMode, input: DeviceEditorInput) throws { switch mode { case .add: - try store.addDevice(name: input.name, ipAddress: input.ipAddress, pairingData: input.pairingData, originalFilename: input.pairingFilename) + try store.addDevice(name: input.name, + ipAddress: input.ipAddress, + pairingData: input.pairingData, + originalFilename: input.pairingFilename, + isTXM: input.isTXM) alert = DeviceAlert(title: "Device Saved", message: "\(input.name) was added to your library.", isError: false) case .edit(let device): try store.update(device: device, name: input.name, ipAddress: input.ipAddress, pairingData: input.pairingData, - originalFilename: input.pairingFilename ?? device.pairingFilename) + originalFilename: input.pairingFilename ?? device.pairingFilename, + isTXM: input.isTXM) alert = DeviceAlert(title: "Device Updated", message: "\(input.name) has been updated.", isError: false) } } @@ -304,6 +309,21 @@ private struct DeviceRow: View { Text("Pairing: \(device.pairingFilename)") .font(.footnote) .foregroundColor(.secondary) + if device.isTXM { + Label("TXM Capable", systemImage: "shield.checkerboard") + .font(.caption2.weight(.semibold)) + .padding(.horizontal, 8) + .padding(.vertical, 2) + .background(Color.green.opacity(0.15), in: Capsule()) + .foregroundColor(.green) + } else { + Label("Non-TXM", systemImage: "xmark.shield") + .font(.caption2.weight(.semibold)) + .padding(.horizontal, 8) + .padding(.vertical, 2) + .background(Color.secondary.opacity(0.15), in: Capsule()) + .foregroundColor(.secondary) + } } } Spacer() @@ -349,6 +369,7 @@ private struct DeviceEditorInput { var ipAddress: String var pairingData: Data? var pairingFilename: String? + var isTXM: Bool } private struct DeviceEditorSheet: View { @@ -366,6 +387,7 @@ private struct DeviceEditorSheet: View { @State private var selectedFilename: String? @State private var errorMessage: String? @State private var showImporter = false + @State private var isTXMDevice: Bool private var accentColor: Color { themeExpansion?.resolvedAccentColor(from: customAccentColorHex) ?? .blue } private var requiresPairing: Bool { @@ -397,9 +419,11 @@ private struct DeviceEditorSheet: View { case .add: _name = State(initialValue: "") _ipAddress = State(initialValue: "10.7.0.1") + _isTXMDevice = State(initialValue: false) case .edit(let device): _name = State(initialValue: device.name) _ipAddress = State(initialValue: device.ipAddress) + _isTXMDevice = State(initialValue: device.isTXM) } _selectedFilename = State(initialValue: nil) _pairingData = State(initialValue: nil) @@ -413,6 +437,13 @@ private struct DeviceEditorSheet: View { .textInputAutocapitalization(.words) TextField("Device IP", text: $ipAddress) .keyboardType(.numbersAndPunctuation) + Toggle(isOn: $isTXMDevice) { + Text("TXM Capable") + } + .tint(accentColor) + Text("Enable if this device includes the Trusted Execution Monitor (TXM).") + .font(.caption) + .foregroundColor(.secondary) } Section(header: Text("Pairing File")) { @@ -493,7 +524,8 @@ private struct DeviceEditorSheet: View { name: name, ipAddress: ipAddress, pairingData: pairingData, - pairingFilename: selectedFilename ?? existingFilename + pairingFilename: selectedFilename ?? existingFilename, + isTXM: isTXMDevice )) dismiss() } catch { diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index b38056a..fa8a023 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -2126,24 +2126,28 @@ struct HomeView: View { // MARK: - TXM detection public extension ProcessInfo { - var hasTXM: Bool { - if isTXMOverridden { - return true - } - - return { - if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36), - let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) { - return access("\(file)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 - } else { - return (FileManager.default.filePath(atPath: "/private/preboot", withLength: 96).map { - access("\($0)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 - }) ?? false - } - }() + var hasTXM: Bool { + if isTXMOverridden { + return true } - - var isTXMOverridden: Bool { - UserDefaults.standard.bool(forKey: UserDefaults.Keys.txmOverride) + if DeviceLibraryStore.shared.isUsingExternalDevice { + return DeviceLibraryStore.shared.activeDevice?.isTXM ?? false + } + return ProcessInfo.detectLocalTXM() + } + + var isTXMOverridden: Bool { + UserDefaults.standard.bool(forKey: UserDefaults.Keys.txmOverride) + } + + private static func detectLocalTXM() -> Bool { + if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36), + let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) { + return access("\(file)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 + } else { + return (FileManager.default.filePath(atPath: "/private/preboot", withLength: 96).map { + access("\($0)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 + }) ?? false } } +}