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
This commit is contained in:
neon443
2025-06-20 13:40:20 +01:00
parent 86dd316e33
commit a06de0b4c4
12 changed files with 239 additions and 45 deletions

View File

@@ -32,6 +32,9 @@
A98554552E05535F009051BD /* KeyManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554542E05535F009051BD /* KeyManagerView.swift */; }; A98554552E05535F009051BD /* KeyManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554542E05535F009051BD /* KeyManagerView.swift */; };
A98554592E0553AA009051BD /* KeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554582E0553AA009051BD /* KeyManager.swift */; }; A98554592E0553AA009051BD /* KeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554582E0553AA009051BD /* KeyManager.swift */; };
A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985545C2E055D4D009051BD /* ConnectionView.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 */; }; A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -93,6 +96,9 @@
A98554542E05535F009051BD /* KeyManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagerView.swift; sourceTree = "<group>"; }; A98554542E05535F009051BD /* KeyManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagerView.swift; sourceTree = "<group>"; };
A98554582E0553AA009051BD /* KeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManager.swift; sourceTree = "<group>"; }; A98554582E0553AA009051BD /* KeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManager.swift; sourceTree = "<group>"; };
A985545C2E055D4D009051BD /* ConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionView.swift; sourceTree = "<group>"; }; A985545C2E055D4D009051BD /* ConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionView.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>"; };
A98554622E0587DF009051BD /* HostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsView.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 */
@@ -189,6 +195,7 @@
children = ( children = (
A98554532E05534F009051BD /* Keys */, A98554532E05534F009051BD /* Keys */,
A92538C52DEE0742007E0A18 /* ContentView.swift */, A92538C52DEE0742007E0A18 /* ContentView.swift */,
A98554622E0587DF009051BD /* HostsView.swift */,
A985545C2E055D4D009051BD /* ConnectionView.swift */, A985545C2E055D4D009051BD /* ConnectionView.swift */,
A98554522E055347009051BD /* Terminal */, A98554522E055347009051BD /* Terminal */,
A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */, A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */,
@@ -243,6 +250,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A93143BF2DF61B3200FCD5DB /* Host.swift */, A93143BF2DF61B3200FCD5DB /* Host.swift */,
A98554602E058433009051BD /* HostsManager.swift */,
); );
path = Host; path = Host;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -251,6 +259,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A98554582E0553AA009051BD /* KeyManager.swift */, A98554582E0553AA009051BD /* KeyManager.swift */,
A985545E2E056EDD009051BD /* KeychainLayer.swift */,
); );
path = Keys; path = Keys;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -413,10 +422,13 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */,
A93143C62DF61FE300FCD5DB /* ViewModifiers.swift in Sources */, A93143C62DF61FE300FCD5DB /* ViewModifiers.swift in Sources */,
A98554632E0587DF009051BD /* HostsView.swift in Sources */,
A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */, A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */,
A93143C02DF61B3200FCD5DB /* Host.swift in Sources */, A93143C02DF61B3200FCD5DB /* Host.swift in Sources */,
A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */, A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */,
A98554612E058433009051BD /* HostsManager.swift in Sources */,
A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */, A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */,
A91AE3B22DF73E0900FF3537 /* TerminalView.swift in Sources */, A91AE3B22DF73E0900FF3537 /* TerminalView.swift in Sources */,
A98554592E0553AA009051BD /* KeyManager.swift in Sources */, A98554592E0553AA009051BD /* KeyManager.swift in Sources */,

View File

@@ -7,7 +7,8 @@
import Foundation import Foundation
protocol HostPr: Codable { protocol HostPr: Codable, Identifiable {
var id: UUID { get set }
var address: String { get set } var address: String { get set }
var port: Int { get set } var port: Int { get set }
var username: String { get set } var username: String { get set }
@@ -16,7 +17,8 @@ protocol HostPr: Codable {
} }
struct Host: HostPr { struct Host: HostPr {
var address: String = "address" var id = UUID()
var address: String = ""
var port: Int var port: Int
var username: String var username: String
var password: String var password: String
@@ -37,18 +39,11 @@ struct Host: HostPr {
} }
} }
struct blankHost: HostPr { extension Host {
var address: String = "" static var blank: Host {
var port: Int = 22 Host(address: "", port: 22, username: "", password: "")
var username: String = "" }
var password: String = "" static var debug: Host {
var key: Data? = nil Host(address: "localhost", port: 22, username: "default", password: "")
} }
struct debugHost: HostPr {
var address: String = "localhost"
var port: Int = 22
var username: String = "default"
var password: String = ""
var key: Data? = nil
} }

View File

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

View File

@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import CryptoKit
struct Key: Identifiable, Hashable { struct Key: Identifiable, Hashable {
var id = UUID() var id = UUID()
@@ -16,6 +17,13 @@ struct Key: Identifiable, Hashable {
} }
class KeyManager: ObservableObject { class KeyManager: ObservableObject {
func generateEd25519() {
let privateKey = Curve25519.Signing.PrivateKey()
let publicKeyData = privateKey.publicKey
dump(privateKey.rawRepresentation)
print(publicKeyData.rawRepresentation)
}
func generateRSA() throws { func generateRSA() throws {
let type = kSecAttrKeyTypeRSA let type = kSecAttrKeyTypeRSA
let tag = "com.neon443.ShhSell.keys.\(Date().timeIntervalSince1970)".data(using: .utf8)! let tag = "com.neon443.ShhSell.keys.\(Date().timeIntervalSince1970)".data(using: .utf8)!

View File

@@ -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<Bytes>(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<D>(genericKeyRepresentation data: D) throws where D: ContiguousBytes
//generic rep of key
var genericKeyRepresentation: SymmetricKey { get }
}
extension Curve25519.KeyAgreement.PrivateKey: GenericPasswordConvertible {
init<D>(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<D>(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<T: SecKeyConvertible>(_ 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
}

View File

@@ -30,7 +30,7 @@ class SSHHandler: ObservableObject {
) { ) {
self.host = host self.host = host
#if DEBUG #if DEBUG
self.host = debugHost() self.host = Host.debug
#endif #endif
} }
@@ -87,9 +87,9 @@ class SSHHandler: ObservableObject {
} }
ssh_disconnect(session) ssh_disconnect(session)
ssh_free(session) ssh_free(session)
session = nil
withAnimation { authorized = false } withAnimation { authorized = false }
withAnimation { connected = false } withAnimation { connected = false }
session = nil
host.key = nil host.key = nil
} }
@@ -296,12 +296,12 @@ class SSHHandler: ObservableObject {
guard status == SSH_OK else { return } guard status == SSH_OK else { return }
self.readTimer = Timer(timeInterval: 0.1, repeats: true) { timer in 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() timer.invalidate()
self.readTimer = nil self.readTimer = nil
return 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() timer.invalidate()
self.readTimer = nil self.readTimer = nil
return return

View File

@@ -9,14 +9,16 @@ import SwiftUI
@main @main
struct ShhShellApp: App { 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 keyManager: KeyManager = KeyManager()
@StateObject var hostsManager: HostsManager = HostsManager()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView( ContentView(
handler: sshHandler, handler: sshHandler,
keyManager: keyManager keyManager: keyManager,
hostsManager: hostsManager
) )
} }
} }

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct ConnectionView: View { struct ConnectionView: View {
@StateObject var handler: SSHHandler @StateObject var handler: SSHHandler
@StateObject var keyManager: KeyManager @StateObject var keyManager: KeyManager
@StateObject var hostsManager: HostsManager
@State var passphrase: String = "" @State var passphrase: String = ""
@@ -156,7 +157,8 @@ struct ConnectionView: View {
#Preview { #Preview {
ConnectionView( ConnectionView(
handler: SSHHandler(host: debugHost()), handler: SSHHandler(host: Host.debug),
keyManager: KeyManager() keyManager: KeyManager(),
hostsManager: HostsManager()
) )
} }

View File

@@ -10,15 +10,16 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
@ObservedObject var handler: SSHHandler @ObservedObject var handler: SSHHandler
@ObservedObject var keyManager: KeyManager @ObservedObject var keyManager: KeyManager
@ObservedObject var hostsManager: HostsManager
var body: some View { var body: some View {
TabView { TabView {
ConnectionView( HostsView(
handler: handler, keyManager: keyManager,
keyManager: keyManager hostsManager: hostsManager
) )
.tabItem { .tabItem {
Label("Connection", systemImage: "powerplug.portrait") Label("Hosts", systemImage: "server.rack")
} }
KeyManagerView(keyManager: keyManager) KeyManagerView(keyManager: keyManager)
.tabItem { .tabItem {
@@ -30,7 +31,8 @@ struct ContentView: View {
#Preview { #Preview {
ContentView( ContentView(
handler: SSHHandler(host: debugHost()), handler: SSHHandler(host: Host.debug),
keyManager: KeyManager() keyManager: KeyManager(),
hostsManager: HostsManager()
) )
} }

View File

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

View File

@@ -10,22 +10,20 @@ import SwiftUI
struct KeyManagerView: View { struct KeyManagerView: View {
@ObservedObject var keyManager: KeyManager @ObservedObject var keyManager: KeyManager
var body: some View { var body: some View {
Button("ed25519") { List {
do { Button("ed25519") {
try keyManager.generateEd25519() keyManager.generateEd25519()
} catch { }
print(error.localizedDescription) Button("rsa") {
do {
try keyManager.generateRSA()
} catch {
print(error.localizedDescription)
}
} }
} }
// Button("rsa") { }
// do {
// try keyManager.generateRSA()
// } catch {
// print(error.localizedDescription)
// }
// }
}
} }
#Preview { #Preview {

View File

@@ -39,5 +39,5 @@ struct TerminalView: View {
} }
#Preview { #Preview {
TerminalView(handler: SSHHandler(host: debugHost())) TerminalView(handler: SSHHandler(host: Host.debug))
} }