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
This commit is contained in:
neon443
2025-06-23 12:51:41 +01:00
parent c9eab9dde8
commit 8df8c77c7a
7 changed files with 113 additions and 75 deletions

View File

@@ -32,6 +32,7 @@
A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985545E2E056EDD009051BD /* KeychainLayer.swift */; }; A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985545E2E056EDD009051BD /* KeychainLayer.swift */; };
A98554612E058433009051BD /* HostsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554602E058433009051BD /* HostsManager.swift */; }; A98554612E058433009051BD /* HostsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554602E058433009051BD /* HostsManager.swift */; };
A98554632E0587DF009051BD /* HostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554622E0587DF009051BD /* HostsView.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 */; }; A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -97,6 +98,7 @@
A985545E2E056EDD009051BD /* KeychainLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainLayer.swift; sourceTree = "<group>"; }; A985545E2E056EDD009051BD /* KeychainLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainLayer.swift; sourceTree = "<group>"; };
A98554602E058433009051BD /* HostsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsManager.swift; sourceTree = "<group>"; }; A98554602E058433009051BD /* HostsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsManager.swift; sourceTree = "<group>"; };
A98554622E0587DF009051BD /* HostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsView.swift; sourceTree = "<group>"; }; A98554622E0587DF009051BD /* HostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsView.swift; sourceTree = "<group>"; };
A9C4140B2E096DB7005E3047 /* SSHError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHError.swift; sourceTree = "<group>"; };
A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHHandler.swift; sourceTree = "<group>"; }; A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHHandler.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -219,6 +221,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */, A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */,
A9C4140B2E096DB7005E3047 /* SSHError.swift */,
); );
path = SSH; path = SSH;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -425,6 +428,7 @@
A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */, A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */,
A98554552E05535F009051BD /* KeyManagerView.swift in Sources */, A98554552E05535F009051BD /* KeyManagerView.swift in Sources */,
A923172D2E07138000ECE1E6 /* SSHTerminalView.swift in Sources */, A923172D2E07138000ECE1E6 /* SSHTerminalView.swift in Sources */,
A9C4140C2E096DB7005E3047 /* SSHError.swift in Sources */,
A923172A2E07113100ECE1E6 /* TerminalController.swift in Sources */, A923172A2E07113100ECE1E6 /* TerminalController.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@@ -7,6 +7,7 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
import Security
struct Key: Identifiable, Hashable { struct Key: Identifiable, Hashable {
var id = UUID() var id = UUID()
@@ -62,10 +63,15 @@ class KeyManager: ObservableObject {
print(privateKey) print(privateKey)
print(SecKeyCopyPublicKey(privateKey) ?? "") print(SecKeyCopyPublicKey(privateKey) ?? "")
print(SecKeyCopyExternalRepresentation(privateKey, nil) as Any)
// do { // do {
// try storeKey(privateKey, label: label) // try storeKey(privateKey, label: label)
// } catch { // } catch {
// print(error.localizedDescription) // print(error.localizedDescription)
// } // }
}
func getPubkey(_ privateKey: SecKey) -> SecKey? {
return SecKeyCopyPublicKey(privateKey)
} }
} }

View File

@@ -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
}

View File

@@ -45,11 +45,31 @@ class SSHHandler: ObservableObject {
return Data(base64Encoded: String(cString: data)) 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 { defer {
getAuthMethods() getAuthMethods()
self.host.key = getHostkey() self.host.key = getHostkey()
@@ -60,7 +80,7 @@ class SSHHandler: ObservableObject {
session = ssh_new() session = ssh_new()
guard session != nil else { guard session != nil else {
withAnimation { connected = false } withAnimation { connected = false }
return throw .backendError("Failed opening session")
} }
ssh_options_set(session, SSH_OPTIONS_HOST, host.address) ssh_options_set(session, SSH_OPTIONS_HOST, host.address)
@@ -72,24 +92,20 @@ class SSHHandler: ObservableObject {
logger.critical("connection not ok: \(status)") logger.critical("connection not ok: \(status)")
logSshGetError() logSshGetError()
withAnimation { connected = false } withAnimation { connected = false }
return throw .connectionFailed("Failed connecting")
} }
withAnimation { connected = true } withAnimation { connected = true }
return return
} }
func disconnect() { func disconnect() {
guard session != nil else { guard session != nil else { return }
print("cant disconnect when im not connected")
return
}
ssh_disconnect(session) ssh_disconnect(session)
ssh_free(session) ssh_free(session)
withAnimation { authorized = false } withAnimation { authorized = false }
withAnimation { connected = false } withAnimation { connected = false }
withAnimation { testSuceeded = nil } withAnimation { testSuceeded = nil }
session = nil session = nil
// host.key = nil
} }
func testExec() { func testExec() {
@@ -135,7 +151,7 @@ class SSHHandler: ObservableObject {
nbytes = ssh_channel_read( nbytes = ssh_channel_read(
channel, channel,
&buffer, &buffer,
UInt32(MemoryLayout.size(ofValue: CChar.self)), UInt32(buffer.count),
0 0
) )
while nbytes > 0 { while nbytes > 0 {
@@ -148,7 +164,7 @@ class SSHHandler: ObservableObject {
withAnimation { testSuceeded = false } withAnimation { testSuceeded = false }
return 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 { if nbytes < 0 {
@@ -168,7 +184,7 @@ class SSHHandler: ObservableObject {
return 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 { guard session != nil else {
withAnimation { authorized = false } withAnimation { authorized = false }
return return
@@ -191,23 +207,21 @@ class SSHHandler: ObservableObject {
var pubkey: ssh_key? var pubkey: ssh_key?
if ssh_pki_import_pubkey_file(tempPubkey.path(), &pubkey) != 0 { 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 { if ssh_userauth_try_publickey(session, nil, pubkey) != 0 {
print("pubkey pubkey auth error") throw .pubkeyRejected
} }
var privkey: ssh_key? var privkey: ssh_key?
if ssh_pki_import_privkey_file(tempKey.path(), pass, nil, nil, &privkey) != 0 { if ssh_pki_import_privkey_file(tempKey.path(), pass, nil, nil, &privkey) != 0 {
print("privkey import error") throw .importPrivkeyError
print("likely incorrect passphrase")
} }
if (ssh_userauth_publickey(session, nil, privkey) != 0) { if (ssh_userauth_publickey(session, nil, privkey) != 0) {
withAnimation { authorized = false } withAnimation { authorized = false }
print("auth failed lol") throw .privkeyRejected
return
} }
//if u got this far, youre authed! //if u got this far, youre authed!
@@ -223,26 +237,24 @@ class SSHHandler: ObservableObject {
return return
} }
func authWithPw() -> Bool { func authWithPw() throws(AuthError) {
var status: CInt var status: CInt
status = ssh_userauth_password(session, host.username, host.password) status = ssh_userauth_password(session, host.username, host.password)
guard status == SSH_AUTH_SUCCESS.rawValue else { guard status == SSH_AUTH_SUCCESS.rawValue else {
print("ssh pw auth error")
logSshGetError() logSshGetError()
return false throw .rejectedCredentials
} }
print("auth success")
withAnimation { authorized = true } withAnimation { authorized = true }
return true return
} }
func authWithNone() -> Bool { func authWithNone() throws(AuthError) {
let status = ssh_userauth_none(session, nil) 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 } withAnimation { authorized = true }
return true return
} }
//always unknown idk why //always unknown idk why
@@ -324,6 +336,10 @@ class SSHHandler: ObservableObject {
logger.critical("\(String(cString: ssh_get_error(&self.session)))") logger.critical("\(String(cString: ssh_get_error(&self.session)))")
} }
private func logCritical(_ logMessage: String) {
logger.critical("\(logMessage)")
}
func writeToChannel(_ string: String?) { func writeToChannel(_ string: String?) {
guard let string = string else { return } guard let string = string else { return }
guard ssh_channel_is_open(channel) != 0 else { return } guard ssh_channel_is_open(channel) != 0 else { return }

View File

@@ -17,9 +17,6 @@ struct ConnectionView: View {
@State var pubkeyStr: String = "" @State var pubkeyStr: String = ""
@State var privkeyStr: String = "" @State var privkeyStr: String = ""
@State var pubkey: Data?
@State var privkey: Data?
@State var privPickerPresented: Bool = false @State var privPickerPresented: Bool = false
@State var pubPickerPresented: Bool = false @State var pubPickerPresented: Bool = false
@@ -65,7 +62,7 @@ struct ConnectionView: View {
TextField("", text: $pubkeyStr, prompt: Text("Public Key")) TextField("", text: $pubkeyStr, prompt: Text("Public Key"))
.onSubmit { .onSubmit {
let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "") let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "")
pubkey = Data(newStr.utf8) handler.host.publicKey = Data(newStr.utf8)
} }
Button() { Button() {
pubPickerPresented.toggle() pubPickerPresented.toggle()
@@ -81,7 +78,7 @@ struct ConnectionView: View {
return return
} }
defer { fileURL.stopAccessingSecurityScopedResource() } defer { fileURL.stopAccessingSecurityScopedResource() }
pubkey = try? Data(contentsOf: fileURL) handler.host.publicKey = try? Data(contentsOf: fileURL)
print(fileURL) print(fileURL)
} catch { } catch {
print(error.localizedDescription) print(error.localizedDescription)
@@ -90,10 +87,10 @@ struct ConnectionView: View {
} }
HStack { HStack {
TextField("", text: $privkeyStr, prompt: Text("Private Key")) SecureField("", text: $privkeyStr, prompt: Text("Private Key"))
.onSubmit { .onSubmit {
let newStr = privkeyStr.replacingOccurrences(of: "\r\n", with: "") let newStr = privkeyStr.replacingOccurrences(of: "\r\n", with: "")
privkey = Data(newStr.utf8) handler.host.privateKey = Data(newStr.utf8)
} }
Button() { Button() {
privPickerPresented.toggle() privPickerPresented.toggle()
@@ -109,8 +106,8 @@ struct ConnectionView: View {
return return
} }
defer { fileURL.stopAccessingSecurityScopedResource() } defer { fileURL.stopAccessingSecurityScopedResource() }
privkey = try? Data(contentsOf: fileURL) handler.host.privateKey = try? Data(contentsOf: fileURL)
print(privkey ?? "") print(handler.host.privateKey ?? "")
print(fileURL) print(fileURL)
} catch { } catch {
print(error.localizedDescription) print(error.localizedDescription)
@@ -164,27 +161,12 @@ struct ConnectionView: View {
.transition(.opacity) .transition(.opacity)
.toolbar { .toolbar {
ToolbarItem() { ToolbarItem() {
if handler.connected { Button() {
Button() { handler.go()
handler.disconnect() } label: {
} label: { Label(
Label("Disconnect", systemImage: "xmark.app.fill") handler.connected ? "Disconnect" : "Connect",
} systemImage: handler.connected ? "xmark.app.fill" : "power"
} 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
) )
} }
} }
@@ -196,6 +178,14 @@ struct ConnectionView: View {
return 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) ?? ""
}
}
} }
} }

View File

@@ -23,12 +23,13 @@ class SSHTerminalView: TerminalView, TerminalViewDelegate {
super.init(frame: frame) super.init(frame: frame)
terminalDelegate = self terminalDelegate = self
sshQueue.async { sshQueue.async { [weak self] in
guard let handler = self.handler else { return } guard let handler = self?.handler else { return }
while handler.connected { while handler.connected {
guard let handler = self.handler else { break } guard let handler = self?.handler else { break }
if let read = handler.readFromChannel() { if let read = handler.readFromChannel() {
DispatchQueue.main.async { self.feed(text: read) } DispatchQueue.main.async { self?.feed(text: read) }
} else { } else {
usleep(1_000) usleep(1_000)
} }

View File

@@ -18,17 +18,12 @@ struct ShellView: View {
.toolbar { .toolbar {
ToolbarItem { ToolbarItem {
Button() { Button() {
if handler.connected { handler.go()
handler.disconnect()
} else {
handler.connect()
}
} label: { } label: {
if handler.connected { Label(
Label("Disconnect", systemImage: "xmark.square.fill") handler.connected ? "Disconnect" : "Connect",
} else { systemImage: handler.connected ? "xmark.app.fill" : "power"
Label("Connect", image: "power") )
}
} }
} }
} }