From a06de0b4c43454e2a099601df0c0e4045aa48060 Mon Sep 17 00:00:00 2001 From: neon443 <69979447+neon443@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:40:20 +0100 Subject: [PATCH] hostsview - list of hosts hostmanager for loading and saving hosts to defaults host.debug/.blank as static vars fix crash on disconnect by checking for connection in timer before checking if channel is open keychain layer? to convert ed25519 to seckey and back for keychain storage generate ed25519 via cryptokit --- ShhShell.xcodeproj/project.pbxproj | 12 +++ ShhShell/Host/Host.swift | 27 +++--- ShhShell/Host/HostsManager.swift | 35 ++++++++ ShhShell/Keys/KeyManager.swift | 8 ++ ShhShell/Keys/KeychainLayer.swift | 95 ++++++++++++++++++++++ ShhShell/SSH/SSHHandler.swift | 8 +- ShhShell/ShhShellApp.swift | 6 +- ShhShell/Views/ConnectionView.swift | 6 +- ShhShell/Views/ContentView.swift | 14 ++-- ShhShell/Views/HostsView.swift | 45 ++++++++++ ShhShell/Views/Keys/KeyManagerView.swift | 26 +++--- ShhShell/Views/Terminal/TerminalView.swift | 2 +- 12 files changed, 239 insertions(+), 45 deletions(-) create mode 100644 ShhShell/Host/HostsManager.swift create mode 100644 ShhShell/Keys/KeychainLayer.swift create mode 100644 ShhShell/Views/HostsView.swift diff --git a/ShhShell.xcodeproj/project.pbxproj b/ShhShell.xcodeproj/project.pbxproj index a4e2a09..e65559c 100644 --- a/ShhShell.xcodeproj/project.pbxproj +++ b/ShhShell.xcodeproj/project.pbxproj @@ -32,6 +32,9 @@ A98554552E05535F009051BD /* KeyManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554542E05535F009051BD /* KeyManagerView.swift */; }; A98554592E0553AA009051BD /* KeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554582E0553AA009051BD /* KeyManager.swift */; }; A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985545C2E055D4D009051BD /* ConnectionView.swift */; }; + A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985545E2E056EDD009051BD /* KeychainLayer.swift */; }; + A98554612E058433009051BD /* HostsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554602E058433009051BD /* HostsManager.swift */; }; + A98554632E0587DF009051BD /* HostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554622E0587DF009051BD /* HostsView.swift */; }; A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */; }; /* End PBXBuildFile section */ @@ -93,6 +96,9 @@ A98554542E05535F009051BD /* KeyManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagerView.swift; sourceTree = ""; }; A98554582E0553AA009051BD /* KeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManager.swift; sourceTree = ""; }; A985545C2E055D4D009051BD /* ConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionView.swift; sourceTree = ""; }; + A985545E2E056EDD009051BD /* KeychainLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainLayer.swift; sourceTree = ""; }; + A98554602E058433009051BD /* HostsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsManager.swift; sourceTree = ""; }; + A98554622E0587DF009051BD /* HostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsView.swift; sourceTree = ""; }; A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHHandler.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -189,6 +195,7 @@ children = ( A98554532E05534F009051BD /* Keys */, A92538C52DEE0742007E0A18 /* ContentView.swift */, + A98554622E0587DF009051BD /* HostsView.swift */, A985545C2E055D4D009051BD /* ConnectionView.swift */, A98554522E055347009051BD /* Terminal */, A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */, @@ -243,6 +250,7 @@ isa = PBXGroup; children = ( A93143BF2DF61B3200FCD5DB /* Host.swift */, + A98554602E058433009051BD /* HostsManager.swift */, ); path = Host; sourceTree = ""; @@ -251,6 +259,7 @@ isa = PBXGroup; children = ( A98554582E0553AA009051BD /* KeyManager.swift */, + A985545E2E056EDD009051BD /* KeychainLayer.swift */, ); path = Keys; sourceTree = ""; @@ -413,10 +422,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */, A93143C62DF61FE300FCD5DB /* ViewModifiers.swift in Sources */, + A98554632E0587DF009051BD /* HostsView.swift in Sources */, A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */, A93143C02DF61B3200FCD5DB /* Host.swift in Sources */, A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */, + A98554612E058433009051BD /* HostsManager.swift in Sources */, A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */, A91AE3B22DF73E0900FF3537 /* TerminalView.swift in Sources */, A98554592E0553AA009051BD /* KeyManager.swift in Sources */, diff --git a/ShhShell/Host/Host.swift b/ShhShell/Host/Host.swift index 9d85fc8..32bece4 100644 --- a/ShhShell/Host/Host.swift +++ b/ShhShell/Host/Host.swift @@ -7,7 +7,8 @@ import Foundation -protocol HostPr: Codable { +protocol HostPr: Codable, Identifiable { + var id: UUID { get set } var address: String { get set } var port: Int { get set } var username: String { get set } @@ -16,7 +17,8 @@ protocol HostPr: Codable { } struct Host: HostPr { - var address: String = "address" + var id = UUID() + var address: String = "" var port: Int var username: String var password: String @@ -37,18 +39,11 @@ struct Host: HostPr { } } -struct blankHost: HostPr { - var address: String = "" - var port: Int = 22 - var username: String = "" - var password: String = "" - var key: Data? = nil -} - -struct debugHost: HostPr { - var address: String = "localhost" - var port: Int = 22 - var username: String = "default" - var password: String = "" - var key: Data? = nil +extension Host { + static var blank: Host { + Host(address: "", port: 22, username: "", password: "") + } + static var debug: Host { + Host(address: "localhost", port: 22, username: "default", password: "") + } } diff --git a/ShhShell/Host/HostsManager.swift b/ShhShell/Host/HostsManager.swift new file mode 100644 index 0000000..a43d2a8 --- /dev/null +++ b/ShhShell/Host/HostsManager.swift @@ -0,0 +1,35 @@ +// +// HostsManager.swift +// ShhShell +// +// Created by neon443 on 20/06/2025. +// + +import Foundation + +class HostsManager: ObservableObject { + private let userDefaults = NSUbiquitousKeyValueStore.default + + @Published var savedHosts: [Host] = [] + + init() { + loadSavedHosts() + } + + func loadSavedHosts() { + let decoder = JSONDecoder() + guard let data = userDefaults.data(forKey: "savedHosts") else { return } + + if let decoded = try? decoder.decode([Host].self, from: data) { + self.savedHosts = decoded + } + } + + func saveSavedHosts() { + let encoder = JSONEncoder() + if let encoded = try? encoder.encode(savedHosts) { + userDefaults.set(encoded, forKey: "savedHosts") + userDefaults.synchronize() + } + } +} diff --git a/ShhShell/Keys/KeyManager.swift b/ShhShell/Keys/KeyManager.swift index d16b6ba..9369690 100644 --- a/ShhShell/Keys/KeyManager.swift +++ b/ShhShell/Keys/KeyManager.swift @@ -6,6 +6,7 @@ // import Foundation +import CryptoKit struct Key: Identifiable, Hashable { var id = UUID() @@ -16,6 +17,13 @@ struct Key: Identifiable, Hashable { } class KeyManager: ObservableObject { + func generateEd25519() { + let privateKey = Curve25519.Signing.PrivateKey() + let publicKeyData = privateKey.publicKey + dump(privateKey.rawRepresentation) + print(publicKeyData.rawRepresentation) + } + func generateRSA() throws { let type = kSecAttrKeyTypeRSA let tag = "com.neon443.ShhSell.keys.\(Date().timeIntervalSince1970)".data(using: .utf8)! diff --git a/ShhShell/Keys/KeychainLayer.swift b/ShhShell/Keys/KeychainLayer.swift new file mode 100644 index 0000000..e0cf5e7 --- /dev/null +++ b/ShhShell/Keys/KeychainLayer.swift @@ -0,0 +1,95 @@ +// +// KeychainLayer.swift +// ShhShell +// +// Created by neon443 on 20/06/2025. +// + +import Foundation +import CryptoKit + +//https://developer.apple.com/documentation/cryptokit/storing-cryptokit-keys-in-the-keychain +protocol SecKeyConvertible: CustomStringConvertible { + // cretes a ket from an x9.63 represenation + init(x963Representation: Bytes) throws where Bytes: ContiguousBytes + + //an x9.63 representation of the key + var x963Representation: Data { get } +} + +protocol GenericPasswordConvertible { + //creates key from generic rep + init(genericKeyRepresentation data: D) throws where D: ContiguousBytes + + //generic rep of key + var genericKeyRepresentation: SymmetricKey { get } +} + +extension Curve25519.KeyAgreement.PrivateKey: GenericPasswordConvertible { + init(genericKeyRepresentation data: D) throws where D: ContiguousBytes { + try self.init(rawRepresentation: data) + } + + var genericKeyRepresentation: SymmetricKey { + self.rawRepresentation.withUnsafeBytes { + SymmetricKey(data: $0) + } + } +} +extension Curve25519.Signing.PrivateKey: GenericPasswordConvertible { + init(genericKeyRepresentation data: D) throws where D: ContiguousBytes { + try self.init(rawRepresentation: data) + } + + var genericKeyRepresentation: SymmetricKey { + self.rawRepresentation.withUnsafeBytes { + SymmetricKey(data: $0) + } + } +} + +enum KeyStoreError: Error { + case KeyStoreError(String) +} + +func storeKey(_ key: T, label: String) throws { + let attributes = [kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom, + kSecAttrKeyClass: kSecAttrKeyClassPrivate] as [String: Any] + + guard let secKey = SecKeyCreateWithData( + key.x963Representation as CFData, + attributes as CFDictionary, + nil + ) else { + throw KeyStoreError.KeyStoreError("unable to create SecKey represntation") + } + + let query = [kSecClass: kSecClassKey, + kSecAttrApplicationLabel: label, + kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked, +kSecUseDataProtectionKeychain: true, + kSecValueRef: secKey] as [String: Any] + let status = SecItemAdd(query as CFDictionary, nil) + guard status == errSecSuccess else { + throw KeyStoreError.KeyStoreError("unable to sstore item \(status)") + } + +} + + +func retrieveKey(label: String) throws -> SecKey? { + let query = [kSecClass: kSecClassKey, + kSecAttrApplicationLabel: label, + kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom, +kSecUseDataProtectionKeychain: true, + kSecReturnRef: true] as [String: Any] + + var item: CFTypeRef? + var secKey: SecKey + switch SecItemCopyMatching(query as CFDictionary, &item) { + case errSecSuccess: secKey = item as! SecKey + case errSecItemNotFound: return nil + case let status: throw KeyStoreError.KeyStoreError("keychain read failed") + } + return secKey +} diff --git a/ShhShell/SSH/SSHHandler.swift b/ShhShell/SSH/SSHHandler.swift index 3369c9b..2809538 100644 --- a/ShhShell/SSH/SSHHandler.swift +++ b/ShhShell/SSH/SSHHandler.swift @@ -30,7 +30,7 @@ class SSHHandler: ObservableObject { ) { self.host = host #if DEBUG - self.host = debugHost() + self.host = Host.debug #endif } @@ -87,9 +87,9 @@ class SSHHandler: ObservableObject { } ssh_disconnect(session) ssh_free(session) - session = nil withAnimation { authorized = false } withAnimation { connected = false } + session = nil host.key = nil } @@ -296,12 +296,12 @@ class SSHHandler: ObservableObject { guard status == SSH_OK else { return } self.readTimer = Timer(timeInterval: 0.1, repeats: true) { timer in - guard ssh_channel_is_open(self.channel) != 0 else { + guard self.connected else { timer.invalidate() self.readTimer = nil return } - guard ssh_channel_is_eof(self.channel) == 0 else { + guard ssh_channel_is_open(self.channel) != 0 && ssh_channel_is_eof(self.channel) == 0 else { timer.invalidate() self.readTimer = nil return diff --git a/ShhShell/ShhShellApp.swift b/ShhShell/ShhShellApp.swift index 5d90a14..beb553e 100644 --- a/ShhShell/ShhShellApp.swift +++ b/ShhShell/ShhShellApp.swift @@ -9,14 +9,16 @@ import SwiftUI @main struct ShhShellApp: App { - @StateObject var sshHandler: SSHHandler = SSHHandler(host: blankHost()) + @StateObject var sshHandler: SSHHandler = SSHHandler(host: Host.blank) @StateObject var keyManager: KeyManager = KeyManager() + @StateObject var hostsManager: HostsManager = HostsManager() var body: some Scene { WindowGroup { ContentView( handler: sshHandler, - keyManager: keyManager + keyManager: keyManager, + hostsManager: hostsManager ) } } diff --git a/ShhShell/Views/ConnectionView.swift b/ShhShell/Views/ConnectionView.swift index b968ac2..72cded5 100644 --- a/ShhShell/Views/ConnectionView.swift +++ b/ShhShell/Views/ConnectionView.swift @@ -10,6 +10,7 @@ import SwiftUI struct ConnectionView: View { @StateObject var handler: SSHHandler @StateObject var keyManager: KeyManager + @StateObject var hostsManager: HostsManager @State var passphrase: String = "" @@ -156,7 +157,8 @@ struct ConnectionView: View { #Preview { ConnectionView( - handler: SSHHandler(host: debugHost()), - keyManager: KeyManager() + handler: SSHHandler(host: Host.debug), + keyManager: KeyManager(), + hostsManager: HostsManager() ) } diff --git a/ShhShell/Views/ContentView.swift b/ShhShell/Views/ContentView.swift index 0fcc878..7e53c8c 100644 --- a/ShhShell/Views/ContentView.swift +++ b/ShhShell/Views/ContentView.swift @@ -10,15 +10,16 @@ import SwiftUI struct ContentView: View { @ObservedObject var handler: SSHHandler @ObservedObject var keyManager: KeyManager + @ObservedObject var hostsManager: HostsManager var body: some View { TabView { - ConnectionView( - handler: handler, - keyManager: keyManager + HostsView( + keyManager: keyManager, + hostsManager: hostsManager ) .tabItem { - Label("Connection", systemImage: "powerplug.portrait") + Label("Hosts", systemImage: "server.rack") } KeyManagerView(keyManager: keyManager) .tabItem { @@ -30,7 +31,8 @@ struct ContentView: View { #Preview { ContentView( - handler: SSHHandler(host: debugHost()), - keyManager: KeyManager() + handler: SSHHandler(host: Host.debug), + keyManager: KeyManager(), + hostsManager: HostsManager() ) } diff --git a/ShhShell/Views/HostsView.swift b/ShhShell/Views/HostsView.swift new file mode 100644 index 0000000..12f2076 --- /dev/null +++ b/ShhShell/Views/HostsView.swift @@ -0,0 +1,45 @@ +// +// HostsView.swift +// ShhShell +// +// Created by neon443 on 20/06/2025. +// + +import SwiftUI + +struct HostsView: View { + @ObservedObject var keyManager: KeyManager + @ObservedObject var hostsManager: HostsManager + + var body: some View { + NavigationStack { + List { + Text("hi") + ForEach(hostsManager.savedHosts) { host in + NavigationLink() { + ConnectionView( + handler: SSHHandler(host: host), + keyManager: keyManager, + hostsManager: hostsManager + ) + } label: { + Text(host.address) + } + } + } + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button { + hostsManager.savedHosts.append(Host.blank) + } label: { + Label("Add", systemImage: "plus") + } + } + } + } + } +} + +#Preview { + HostsView(keyManager: KeyManager(), hostsManager: HostsManager()) +} diff --git a/ShhShell/Views/Keys/KeyManagerView.swift b/ShhShell/Views/Keys/KeyManagerView.swift index d4a64ce..ed41e2f 100644 --- a/ShhShell/Views/Keys/KeyManagerView.swift +++ b/ShhShell/Views/Keys/KeyManagerView.swift @@ -10,22 +10,20 @@ import SwiftUI struct KeyManagerView: View { @ObservedObject var keyManager: KeyManager - var body: some View { - Button("ed25519") { - do { - try keyManager.generateEd25519() - } catch { - print(error.localizedDescription) + var body: some View { + List { + Button("ed25519") { + keyManager.generateEd25519() + } + Button("rsa") { + do { + try keyManager.generateRSA() + } catch { + print(error.localizedDescription) + } } } -// Button("rsa") { -// do { -// try keyManager.generateRSA() -// } catch { -// print(error.localizedDescription) -// } -// } - } + } } #Preview { diff --git a/ShhShell/Views/Terminal/TerminalView.swift b/ShhShell/Views/Terminal/TerminalView.swift index 39ca391..1294e9b 100644 --- a/ShhShell/Views/Terminal/TerminalView.swift +++ b/ShhShell/Views/Terminal/TerminalView.swift @@ -39,5 +39,5 @@ struct TerminalView: View { } #Preview { - TerminalView(handler: SSHHandler(host: debugHost())) + TerminalView(handler: SSHHandler(host: Host.debug)) }