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 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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user