From bd88e1fdd325f81b8d0a9af6b53fdd54b4af3532 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Jun 2025 20:19:42 -0700 Subject: [PATCH 1/3] Bump version, add channel help to settings channel update locks to be red only if precise location is being shared on an unencrypted channel --- Localizable.xcstrings | 14 ++++++- Meshtastic.xcodeproj/project.pbxproj | 4 ++ Meshtastic/Helpers/BLEManager.swift | 6 +-- Meshtastic/Views/Helpers/ChannelLock.swift | 35 ++++++++++++++++ .../Views/Helpers/Help/ChannelsHelp.swift | 33 ++++++++++++--- Meshtastic/Views/Messages/ChannelList.swift | 10 +---- Meshtastic/Views/Settings/Channels.swift | 33 +++++++++++---- Meshtastic/Views/Settings/ShareChannels.swift | 40 ++++++++++++++----- 8 files changed, 138 insertions(+), 37 deletions(-) create mode 100644 Meshtastic/Views/Helpers/ChannelLock.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 28dd7f9c..ba0d7976 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1693,7 +1693,7 @@ } } }, - "A channel index of 0 indicates the primary channel where all broadcast packets are sent from." : { + "A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward." : { }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { @@ -1757,7 +1757,10 @@ } } }, - "A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted." : { + "A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key." : { + + }, + "A red open lock with a warning means the channel is not securely encrypted and is used for precise location data which is being uplinked to the internet via MQTT, it uses either no key at all or a 1 byte known key." : { }, "A Trace Route was sent, no response has been received." : { @@ -1781,6 +1784,9 @@ } } } + }, + "A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key." : { + }, "About" : { "localizations" : { @@ -28238,6 +28244,7 @@ } }, "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -28252,6 +28259,9 @@ } } } + }, + "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to start the live activity." : { + }, "Shut Down" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 956f7e9f..876c41c5 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */; }; DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */; }; DD1BEF502E0528AA0090CE24 /* PersistantTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */; }; + DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -380,6 +381,7 @@ DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = ""; }; DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsHelp.swift; sourceTree = ""; }; DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistantTips.swift; sourceTree = ""; }; + DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelLock.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -1057,6 +1059,7 @@ DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */, DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */, DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */, + DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */, DD47E3D526F17ED900029299 /* CircleText.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */, @@ -1384,6 +1387,7 @@ 25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */, 259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */, BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */, + DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */, 259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */, 259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */, DDDB444829F8A9C900EE2349 /* String.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index ffe00410..389e812a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -725,7 +725,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let message = CocoaMQTTMessage(topic: decodedInfo.mqttClientProxyMessage.topic, payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), retained: decodedInfo.mqttClientProxyMessage.retained) mqttManager.mqttClientProxy?.publish(message) } else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) { - var path = "meshtastic:///settings/debugLogs" if decodedInfo.clientNotification.hasReplyID { /// Set Sent bool on TraceRouteEntity to false if we got rate limited @@ -740,8 +739,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let nsError = error as NSError Logger.data.error("💥 [TraceRouteEntity] Error Updating Core Data: \(nsError, privacy: .public)") } - } else if decodedInfo.clientNotification.message.starts(with: "You Device is configured with a low entropy") || decodedInfo.clientNotification.message.starts(with: "Compromised keys detected") - || decodedInfo.clientNotification.message.starts(with: "Remote device"){ + } + if decodedInfo.clientNotification.payloadVariant == ClientNotification.OneOf_PayloadVariant.lowEntropyKey(decodedInfo.clientNotification.lowEntropyKey) || + decodedInfo.clientNotification.payloadVariant == ClientNotification.OneOf_PayloadVariant.duplicatedPublicKey(decodedInfo.clientNotification.duplicatedPublicKey) { path = "meshtastic:///settings/security" } } diff --git a/Meshtastic/Views/Helpers/ChannelLock.swift b/Meshtastic/Views/Helpers/ChannelLock.swift new file mode 100644 index 00000000..3a66dc5a --- /dev/null +++ b/Meshtastic/Views/Helpers/ChannelLock.swift @@ -0,0 +1,35 @@ +// +// ChannelLock.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/22/25. +// +import SwiftUI + +struct ChannelLock: View { + + @ObservedObject var channel: ChannelEntity + + var body: some View { + /// Unencrypted - using no key at all or a known 1 byte key + if channel.psk?.hexDescription.count ?? 0 < 3 { + let preciseLoction = 17...32 + // Using precise location and have MQTT uplink enabled + if channel.uplinkEnabled && preciseLoction ~= (Int(channel.positionPrecision)) { + Image(systemName: "lock.open.trianglebadge.exclamationmark.fill") + .foregroundColor(.red) + // Using precise location + } else if preciseLoction ~= (Int(channel.positionPrecision)) { + Image(systemName: "lock.open.fill") + .foregroundColor(.red) + // Just unencrypted without any location or MQTT + } else { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + } + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } +} diff --git a/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift index 830fe3cd..ad8b3b06 100644 --- a/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift +++ b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift @@ -21,25 +21,46 @@ struct ChannelsHelp: View { CircleText(text: String(0), color: .accentColor) .brightness(0.2) .offset(y: -10) - Text("A channel index of 0 indicates the primary channel where all broadcast packets are sent from.") + Text("A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward.") .fixedSize(horizontal: false, vertical: true) .padding(.bottom) + .padding(.leading, 7) } HStack { Image(systemName: "lock.fill") - .padding(.bottom) + .padding(.leading) + .padding(.trailing, 7) .foregroundColor(Color.green) - .font(.largeTitle) + .font(.title) Text("A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key.") .fixedSize(horizontal: false, vertical: true) .padding(.bottom) } HStack { - Image(systemName: "lock.slash.fill") + Image(systemName: "lock.open.fill") + .padding(.leading) + .foregroundColor(Color.yellow) + .font(.title) + Text("A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key.") + .fixedSize(horizontal: false, vertical: true) .padding(.bottom) + } + HStack { + Image(systemName: "lock.open.fill") + .padding(.leading) .foregroundColor(Color.red) - .font(.largeTitle) - Text("A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted.") + .font(.title) + Text("A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "lock.open.trianglebadge.exclamationmark.fill") + .padding(.leading) + .symbolRenderingMode(.multicolor) + .foregroundColor(Color.red) + .font(.title) + Text("A red open lock with a warning means the channel is not securely encrypted and is used for precise location data which is being uplinked to the internet via MQTT, it uses either no key at all or a 1 byte known key.") .fixedSize(horizontal: false, vertical: true) .padding(.bottom) } diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 940396b9..835c662d 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -58,13 +58,7 @@ struct ChannelList: View { VStack(alignment: .leading) { HStack { - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash.fill") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + ChannelLock(channel: channel) if channel.name?.isEmpty ?? false { if channel.role == 1 { Text(String("PrimaryChannel").camelCaseToWords()) @@ -173,7 +167,7 @@ struct ChannelList: View { } .sheet(isPresented: $showingHelp) { ChannelsHelp() - .presentationDetents([.medium, .large]) + .presentationDetents([.large]) .presentationDragIndicator(.visible) } .safeAreaInset(edge: .bottom, alignment: .leading) { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index d479403f..8e38f27b 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -47,6 +47,7 @@ struct Channels: View { /// Minimum Version for granular position configuration @State var minimumVersion = "2.2.24" + @State private var showingHelp = false @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), @@ -124,13 +125,7 @@ struct Channels: View { .brightness(0.1) VStack { HStack { - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash.fill") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + ChannelLock(channel: channel) if channel.name?.isEmpty ?? false { if channel.role == 1 { Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) @@ -246,6 +241,7 @@ struct Channels: View { #endif } } + if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { Button { @@ -286,6 +282,29 @@ struct Channels: View { .padding() } } + .sheet(isPresented: $showingHelp) { + ChannelsHelp() + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } + .safeAreaInset(edge: .bottom, alignment: .leading) { + HStack { + Button(action: { + withAnimation { + showingHelp = !showingHelp + } + }) { + Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) .navigationTitle("Channels") .navigationBarItems(trailing: ZStack { diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 7e4068e2..a3788ff0 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -49,6 +49,7 @@ struct ShareChannels: View { var node: NodeInfoEntity? @State private var channelsUrl = "https://www.meshtastic.org/e/#" var qrCodeImage = QrCodeImage() + @State private var showingHelp = false var body: some View { @@ -82,13 +83,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()) - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash.fill") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + ChannelLock(channel: channel) } else if channel.index == 1 && channel.role > 0 { Toggle("Channel 1 Included", isOn: $includeChannel1) .toggleStyle(.switch) @@ -216,16 +211,39 @@ struct ShareChannels: View { .resizable() .scaledToFit() .frame( - minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), + minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), alignment: .top ) } } } } + .sheet(isPresented: $showingHelp) { + ChannelsHelp() + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } + .safeAreaInset(edge: .bottom, alignment: .leading) { + HStack { + Button(action: { + withAnimation { + showingHelp = !showingHelp + } + }) { + Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) .navigationTitle("Generate QR Code") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: From bd6f4ad7ca23ba184ad3ae62ab0442f336df1694 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Jun 2025 20:34:13 -0700 Subject: [PATCH 2/3] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 876c41c5..55581bc9 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1828,7 +1828,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1861,7 +1861,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1892,7 +1892,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1924,7 +1924,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From fc206bbc05e1d609a939d50939f54ea9bb6cf03a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 23 Jun 2025 09:25:39 -0700 Subject: [PATCH 3/3] Reboot on key changes --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index e252819a..1897bf43 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -349,6 +349,14 @@ struct SecurityConfig: View { } } hasChanges = false + if keyUpdated { + if !bleManager.sendReboot( + fromUser: fromUser, + toUser: toUser + ) { + Logger.mesh.warning("Reboot Failed") + } + } goBack() } }