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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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