mirror of
https://github.com/neon443/ShhShell.git
synced 2026-03-11 13:26:16 +00:00
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:
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
Reference in New Issue
Block a user