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
This commit is contained in:
neon443
2025-07-02 21:18:48 +01:00
parent 421444b2f8
commit af912f234f
8 changed files with 131 additions and 201 deletions

View File

@@ -17,10 +17,7 @@ protocol HostPr: Codable, Identifiable, Equatable, Hashable {
var port: Int { get set } var port: Int { get set }
var username: String { get set } var username: String { get set }
var password: String { get set } var password: String { get set }
var publicKey: Data? { get set }
var privateKey: Data? { get set }
var privateKeyID: UUID? { get set } var privateKeyID: UUID? { get set }
var passphrase: String { get set }
var key: String? { get set } var key: String? { get set }
} }
@@ -33,10 +30,7 @@ struct Host: HostPr {
var port: Int var port: Int
var username: String var username: String
var password: String var password: String
var publicKey: Data?
var privateKey: Data?
var privateKeyID: UUID? var privateKeyID: UUID?
var passphrase: String
var key: String? var key: String?
var description: String { var description: String {
@@ -59,9 +53,7 @@ struct Host: HostPr {
port: Int = 22, port: Int = 22,
username: String = "", username: String = "",
password: String = "", password: String = "",
publicKey: Data? = nil, privateKeyID: UUID? = nil,
privateKey: Data? = nil,
passphrase: String = "",
hostkey: String? = nil hostkey: String? = nil
) { ) {
self.name = name self.name = name
@@ -71,9 +63,7 @@ struct Host: HostPr {
self.port = port self.port = port
self.username = username self.username = username
self.password = password self.password = password
self.publicKey = publicKey self.privateKeyID = privateKeyID
self.privateKey = privateKey
self.passphrase = passphrase
self.key = hostkey self.key = hostkey
} }
} }
@@ -90,9 +80,7 @@ extension Host {
port: 22, port: 22,
username: "neon443", username: "neon443",
password: "password", password: "password",
publicKey: nil, privateKeyID: nil,
privateKey: nil,
passphrase: "",
hostkey: nil hostkey: nil
) )
} }

View File

@@ -172,17 +172,7 @@ class HostsManager: ObservableObject, @unchecked Sendable {
func getKeys() -> [Keypair] { func getKeys() -> [Keypair] {
var result: [Keypair] = [] var result: [Keypair] = []
for host in hosts { for host in hosts {
guard let privateKey = host.privateKey else { continue } guard let keyID = host.privateKeyID 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)
}
} }
return result return result
} }
@@ -191,27 +181,10 @@ class HostsManager: ObservableObject, @unchecked Sendable {
var result: [Host] = [] var result: [Host] = []
for key in keys { for key in keys {
let hosts = hosts.filter({ let hosts = hosts.filter({
$0.privateKeyID == key.id || $0.privateKeyID == key.id
$0.publicKey == key.publicKey &&
$0.privateKey == key.privateKey
}) })
result += hosts result += hosts
} }
return result 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)
}
}
}
} }

View File

@@ -9,32 +9,41 @@ import Foundation
import CryptoKit import CryptoKit
import Security import Security
import SwiftUI import SwiftUI
import LocalAuthentication
struct Key: Identifiable, Hashable {
var id = UUID()
var privateKey: SecKey
var publicKey: SecKey {
SecKeyCopyPublicKey(privateKey)!
}
}
class KeyManager: ObservableObject { class KeyManager: ObservableObject {
private let userdefaults = NSUbiquitousKeyValueStore.default private let userdefaults = NSUbiquitousKeyValueStore.default
private let passwordStore = GenericPasswordStore()
@Published var keypairs: [Keypair] = [] @Published var keypairs: [Keypair] = []
var keyTypes: [UUID: KeyType] = [:] @Published var keyTypes: [UUID: KeyType] = [:]
var keyNames: [UUID: String] = [:] @Published var keyNames: [UUID: String] = [:]
private let baseTag = "com.neon443.ShhShell.keys".data(using: .utf8)! private let baseTag = "com.neon443.ShhShell.keys".data(using: .utf8)!
init() { init() {
loadKeypairs()
}
func loadKeypairs() {
loadKeyIDs() loadKeyIDs()
keypairs = []
for id in keyTypes.keys { for id in keyTypes.keys {
guard let keypair = getFromKeychain(keyID: id) else { continue } guard let keypair = getFromKeychain(keyID: id) else { continue }
keypairs.append(keypair) 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() { func loadKeyIDs() {
userdefaults.synchronize() userdefaults.synchronize()
let decoder = JSONDecoder() let decoder = JSONDecoder()
@@ -55,25 +64,22 @@ class KeyManager: ObservableObject {
guard let encodedNames = try? encoder.encode(keyNames) else { return } guard let encodedNames = try? encoder.encode(keyNames) else { return }
userdefaults.set(encodedNames, forKey: "keyNames") userdefaults.set(encodedNames, forKey: "keyNames")
userdefaults.synchronize() userdefaults.synchronize()
loadKeypairs()
} }
func saveToKeychain(_ keypair: Keypair) { func saveToKeychain(_ keypair: Keypair) {
withAnimation { keyTypes.updateValue(keypair.type, forKey: keypair.id)
keyTypes.updateValue(keypair.type, forKey: keypair.id) keyNames.updateValue(keypair.name, forKey: keypair.id)
keyNames.updateValue(keypair.name, forKey: keypair.id)
}
saveKeyIDs()
if keypair.type == .ed25519 { if keypair.type == .ed25519 {
let curve25519 = try! Curve25519.Signing.PrivateKey(rawRepresentation: keypair.privateKey) 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 { } 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 } guard let keyName = keyNames[keyID] else { return nil }
if keyType == .ed25519 { if keyType == .ed25519 {
var key: Curve25519.Signing.PrivateKey? 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 } 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 { } else {
let tag = baseTag+keyID.uuidString.data(using: .utf8)! let tag = baseTag+keyID.uuidString.data(using: .utf8)!
let getQuery: [String: Any] = [kSecClass as String: kSecClassKey, 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) { func importKey(type: KeyType, priv: String, name: String) {
if type == .ed25519 { if type == .ed25519 {
saveToKeychain(KeyManager.importSSHPrivkey(priv: priv)) saveToKeychain(KeyManager.importSSHPrivkey(priv: priv))
saveKeypairs()
} else { fatalError() } } else { fatalError() }
} }
@@ -118,9 +154,11 @@ class KeyManager: ObservableObject {
privateKey: Curve25519.Signing.PrivateKey().rawRepresentation privateKey: Curve25519.Signing.PrivateKey().rawRepresentation
) )
saveToKeychain(keypair) saveToKeychain(keypair)
saveKeypairs()
case .rsa: case .rsa:
fatalError("unimplemented") fatalError("unimplemented")
} }
loadKeypairs()
} }
static func importSSHPubkey(pub: String) -> Data { static func importSSHPubkey(pub: String) -> Data {
@@ -277,3 +315,18 @@ class KeyManager: ObservableObject {
return extracted 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)
}
}
}

View File

@@ -81,27 +81,23 @@ class SSHHandler: @unchecked Sendable, ObservableObject {
return return
} }
try? authWithPubkey2() guard state != .authorized else { return }
// fatalError() if !host.password.isEmpty {
// if state != .authorized { do { try authWithPw() } catch {
// if !host.password.isEmpty { print("pw auth error")
// do { try authWithPw() } catch { print(error.localizedDescription)
// print("pw auth error") }
// print(error.localizedDescription) } else {
// } do {
// } else { if host.privateKeyID != nil {
// do { try authWithPubkey()
// if let publicKey = host.publicKey, }
// let privateKey = host.privateKey { } catch {
// try authWithPubkey() print("error with pubkey auth")
// } print(error.localizedDescription)
// } catch { }
// print("error with pubkey auth") }
// print(error.localizedDescription)
// }
// }
// }
ssh_channel_request_env(channel, "TERM", "xterm-256color") ssh_channel_request_env(channel, "TERM", "xterm-256color")
ssh_channel_request_env(channel, "LANG", "en_US.UTF-8") ssh_channel_request_env(channel, "LANG", "en_US.UTF-8")
@@ -274,7 +270,7 @@ class SSHHandler: @unchecked Sendable, ObservableObject {
} }
//MARK: auth //MARK: auth
func authWithPubkey2() throws(KeyError) { func authWithPubkey() throws(KeyError) {
guard let keyID = self.host.privateKeyID else { throw .importPrivkeyError } guard let keyID = self.host.privateKeyID else { throw .importPrivkeyError }
guard let keypair = keyManager.keypairs.first(where: { $0.id == keyID }) else { guard let keypair = keyManager.keypairs.first(where: { $0.id == keyID }) else {
throw .importPrivkeyError throw .importPrivkeyError
@@ -297,66 +293,6 @@ class SSHHandler: @unchecked Sendable, ObservableObject {
state = .authorized 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) { func authWithPw() throws(AuthError) {
var status: CInt var status: CInt
status = ssh_userauth_password(session, host.username, host.password) status = ssh_userauth_password(session, host.username, host.password)

View File

@@ -84,31 +84,9 @@ struct ConnectionView: View {
.tag(nil as UUID?) .tag(nil as UUID?)
ForEach(keyManager.keypairs) { keypair in ForEach(keyManager.keypairs) { keypair in
Text(keypair.label) 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() { Button() {
@@ -141,14 +119,6 @@ struct ConnectionView: View {
.onDisappear { .onDisappear {
hostsManager.updateHost(handler.host) 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 { .onAppear {
if shellView == nil { if shellView == nil {
shellView = ShellView(handler: handler, hostsManager: hostsManager) shellView = ShellView(handler: handler, hostsManager: hostsManager)

View File

@@ -9,7 +9,10 @@ import SwiftUI
struct KeyDetailView: View { struct KeyDetailView: View {
@ObservedObject var hostsManager: HostsManager @ObservedObject var hostsManager: HostsManager
@ObservedObject var keyManager: KeyManager
@State var keypair: Keypair @State var keypair: Keypair
@State var keyname: String = ""
@State private var reveal: Bool = false @State private var reveal: Bool = false
var body: some View { var body: some View {
@@ -17,6 +20,11 @@ struct KeyDetailView: View {
hostsManager.selectedTheme.background.suiColor.opacity(0.7) hostsManager.selectedTheme.background.suiColor.opacity(0.7)
.ignoresSafeArea(.all) .ignoresSafeArea(.all)
List { 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) { VStack(alignment: .leading) {
Text("Used on") Text("Used on")
.bold() .bold()
@@ -52,7 +60,7 @@ struct KeyDetailView: View {
.onTapGesture { .onTapGesture {
Task { Task {
if !reveal { if !reveal {
guard await hostsManager.authWithBiometrics() else { return } guard await authWithBiometrics() else { return }
} }
withAnimation(.spring) { reveal.toggle() } withAnimation(.spring) { reveal.toggle() }
} }
@@ -62,13 +70,13 @@ struct KeyDetailView: View {
Button { Button {
UIPasteboard.general.string = keypair.openSshPubkey UIPasteboard.general.string = keypair.openSshPubkey
} label: { } label: {
CenteredLabel(title: "Copy private key", systemName: "document.on.document") CenteredLabel(title: "Copy public key", systemName: "document.on.document")
} }
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
Button { Button {
Task { Task {
guard await hostsManager.authWithBiometrics() else { return } guard await authWithBiometrics() else { return }
UIPasteboard.general.string = String(data: KeyManager.makeSSHPrivkey(keypair), encoding: .utf8) ?? "" UIPasteboard.general.string = String(data: KeyManager.makeSSHPrivkey(keypair), encoding: .utf8) ?? ""
} }
} label: { } label: {
@@ -85,6 +93,7 @@ import CryptoKit
#Preview { #Preview {
KeyDetailView( KeyDetailView(
hostsManager: HostsManager(), hostsManager: HostsManager(),
keyManager: KeyManager(),
keypair: Keypair( keypair: Keypair(
type: .ed25519, type: .ed25519,
name: "previewKey", name: "previewKey",

View File

@@ -41,7 +41,10 @@ struct KeyImporterView: View {
TextEditor(text: $privkeyStr) TextEditor(text: $privkeyStr)
TextEditor(text: .constant(keypair.openSshPubkey)) if !keypair.openSshPubkey.isEmpty {
TextEditor(text: .constant(keypair.openSshPubkey))
.foregroundStyle(.gray)
}
Button() { Button() {
keyManager.importKey(type: keyType, priv: privkeyStr, name: keyName) keyManager.importKey(type: keyType, priv: privkeyStr, name: keyName)

View File

@@ -19,34 +19,32 @@ struct KeyManagerView: View {
.ignoresSafeArea(.all) .ignoresSafeArea(.all)
NavigationStack { NavigationStack {
List { List {
Section {
ForEach(hostsManager.getKeys()) { keypair in
NavigationLink {
KeyDetailView(hostsManager: hostsManager, keypair: keypair)
} label: {
Text(keypair.openSshPubkey)
}
}
}
Section() { Section() {
ForEach(keyManager.keypairs) { kp in ForEach(keyManager.keypairs) { kp in
NavigationLink { NavigationLink {
KeyDetailView(hostsManager: hostsManager, keypair: kp) KeyDetailView(
hostsManager: hostsManager,
keyManager: keyManager,
keypair: kp
)
} label: { } label: {
Image(systemName: "key") HStack {
Text(kp.label) Image(systemName: "key")
Spacer() Text(kp.label)
Text(kp.type.description) Spacer()
Text(kp.type.description)
.foregroundStyle(.gray)
}
} }
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {
keyManager.deleteKey(kp)
} label: { } label: {
Label("Delete", systemImage: "trash") Label("Delete", systemImage: "trash")
} }
} }
} }
.id(keyManager.keypairs)
} }
Button("Generate a new Ed25519 Key") { Button("Generate a new Ed25519 Key") {