diff --git a/ShhShell.xcodeproj/project.pbxproj b/ShhShell.xcodeproj/project.pbxproj index c6da5bb..0c0e213 100644 --- a/ShhShell.xcodeproj/project.pbxproj +++ b/ShhShell.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ A95FAA572DF4B62A00DE2F5A /* openssl.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A95FAA512DF4B62100DE2F5A /* openssl.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A96C6A8A2E0C0B1100F377FE /* SSHState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96C6A892E0C0B1100F377FE /* SSHState.swift */; }; A96C6AFE2E0C43B600F377FE /* Keypair.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96C6AFD2E0C43B600F377FE /* Keypair.swift */; }; + A96C6B002E0C45FE00F377FE /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */; }; + A96C6B022E0C49E800F377FE /* CenteredLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96C6B012E0C49E800F377FE /* CenteredLabel.swift */; }; A98554552E05535F009051BD /* KeyManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554542E05535F009051BD /* KeyManagerView.swift */; }; A98554592E0553AA009051BD /* KeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554582E0553AA009051BD /* KeyManager.swift */; }; A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985545C2E055D4D009051BD /* ConnectionView.swift */; }; @@ -97,6 +99,8 @@ A95FAA5C2DF4B7A300DE2F5A /* ci_prost_xcodebuild.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = ci_prost_xcodebuild.sh; sourceTree = ""; }; A96C6A892E0C0B1100F377FE /* SSHState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHState.swift; sourceTree = ""; }; A96C6AFD2E0C43B600F377FE /* Keypair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keypair.swift; sourceTree = ""; }; + A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailView.swift; sourceTree = ""; }; + A96C6B012E0C49E800F377FE /* CenteredLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenteredLabel.swift; sourceTree = ""; }; A98554542E05535F009051BD /* KeyManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagerView.swift; sourceTree = ""; }; A98554582E0553AA009051BD /* KeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManager.swift; sourceTree = ""; }; A985545C2E055D4D009051BD /* ConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionView.swift; sourceTree = ""; }; @@ -267,6 +271,8 @@ children = ( A98554582E0553AA009051BD /* KeyManager.swift */, A985545E2E056EDD009051BD /* KeychainLayer.swift */, + A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */, + A96C6B012E0C49E800F377FE /* CenteredLabel.swift */, ); path = Keys; sourceTree = ""; @@ -424,6 +430,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A96C6B022E0C49E800F377FE /* CenteredLabel.swift in Sources */, A923172F2E08851200ECE1E6 /* ShellView.swift in Sources */, A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */, A93143C62DF61FE300FCD5DB /* ViewModifiers.swift in Sources */, @@ -433,6 +440,7 @@ A93143C02DF61B3200FCD5DB /* Host.swift in Sources */, A9B15A9A2E0ABA0400F66E02 /* DialogView.swift in Sources */, A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */, + A96C6B002E0C45FE00F377FE /* KeyDetailView.swift in Sources */, A98554612E058433009051BD /* HostsManager.swift in Sources */, A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */, A98554592E0553AA009051BD /* KeyManager.swift in Sources */, @@ -613,6 +621,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; + INFOPLIST_KEY_NSFaceIDUsageDescription = "ShhShell uses Face ID to verify your identity"; INFOPLIST_KEY_NSLocalNetworkUsageDescription = _; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -649,6 +658,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; + INFOPLIST_KEY_NSFaceIDUsageDescription = "ShhShell uses Face ID to verify your identity"; INFOPLIST_KEY_NSLocalNetworkUsageDescription = _; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/ShhShell/Host/HostsManager.swift b/ShhShell/Host/HostsManager.swift index 675dca5..c6d5c07 100644 --- a/ShhShell/Host/HostsManager.swift +++ b/ShhShell/Host/HostsManager.swift @@ -6,8 +6,9 @@ // import Foundation +import LocalAuthentication -class HostsManager: ObservableObject { +class HostsManager: ObservableObject, @unchecked Sendable { private let userDefaults = NSUbiquitousKeyValueStore.default @Published var savedHosts: [Host] = [] @@ -61,7 +62,7 @@ class HostsManager: ObservableObject { func getKeys() -> [Keypair] { var result: [Keypair] = [] for host in savedHosts { - if !result.contains(where: { $0 == Keypair(publicKey: host.publicKey, privateKey: host.privateKey)}) { + if result.contains(where: { $0 == Keypair(publicKey: host.publicKey, privateKey: host.privateKey)}) { } else { result.append(Keypair(publicKey: host.publicKey, privateKey: host.privateKey)) @@ -69,4 +70,19 @@ class HostsManager: ObservableObject { } 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 with Face ID to view private keys" + return await withCheckedContinuation { continuation in + context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in + continuation.resume(returning: success) + } + } + } } diff --git a/ShhShell/Keys/CenteredLabel.swift b/ShhShell/Keys/CenteredLabel.swift new file mode 100644 index 0000000..0ee9cbd --- /dev/null +++ b/ShhShell/Keys/CenteredLabel.swift @@ -0,0 +1,25 @@ +// +// CenteredLabel.swift +// ShhShell +// +// Created by neon443 on 25/06/2025. +// + +import SwiftUI + +struct CenteredLabel: View { + @State var title: String + @State var systemName: String + var body: some View { + HStack { + Spacer() + Image(systemName: systemName) + Text(title) + Spacer() + } + } +} + +#Preview { + CenteredLabel(title: "Button Text Labek", systemName: "pencil.tip.crop.circle") +} diff --git a/ShhShell/Keys/KeyDetailView.swift b/ShhShell/Keys/KeyDetailView.swift new file mode 100644 index 0000000..f6b548b --- /dev/null +++ b/ShhShell/Keys/KeyDetailView.swift @@ -0,0 +1,77 @@ +// +// KeyDetailView.swift +// ShhShell +// +// Created by neon443 on 25/06/2025. +// + +import SwiftUI + +struct KeyDetailView: View { + @ObservedObject var hostsManager: HostsManager + @State var keypair: Keypair + @State private var reveal: Bool = false + + var body: some View { + List { + VStack(alignment: .leading) { + Text("Public key") + .bold() + Text(String(data: keypair.publicKey!, encoding: .utf8) ?? "nil") + } + VStack(alignment: .leading) { + Text("Private key") + .bold() + ZStack { + Text(String(data: keypair.privateKey!, encoding: .utf8) ?? "nil") + .blur(radius: reveal ? 0 : 5) + VStack { + Image(systemName: "eye.slash.fill") + .resizable().scaledToFit() + .frame(width: 50) + Text("Tap to reveal") + } + .opacity(reveal ? 0 : 1) + } + .onTapGesture { + Task { + if !reveal { + guard await hostsManager.authWithBiometrics() else { return } + } + withAnimation(.spring) { reveal.toggle() } + } + } + } + + Button { + Task { + guard await hostsManager.authWithBiometrics() else { return } + if let privateKey = keypair.privateKey { + UIPasteboard.general.string = String(data: privateKey, encoding: .utf8) + } + } + } label: { + CenteredLabel(title: "Copy private key", systemName: "document.on.document") + } + .listRowSeparator(.hidden) + } + } +} + +#Preview { + KeyDetailView( + hostsManager: HostsManager(), + keypair: Keypair( + publicKey: "ssh-ed25519 dskjhfajkdhfjkdashfgjkhadsjkgfbhalkjhfjkhdask user@mac".data(using: .utf8), + privateKey: """ + -----BEGIN OPENSSH PRIVATE KEY----- + Lorem ipsum dolor sit amet, consectetur adipiscing elit + sed do eiusmod tempor incididunt ut labore et dolore magna aliqu + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat + -----END OPENSSH PRIVATE KEY----- + """ + .data(using: .utf8) + ) + ) +} diff --git a/ShhShell/Views/Keys/KeyManagerView.swift b/ShhShell/Views/Keys/KeyManagerView.swift index f3f95ac..a34d839 100644 --- a/ShhShell/Views/Keys/KeyManagerView.swift +++ b/ShhShell/Views/Keys/KeyManagerView.swift @@ -15,11 +15,11 @@ struct KeyManagerView: View { NavigationStack { List { Section { - ForEach(hostsManager.savedHosts) { host in + ForEach(hostsManager.getKeys()) { keypair in NavigationLink { - + KeyDetailView(hostsManager: hostsManager, keypair: keypair) } label: { - + Text(String(data: keypair.publicKey!, encoding: .utf8) ?? "nil") } } } @@ -27,9 +27,11 @@ struct KeyManagerView: View { NavigationLink { List { ForEach(hostsManager.savedHosts) { host in - Text(host.address) - .bold() - Text(host.key ?? "nil") + VStack(alignment: .leading) { + Text(host.address) + .bold() + Text(host.key ?? "nil") + } } } } label: { diff --git a/ShhShell/Views/Keys/Keypair.swift b/ShhShell/Views/Keys/Keypair.swift index 17f8e72..a9b1c36 100644 --- a/ShhShell/Views/Keys/Keypair.swift +++ b/ShhShell/Views/Keys/Keypair.swift @@ -7,12 +7,13 @@ import Foundation -protocol KeypairProtocol: Equatable, Codable, Hashable { +protocol KeypairProtocol: Identifiable, Equatable, Codable, Hashable { var publicKey: Data? { get set } var privateKey: Data? { get set } } struct Keypair: KeypairProtocol { + var id = UUID() var publicKey: Data? var privateKey: Data?