From 7edd9764d966b2419ed473093738bb0c3ba15948 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 17 Jun 2025 23:17:22 -0700 Subject: [PATCH 1/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 6f72607e..244ee935 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1808,7 +1808,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 +1841,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 +1872,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 +1904,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 = ""; From 24e7974f6e4f272b8486e3af494fcd3afb554974 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 01:00:27 -0700 Subject: [PATCH 2/3] iCloud Private key backup initial commit --- Localizable.xcstrings | 23 ++++ Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Helpers/KeychainHelper.swift | 66 ++++++++++ Meshtastic/Meshtastic.entitlements | 12 +- .../Settings/Config/SecurityConfig.swift | 122 +++++++++++++----- 5 files changed, 191 insertions(+), 36 deletions(-) create mode 100644 Meshtastic/Helpers/KeychainHelper.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9a6c28c1..b0443c35 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2251,6 +2251,7 @@ } }, "Admin & Direct Message Keys" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -2283,6 +2284,9 @@ } } } + }, + "Admin Keys" : { + }, "Administration" : { "localizations" : { @@ -3777,6 +3781,12 @@ } } } + }, + "Backup" : { + + }, + "Backup your private key to your iCloud keychain." : { + }, "Bad" : { "localizations" : { @@ -9373,6 +9383,9 @@ } } } + }, + "Direct Message Key" : { + }, "Direct Messages" : { "localizations" : { @@ -13128,6 +13141,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 +15526,9 @@ } } } + }, + "Key Backup" : { + }, "Key Mapping" : { "localizations" : { @@ -24541,6 +24560,9 @@ } } } + }, + "Restore" : { + }, "Resume" : { "localizations" : { @@ -27216,6 +27238,7 @@ } }, "Sent out to other nodes on the mesh to allow them to compute a shared secret key." : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 244ee935..d4ea70bc 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ 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 */; }; 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 +373,7 @@ DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = ""; }; DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = ""; }; DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = ""; }; + DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.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 = ""; }; @@ -1067,6 +1069,7 @@ children = ( DDD43FE12A78C86B0083A3E9 /* Mqtt */, DDAF8C5226EB1DF10058C060 /* BLEManager.swift */, + DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, @@ -1576,6 +1579,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 */, diff --git a/Meshtastic/Helpers/KeychainHelper.swift b/Meshtastic/Helpers/KeychainHelper.swift new file mode 100644 index 00000000..73242474 --- /dev/null +++ b/Meshtastic/Helpers/KeychainHelper.swift @@ -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 + } +} diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index 0d2247ee..3689ef3e 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -2,13 +2,15 @@ - com.apple.developer.usernotifications.critical-alerts - com.apple.developer.associated-domains applinks:meshtastic.org/e/* applinks:meshtastic.org/v/* + com.apple.developer.carplay-communication + + com.apple.developer.usernotifications.critical-alerts + com.apple.developer.weatherkit com.apple.security.app-sandbox @@ -21,7 +23,9 @@ com.apple.security.personal-information.location - com.apple.developer.carplay-communication - + keychain-access-groups + + $(AppIdentifierPrefix)gvh.MeshtasticClient + diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 5954f47d..dc2d62b8 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -51,7 +51,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 +66,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 +79,63 @@ 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 { + print("Value saved successfully!") + } else { + print("Error saving value: \(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 + } else { + print("No value found in Keychain for key: \(keychainKey)") + } + } + 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 { + print("Value deleted successfully!") + } else { + print("Error deleting value: \(status)") + } + } + label: { + Image(systemName: "trash") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + } + 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 +152,39 @@ 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) } } + 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") From 2d8ede1c7d00f9b7095ad87496f44a4107a6ba12 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Jun 2025 08:44:13 -0700 Subject: [PATCH 3/3] Success and Error states for key backup --- Localizable.xcstrings | 3 ++ Meshtastic.xcodeproj/project.pbxproj | 4 ++ Meshtastic/Enums/KeyBackupStatus.swift | 47 +++++++++++++++++++ .../Settings/Config/SecurityConfig.swift | 23 ++++++--- 4 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 Meshtastic/Enums/KeyBackupStatus.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b0443c35..2676148d 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -13083,6 +13083,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" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index d4ea70bc..745c3602 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -98,6 +98,7 @@ 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 */; }; 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 */; }; @@ -374,6 +375,7 @@ DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = ""; }; DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = ""; }; DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; + DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.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 = ""; }; @@ -878,6 +880,7 @@ DD8ED9C6289CE4A100B3B0AB /* Enums */ = { isa = PBXGroup; children = ( + DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */, DDA951592BC6624100CEA535 /* TelemetryWeather.swift */, DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */, DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */, @@ -1446,6 +1449,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 */, diff --git a/Meshtastic/Enums/KeyBackupStatus.swift b/Meshtastic/Enums/KeyBackupStatus.swift new file mode 100644 index 00000000..ff5b2438 --- /dev/null +++ b/Meshtastic/Enums/KeyBackupStatus.swift @@ -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 + } + } +} diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index dc2d62b8..52a72e93 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -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), @@ -87,9 +89,10 @@ struct SecurityConfig: View { Button { let status = KeychainHelper.standard.save(key: keychainKey, value: privateKey) if status == errSecSuccess { - print("Value saved successfully!") + backupStatus = KeyBackupStatus.saved } else { - print("Error saving value: \(status)") + backupStatus = KeyBackupStatus.saveFailed + backupStatusError = status } } label: { @@ -104,8 +107,9 @@ struct SecurityConfig: View { if let value = KeychainHelper.standard.read(key: keychainKey) { self.privateKey = value self.privateKeyIsSecure = false + backupStatus = KeyBackupStatus.restored } else { - print("No value found in Keychain for key: \(keychainKey)") + backupStatus = KeyBackupStatus.restoreFailed } } label: { @@ -119,9 +123,9 @@ struct SecurityConfig: View { Button { let status = KeychainHelper.standard.delete(key: keychainKey) if status == errSecSuccess { - print("Value deleted successfully!") + backupStatus = KeyBackupStatus.deleted } else { - print("Error deleting value: \(status)") + backupStatus = KeyBackupStatus.deleteFailed } } label: { @@ -131,11 +135,17 @@ struct SecurityConfig: View { .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() } + Divider() HStack(alignment: .firstTextBaseline) { Label("Regenerate Private Key", systemImage: "arrow.clockwise.circle") Spacer() @@ -152,6 +162,7 @@ struct SecurityConfig: View { .buttonBorderShape(.capsule) .controlSize(.small) } + 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")) {