From 8df8c77c7a54707058e8a04057986f6aa11376ae Mon Sep 17 00:00:00 2001 From: neon443 <69979447+neon443@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:51:41 +0100 Subject: [PATCH] implemented actually using Host.publicKey and Host.privateKey to store pubkeys and privkeys - this gives u icloud synced pub/privkeys - per host!! made it load the key's string representation (if present) into the textboxes to tell the user that the key is there made the privkey box a securefield to show .... instead of the key added a handler.go() to handler disconnecting if connected and choosing password/pubkey auth - really cleaned up the view code added getPubkey(_: SecKey) to keychainmanager added SSHErrors - commfail, connectionFail, backenderror added AuthError - rejectedCreds, notconnected added KeyError - importpub/privError, pub/priv rejected made most functions throw in sshhandler fix memoryLayoutsize(ofvalue to buffer.count in testexec [weak self] in the async loop to prevent memory leaks i guess added loccritical for easy red logging --- ShhShell.xcodeproj/project.pbxproj | 4 + ShhShell/Keys/KeyManager.swift | 10 ++- ShhShell/SSH/SSHError.swift | 26 +++++++ ShhShell/SSH/SSHHandler.swift | 74 +++++++++++-------- ShhShell/Views/ConnectionView.swift | 50 +++++-------- ShhShell/Views/Terminal/SSHTerminalView.swift | 9 ++- ShhShell/Views/Terminal/ShellView.swift | 15 ++-- 7 files changed, 113 insertions(+), 75 deletions(-) create mode 100644 ShhShell/SSH/SSHError.swift diff --git a/ShhShell.xcodeproj/project.pbxproj b/ShhShell.xcodeproj/project.pbxproj index 69af7d6..ecfabd1 100644 --- a/ShhShell.xcodeproj/project.pbxproj +++ b/ShhShell.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 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 */; }; + A9C4140C2E096DB7005E3047 /* SSHError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C4140B2E096DB7005E3047 /* SSHError.swift */; }; A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */; }; /* End PBXBuildFile section */ @@ -97,6 +98,7 @@ 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 = ""; }; + A9C4140B2E096DB7005E3047 /* SSHError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHError.swift; sourceTree = ""; }; A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHHandler.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -219,6 +221,7 @@ isa = PBXGroup; children = ( A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */, + A9C4140B2E096DB7005E3047 /* SSHError.swift */, ); path = SSH; sourceTree = ""; @@ -425,6 +428,7 @@ A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */, A98554552E05535F009051BD /* KeyManagerView.swift in Sources */, A923172D2E07138000ECE1E6 /* SSHTerminalView.swift in Sources */, + A9C4140C2E096DB7005E3047 /* SSHError.swift in Sources */, A923172A2E07113100ECE1E6 /* TerminalController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ShhShell/Keys/KeyManager.swift b/ShhShell/Keys/KeyManager.swift index 2f6b80c..3ee7fbb 100644 --- a/ShhShell/Keys/KeyManager.swift +++ b/ShhShell/Keys/KeyManager.swift @@ -7,6 +7,7 @@ import Foundation import CryptoKit +import Security struct Key: Identifiable, Hashable { var id = UUID() @@ -62,10 +63,15 @@ class KeyManager: ObservableObject { print(privateKey) print(SecKeyCopyPublicKey(privateKey) ?? "") + print(SecKeyCopyExternalRepresentation(privateKey, nil) as Any) // do { // try storeKey(privateKey, label: label) // } catch { -// print(error.localizedDescription) -// } + // print(error.localizedDescription) + // } + } + + func getPubkey(_ privateKey: SecKey) -> SecKey? { + return SecKeyCopyPublicKey(privateKey) } } diff --git a/ShhShell/SSH/SSHError.swift b/ShhShell/SSH/SSHError.swift new file mode 100644 index 0000000..7da648e --- /dev/null +++ b/ShhShell/SSH/SSHError.swift @@ -0,0 +1,26 @@ +// +// SSHError.swift +// ShhShell +// +// Created by neon443 on 23/06/2025. +// + +import Foundation + +enum SSHError: Error { + case connectionFailed(String) + case communicationError(String) + case backendError(String) +} + +enum AuthError: Error { + case rejectedCredentials + case notConnected +} + +enum KeyError: Error { + case importPubkeyError + case importPrivkeyError + case pubkeyRejected + case privkeyRejected +} diff --git a/ShhShell/SSH/SSHHandler.swift b/ShhShell/SSH/SSHHandler.swift index 423c1ea..18a3c3a 100644 --- a/ShhShell/SSH/SSHHandler.swift +++ b/ShhShell/SSH/SSHHandler.swift @@ -45,11 +45,31 @@ class SSHHandler: ObservableObject { return Data(base64Encoded: String(cString: data)) } -// func connect(_: Host) { -// -// } + func go() { + guard !connected else { + disconnect() + return + } + + guard let _ = try? connect() else { return } + if !host.password.isEmpty { + do { try authWithPw() } catch { + print("pw auth error") + } + } else { + do { + if let publicKey = host.publicKey, + let privateKey = host.privateKey { + try authWithPubkey(pub: publicKey, priv: privateKey, pass: host.passphrase) + } + } catch { + print("error with pubkey auth") + } + } + openShell() + } - func connect() { + func connect() throws(SSHError) { defer { getAuthMethods() self.host.key = getHostkey() @@ -60,7 +80,7 @@ class SSHHandler: ObservableObject { session = ssh_new() guard session != nil else { withAnimation { connected = false } - return + throw .backendError("Failed opening session") } ssh_options_set(session, SSH_OPTIONS_HOST, host.address) @@ -72,24 +92,20 @@ class SSHHandler: ObservableObject { logger.critical("connection not ok: \(status)") logSshGetError() withAnimation { connected = false } - return + throw .connectionFailed("Failed connecting") } withAnimation { connected = true } return } func disconnect() { - guard session != nil else { - print("cant disconnect when im not connected") - return - } + guard session != nil else { return } ssh_disconnect(session) ssh_free(session) withAnimation { authorized = false } withAnimation { connected = false } withAnimation { testSuceeded = nil } session = nil -// host.key = nil } func testExec() { @@ -135,7 +151,7 @@ class SSHHandler: ObservableObject { nbytes = ssh_channel_read( channel, &buffer, - UInt32(MemoryLayout.size(ofValue: CChar.self)), + UInt32(buffer.count), 0 ) while nbytes > 0 { @@ -148,7 +164,7 @@ class SSHHandler: ObservableObject { withAnimation { testSuceeded = false } return } - nbytes = ssh_channel_read(channel, &buffer, UInt32(MemoryLayout.size(ofValue: Character.self)), 0) + nbytes = ssh_channel_read(channel, &buffer, UInt32(buffer.count), 0) } if nbytes < 0 { @@ -168,7 +184,7 @@ class SSHHandler: ObservableObject { return } - func authWithPubkey(pub pubInp: Data, priv privInp: Data, pass: String) { + func authWithPubkey(pub pubInp: Data, priv privInp: Data, pass: String) throws(KeyError) { guard session != nil else { withAnimation { authorized = false } return @@ -191,23 +207,21 @@ class SSHHandler: ObservableObject { var pubkey: ssh_key? if ssh_pki_import_pubkey_file(tempPubkey.path(), &pubkey) != 0 { - print("pubkey import error") + throw .importPrivkeyError } if ssh_userauth_try_publickey(session, nil, pubkey) != 0 { - print("pubkey pubkey auth error") + throw .pubkeyRejected } var privkey: ssh_key? if ssh_pki_import_privkey_file(tempKey.path(), pass, nil, nil, &privkey) != 0 { - print("privkey import error") - print("likely incorrect passphrase") + throw .importPrivkeyError } if (ssh_userauth_publickey(session, nil, privkey) != 0) { withAnimation { authorized = false } - print("auth failed lol") - return + throw .privkeyRejected } //if u got this far, youre authed! @@ -223,26 +237,24 @@ class SSHHandler: ObservableObject { return } - func authWithPw() -> Bool { + func authWithPw() throws(AuthError) { var status: CInt status = ssh_userauth_password(session, host.username, host.password) guard status == SSH_AUTH_SUCCESS.rawValue else { - print("ssh pw auth error") logSshGetError() - return false + throw .rejectedCredentials } - print("auth success") withAnimation { authorized = true } - return true + return } - func authWithNone() -> Bool { + func authWithNone() throws(AuthError) { let status = ssh_userauth_none(session, nil) - guard status == SSH_AUTH_SUCCESS.rawValue else { return false } + guard status == SSH_AUTH_SUCCESS.rawValue else { throw .rejectedCredentials } - print("no security moment lol") + logCritical("no security moment lol") withAnimation { authorized = true } - return true + return } //always unknown idk why @@ -324,6 +336,10 @@ class SSHHandler: ObservableObject { logger.critical("\(String(cString: ssh_get_error(&self.session)))") } + private func logCritical(_ logMessage: String) { + logger.critical("\(logMessage)") + } + func writeToChannel(_ string: String?) { guard let string = string else { return } guard ssh_channel_is_open(channel) != 0 else { return } diff --git a/ShhShell/Views/ConnectionView.swift b/ShhShell/Views/ConnectionView.swift index 0cc6836..cd034dc 100644 --- a/ShhShell/Views/ConnectionView.swift +++ b/ShhShell/Views/ConnectionView.swift @@ -17,9 +17,6 @@ struct ConnectionView: View { @State var pubkeyStr: String = "" @State var privkeyStr: String = "" - @State var pubkey: Data? - @State var privkey: Data? - @State var privPickerPresented: Bool = false @State var pubPickerPresented: Bool = false @@ -65,7 +62,7 @@ struct ConnectionView: View { TextField("", text: $pubkeyStr, prompt: Text("Public Key")) .onSubmit { let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "") - pubkey = Data(newStr.utf8) + handler.host.publicKey = Data(newStr.utf8) } Button() { pubPickerPresented.toggle() @@ -81,7 +78,7 @@ struct ConnectionView: View { return } defer { fileURL.stopAccessingSecurityScopedResource() } - pubkey = try? Data(contentsOf: fileURL) + handler.host.publicKey = try? Data(contentsOf: fileURL) print(fileURL) } catch { print(error.localizedDescription) @@ -90,10 +87,10 @@ struct ConnectionView: View { } HStack { - TextField("", text: $privkeyStr, prompt: Text("Private Key")) + SecureField("", text: $privkeyStr, prompt: Text("Private Key")) .onSubmit { let newStr = privkeyStr.replacingOccurrences(of: "\r\n", with: "") - privkey = Data(newStr.utf8) + handler.host.privateKey = Data(newStr.utf8) } Button() { privPickerPresented.toggle() @@ -109,8 +106,8 @@ struct ConnectionView: View { return } defer { fileURL.stopAccessingSecurityScopedResource() } - privkey = try? Data(contentsOf: fileURL) - print(privkey ?? "") + handler.host.privateKey = try? Data(contentsOf: fileURL) + print(handler.host.privateKey ?? "") print(fileURL) } catch { print(error.localizedDescription) @@ -164,27 +161,12 @@ struct ConnectionView: View { .transition(.opacity) .toolbar { ToolbarItem() { - if handler.connected { - Button() { - handler.disconnect() - } label: { - Label("Disconnect", systemImage: "xmark.app.fill") - } - } else { - Button() { - handler.connect() - if pubkey != nil && privkey != nil { - handler.authWithPubkey(pub: pubkey!, priv: privkey!, pass: passphrase) - } else { - let _ = handler.authWithPw() - } - handler.openShell() - } label: { - Label("Connect", systemImage: "power") - } - .disabled( - pubkey == nil && privkey == nil && - handler.host.username.isEmpty && handler.host.password.isEmpty + Button() { + handler.go() + } label: { + Label( + handler.connected ? "Disconnect" : "Connect", + systemImage: handler.connected ? "xmark.app.fill" : "power" ) } } @@ -196,6 +178,14 @@ struct ConnectionView: View { return } } + .task { + if let publicKeyData = handler.host.publicKey { + pubkeyStr = String(data: publicKeyData, encoding: .utf8) ?? "" + } + if let privateKeyData = handler.host.privateKey { + privkeyStr = String(data: privateKeyData, encoding: .utf8) ?? "" + } + } } } diff --git a/ShhShell/Views/Terminal/SSHTerminalView.swift b/ShhShell/Views/Terminal/SSHTerminalView.swift index 936ca41..956dc8c 100644 --- a/ShhShell/Views/Terminal/SSHTerminalView.swift +++ b/ShhShell/Views/Terminal/SSHTerminalView.swift @@ -23,12 +23,13 @@ class SSHTerminalView: TerminalView, TerminalViewDelegate { super.init(frame: frame) terminalDelegate = self - sshQueue.async { - guard let handler = self.handler else { return } + sshQueue.async { [weak self] in + guard let handler = self?.handler else { return } + while handler.connected { - guard let handler = self.handler else { break } + guard let handler = self?.handler else { break } if let read = handler.readFromChannel() { - DispatchQueue.main.async { self.feed(text: read) } + DispatchQueue.main.async { self?.feed(text: read) } } else { usleep(1_000) } diff --git a/ShhShell/Views/Terminal/ShellView.swift b/ShhShell/Views/Terminal/ShellView.swift index 6436981..7cd36e3 100644 --- a/ShhShell/Views/Terminal/ShellView.swift +++ b/ShhShell/Views/Terminal/ShellView.swift @@ -18,17 +18,12 @@ struct ShellView: View { .toolbar { ToolbarItem { Button() { - if handler.connected { - handler.disconnect() - } else { - handler.connect() - } + handler.go() } label: { - if handler.connected { - Label("Disconnect", systemImage: "xmark.square.fill") - } else { - Label("Connect", image: "power") - } + Label( + handler.connected ? "Disconnect" : "Connect", + systemImage: handler.connected ? "xmark.app.fill" : "power" + ) } } }