From 2548dde85b1fec0bd70970fb3b0d7bf662668de9 Mon Sep 17 00:00:00 2001 From: neon443 <69979447+neon443@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:46:41 +0100 Subject: [PATCH] added keydetail view added faceid authentication to view private keys added authwithbiometrics for secure displaying of sensitive data fix getkeys returns nothing added centredlabel (hstack with two spacers) made keypair identifieable fix alignment of text --- ShhShell.xcodeproj/project.pbxproj | 10 +++ ShhShell/Host/HostsManager.swift | 20 +++++- ShhShell/Keys/CenteredLabel.swift | 25 ++++++++ ShhShell/Keys/KeyDetailView.swift | 77 ++++++++++++++++++++++++ ShhShell/Views/Keys/KeyManagerView.swift | 14 +++-- ShhShell/Views/Keys/Keypair.swift | 3 +- 6 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 ShhShell/Keys/CenteredLabel.swift create mode 100644 ShhShell/Keys/KeyDetailView.swift 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?