diff --git a/ShhShell.xcodeproj/project.pbxproj b/ShhShell.xcodeproj/project.pbxproj index b53386a..a4e2a09 100644 --- a/ShhShell.xcodeproj/project.pbxproj +++ b/ShhShell.xcodeproj/project.pbxproj @@ -29,6 +29,9 @@ A95FAA552DF4B62900DE2F5A /* LibSSH.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A95FAA502DF4B62100DE2F5A /* LibSSH.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A95FAA562DF4B62A00DE2F5A /* openssl.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A95FAA512DF4B62100DE2F5A /* openssl.xcframework */; }; A95FAA572DF4B62A00DE2F5A /* openssl.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A95FAA512DF4B62100DE2F5A /* openssl.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 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 */; }; A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */; }; /* End PBXBuildFile section */ @@ -87,6 +90,9 @@ A95FAA5A2DF4B79900DE2F5A /* ci_post_clone.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = ci_post_clone.sh; sourceTree = ""; }; A95FAA5B2DF4B7A000DE2F5A /* ci_pre_xcodebuild.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = ci_pre_xcodebuild.sh; sourceTree = ""; }; A95FAA5C2DF4B7A300DE2F5A /* ci_prost_xcodebuild.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = ci_prost_xcodebuild.sh; sourceTree = ""; }; + 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 = ""; }; A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHHandler.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -153,8 +159,10 @@ children = ( A93143C22DF61F5700FCD5DB /* ShhShell.entitlements */, A92538C62DEE0742007E0A18 /* ShhShellApp.swift */, - A92538D32DEE0749007E0A18 /* Views */, + A98554572E055398009051BD /* Keys */, + A98554562E055394009051BD /* Host */, A93143C12DF61E8500FCD5DB /* SSH */, + A92538D32DEE0749007E0A18 /* Views */, ); path = ShhShell; sourceTree = ""; @@ -179,9 +187,10 @@ A92538D32DEE0749007E0A18 /* Views */ = { isa = PBXGroup; children = ( + A98554532E05534F009051BD /* Keys */, A92538C52DEE0742007E0A18 /* ContentView.swift */, - A91AE3B12DF73E0900FF3537 /* TerminalView.swift */, - A91AE3BC2DF7402100FF3537 /* TextViewController.swift */, + A985545C2E055D4D009051BD /* ConnectionView.swift */, + A98554522E055347009051BD /* Terminal */, A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */, ); path = Views; @@ -198,7 +207,6 @@ A93143C12DF61E8500FCD5DB /* SSH */ = { isa = PBXGroup; children = ( - A93143BF2DF61B3200FCD5DB /* Host.swift */, A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */, ); path = SSH; @@ -214,6 +222,39 @@ path = ci_scripts; sourceTree = ""; }; + A98554522E055347009051BD /* Terminal */ = { + isa = PBXGroup; + children = ( + A91AE3B12DF73E0900FF3537 /* TerminalView.swift */, + A91AE3BC2DF7402100FF3537 /* TextViewController.swift */, + ); + path = Terminal; + sourceTree = ""; + }; + A98554532E05534F009051BD /* Keys */ = { + isa = PBXGroup; + children = ( + A98554542E05535F009051BD /* KeyManagerView.swift */, + ); + path = Keys; + sourceTree = ""; + }; + A98554562E055394009051BD /* Host */ = { + isa = PBXGroup; + children = ( + A93143BF2DF61B3200FCD5DB /* Host.swift */, + ); + path = Host; + sourceTree = ""; + }; + A98554572E055398009051BD /* Keys */ = { + isa = PBXGroup; + children = ( + A98554582E0553AA009051BD /* KeyManager.swift */, + ); + path = Keys; + sourceTree = ""; + }; A9C8976F2DF1980900EF9A5F /* Frameworks */ = { isa = PBXGroup; children = ( @@ -376,9 +417,12 @@ A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */, A93143C02DF61B3200FCD5DB /* Host.swift in Sources */, A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */, + A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */, A91AE3B22DF73E0900FF3537 /* TerminalView.swift in Sources */, + A98554592E0553AA009051BD /* KeyManager.swift in Sources */, A91AE3BD2DF7402100FF3537 /* TextViewController.swift in Sources */, A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */, + A98554552E05535F009051BD /* KeyManagerView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ShhShell/SSH/Host.swift b/ShhShell/Host/Host.swift similarity index 100% rename from ShhShell/SSH/Host.swift rename to ShhShell/Host/Host.swift diff --git a/ShhShell/Keys/KeyManager.swift b/ShhShell/Keys/KeyManager.swift new file mode 100644 index 0000000..d16b6ba --- /dev/null +++ b/ShhShell/Keys/KeyManager.swift @@ -0,0 +1,38 @@ +// +// KeyManager.swift +// ShhShell +// +// Created by neon443 on 20/06/2025. +// + +import Foundation + +struct Key: Identifiable, Hashable { + var id = UUID() + var privateKey: SecKey + var publicKey: SecKey { + SecKeyCopyPublicKey(privateKey)! + } +} + +class KeyManager: ObservableObject { + func generateRSA() throws { + let type = kSecAttrKeyTypeRSA + let tag = "com.neon443.ShhSell.keys.\(Date().timeIntervalSince1970)".data(using: .utf8)! + let attributes: [String: Any] = + [kSecAttrKeyType as String: type, + kSecAttrKeySizeInBits as String: 4096, + kSecPrivateKeyAttrs as String: + [kSecAttrIsPermanent as String: true, + kSecAttrApplicationTag as String: tag] + ] + + var error: Unmanaged? + guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else { + throw error!.takeRetainedValue() as Error + } + print(privateKey) + + print(SecKeyCopyPublicKey(privateKey)) + } +} diff --git a/ShhShell/ShhShellApp.swift b/ShhShell/ShhShellApp.swift index b8a1c4e..5d90a14 100644 --- a/ShhShell/ShhShellApp.swift +++ b/ShhShell/ShhShellApp.swift @@ -10,10 +10,14 @@ import SwiftUI @main struct ShhShellApp: App { @StateObject var sshHandler: SSHHandler = SSHHandler(host: blankHost()) + @StateObject var keyManager: KeyManager = KeyManager() var body: some Scene { WindowGroup { - ContentView(handler: sshHandler) + ContentView( + handler: sshHandler, + keyManager: keyManager + ) } } } diff --git a/ShhShell/Views/ConnectionView.swift b/ShhShell/Views/ConnectionView.swift new file mode 100644 index 0000000..b968ac2 --- /dev/null +++ b/ShhShell/Views/ConnectionView.swift @@ -0,0 +1,162 @@ +// +// ConnectionView.swift +// ShhShell +// +// Created by neon443 on 20/06/2025. +// + +import SwiftUI + +struct ConnectionView: View { + @StateObject var handler: SSHHandler + @StateObject var keyManager: KeyManager + + @State var passphrase: String = "" + + @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 + + var body: some View { + NavigationStack { + List { + HStack { + TextField("", text: $pubkeyStr, prompt: Text("Public Key")) + .onSubmit { + pubkey = Data(pubkeyStr.utf8) + } + Button() { + pubPickerPresented.toggle() + } label: { + Image(systemName: "folder") + } + .buttonStyle(.plain) + .fileImporter(isPresented: $pubPickerPresented, allowedContentTypes: [.item, .content, .data]) { (Result) in + do { + let fileURL = try Result.get() + pubkey = try! Data(contentsOf: fileURL) + print(fileURL) + } catch { + print(error.localizedDescription) + } + } + } + + HStack { + TextField("", text: $privkeyStr, prompt: Text("Private Key")) + .onSubmit { + privkey = Data(privkeyStr.utf8) + } + Button() { + privPickerPresented.toggle() + } label: { + Image(systemName: "folder") + } + .buttonStyle(.plain) + .fileImporter(isPresented: $privPickerPresented, allowedContentTypes: [.item, .content, .data]) { (Result) in + do { + let fileURL = try Result.get() + privkey = try! Data(contentsOf: fileURL) + print(fileURL) + } catch { + print(error.localizedDescription) + } + } + } + + TextField("", text: $passphrase) + HStack { + Text(handler.connected ? "connected" : "not connected") + .modifier(foregroundColorStyle(handler.connected ? .green : .red)) + + Text(handler.authorized ? "authorized" : "unauthorized") + .modifier(foregroundColorStyle(handler.authorized ? .green : .red)) + } + + // if let testSucceded = testSucceded { + // Image(systemName: testSucceded ? "checkmark.circle" : "xmark.circle") + // .modifier(foregroundColorStyle(testSucceded ? .green : .red)) + // } + + if handler.host.key != nil { + Text("Hostkey: \(handler.host.key!.base64EncodedString())") + } + + TextField("address", text: $handler.host.address) + .textFieldStyle(.roundedBorder) + + TextField( + "port", + text: Binding( + get: { String(handler.host.port) }, + set: { handler.host.port = Int($0) ?? 22} ) + ) + .keyboardType(.numberPad) + .textFieldStyle(.roundedBorder) + + TextField("username", text: $handler.host.username) + .textFieldStyle(.roundedBorder) + + SecureField("password", text: $handler.host.password) + .textFieldStyle(.roundedBorder) + + 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: "powerplug.portrait") + } + .disabled( + pubkey == nil && privkey == nil || + handler.host.username.isEmpty && handler.host.password.isEmpty + ) + } + + NavigationLink() { + TerminalView(handler: handler) + } label: { + Label("Open Terminal", systemImage: "apple.terminal") + } + .disabled(!(handler.connected && handler.authorized)) + + Button() { + withAnimation { handler.testExec() } + } label: { + if handler.testSuceeded { + Image(systemName: handler.testSuceeded ? "checkmark.circle" : "xmark.circle") + .modifier(foregroundColorStyle(handler.testSuceeded ? .green : .red)) + } else { + Label("Test Connection", systemImage: "checkmark") + } + } + .disabled(!(handler.connected && handler.authorized)) + } + .transition(.opacity) + } + } +} + + +#Preview { + ConnectionView( + handler: SSHHandler(host: debugHost()), + keyManager: KeyManager() + ) +} diff --git a/ShhShell/Views/ContentView.swift b/ShhShell/Views/ContentView.swift index 1870aa0..0fcc878 100644 --- a/ShhShell/Views/ContentView.swift +++ b/ShhShell/Views/ContentView.swift @@ -9,151 +9,28 @@ import SwiftUI struct ContentView: View { @ObservedObject var handler: SSHHandler - - @State var passphrase: String = "" - - @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 - + @ObservedObject var keyManager: KeyManager + var body: some View { - NavigationStack { - List { - HStack { - TextField("", text: $pubkeyStr, prompt: Text("Public Key")) - .onSubmit { - pubkey = Data(pubkeyStr.utf8) - } - Button() { - pubPickerPresented.toggle() - } label: { - Image(systemName: "folder") - } - .buttonStyle(.plain) - .fileImporter(isPresented: $pubPickerPresented, allowedContentTypes: [.item, .content, .data]) { (Result) in - do { - let fileURL = try Result.get() - pubkey = try! Data(contentsOf: fileURL) - print(fileURL) - } catch { - print(error.localizedDescription) - } - } - } - - HStack { - TextField("", text: $privkeyStr, prompt: Text("Private Key")) - .onSubmit { - privkey = Data(privkeyStr.utf8) - } - Button() { - privPickerPresented.toggle() - } label: { - Image(systemName: "folder") - } - .buttonStyle(.plain) - .fileImporter(isPresented: $privPickerPresented, allowedContentTypes: [.item, .content, .data]) { (Result) in - do { - let fileURL = try Result.get() - privkey = try! Data(contentsOf: fileURL) - print(fileURL) - } catch { - print(error.localizedDescription) - } - } - } - - TextField("", text: $passphrase) - HStack { - Text(handler.connected ? "connected" : "not connected") - .modifier(foregroundColorStyle(handler.connected ? .green : .red)) - - Text(handler.authorized ? "authorized" : "unauthorized") - .modifier(foregroundColorStyle(handler.authorized ? .green : .red)) - } - -// if let testSucceded = testSucceded { -// Image(systemName: testSucceded ? "checkmark.circle" : "xmark.circle") -// .modifier(foregroundColorStyle(testSucceded ? .green : .red)) -// } - - if handler.host.key != nil { - Text("Hostkey: \(handler.host.key!.base64EncodedString())") - } - - TextField("address", text: $handler.host.address) - .textFieldStyle(.roundedBorder) - - TextField( - "port", - text: Binding( - get: { String(handler.host.port) }, - set: { handler.host.port = Int($0) ?? 22} ) - ) - .keyboardType(.numberPad) - .textFieldStyle(.roundedBorder) - - TextField("username", text: $handler.host.username) - .textFieldStyle(.roundedBorder) - - SecureField("password", text: $handler.host.password) - .textFieldStyle(.roundedBorder) - - 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: "powerplug.portrait") - } - .disabled( - pubkey == nil && privkey == nil || - handler.host.username.isEmpty && handler.host.password.isEmpty - ) - } - - NavigationLink() { - TerminalView(handler: handler) - } label: { - Label("Open Terminal", systemImage: "apple.terminal") - } - .disabled(!(handler.connected && handler.authorized)) - - Button() { - withAnimation { handler.testExec() } - } label: { - if handler.testSuceeded { - Image(systemName: handler.testSuceeded ? "checkmark.circle" : "xmark.circle") - .modifier(foregroundColorStyle(handler.testSuceeded ? .green : .red)) - } else { - Label("Test Connection", systemImage: "checkmark") - } - } - .disabled(!(handler.connected && handler.authorized)) + TabView { + ConnectionView( + handler: handler, + keyManager: keyManager + ) + .tabItem { + Label("Connection", systemImage: "powerplug.portrait") } - .transition(.opacity) + KeyManagerView(keyManager: keyManager) + .tabItem { + Label("Keys", systemImage: "key.2.on.ring") + } } } } #Preview { ContentView( - handler: SSHHandler(host: debugHost()) + handler: SSHHandler(host: debugHost()), + keyManager: KeyManager() ) } diff --git a/ShhShell/Views/Keys/KeyManagerView.swift b/ShhShell/Views/Keys/KeyManagerView.swift new file mode 100644 index 0000000..d4a64ce --- /dev/null +++ b/ShhShell/Views/Keys/KeyManagerView.swift @@ -0,0 +1,33 @@ +// +// KeyManagerView.swift +// ShhShell +// +// Created by neon443 on 20/06/2025. +// + +import SwiftUI + +struct KeyManagerView: View { + @ObservedObject var keyManager: KeyManager + + var body: some View { + Button("ed25519") { + do { + try keyManager.generateEd25519() + } catch { + print(error.localizedDescription) + } + } +// Button("rsa") { +// do { +// try keyManager.generateRSA() +// } catch { +// print(error.localizedDescription) +// } +// } + } +} + +#Preview { + KeyManagerView(keyManager: KeyManager()) +} diff --git a/ShhShell/Views/TerminalView.swift b/ShhShell/Views/Terminal/TerminalView.swift similarity index 100% rename from ShhShell/Views/TerminalView.swift rename to ShhShell/Views/Terminal/TerminalView.swift diff --git a/ShhShell/Views/TextViewController.swift b/ShhShell/Views/Terminal/TextViewController.swift similarity index 100% rename from ShhShell/Views/TextViewController.swift rename to ShhShell/Views/Terminal/TextViewController.swift