diff --git a/ShhShell.xcodeproj/project.pbxproj b/ShhShell.xcodeproj/project.pbxproj index 693bd66..5db79c5 100644 --- a/ShhShell.xcodeproj/project.pbxproj +++ b/ShhShell.xcodeproj/project.pbxproj @@ -63,7 +63,7 @@ A9D8192F2E0F1BEE00442D38 /* ThemePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8192E2E0F1BEE00442D38 /* ThemePreview.swift */; }; A9D819312E102D8700442D38 /* HostkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D819302E102D8700442D38 /* HostkeysView.swift */; }; A9DA97712E0D30ED00142DDC /* HostSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97702E0D30ED00142DDC /* HostSymbol.swift */; }; - A9DA97732E0D40C100142DDC /* SymbolPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97722E0D40C100142DDC /* SymbolPreview.swift */; }; + A9DA97732E0D40C100142DDC /* HostSymbolPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97722E0D40C100142DDC /* HostSymbolPreview.swift */; }; A9FD37552E143D23005319A8 /* SecKeyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37542E143D23005319A8 /* SecKeyConvertible.swift */; }; A9FD37572E143D5A005319A8 /* SecKeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37562E143D5A005319A8 /* SecKeyStore.swift */; }; A9FD37592E143D74005319A8 /* GenericPasswordConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */; }; @@ -165,7 +165,7 @@ A9D8192E2E0F1BEE00442D38 /* ThemePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreview.swift; sourceTree = ""; }; A9D819302E102D8700442D38 /* HostkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostkeysView.swift; sourceTree = ""; }; A9DA97702E0D30ED00142DDC /* HostSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostSymbol.swift; sourceTree = ""; }; - A9DA97722E0D40C100142DDC /* SymbolPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolPreview.swift; sourceTree = ""; }; + A9DA97722E0D40C100142DDC /* HostSymbolPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostSymbolPreview.swift; sourceTree = ""; }; A9FD37542E143D23005319A8 /* SecKeyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecKeyConvertible.swift; sourceTree = ""; }; A9FD37562E143D5A005319A8 /* SecKeyStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecKeyStore.swift; sourceTree = ""; }; A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordConvertible.swift; sourceTree = ""; }; @@ -349,6 +349,7 @@ A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */, A9B15A992E0ABA0400F66E02 /* DialogView.swift */, A96C90A02E12B87900724253 /* TextBox.swift */, + A9DA97722E0D40C100142DDC /* HostSymbolPreview.swift */, ); path = Misc; sourceTree = ""; @@ -379,7 +380,6 @@ A93143BF2DF61B3200FCD5DB /* Host.swift */, A98554602E058433009051BD /* HostsManager.swift */, A9DA97702E0D30ED00142DDC /* HostSymbol.swift */, - A9DA97722E0D40C100142DDC /* SymbolPreview.swift */, ); path = Host; sourceTree = ""; @@ -601,7 +601,7 @@ A93143C62DF61FE300FCD5DB /* ViewModifiers.swift in Sources */, A98554632E0587DF009051BD /* HostsView.swift in Sources */, A96C6A8A2E0C0B1100F377FE /* SSHState.swift in Sources */, - A9DA97732E0D40C100142DDC /* SymbolPreview.swift in Sources */, + A9DA97732E0D40C100142DDC /* HostSymbolPreview.swift in Sources */, A96BE6A62E113DB000C0FEE9 /* ColorCodable.swift in Sources */, A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */, A96BE6A42E113D9400C0FEE9 /* ThemeCodable.swift in Sources */, diff --git a/ShhShell/Host/HostsManager.swift b/ShhShell/Host/HostsManager.swift index 1b3acb9..b1fc339 100644 --- a/ShhShell/Host/HostsManager.swift +++ b/ShhShell/Host/HostsManager.swift @@ -169,12 +169,10 @@ class HostsManager: ObservableObject, @unchecked Sendable { } } - func getKeys() -> [Keypair] { - var result: [Keypair] = [] - for host in hosts { - guard let keyID = host.privateKeyID else { continue } - } - return result + func set(keypair: Keypair, onHost: Host) { + guard let index = hosts.firstIndex(where: { $0.id == onHost.id }) else { return } + hosts[index].privateKeyID = keypair.id + saveHosts() } func getHostsUsingKeys(_ keys: [Keypair]) -> [Host] { diff --git a/ShhShell/Keys/KeyManager.swift b/ShhShell/Keys/KeyManager.swift index 4e8e634..a86cef6 100644 --- a/ShhShell/Keys/KeyManager.swift +++ b/ShhShell/Keys/KeyManager.swift @@ -140,7 +140,8 @@ class KeyManager: ObservableObject { func importKey(type: KeyType, priv: String, name: String) { if type == .ed25519 { - saveToKeychain(KeyManager.importSSHPrivkey(priv: priv)) + guard let importedKeypair = KeyManager.importSSHPrivkey(priv: priv) else { return } + saveToKeychain(importedKeypair) saveKeypairs() } else { fatalError() } } @@ -156,8 +157,6 @@ class KeyManager: ObservableObject { ) saveToKeychain(keypair) saveKeypairs() - case .rsa: - fatalError("unimplemented") } loadKeypairs() } @@ -187,7 +186,8 @@ class KeyManager: ObservableObject { return Data(pubkeyline.utf8) } - static func importSSHPrivkey(priv: String) -> Keypair { + static func importSSHPrivkey(priv: String) -> Keypair? { + guard !priv.isEmpty else { return nil } var split = priv.replacingOccurrences(of: "-----BEGIN OPENSSH PRIVATE KEY-----\n", with: "") split = split.replacingOccurrences(of: "-----BEGIN OPENSSH PRIVATE KEY-----", with: "") split = split.replacingOccurrences(of: "\n-----END OPENSSH PRIVATE KEY-----\n", with: "") diff --git a/ShhShell/Keys/KeyType.swift b/ShhShell/Keys/KeyType.swift index 5ac7421..bd36615 100644 --- a/ShhShell/Keys/KeyType.swift +++ b/ShhShell/Keys/KeyType.swift @@ -9,14 +9,11 @@ import Foundation enum KeyType: Codable, Equatable, Hashable, CustomStringConvertible, CaseIterable { case ed25519 - case rsa var description: String { switch self { case .ed25519: return "Ed25519" - case .rsa: - return "RSA" } } } diff --git a/ShhShell/Views/Hosts/ConnectionView.swift b/ShhShell/Views/Hosts/ConnectionView.swift index c018565..b0be1e6 100644 --- a/ShhShell/Views/Hosts/ConnectionView.swift +++ b/ShhShell/Views/Hosts/ConnectionView.swift @@ -35,7 +35,7 @@ struct ConnectionView: View { RoundedRectangle(cornerRadius: 10) .fill(.gray.opacity(0.5)) } - SymbolPreview(symbol: symbol, label: handler.host.label) + HostSymbolPreview(symbol: symbol, label: handler.host.label) .padding(5) } .frame(width: 60, height: 60) @@ -47,7 +47,7 @@ struct ConnectionView: View { } HStack { - SymbolPreview(symbol: handler.host.symbol, label: handler.host.label) + HostSymbolPreview(symbol: handler.host.symbol, label: handler.host.label) .id(handler.host) .frame(width: 60, height: 60) diff --git a/ShhShell/Views/Hosts/HostsView.swift b/ShhShell/Views/Hosts/HostsView.swift index c639ad0..61eaca6 100644 --- a/ShhShell/Views/Hosts/HostsView.swift +++ b/ShhShell/Views/Hosts/HostsView.swift @@ -26,7 +26,7 @@ struct HostsView: View { keyManager: keyManager ) } label: { - SymbolPreview(symbol: host.symbol, label: host.label) + HostSymbolPreview(symbol: host.symbol, label: host.label) .frame(width: 40, height: 40) Text(host.description) } diff --git a/ShhShell/Views/Keys/KeyDetailView.swift b/ShhShell/Views/Keys/KeyDetailView.swift index cc64600..3be167f 100644 --- a/ShhShell/Views/Keys/KeyDetailView.swift +++ b/ShhShell/Views/Keys/KeyDetailView.swift @@ -22,79 +22,102 @@ struct KeyDetailView: View { hostsManager.selectedTheme.background.suiColor.opacity(0.7) .ignoresSafeArea(.all) List { - TextBox(label: "Name", text: $keyname, prompt: "A name for your key") - .onAppear { - keyname = keypair.name - } - .onChange(of: keyname) { _ in - keyManager.renameKey(keypair: keypair, newName: keyname) - } - VStack(alignment: .leading) { Text("Used on") .bold() ForEach(hostsManager.getHostsUsingKeys([keypair])) { host in HStack { - SymbolPreview(symbol: host.symbol, label: host.label) + HostSymbolPreview(symbol: host.symbol, label: host.label) .frame(width: 40, height: 40) Text(host.description) } } - } - VStack(alignment: .leading) { - Text("Public key") - .bold() - Text(keypair.openSshPubkey) - } - VStack(alignment: .leading) { - Text("Private key") - .bold() - .frame(maxWidth: .infinity) - ZStack(alignment: .center) { - Text(keypair.openSshPrivkey) - .blur(radius: reveal ? 0 : 5) - VStack { - Image(systemName: "eye.slash.fill") - .resizable().scaledToFit() - .frame(width: 50) - Text("Tap to reveal") - } - .opacity(reveal ? 0 : 1) - } - .frame(maxWidth: .infinity) - .onTapGesture { - Task { - if !reveal { - guard await authWithBiometrics() else { return } + Menu("Add") { + let hostsNotUsingKey = hostsManager.hosts.filter( + { + hostsManager.getHostsUsingKeys([keypair]).contains($0) + }) + ForEach(hostsNotUsingKey) { host in + Button() { + hostsManager.set(keypair: keypair, onHost: host) + } label: { + Image(systemName: "plus") + .resizable().scaledToFit() + .foregroundStyle(.blue) + .frame(width: 30, height: 30) + Text("Add") + .foregroundStyle(.blue) } - withAnimation(.spring) { reveal.toggle() } } } } - Button { - UIPasteboard.general.string = keypair.openSshPubkey - } label: { - CenteredLabel(title: "Copy public key", systemName: "document.on.document") - } - .listRowSeparator(.hidden) - - Button { - Task { - guard await authWithBiometrics() else { return } - UIPasteboard.general.string = String(data: KeyManager.makeSSHPrivkey(keypair), encoding: .utf8) ?? "" + Section() { + TextBox(label: "Name", text: $keyname, prompt: "A name for your key") + .onAppear { + keyname = keypair.name + } + .onChange(of: keyname) { _ in + keyManager.renameKey(keypair: keypair, newName: keyname) + } + + Button { + UIPasteboard.general.string = keypair.openSshPubkey + } label: { + CenteredLabel(title: "Copy public key", systemName: "document.on.document") } - } label: { - CenteredLabel(title: "Copy private key", systemName: "document.on.document") - } - .listRowSeparator(.hidden) - - CenteredLabel(title: "Delete", systemName: "trash") - .foregroundStyle(.red) - .onTapGesture { - keyManager.deleteKey(keypair) - dismiss() + .listRowSeparator(.hidden) + + Button { + Task { + guard await authWithBiometrics() else { return } + UIPasteboard.general.string = String(data: KeyManager.makeSSHPrivkey(keypair), encoding: .utf8) ?? "" + } + } label: { + CenteredLabel(title: "Copy private key", systemName: "document.on.document") } + .listRowSeparator(.hidden) + + CenteredLabel(title: "Delete", systemName: "trash") + .foregroundStyle(.red) + .onTapGesture { + keyManager.deleteKey(keypair) + dismiss() + } + } + + Section("Key") { + VStack(alignment: .leading) { + Text("Public key") + .bold() + Text(keypair.openSshPubkey.dropLast(2)) + } + VStack(alignment: .leading) { + Text("Private key") + .bold() + .frame(maxWidth: .infinity) + ZStack(alignment: .center) { + Text(keypair.openSshPrivkey.dropLast(2)) + .blur(radius: reveal ? 0 : 5) + VStack { + Image(systemName: "eye.slash.fill") + .resizable().scaledToFit() + .frame(width: 50) + Text("Tap to reveal") + } + .opacity(reveal ? 0 : 1) + } + .frame(maxWidth: .infinity) + .onTapGesture { + Task { + if !reveal { + guard await authWithBiometrics() else { return } + } + withAnimation(.spring) { reveal.toggle() } + } + } + } + } } .scrollContentBackground(.hidden) } diff --git a/ShhShell/Views/Keys/KeyImporterView.swift b/ShhShell/Views/Keys/KeyImporterView.swift index 7507d0b..523dfde 100644 --- a/ShhShell/Views/Keys/KeyImporterView.swift +++ b/ShhShell/Views/Keys/KeyImporterView.swift @@ -46,14 +46,18 @@ struct KeyImporterView: View { .foregroundStyle(.gray) } - Button() { - keyManager.importKey(type: keyType, priv: privkeyStr, name: keyName) - dismiss() - } label: { - Label("Import", systemImage: "key.horizontal") - } - .buttonStyle(.borderedProminent) } + + Button() { + keyManager.importKey(type: keyType, priv: privkeyStr, name: keyName) + dismiss() + } label: { + Text("Import") + } + .onTapGesture { + UINotificationFeedbackGenerator().notificationOccurred(.success) + } + .buttonStyle(.borderedProminent) } } diff --git a/ShhShell/Host/SymbolPreview.swift b/ShhShell/Views/Misc/HostSymbolPreview.swift similarity index 79% rename from ShhShell/Host/SymbolPreview.swift rename to ShhShell/Views/Misc/HostSymbolPreview.swift index 72209f7..dfe9d0a 100644 --- a/ShhShell/Host/SymbolPreview.swift +++ b/ShhShell/Views/Misc/HostSymbolPreview.swift @@ -1,5 +1,5 @@ // -// SymbolPreview.swift +// HostSymbolPreview.swift // ShhShell // // Created by neon443 on 26/06/2025. @@ -7,7 +7,7 @@ import SwiftUI -struct SymbolPreview: View { +struct HostSymbolPreview: View { @State var symbol: HostSymbol @State var label: String @@ -30,5 +30,5 @@ struct SymbolPreview: View { } #Preview { - SymbolPreview(symbol: HostSymbol.desktopcomputer, label: "lo0") + HostSymbolPreview(symbol: HostSymbol.desktopcomputer, label: "lo0") } diff --git a/ShhShell/Views/Sessions/SessionView.swift b/ShhShell/Views/Sessions/SessionView.swift index 69fb742..7cb0f15 100644 --- a/ShhShell/Views/Sessions/SessionView.swift +++ b/ShhShell/Views/Sessions/SessionView.swift @@ -28,7 +28,7 @@ struct SessionView: View { .resizable().scaledToFit() .frame(width: 40, height: 40) .foregroundStyle(.terminalGreen) - SymbolPreview(symbol: host.symbol, label: host.label) + HostSymbolPreview(symbol: host.symbol, label: host.label) .frame(width: 40, height: 40) Text(host.description) }