mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
commit
911ae282e3
15 changed files with 402 additions and 55 deletions
|
|
@ -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" : {
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
|
|
|
|||
47
Meshtastic/Enums/KeyBackupStatus.swift
Normal file
47
Meshtastic/Enums/KeyBackupStatus.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
66
Meshtastic/Helpers/KeychainHelper.swift
Normal file
66
Meshtastic/Helpers/KeychainHelper.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
76
Meshtastic/Views/Helpers/Help/ChannelsHelp.swift
Normal file
76
Meshtastic/Views/Helpers/Help/ChannelsHelp.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ struct NodeDetail: View {
|
|||
.textSelection(.enabled)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
|
||||
|
||||
if node.user?.keyMatch ?? false {
|
||||
if let publicKey = node.user?.publicKey {
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
|
|
|
|||
|
|
@ -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())")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue