diff --git a/ShhShell.xcodeproj/project.pbxproj b/ShhShell.xcodeproj/project.pbxproj index 5d9bad7..693bd66 100644 --- a/ShhShell.xcodeproj/project.pbxproj +++ b/ShhShell.xcodeproj/project.pbxproj @@ -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 = ""; }; A9FD375A2E143D77005319A8 /* GenericPasswordStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordStore.swift; sourceTree = ""; }; A9FD375C2E143D7E005319A8 /* KeyStoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyStoreError.swift; sourceTree = ""; }; + A9FD375E2E14648E005319A8 /* KeyImporterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyImporterView.swift; sourceTree = ""; }; /* 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 = ""; @@ -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 */, diff --git a/ShhShell/Host/Host.swift b/ShhShell/Host/Host.swift index 6e55075..e363606 100644 --- a/ShhShell/Host/Host.swift +++ b/ShhShell/Host/Host.swift @@ -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, diff --git a/ShhShell/Host/HostsManager.swift b/ShhShell/Host/HostsManager.swift index 3c7e644..c7b4e84 100644 --- a/ShhShell/Host/HostsManager.swift +++ b/ShhShell/Host/HostsManager.swift @@ -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() diff --git a/ShhShell/Keys/KeyManager.swift b/ShhShell/Keys/KeyManager.swift index 9f4da40..ac09034 100644 --- a/ShhShell/Keys/KeyManager.swift +++ b/ShhShell/Keys/KeyManager.swift @@ -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 { diff --git a/ShhShell/Keys/Keypair.swift b/ShhShell/Keys/Keypair.swift index 4da3ae4..74a2102 100644 --- a/ShhShell/Keys/Keypair.swift +++ b/ShhShell/Keys/Keypair.swift @@ -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 { diff --git a/ShhShell/Views/Hosts/ConnectionView.swift b/ShhShell/Views/Hosts/ConnectionView.swift index 23f3da9..7190d28 100644 --- a/ShhShell/Views/Hosts/ConnectionView.swift +++ b/ShhShell/Views/Hosts/ConnectionView.swift @@ -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: "") diff --git a/ShhShell/Views/Hosts/HostsView.swift b/ShhShell/Views/Hosts/HostsView.swift index 28e24e0..0237481 100644 --- a/ShhShell/Views/Hosts/HostsView.swift +++ b/ShhShell/Views/Hosts/HostsView.swift @@ -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) diff --git a/ShhShell/Views/Keys/KeyDetailView.swift b/ShhShell/Views/Keys/KeyDetailView.swift index dbce857..fb00a02 100644 --- a/ShhShell/Views/Keys/KeyDetailView.swift +++ b/ShhShell/Views/Keys/KeyDetailView.swift @@ -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") } diff --git a/ShhShell/Views/Keys/KeyImporterView.swift b/ShhShell/Views/Keys/KeyImporterView.swift new file mode 100644 index 0000000..672309c --- /dev/null +++ b/ShhShell/Views/Keys/KeyImporterView.swift @@ -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()) +} diff --git a/ShhShell/Views/Keys/KeyManagerView.swift b/ShhShell/Views/Keys/KeyManagerView.swift index a86ee3b..5ce6e44 100644 --- a/ShhShell/Views/Keys/KeyManagerView.swift +++ b/ShhShell/Views/Keys/KeyManagerView.swift @@ -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,9 +49,15 @@ 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) .navigationTitle("Keys") diff --git a/ShhShell/Views/Sessions/SessionView.swift b/ShhShell/Views/Sessions/SessionView.swift index 1afe39c..7e83cdc 100644 --- a/ShhShell/Views/Sessions/SessionView.swift +++ b/ShhShell/Views/Sessions/SessionView.swift @@ -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) {