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") {