From af912f234f85d3f39817a2c199d429ae0265f1a6 Mon Sep 17 00:00:00 2001 From: neon443 <69979447+neon443@users.noreply.github.com> Date: Wed, 2 Jul 2025 21:18:48 +0100 Subject: [PATCH] add suport for rewritten authwithpubkey in sshhandler added removefromkeychain added renamekey added deletekey updatekymanagerview to add deleting and ui uodates remove publickey and passphrase from host remove key related texboxes in connectionview added a passwordstore instance made keytypes and names published added savekeypairs updatedsavetokeychain to remove and readd if it exists in the keychain update getkeys remove authwithbiometrics from hostmanager trying to add key renaming support remove Key (unused) cleanup --- ShhShell/Host/Host.swift | 18 +--- ShhShell/Host/HostsManager.swift | 31 +------ ShhShell/Keys/KeyManager.swift | 103 ++++++++++++++++------ ShhShell/SSH/SSHHandler.swift | 98 ++++---------------- ShhShell/Views/Hosts/ConnectionView.swift | 32 +------ ShhShell/Views/Keys/KeyDetailView.swift | 15 +++- ShhShell/Views/Keys/KeyImporterView.swift | 5 +- ShhShell/Views/Keys/KeyManagerView.swift | 30 +++---- 8 files changed, 131 insertions(+), 201 deletions(-) diff --git a/ShhShell/Host/Host.swift b/ShhShell/Host/Host.swift index e363606..e33bbfa 100644 --- a/ShhShell/Host/Host.swift +++ b/ShhShell/Host/Host.swift @@ -17,10 +17,7 @@ protocol HostPr: Codable, Identifiable, Equatable, Hashable { var port: Int { get set } var username: String { get set } var password: String { get set } - var publicKey: Data? { get set } - var privateKey: Data? { get set } var privateKeyID: UUID? { get set } - var passphrase: String { get set } var key: String? { get set } } @@ -33,10 +30,7 @@ struct Host: HostPr { var port: Int var username: String var password: String - var publicKey: Data? - var privateKey: Data? var privateKeyID: UUID? - var passphrase: String var key: String? var description: String { @@ -59,9 +53,7 @@ struct Host: HostPr { port: Int = 22, username: String = "", password: String = "", - publicKey: Data? = nil, - privateKey: Data? = nil, - passphrase: String = "", + privateKeyID: UUID? = nil, hostkey: String? = nil ) { self.name = name @@ -71,9 +63,7 @@ struct Host: HostPr { self.port = port self.username = username self.password = password - self.publicKey = publicKey - self.privateKey = privateKey - self.passphrase = passphrase + self.privateKeyID = privateKeyID self.key = hostkey } } @@ -90,9 +80,7 @@ extension Host { port: 22, username: "neon443", password: "password", - publicKey: nil, - privateKey: nil, - passphrase: "", + privateKeyID: nil, hostkey: nil ) } diff --git a/ShhShell/Host/HostsManager.swift b/ShhShell/Host/HostsManager.swift index c7b4e84..1b3acb9 100644 --- a/ShhShell/Host/HostsManager.swift +++ b/ShhShell/Host/HostsManager.swift @@ -172,17 +172,7 @@ class HostsManager: ObservableObject, @unchecked Sendable { func getKeys() -> [Keypair] { var result: [Keypair] = [] for host in hosts { - guard let privateKey = host.privateKey else { continue } - var keypair: Keypair - if let string = String(data: privateKey, encoding: .utf8), - string.contains("-----") { - keypair = KeyManager.importSSHPrivkey(priv: string) - } else { - keypair = Keypair(type: .ed25519, name: UUID().uuidString, privateKey: privateKey) - } - if !result.contains(keypair) { - result.append(keypair) - } + guard let keyID = host.privateKeyID else { continue } } return result } @@ -191,27 +181,10 @@ class HostsManager: ObservableObject, @unchecked Sendable { var result: [Host] = [] for key in keys { let hosts = hosts.filter({ - $0.privateKeyID == key.id || - $0.publicKey == key.publicKey && - $0.privateKey == key.privateKey + $0.privateKeyID == key.id }) result += hosts } return result } - - func authWithBiometrics() async -> Bool { - let context = LAContext() - var error: NSError? - guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { - return false - } - - let reason = "Authenticate yourself to view private keys" - return await withCheckedContinuation { continuation in - context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in - continuation.resume(returning: success) - } - } - } } diff --git a/ShhShell/Keys/KeyManager.swift b/ShhShell/Keys/KeyManager.swift index ac09034..70e1343 100644 --- a/ShhShell/Keys/KeyManager.swift +++ b/ShhShell/Keys/KeyManager.swift @@ -9,32 +9,41 @@ import Foundation import CryptoKit import Security import SwiftUI - -struct Key: Identifiable, Hashable { - var id = UUID() - var privateKey: SecKey - var publicKey: SecKey { - SecKeyCopyPublicKey(privateKey)! - } -} +import LocalAuthentication class KeyManager: ObservableObject { private let userdefaults = NSUbiquitousKeyValueStore.default + private let passwordStore = GenericPasswordStore() @Published var keypairs: [Keypair] = [] - var keyTypes: [UUID: KeyType] = [:] - var keyNames: [UUID: String] = [:] + @Published var keyTypes: [UUID: KeyType] = [:] + @Published var keyNames: [UUID: String] = [:] private let baseTag = "com.neon443.ShhShell.keys".data(using: .utf8)! init() { + loadKeypairs() + } + + func loadKeypairs() { loadKeyIDs() + keypairs = [] for id in keyTypes.keys { guard let keypair = getFromKeychain(keyID: id) else { continue } keypairs.append(keypair) } } + func saveKeypairs() { + for keypair in keypairs { + keyTypes.updateValue(keypair.type, forKey: keypair.id) + keyNames.updateValue(keypair.name, forKey: keypair.id) + saveToKeychain(keypair) + } + saveKeyIDs() + loadKeypairs() + } + func loadKeyIDs() { userdefaults.synchronize() let decoder = JSONDecoder() @@ -55,25 +64,22 @@ class KeyManager: ObservableObject { guard let encodedNames = try? encoder.encode(keyNames) else { return } userdefaults.set(encodedNames, forKey: "keyNames") userdefaults.synchronize() + loadKeypairs() } func saveToKeychain(_ keypair: Keypair) { - withAnimation { - keyTypes.updateValue(keypair.type, forKey: keypair.id) - keyNames.updateValue(keypair.name, forKey: keypair.id) - } - saveKeyIDs() + keyTypes.updateValue(keypair.type, forKey: keypair.id) + keyNames.updateValue(keypair.name, forKey: keypair.id) if keypair.type == .ed25519 { let curve25519 = try! Curve25519.Signing.PrivateKey(rawRepresentation: keypair.privateKey) - try! GenericPasswordStore().storeKey(curve25519.genericKeyRepresentation, account: keypair.id.uuidString) + let readKey: Curve25519.Signing.PrivateKey? + readKey = try! passwordStore.readKey(account: keypair.id.uuidString) + if readKey != nil { + try! passwordStore.deleteKey(account: keypair.id.uuidString) + } + try! passwordStore.storeKey(curve25519.genericKeyRepresentation, account: keypair.id.uuidString) } else { - let tag = baseTag+keypair.id.uuidString.data(using: .utf8)! - let addQuery: [String: Any] = [kSecClass as String: kSecClassGenericPassword, - kSecAttrApplicationTag as String: tag, - kSecValueRef as String: keypair.privateKey, - kSecAttrSynchronizable as String: kCFBooleanTrue!] - let status = SecItemAdd(addQuery as CFDictionary, nil) - guard status == errSecSuccess else { fatalError() } + } } @@ -82,9 +88,9 @@ class KeyManager: ObservableObject { guard let keyName = keyNames[keyID] else { return nil } if keyType == .ed25519 { var key: Curve25519.Signing.PrivateKey? - key = try? GenericPasswordStore().readKey(account: keyID.uuidString) + key = try? passwordStore.readKey(account: keyID.uuidString) guard let key else { return nil } - return Keypair(type: keyType, name: keyName, privateKey: key.rawRepresentation) + return Keypair(id: keyID, type: keyType, name: keyName, privateKey: key.rawRepresentation) } else { let tag = baseTag+keyID.uuidString.data(using: .utf8)! let getQuery: [String: Any] = [kSecClass as String: kSecClassKey, @@ -102,9 +108,39 @@ class KeyManager: ObservableObject { } } + func removeFromKeycahin(keypair: Keypair) { + if keypair.type == .ed25519 { + do { + try passwordStore.deleteKey(account: keypair.id.uuidString) + } catch { + fatalError() + } + } + keyNames.removeValue(forKey: keypair.id) + keyTypes.removeValue(forKey: keypair.id) + saveKeyIDs() + } + + func renameKey(keypair: Keypair, newName: String) { + let keyID = keypair.id + guard let index = keypairs.firstIndex(where: { $0.id == keyID }) else { return } + var keypairWithNewName = keypair + keypairWithNewName.name = newName + withAnimation { keypairs[index] = keypairWithNewName } + saveKeypairs() + } + + func deleteKey(_ keypair: Keypair) { + removeFromKeycahin(keypair: keypair) + let keyID = keypair.id + withAnimation { keypairs.removeAll(where: { $0.id == keyID }) } + saveKeypairs() + } + func importKey(type: KeyType, priv: String, name: String) { if type == .ed25519 { saveToKeychain(KeyManager.importSSHPrivkey(priv: priv)) + saveKeypairs() } else { fatalError() } } @@ -118,9 +154,11 @@ class KeyManager: ObservableObject { privateKey: Curve25519.Signing.PrivateKey().rawRepresentation ) saveToKeychain(keypair) + saveKeypairs() case .rsa: fatalError("unimplemented") } + loadKeypairs() } static func importSSHPubkey(pub: String) -> Data { @@ -277,3 +315,18 @@ class KeyManager: ObservableObject { return extracted } } + +func authWithBiometrics() async -> Bool { + let context = LAContext() + var error: NSError? + guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { + return false + } + + let reason = "Authenticate yourself to view private keys" + return await withCheckedContinuation { continuation in + context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in + continuation.resume(returning: success) + } + } +} diff --git a/ShhShell/SSH/SSHHandler.swift b/ShhShell/SSH/SSHHandler.swift index bdba1fc..e7849a6 100644 --- a/ShhShell/SSH/SSHHandler.swift +++ b/ShhShell/SSH/SSHHandler.swift @@ -81,27 +81,23 @@ class SSHHandler: @unchecked Sendable, ObservableObject { return } - try? authWithPubkey2() + guard state != .authorized else { return } -// fatalError() -// if state != .authorized { -// if !host.password.isEmpty { -// do { try authWithPw() } catch { -// print("pw auth error") -// print(error.localizedDescription) -// } -// } else { -// do { -// if let publicKey = host.publicKey, -// let privateKey = host.privateKey { -// try authWithPubkey() -// } -// } catch { -// print("error with pubkey auth") -// print(error.localizedDescription) -// } -// } -// } + if !host.password.isEmpty { + do { try authWithPw() } catch { + print("pw auth error") + print(error.localizedDescription) + } + } else { + do { + if host.privateKeyID != nil { + try authWithPubkey() + } + } catch { + print("error with pubkey auth") + print(error.localizedDescription) + } + } ssh_channel_request_env(channel, "TERM", "xterm-256color") ssh_channel_request_env(channel, "LANG", "en_US.UTF-8") @@ -274,7 +270,7 @@ class SSHHandler: @unchecked Sendable, ObservableObject { } //MARK: auth - func authWithPubkey2() throws(KeyError) { + func authWithPubkey() throws(KeyError) { guard let keyID = self.host.privateKeyID else { throw .importPrivkeyError } guard let keypair = keyManager.keypairs.first(where: { $0.id == keyID }) else { throw .importPrivkeyError @@ -297,66 +293,6 @@ class SSHHandler: @unchecked Sendable, ObservableObject { state = .authorized } - func authWithPubkey(pub pubInp: Data, priv privInp: Data, pass: String) throws(KeyError) { - guard session != nil else { throw .notConnected } - - let fileManager = FileManager.default - let tempDir = fileManager.temporaryDirectory - let tempPubkey = tempDir.appendingPathComponent("\(UUID())key.pub") - let tempKey = tempDir.appendingPathComponent("\(UUID())key") - - fileManager.createFile(atPath: tempPubkey.path(), contents: nil) - fileManager.createFile(atPath: tempKey.path(), contents: nil) - - do { - try pubInp.write(to: tempPubkey, options: .completeFileProtection) - try privInp.write(to: tempKey, options: .completeFileProtection) - } catch { - print("file writing error") -// print(error.localizedDescription) - } - - let attributes: [FileAttributeKey: Any] = [.posixPermissions: 0o600] - do { - try fileManager.setAttributes(attributes, ofItemAtPath: tempPubkey.path()) - try fileManager.setAttributes(attributes, ofItemAtPath: tempKey.path()) - } catch { -// logCritical("permission settig failed\(error.localizedDescription)") - } - - var pubkey: ssh_key? - if ssh_pki_import_pubkey_file(tempPubkey.path(), &pubkey) != 0 { - throw .importPrivkeyError - } - defer { ssh_key_free(pubkey) } - - if ssh_userauth_try_publickey(session, nil, pubkey) != 0 { - throw .pubkeyRejected - } - - var privkey: ssh_key? - if ssh_pki_import_privkey_file(tempKey.path(), pass, nil, nil, &privkey) != 0 { - throw .importPrivkeyError - } - defer { ssh_key_free(privkey) } - - if (ssh_userauth_publickey(session, nil, privkey) != 0) { - throw .privkeyRejected - } - - //if u got this far, youre authed! - withAnimation { state = .authorized } - - do { - try FileManager.default.removeItem(at: tempPubkey) - try FileManager.default.removeItem(at: tempKey) - } catch { - print("error removing file") - print(error.localizedDescription) - } - return - } - func authWithPw() throws(AuthError) { var status: CInt status = ssh_userauth_password(session, host.username, host.password) diff --git a/ShhShell/Views/Hosts/ConnectionView.swift b/ShhShell/Views/Hosts/ConnectionView.swift index 2ad0333..c018565 100644 --- a/ShhShell/Views/Hosts/ConnectionView.swift +++ b/ShhShell/Views/Hosts/ConnectionView.swift @@ -84,31 +84,9 @@ struct ConnectionView: View { .tag(nil as UUID?) ForEach(keyManager.keypairs) { keypair in Text(keypair.label) - .tag(keypair.id as UUID?) + .tag(keypair.id) } } - - TextBox(label: "Publickey", text: $pubkeyStr, prompt: "in openssh format") - .onChange(of: pubkeyStr) { _ in - let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "") - handler.host.publicKey = Data(newStr.utf8) - } - .onSubmit { - let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "") - handler.host.publicKey = Data(newStr.utf8) - } - - TextBox(label: "Privatekey", text: $privkeyStr, prompt: "required if using publickeys", secure: true) - .onSubmit { - let newStr = privkeyStr.replacingOccurrences(of: "\r\n", with: "") - handler.host.privateKey = Data(newStr.utf8) - } - .onChange(of: privkeyStr) { _ in - let newStr = privkeyStr.replacingOccurrences(of: "\r\n", with: "") - handler.host.privateKey = Data(newStr.utf8) - } - - TextBox(label: "Passphrase", text: $handler.host.passphrase, prompt: "optional") } Button() { @@ -141,14 +119,6 @@ struct ConnectionView: View { .onDisappear { hostsManager.updateHost(handler.host) } - .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) ?? "" - } - } .onAppear { if shellView == nil { shellView = ShellView(handler: handler, hostsManager: hostsManager) diff --git a/ShhShell/Views/Keys/KeyDetailView.swift b/ShhShell/Views/Keys/KeyDetailView.swift index fb00a02..fdc6712 100644 --- a/ShhShell/Views/Keys/KeyDetailView.swift +++ b/ShhShell/Views/Keys/KeyDetailView.swift @@ -9,7 +9,10 @@ import SwiftUI struct KeyDetailView: View { @ObservedObject var hostsManager: HostsManager + @ObservedObject var keyManager: KeyManager @State var keypair: Keypair + + @State var keyname: String = "" @State private var reveal: Bool = false var body: some View { @@ -17,6 +20,11 @@ struct KeyDetailView: View { hostsManager.selectedTheme.background.suiColor.opacity(0.7) .ignoresSafeArea(.all) List { + TextBox(label: "Name", text: $keyname, prompt: "A name for your key") + .onChange(of: keypair.name) { _ in + keyManager.renameKey(keypair: keypair, newName: keyname) + } + VStack(alignment: .leading) { Text("Used on") .bold() @@ -52,7 +60,7 @@ struct KeyDetailView: View { .onTapGesture { Task { if !reveal { - guard await hostsManager.authWithBiometrics() else { return } + guard await authWithBiometrics() else { return } } withAnimation(.spring) { reveal.toggle() } } @@ -62,13 +70,13 @@ struct KeyDetailView: View { Button { UIPasteboard.general.string = keypair.openSshPubkey } label: { - CenteredLabel(title: "Copy private key", systemName: "document.on.document") + CenteredLabel(title: "Copy public key", systemName: "document.on.document") } .listRowSeparator(.hidden) Button { Task { - guard await hostsManager.authWithBiometrics() else { return } + guard await authWithBiometrics() else { return } UIPasteboard.general.string = String(data: KeyManager.makeSSHPrivkey(keypair), encoding: .utf8) ?? "" } } label: { @@ -85,6 +93,7 @@ import CryptoKit #Preview { KeyDetailView( hostsManager: HostsManager(), + keyManager: KeyManager(), keypair: Keypair( type: .ed25519, name: "previewKey", diff --git a/ShhShell/Views/Keys/KeyImporterView.swift b/ShhShell/Views/Keys/KeyImporterView.swift index 9d595df..7507d0b 100644 --- a/ShhShell/Views/Keys/KeyImporterView.swift +++ b/ShhShell/Views/Keys/KeyImporterView.swift @@ -41,7 +41,10 @@ struct KeyImporterView: View { TextEditor(text: $privkeyStr) - TextEditor(text: .constant(keypair.openSshPubkey)) + if !keypair.openSshPubkey.isEmpty { + TextEditor(text: .constant(keypair.openSshPubkey)) + .foregroundStyle(.gray) + } Button() { keyManager.importKey(type: keyType, priv: privkeyStr, name: keyName) diff --git a/ShhShell/Views/Keys/KeyManagerView.swift b/ShhShell/Views/Keys/KeyManagerView.swift index 5ce6e44..bdabd35 100644 --- a/ShhShell/Views/Keys/KeyManagerView.swift +++ b/ShhShell/Views/Keys/KeyManagerView.swift @@ -19,34 +19,32 @@ struct KeyManagerView: View { .ignoresSafeArea(.all) NavigationStack { List { - Section { - ForEach(hostsManager.getKeys()) { keypair in - NavigationLink { - KeyDetailView(hostsManager: hostsManager, keypair: keypair) - } label: { - Text(keypair.openSshPubkey) - } - } - } - Section() { ForEach(keyManager.keypairs) { kp in NavigationLink { - KeyDetailView(hostsManager: hostsManager, keypair: kp) + KeyDetailView( + hostsManager: hostsManager, + keyManager: keyManager, + keypair: kp + ) } label: { - Image(systemName: "key") - Text(kp.label) - Spacer() - Text(kp.type.description) + HStack { + Image(systemName: "key") + Text(kp.label) + Spacer() + Text(kp.type.description) + .foregroundStyle(.gray) + } } .swipeActions(edge: .trailing) { Button(role: .destructive) { - + keyManager.deleteKey(kp) } label: { Label("Delete", systemImage: "trash") } } } + .id(keyManager.keypairs) } Button("Generate a new Ed25519 Key") {