Merge pull request #1280 from meshtastic/2.6.8

2.6.8 Working Changes
This commit is contained in:
Garth Vander Houwen 2025-06-18 17:57:08 -07:00 committed by GitHub
commit 911ae282e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 402 additions and 55 deletions

View file

@ -1683,6 +1683,12 @@
}
}
}
},
"A channel index of 0 indicates the primary channel where all broadcast packets are sent from." : {
},
"A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : {
},
"A Meshtastic QR code contains the LoRa config and channel values needed for radios to communicate. You can share a complete channel configuration using the Replace Channels option, if you choose Add Channels your shared channels will be added to the channels on the receiving radio." : {
"localizations" : {
@ -1741,6 +1747,9 @@
}
}
}
},
"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 Trace Route was sent, no response has been received." : {
"localizations" : {
@ -2251,6 +2260,7 @@
}
},
"Admin & Direct Message Keys" : {
"extractionState" : "stale",
"localizations" : {
"de" : {
"stringUnit" : {
@ -2283,6 +2293,9 @@
}
}
}
},
"Admin Keys" : {
},
"Administration" : {
"localizations" : {
@ -3777,6 +3790,12 @@
}
}
}
},
"Backup" : {
},
"Backup your private key to your iCloud keychain." : {
},
"Bad" : {
"localizations" : {
@ -6152,6 +6171,9 @@
}
}
}
},
"Channels Help" : {
},
"Chart" : {
"localizations" : {
@ -9373,6 +9395,9 @@
}
}
}
},
"Direct Message Key" : {
},
"Direct Messages" : {
"localizations" : {
@ -13070,6 +13095,9 @@
}
}
}
},
"Generate a new private key to replace the one currently in use. Public key will automatically be regenerated as well." : {
},
"Generate QR Code" : {
"localizations" : {
@ -13128,6 +13156,9 @@
}
}
}
},
"Generated from your public key and sent out to other nodes on the mesh to allow them to compute a shared secret key." : {
},
"Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets." : {
"localizations" : {
@ -15510,6 +15541,9 @@
}
}
}
},
"Key Backup" : {
},
"Key Mapping" : {
"localizations" : {
@ -24541,6 +24575,9 @@
}
}
}
},
"Restore" : {
},
"Resume" : {
"localizations" : {
@ -27216,6 +27253,7 @@
}
},
"Sent out to other nodes on the mesh to allow them to compute a shared secret key." : {
"extractionState" : "stale",
"localizations" : {
"de" : {
"stringUnit" : {

View file

@ -97,6 +97,9 @@
DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0EA2C601795008C0C70 /* CLLocation.swift */; };
DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */; };
DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */; };
DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF492E0292220090CE24 /* KeychainHelper.swift */; };
DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */; };
DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4D2E0391620090CE24 /* ChannelsHelp.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 */; };
@ -372,6 +375,9 @@
DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = "<group>"; };
DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = "<group>"; };
DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = "<group>"; };
DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = "<group>"; };
DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = "<group>"; };
DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsHelp.swift; sourceTree = "<group>"; };
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = "<group>"; };
DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = "<group>"; };
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
@ -846,6 +852,7 @@
DD6F65772C6EAB860053C113 /* Help */ = {
isa = PBXGroup;
children = (
DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */,
DD6F65752C6EA5490053C113 /* AckErrors.swift */,
DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */,
DD6F657A2C6EC2900053C113 /* LockLegend.swift */,
@ -876,6 +883,7 @@
DD8ED9C6289CE4A100B3B0AB /* Enums */ = {
isa = PBXGroup;
children = (
DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */,
DDA951592BC6624100CEA535 /* TelemetryWeather.swift */,
DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */,
DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */,
@ -1067,6 +1075,7 @@
children = (
DDD43FE12A78C86B0083A3E9 /* Mqtt */,
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */,
DD1BEF492E0292220090CE24 /* KeychainHelper.swift */,
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */,
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */,
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */,
@ -1422,6 +1431,7 @@
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */,
DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */,
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */,
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */,
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */,
@ -1443,6 +1453,7 @@
233E99C32D849D7A00CC3A77 /* WeightCompactWidget.swift in Sources */,
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */,
DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */,
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */,
DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */,
@ -1576,6 +1587,7 @@
233E99C72D84A70900CC3A77 /* SoilCompactWidgets.swift in Sources */,
BCE2D3C92C7C377F008E6199 /* FactoryResetNodeIntent.swift in Sources */,
DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */,
DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */,
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */,
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */,
DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */,
@ -1808,7 +1820,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.7;
MARKETING_VERSION = 2.6.8;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1841,7 +1853,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.7;
MARKETING_VERSION = 2.6.8;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1872,7 +1884,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.7;
MARKETING_VERSION = 2.6.8;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1904,7 +1916,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.7;
MARKETING_VERSION = 2.6.8;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -0,0 +1,47 @@
//
// iCloudStats.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 6/18/25.
//
enum KeyBackupStatus: String, CaseIterable, Equatable, Decodable {
case saved
case restored
case deleted
case saveFailed
case restoreFailed
case deleteFailed
var description: String {
switch self {
case .saved:
return "Private Key saved successfully to iCloud keychain.".localized
case .restored:
return "Private Key restored successfully from iCloud keychain.".localized
case .deleted:
return "Private Key deleted successfully from iCloud keychain.".localized
case .saveFailed:
return "Private Key failed to save to iCloud keychain.".localized
case .restoreFailed:
return "Private Key value not found in iCloud keychain.".localized
case .deleteFailed:
return "Private Key failed to delete from iCloud keychain.".localized
}
}
var success: Bool {
switch self {
case .saved:
return true
case .restored:
return true
case .deleted:
return true
case .saveFailed:
return false
case .restoreFailed:
return false
case .deleteFailed:
return false
}
}
}

View file

@ -118,10 +118,8 @@ public func createUser(num: Int64, context: NSManagedObjectContext) throws -> Us
context.performAndWait {
newUser = UserEntity(context: context)
newUser.num = num
let userId = String(format: "%016llX", num)
newUser.userId = "!\(userId)"
let userId = num.toHex()
newUser.userId = userId
let last4 = String(userId.suffix(4))
newUser.longName = "Meshtastic \(last4)"
newUser.shortName = last4

View file

@ -0,0 +1,66 @@
//
// KeychainHelper.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 6/17/25.
//
import Foundation
import Security
class KeychainHelper {
static let standard = KeychainHelper()
private init() {}
func save(key: String, value: String, service: String = Bundle.main.bundleIdentifier!) -> OSStatus {
let data = value.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrSynchronizable as String: kCFBooleanTrue!,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
]
SecItemDelete(query as CFDictionary) // Delete existing item if any
let status = SecItemAdd(query as CFDictionary, nil)
return status
}
func read(key: String, service: String = Bundle.main.bundleIdentifier!) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: kCFBooleanTrue,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecAttrSynchronizable as String: kCFBooleanTrue!
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == errSecSuccess {
if let data = item as? Data {
return String(data: data, encoding: .utf8)
}
}
return nil
}
func delete(key: String, service: String = Bundle.main.bundleIdentifier!) -> OSStatus {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecAttrSynchronizable as String: kCFBooleanTrue!
]
let status = SecItemDelete(query as CFDictionary)
return status
}
}

View file

@ -296,7 +296,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
if nodeInfo.hasUser {
let newUser = UserEntity(context: context)
newUser.userId = nodeInfo.user.id
newUser.userId = nodeInfo.num.toHex()
newUser.num = Int64(nodeInfo.num)
newUser.longName = nodeInfo.user.longName
newUser.shortName = nodeInfo.user.shortName
@ -394,7 +394,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
fetchedNode[0].user?.pkiEncrypted = true
fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey
}
fetchedNode[0].user?.userId = nodeInfo.user.id
fetchedNode[0].user?.userId = nodeInfo.num.toHex()
fetchedNode[0].user?.num = Int64(nodeInfo.num)
fetchedNode[0].user?.numString = String(nodeInfo.num)
fetchedNode[0].user?.longName = nodeInfo.user.longName

View file

@ -2,13 +2,15 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.usernotifications.critical-alerts</key>
<true/>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:meshtastic.org/e/*</string>
<string>applinks:meshtastic.org/v/*</string>
</array>
<key>com.apple.developer.carplay-communication</key>
<true/>
<key>com.apple.developer.usernotifications.critical-alerts</key>
<true/>
<key>com.apple.developer.weatherkit</key>
<true/>
<key>com.apple.security.app-sandbox</key>
@ -21,7 +23,9 @@
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.developer.carplay-communication</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)gvh.MeshtasticClient</string>
</array>
</dict>
</plist>

View file

@ -59,7 +59,7 @@ struct MeshtasticAppleApp: App {
self.saveChannels = false
if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true {
handleContactUrl(url: self.incomingUrl!)
} else if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#") == true {
} else if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/") == true {
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false
if (self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil {
@ -87,7 +87,7 @@ struct MeshtasticAppleApp: App {
self.incomingUrl = url
if url.absoluteString.lowercased().contains("meshtastic.org/v/#") {
handleContactUrl(url: url)
} else if url.absoluteString.lowercased().contains("meshtastic.org/e/#") {
} else if url.absoluteString.lowercased().contains("meshtastic.org/e/") {
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false
if self.incomingUrl?.absoluteString.lowercased().contains("?") != nil {

View file

@ -182,7 +182,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
} else {
let newUser = UserEntity(context: context)
newUser.userId = newUserMessage.id
newUser.userId = newNode.num.toHex()
newUser.num = Int64(packet.from)
newUser.longName = newUserMessage.longName
newUser.shortName = newUserMessage.shortName
@ -306,7 +306,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries)
}
if nodeInfoMessage.hasUser {
fetchedNode[0].user?.userId = nodeInfoMessage.user.id
fetchedNode[0].user?.userId = nodeInfoMessage.num.toHex()
fetchedNode[0].user?.num = Int64(nodeInfoMessage.num)
fetchedNode[0].user?.longName = nodeInfoMessage.user.longName
fetchedNode[0].user?.shortName = nodeInfoMessage.user.shortName

View file

@ -0,0 +1,76 @@
//
// ChannelHelp.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 6/18/25.
//
import SwiftUI
struct ChannelsHelp: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@Environment(\.dismiss) private var dismiss
var body: some View {
ScrollView {
Label("Channels Help", systemImage: "questionmark.circle")
.font(.title)
.padding(.vertical)
VStack(alignment: .leading) {
HStack {
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.")
.fixedSize(horizontal: false, vertical: true)
.padding(.bottom)
}
HStack {
Image(systemName: "lock.fill")
.padding(.bottom)
.foregroundColor(Color.green)
.font(.largeTitle)
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")
.padding(.bottom)
.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.")
.fixedSize(horizontal: false, vertical: true)
.padding(.bottom)
}
}
#if targetEnvironment(macCatalyst)
Spacer()
Button {
dismiss()
} label: {
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
#endif
}
.frame(minHeight: 0, maxHeight: .infinity, alignment: .leading)
.padding()
.presentationDetents([.large])
.presentationContentInteraction(.scrolls)
.presentationDragIndicator(.visible)
.presentationBackgroundInteraction(.enabled(upThrough: .large))
}
}
struct ChannelHelpPreviews: PreviewProvider {
static var previews: some View {
VStack {
ChannelsHelp()
}
}
}

View file

@ -21,8 +21,8 @@ struct ChannelList: View {
var channelSelection: ChannelEntity?
@State private var isPresentingDeleteChannelMessagesConfirm: Bool = false
@State private var isPresentingTraceRouteSentAlert = false
@State private var showingHelp = false
var restrictedChannels = ["gpio", "mqtt", "serial", "admin"]
@ -168,8 +168,31 @@ struct ChannelList: View {
}
.padding([.top, .bottom])
.listStyle(.plain)
.navigationTitle("Channels")
}
}
.navigationTitle("Channels")
.sheet(isPresented: $showingHelp) {
ChannelsHelp()
.presentationDetents([.medium, .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)
}
}

View file

@ -122,7 +122,7 @@ struct NodeDetail: View {
.textSelection(.enabled)
}
.accessibilityElement(children: .combine)
if node.user?.keyMatch ?? false {
if let publicKey = node.user?.publicKey {
HStack {

View file

@ -124,6 +124,13 @@ 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)
}
if channel.name?.isEmpty ?? false {
if channel.role == 1 {
Text(String("PrimaryChannel").camelCaseToWords()).font(.headline)

View file

@ -35,6 +35,8 @@ struct SecurityConfig: View {
@State var serialEnabled = false
@State var debugLogApiEnabled = false
@State var privateKeyIsSecure = true
@State var backupStatus: KeyBackupStatus?
@State var backupStatusError: OSStatus?
private var isValidKeyPair: Bool {
guard let privateKeyBytes = Data(base64Encoded: privateKey),
@ -51,7 +53,7 @@ struct SecurityConfig: View {
ConfigHeader(title: "Security", config: \.securityConfig, node: node, onAppear: setSecurityValues)
Text("Security Config Settings require a firmware version 2.5+")
.font(.title3)
Section(header: Text("Admin & Direct Message Keys")) {
Section(header: Text("Direct Message Key")) {
VStack(alignment: .leading) {
Label("Public Key", systemImage: "key")
Text(publicKey)
@ -66,7 +68,7 @@ struct SecurityConfig: View {
RoundedRectangle(cornerRadius: 10.0)
.stroke(isValidKeyPair ? Color.clear : Color.red, lineWidth: 2.0)
)
Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.")
Text("Generated from your public key and sent out to other nodes on the mesh to allow them to compute a shared secret key.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
Divider()
@ -79,6 +81,71 @@ struct SecurityConfig: View {
Text("Used to create a shared key with a remote device.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
if let currentNode = node {
Divider()
Label("Key Backup", systemImage: "icloud")
HStack(alignment: .firstTextBaseline) {
let keychainKey = "PrivateKeyNode\(currentNode.num)"
Button {
let status = KeychainHelper.standard.save(key: keychainKey, value: privateKey)
if status == errSecSuccess {
backupStatus = KeyBackupStatus.saved
} else {
backupStatus = KeyBackupStatus.saveFailed
backupStatusError = status
}
}
label: {
Image(systemName: "icloud.and.arrow.up")
Text("Backup")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.small)
Spacer()
Button {
if let value = KeychainHelper.standard.read(key: keychainKey) {
self.privateKey = value
self.privateKeyIsSecure = false
backupStatus = KeyBackupStatus.restored
} else {
backupStatus = KeyBackupStatus.restoreFailed
}
}
label: {
Image(systemName: "key.icloud")
Text("Restore")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.small)
Spacer()
Button {
let status = KeychainHelper.standard.delete(key: keychainKey)
if status == errSecSuccess {
backupStatus = KeyBackupStatus.deleted
} else {
backupStatus = KeyBackupStatus.deleteFailed
}
}
label: {
Image(systemName: "trash")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.small)
}
if let status = backupStatus {
let state = status.success
Text("\(status.description)")
.font(.caption)
.foregroundColor(state ? .green : .red)
}
Text("Backup your private key to your iCloud keychain.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
}
Divider()
HStack(alignment: .firstTextBaseline) {
Label("Regenerate Private Key", systemImage: "arrow.clockwise.circle")
Spacer()
@ -95,38 +162,40 @@ struct SecurityConfig: View {
.buttonBorderShape(.capsule)
.controlSize(.small)
}
Divider()
Label("Primary Admin Key", systemImage: "key.viewfinder")
SecureInput("Primary Admin Key", text: $adminKey, isValid: $hasValidAdminKey)
.background(
RoundedRectangle(cornerRadius: 10.0)
.stroke(hasValidAdminKey ? Color.clear : Color.red, lineWidth: 2.0)
)
Text("The primary public key authorized to send admin messages to this node.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
Divider()
Label("Secondary Admin Key", systemImage: "key.viewfinder")
SecureInput("Secondary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2)
.background(
RoundedRectangle(cornerRadius: 10.0)
.stroke(hasValidAdminKey2 ? Color.clear : Color.red, lineWidth: 2.0)
)
Text("The secondary public key authorized to send admin messages to this node.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
Divider()
Label("Tertiary Admin Key", systemImage: "key.viewfinder")
SecureInput("Tertiary Admin Key", text: $adminKey3, isValid: $hasValidAdminKey3)
.background(
RoundedRectangle(cornerRadius: 10.0)
.stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0)
)
Text("The tertiary public key authorized to send admin messages to this node.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
Text("Generate a new private key to replace the one currently in use. Public key will automatically be regenerated as well.")
}
}
Section(header: Text("Admin Keys")) {
Label("Primary Admin Key", systemImage: "key.viewfinder")
SecureInput("Primary Admin Key", text: $adminKey, isValid: $hasValidAdminKey)
.background(
RoundedRectangle(cornerRadius: 10.0)
.stroke(hasValidAdminKey ? Color.clear : Color.red, lineWidth: 2.0)
)
Text("The primary public key authorized to send admin messages to this node.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
Divider()
Label("Secondary Admin Key", systemImage: "key.viewfinder")
SecureInput("Secondary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2)
.background(
RoundedRectangle(cornerRadius: 10.0)
.stroke(hasValidAdminKey2 ? Color.clear : Color.red, lineWidth: 2.0)
)
Text("The secondary public key authorized to send admin messages to this node.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
Divider()
Label("Tertiary Admin Key", systemImage: "key.viewfinder")
SecureInput("Tertiary Admin Key", text: $adminKey3, isValid: $hasValidAdminKey3)
.background(
RoundedRectangle(cornerRadius: 10.0)
.stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0)
)
Text("The tertiary public key authorized to send admin messages to this node.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
}
Section(header: Text("Logs")) {
Toggle(isOn: $serialEnabled) {
Label("Serial Console", systemImage: "terminal")
@ -305,7 +374,14 @@ struct SecurityConfig: View {
}
if status == errSecSuccess {
return randomBytes
// Generate a random "f" value and then adjust the value to make
// it valid as an "s" value for eval(). According to the specification
// we need to mask off the 3 right-most bits of f[0], mask off the
// left-most bit of f[31], and set the second to left-most bit of f[31].
var f = randomBytes
f[0] &= 0xF8
f[31] = (f[31] & 0x7F) | 0x40
return f
} else {
// Handle error, perhaps by logging or throwing an exception
Logger.mesh.debug("Error generating random bytes: \(status)")

View file

@ -313,7 +313,7 @@ struct ShareChannels: View {
guard let settingsString = try? channelSet.serializedData().base64EncodedString() else {
return
}
channelsUrl = ("https://meshtastic.org/e/\(replaceChannels ? "" : "?add=true")#" + settingsString.base64ToBase64url())
channelsUrl = ("https://meshtastic.org/e/\(replaceChannels ? "" : "?add=true")#\(settingsString.base64ToBase64url())")
}
}
}