diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift index aaed8bd1..687cc6fe 100644 --- a/Meshtastic/Views/Helpers/SecureInput.swift +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -12,19 +12,28 @@ struct SecureInput: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Binding private var text: String @Binding private var isValid: Bool - @State var isSecure: Bool = true private var title: String - init(_ title: String, text: Binding, isValid: Binding) { + // Local state to store the value of iSSecure, or optionally a binding + private var isSecureBinding: Binding? + @State private var isSecureLocal: Bool = true + + private var isSecure: Binding { + // Use the binding if we have one, otherwise fallback to the local state variable + isSecureBinding ?? $isSecureLocal + } + + init(_ title: String, text: Binding, isValid: Binding, isSecure: Binding? = nil) { self.title = title self._text = text self._isValid = isValid + self.isSecureBinding = isSecure } var body: some View { ZStack(alignment: .trailing) { Group { - if isSecure { + if isSecure.wrappedValue { SecureField(title, text: $text) .font(idiom == .phone ? .caption : .callout) .allowsTightening(true) @@ -51,9 +60,9 @@ struct SecureInput: View { if !text.isEmpty { Button(action: { - isSecure.toggle() + isSecure.wrappedValue.toggle() }) { - Image(systemName: self.isSecure ? "eye.slash" : "eye") + Image(systemName: self.isSecure.wrappedValue ? "eye.slash" : "eye") .accentColor(.secondary) }.buttonStyle(BorderlessButtonStyle()) } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 2095ae9b..7c22bcf3 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -10,6 +10,7 @@ import SwiftUI import CoreData import MeshtasticProtobufs import OSLog +import CryptoKit struct SecurityConfig: View { @@ -33,6 +34,15 @@ struct SecurityConfig: View { @State var isManaged = false @State var serialEnabled = false @State var debugLogApiEnabled = false + @State var privateKeyIsSecure = true + + private var isValidKeyPair: Bool { + if let privateKeyBytes = Data(base64Encoded: privateKey), + let calculatedPublicKey = generatePublicKey(from: privateKeyBytes) { + return calculatedPublicKey.base64EncodedString() == publicKey + } + return false + } var body: some View { VStack { @@ -51,12 +61,16 @@ struct SecurityConfig: View { .foregroundStyle(.tertiary) .disableAutocorrection(true) .textSelection(.enabled) + .background( + 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.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) Divider() Label("Private Key", systemImage: "key.fill") - SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey) + SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey, isSecure: $privateKeyIsSecure) .background( RoundedRectangle(cornerRadius: 10.0) .stroke(hasValidPrivateKey ? Color.clear : Color.red, lineWidth: 2.0) @@ -70,6 +84,7 @@ struct SecurityConfig: View { Button { if let keyBytes = generatePrivateKey(count: 32) { privateKey = keyBytes.base64EncodedString() + self.privateKeyIsSecure = false } } label: { Image(systemName: "lock.rotation") @@ -156,6 +171,10 @@ struct SecurityConfig: View { let tempKey = Data(base64Encoded: privateKey) ?? Data() if tempKey.count == 32 { hasValidPrivateKey = true + if let privateKeyBytes = Data(base64Encoded: privateKey), privateKeyBytes.count == 32 { + // Valid private key -- generate the public key + publicKey = generatePublicKey(from: privateKeyBytes)?.base64EncodedString() ?? "" + } } else { hasValidPrivateKey = false } @@ -287,7 +306,24 @@ struct SecurityConfig: View { return randomBytes } else { // Handle error, perhaps by logging or throwing an exception - print("Error generating random bytes: \(status)") + Logger.mesh.debug("Error generating random bytes: \(status)") + return nil + } + } + + func generatePublicKey(from privateKeyData: Data) -> Data? { + guard privateKeyData.count == 32 else { + Logger.mesh.debug("Invalid private key length. Must be 32 bytes for Curve25519.") + return nil + } + + do { + // Create a Curve25519 private key from raw representation + let privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: privateKeyData) + let publicKey = privateKey.publicKey + return publicKey.rawRepresentation + } catch { + Logger.mesh.debug("Failed to create Curve25519 key: \(error)") return nil } }