Add a TXM value for external devices

This commit is contained in:
Stephen B 2025-12-08 00:10:12 -05:00
parent 4e4cb2b782
commit 1ce0c91282
3 changed files with 114 additions and 24 deletions

View file

@ -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")
}

View file

@ -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 {

View file

@ -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
}
}
}