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 */; };
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 = "<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>"; };
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>"; };
/* 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 = "<group>";
@@ -251,6 +259,7 @@
isa = PBXGroup;
children = (
A98554582E0553AA009051BD /* KeyManager.swift */,
A985545E2E056EDD009051BD /* KeychainLayer.swift */,
);
path = Keys;
sourceTree = "<group>";
@@ -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 */,

View File

@@ -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: "")
}
}

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

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
#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

View File

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

View File

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

View File

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

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

View File

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