added keyimporter view to import keys

added importkey to import keys and save to keychain
added a picker to select a key id in connection view
fix publickey and opensshpublickey returning a pubkey when privatekey is empty
added generate key button and import button
change HostManager.makeLabel() to a computed property on Host
This commit is contained in:
neon443
2025-07-01 20:16:55 +01:00
parent 0af64a4efd
commit 8a37a1464a
11 changed files with 110 additions and 27 deletions

View File

@@ -69,6 +69,7 @@
A9FD37592E143D74005319A8 /* GenericPasswordConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */; };
A9FD375B2E143D77005319A8 /* GenericPasswordStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD375A2E143D77005319A8 /* GenericPasswordStore.swift */; };
A9FD375D2E143D7E005319A8 /* KeyStoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD375C2E143D7E005319A8 /* KeyStoreError.swift */; };
A9FD375F2E14648E005319A8 /* KeyImporterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD375E2E14648E005319A8 /* KeyImporterView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -170,6 +171,7 @@
A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordConvertible.swift; sourceTree = "<group>"; };
A9FD375A2E143D77005319A8 /* GenericPasswordStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordStore.swift; sourceTree = "<group>"; };
A9FD375C2E143D7E005319A8 /* KeyStoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyStoreError.swift; sourceTree = "<group>"; };
A9FD375E2E14648E005319A8 /* KeyImporterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyImporterView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -366,6 +368,7 @@
A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */,
A98554542E05535F009051BD /* KeyManagerView.swift */,
A9D819302E102D8700442D38 /* HostkeysView.swift */,
A9FD375E2E14648E005319A8 /* KeyImporterView.swift */,
);
path = Keys;
sourceTree = "<group>";
@@ -602,6 +605,7 @@
A96BE6A62E113DB000C0FEE9 /* ColorCodable.swift in Sources */,
A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */,
A96BE6A42E113D9400C0FEE9 /* ThemeCodable.swift in Sources */,
A9FD375F2E14648E005319A8 /* KeyImporterView.swift in Sources */,
A93143C02DF61B3200FCD5DB /* Host.swift in Sources */,
A9B15A9A2E0ABA0400F66E02 /* DialogView.swift in Sources */,
A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */,

View File

@@ -39,6 +39,18 @@ struct Host: HostPr {
var passphrase: String
var key: String?
var description: String {
if name.isEmpty && address.isEmpty {
return id.uuidString
} else if name.isEmpty {
return address
} else if address.isEmpty {
return name
} else {
return name
}
}
init(
name: String = "",
symbol: HostSymbol = .genericServer,

View File

@@ -133,19 +133,6 @@ class HostsManager: ObservableObject, @unchecked Sendable {
}
}
func makeLabel(forHost: Host?) -> String {
guard let forHost else { return "" }
if forHost.name.isEmpty && forHost.address.isEmpty {
return forHost.id.uuidString
} else if forHost.name.isEmpty {
return forHost.address
} else if forHost.address.isEmpty {
return forHost.name
} else {
return forHost.name
}
}
func moveHost(from: IndexSet, to: Int) {
hosts.move(fromOffsets: from, toOffset: to)
saveHosts()

View File

@@ -22,13 +22,14 @@ class KeyManager: ObservableObject {
private let userdefaults = NSUbiquitousKeyValueStore.default
@Published var keypairs: [Keypair] = []
var keyIDs: [UUID: KeyType] = [:]
var keyTypes: [UUID: KeyType] = [:]
var keyNames: [UUID: String] = [:]
private let baseTag = "com.neon443.ShhShell.keys".data(using: .utf8)!
init() {
loadKeyIDs()
for id in keyIDs.keys {
for id in keyTypes.keys {
guard let keypair = getFromKeychain(keyID: id) else { continue }
keypairs.append(keypair)
}
@@ -39,7 +40,7 @@ class KeyManager: ObservableObject {
let decoder = JSONDecoder()
guard let data = userdefaults.data(forKey: "keyIDs") else { return }
guard let decoded = try? decoder.decode([UUID:KeyType].self, from: data) else { return }
keyIDs = decoded
keyTypes = decoded
guard let dataNames = userdefaults.data(forKey: "keyNames") else { return }
guard let decodedNames = try? decoder.decode([UUID:String].self, from: dataNames) else { return }
@@ -48,7 +49,7 @@ class KeyManager: ObservableObject {
func saveKeyIDs() {
let encoder = JSONEncoder()
guard let encoded = try? encoder.encode(keyIDs) else { return }
guard let encoded = try? encoder.encode(keyTypes) else { return }
userdefaults.set(encoded, forKey: "keyIDs")
guard let encodedNames = try? encoder.encode(keyNames) else { return }
@@ -58,7 +59,7 @@ class KeyManager: ObservableObject {
func saveToKeychain(_ keypair: Keypair) {
withAnimation {
keyIDs.updateValue(keypair.type, forKey: keypair.id)
keyTypes.updateValue(keypair.type, forKey: keypair.id)
keyNames.updateValue(keypair.name, forKey: keypair.id)
}
saveKeyIDs()
@@ -77,7 +78,7 @@ class KeyManager: ObservableObject {
}
func getFromKeychain(keyID: UUID) -> Keypair? {
guard let keyType = keyIDs[keyID] else { return nil }
guard let keyType = keyTypes[keyID] else { return nil }
guard let keyName = keyNames[keyID] else { return nil }
if keyType == .ed25519 {
var key: Curve25519.Signing.PrivateKey?
@@ -101,6 +102,12 @@ class KeyManager: ObservableObject {
}
}
func importKey(type: KeyType, priv: String, name: String) {
if type == .ed25519 {
saveToKeychain(KeyManager.importSSHPrivkey(priv: priv))
} else { fatalError() }
}
//MARK: generate keys
func generateKey(type: KeyType, comment: String) {
switch type {

View File

@@ -25,7 +25,11 @@ struct Keypair: KeypairProtocol {
var type: KeyType = .ed25519
var name: String = ""
var publicKey: Data {
(try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKey).publicKey.rawRepresentation) ?? Data()
if privateKey.isEmpty {
return Data()
} else {
return (try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKey).publicKey.rawRepresentation) ?? Data()
}
}
var privateKey: Data
var passphrase: String = ""
@@ -39,7 +43,12 @@ struct Keypair: KeypairProtocol {
}
var openSshPubkey: String {
String(data: KeyManager.makeSSHPubkey(self), encoding: .utf8) ?? "OpenSSH key format error"
if privateKey.isEmpty {
return ""
} else {
return String(data: KeyManager.makeSSHPubkey(self), encoding: .utf8) ?? "OpenSSH key format error"
}
}
var openSshPrivkey: String {

View File

@@ -79,6 +79,13 @@ struct ConnectionView: View {
TextBox(label: "Password", text: $handler.host.password, prompt: "not required if using publickeys", secure: true)
Picker("Private key", selection: $handler.host.privateKeyID) {
ForEach(keyManager.keypairs) { keypair in
Text(keypair.label)
.tag(keypair.id)
}
}
TextBox(label: "Publickey", text: $pubkeyStr, prompt: "in openssh format")
.onChange(of: pubkeyStr) { _ in
let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "")

View File

@@ -28,7 +28,7 @@ struct HostsView: View {
} label: {
SymbolPreview(symbol: host.symbol, label: host.label)
.frame(width: 40, height: 40)
Text(hostsManager.makeLabel(forHost: host))
Text(host.description)
}
.id(host)
.animation(.default, value: host)

View File

@@ -24,7 +24,7 @@ struct KeyDetailView: View {
HStack {
SymbolPreview(symbol: host.symbol, label: host.label)
.frame(width: 40, height: 40)
Text(hostsManager.makeLabel(forHost: host))
Text(host.description)
}
}
}
@@ -60,7 +60,7 @@ struct KeyDetailView: View {
}
Button {
UIPasteboard.general.string = String(data: KeyManager.makeSSHPubkey(keypair), encoding: .utf8) ?? ""
UIPasteboard.general.string = keypair.openSshPubkey
} label: {
CenteredLabel(title: "Copy private key", systemName: "document.on.document")
}

View File

@@ -0,0 +1,49 @@
//
// KeyImporterView.swift
// ShhShell
//
// Created by neon443 on 01/07/2025.
//
import SwiftUI
struct KeyImporterView: View {
@ObservedObject var keyManager: KeyManager
@Environment(\.dismiss) var dismiss
@State var keyName: String = UIDevice().model + " " + Date().formatted()
@State var privkeyStr: String = ""
@State var keyType: KeyType = .ed25519
var keypair: Keypair {
Keypair(type: keyType, name: keyName, privateKey: privkeyStr.data(using: .utf8) ?? Data())
}
var body: some View {
List {
TextBox(label: "Name", text: $keyName, prompt: "A name for your key")
HStack {
Text("Private Key")
Spacer()
Text("Required")
.foregroundStyle(.red)
}
TextEditor(text: $privkeyStr)
TextEditor(text: .constant(keypair.openSshPubkey))
Button() {
keyManager.importKey(type: keyType, priv: privkeyStr, name: keyName)
dismiss()
} label: {
Label("Import", systemImage: "key.horizontal")
}
.buttonStyle(.borderedProminent)
}
}
}
#Preview {
KeyImporterView(keyManager: KeyManager())
}

View File

@@ -11,6 +11,8 @@ struct KeyManagerView: View {
@ObservedObject var hostsManager: HostsManager
@ObservedObject var keyManager: KeyManager
@State var showImporter: Bool = false
var body: some View {
ZStack {
hostsManager.selectedTheme.background.suiColor.opacity(0.7)
@@ -47,8 +49,14 @@ struct KeyManagerView: View {
}
}
Button("ed25519") {
Button("Generate a new Ed25519 Key") {
let comment = UIDevice().model + " " + Date().formatted()
keyManager.generateKey(type: .ed25519, comment: comment)
}
Button("Import Key") { showImporter.toggle() }
.sheet(isPresented: $showImporter) {
KeyImporterView(keyManager: keyManager)
}
}
.scrollContentBackground(.hidden)

View File

@@ -29,7 +29,7 @@ struct SessionView: View {
.foregroundStyle(.terminalGreen)
SymbolPreview(symbol: host.symbol, label: host.label)
.frame(width: 40, height: 40)
Text(hostsManager.makeLabel(forHost: host))
Text(host.description)
}
}
.fullScreenCover(isPresented: $shellPresented) {