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