Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -69928,6 +69928,17 @@
}
}
},
"Remote Signed Admin: %@" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Remote Signed Admin: %@"
}
}
}
},
"Remove" : {
"localizations" : {
"da" : {
Expand Down Expand Up @@ -70531,6 +70542,17 @@
}
}
},
"Request Signed Admin: %@" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Request Signed Admin: %@"
}
}
}
},
"Requested" : {
"comment" : "A label indicating that a trace route was requested.",
"isCommentAutoGenerated" : true
Expand Down Expand Up @@ -103259,4 +103281,4 @@
}
},
"version" : "1.1"
}
}
4 changes: 4 additions & 0 deletions Meshtastic.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@
DD00003ASUPPORTLVL0000001 /* SupportLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD00003ASUPPORTLVL0000000 /* SupportLevel.swift */; };
DD00003C000SNAP00000001 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = DD00003B000SNAP00000000 /* SnapshotTesting */; };
DD0000CHSETST0000000001 /* ChannelSetSaveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0000CHSETST0000000000 /* ChannelSetSaveTests.swift */; };
DD0000RADMINTST00000001 /* RemoteAdminTransportPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0000RADMINTST00000000 /* RemoteAdminTransportPolicyTests.swift */; };
DD0000MQTTFWDFLTTST00001 /* MqttForwardFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0000MQTTFWDFLTTST00000 /* MqttForwardFilterTests.swift */; };
DD0000DEVLNKTST000000001 /* DeviceLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0000DEVLNKTST000000000 /* DeviceLinkTests.swift */; };
DD1827FB0000000000000002 /* RegionCodesFirmwareLocaleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1827FA0000000000000002 /* RegionCodesFirmwareLocaleTests.swift */; };
Expand Down Expand Up @@ -943,6 +944,7 @@
DD000031FWEDTENUM0000000 /* FirmwareEditionEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareEditionEnum.swift; sourceTree = "<group>"; };
DD00003ASUPPORTLVL0000000 /* SupportLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportLevel.swift; sourceTree = "<group>"; };
DD0000CHSETST0000000000 /* ChannelSetSaveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelSetSaveTests.swift; sourceTree = "<group>"; };
DD0000RADMINTST00000000 /* RemoteAdminTransportPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAdminTransportPolicyTests.swift; sourceTree = "<group>"; };
DD0000MQTTFWDFLTTST00000 /* MqttForwardFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttForwardFilterTests.swift; sourceTree = "<group>"; };
DD0000DEVLNKTST000000000 /* DeviceLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinkTests.swift; sourceTree = "<group>"; };
DD1827FA0000000000000002 /* RegionCodesFirmwareLocaleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionCodesFirmwareLocaleTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1692,6 +1694,7 @@
DD0000DEVLNKTST000000000 /* DeviceLinkTests.swift */,
BEEF00102F00000100000003 /* MeshtasticChannelURLTests.swift */,
DD0000CHSETST0000000000 /* ChannelSetSaveTests.swift */,
DD0000RADMINTST00000000 /* RemoteAdminTransportPolicyTests.swift */,
DD0000MQTTFWDFLTTST00000 /* MqttForwardFilterTests.swift */,
DD1827FA0000000000000002 /* RegionCodesFirmwareLocaleTests.swift */,
DD000031STATMSGDISP00000 /* StatusMessageDisplayTests.swift */,
Expand Down Expand Up @@ -2776,6 +2779,7 @@
DD7E5750000000000000008B /* LockdownPassphraseStoreTests.swift in Sources */,
DD0000DEVLNKTST000000001 /* DeviceLinkTests.swift in Sources */,
DD0000CHSETST0000000001 /* ChannelSetSaveTests.swift in Sources */,
DD0000RADMINTST00000001 /* RemoteAdminTransportPolicyTests.swift in Sources */,
DD0000MQTTFWDFLTTST00001 /* MqttForwardFilterTests.swift in Sources */,
DD1827FB0000000000000002 /* RegionCodesFirmwareLocaleTests.swift in Sources */,
DD000031STATMSGDISP00001 /* StatusMessageDisplayTests.swift in Sources */,
Expand Down
31 changes: 30 additions & 1 deletion Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -518,12 +518,41 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
await active.connection.isConnected else {
throw AccessoryError.connectionFailed("Not connected to any device")
}
try await active.connection.send(data)
try await active.connection.send(applyingLicensedRemoteAdminPolicy(to: data, connectedDeviceNum: active.device.num))
if let debugDescription {
Logger.transport.info("📻 \(debugDescription, privacy: .public)")
}
}

/// Licensed-mode remote administration is authenticated by the firmware's verified packet
/// signature and must remain plaintext on air. Apply that invariant at the final send boundary
/// so both the shared Admin helper and older direct-send call sites receive the same treatment.
private func applyingLicensedRemoteAdminPolicy(to data: ToRadio, connectedDeviceNum: Int64?) -> ToRadio {
guard let connectedDeviceNum else { return data }
guard case let .packet(packet) = data.payloadVariant,
case let .decoded(decoded) = packet.payloadVariant,
decoded.portnum == .adminApp,
packet.to != Constants.maximumNodeNum,
packet.to != UInt32(truncatingIfNeeded: connectedDeviceNum)
else { return data }

let ownerNum = connectedDeviceNum
var descriptor = FetchDescriptor<UserEntity>(predicate: #Predicate { $0.num == ownerNum })
descriptor.fetchLimit = 1
guard let connectedOwner = try? context.fetch(descriptor).first,
connectedOwner.isLicensed
else { return data }

var plaintextPacket = packet
plaintextPacket.channel = 0
plaintextPacket.pkiEncrypted = false
plaintextPacket.publicKey = Data()

var result = data
result.packet = plaintextPacket
return result
}

func didReceive(_ event: ConnectionEvent) async {
let shouldIgnoreTransientEvent = isClosingConnection || userRequestedConnectionCancellation || activeConnection == nil

Expand Down
38 changes: 34 additions & 4 deletions Meshtastic/Views/Settings/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ import OSLog
import TipKit
import MeshtasticProtobufs

enum RemoteAdminWording {
struct ActivePresentation: Equatable {
let format: String
let systemImage: String
let representsVerifiedSignature: Bool
}

static func activePresentation(localOwnerIsLicensed: Bool) -> ActivePresentation {
localOwnerIsLicensed
? ActivePresentation(format: "Remote Signed Admin: %@", systemImage: "checkmark.shield", representsVerifiedSignature: true)
: ActivePresentation(format: "Remote PKI Admin: %@", systemImage: "av.remote", representsVerifiedSignature: false)
}

static func requestFormat(localOwnerIsLicensed: Bool) -> String {
localOwnerIsLicensed ? "Request Signed Admin: %@" : "Request PKI Admin: %@"
}
}

struct Settings: View {
@Environment(\.modelContext) private var context
@Environment(\.colorScheme) private var colorScheme
Expand All @@ -28,6 +46,11 @@ struct Settings: View {
NodeInfoEntity.adminPickerOrder(nodes)
}

private var localOwnerIsLicensed: Bool {
guard let activeDeviceNum = accessoryManager.activeDeviceNum else { return false }
return nodes.first(where: { $0.num == activeDeviceNum })?.user?.isLicensed == true
}

@State private var selectedNode: Int = 0
@State private var preferredNodeNum: Int = 0

Expand Down Expand Up @@ -573,11 +596,15 @@ struct Settings: View {
accessoryManager.activeConnection?.device.transportType.icon ?? Image("questionmark.circle")
}
.tag(Int(node.num))
} else if node.canRemoteAdmin && UserDefaults.enableAdministration && node.sessionPasskey != nil { /// Nodes using the new PKI system
} else if node.canRemoteAdmin && UserDefaults.enableAdministration && node.sessionPasskey != nil {
let presentation = RemoteAdminWording.activePresentation(localOwnerIsLicensed: localOwnerIsLicensed)
Label {
Text("Remote PKI Admin: \(node.user?.longName ?? "Unknown".localized)")
Text(String.localizedStringWithFormat(
presentation.format.localized,
node.user?.longName ?? "Unknown".localized
))
} icon: {
Image(systemName: "av.remote")
Image(systemName: presentation.systemImage)
}
.font(.caption2)
.tag(Int(node.num))
Expand All @@ -590,7 +617,10 @@ struct Settings: View {
.tag(Int(node.num))
} else if UserDefaults.enableAdministration && node.user?.pkiEncrypted ?? false {
Label {
Text("Request PKI Admin: \(node.user?.longName?.addingVariationSelectors ?? "Unknown".localized)")
Text(String.localizedStringWithFormat(
RemoteAdminWording.requestFormat(localOwnerIsLicensed: localOwnerIsLicensed).localized,
node.user?.longName?.addingVariationSelectors ?? "Unknown".localized
))
} icon: {
Image(systemName: "rectangle.and.hand.point.up.left")
}
Expand Down
Loading
Loading