diff --git a/Sources/Packages/Resources/Localizable.xcstrings b/Sources/Packages/Resources/Localizable.xcstrings index 7769e67..fc6fea2 100644 --- a/Sources/Packages/Resources/Localizable.xcstrings +++ b/Sources/Packages/Resources/Localizable.xcstrings @@ -5372,6 +5372,17 @@ } } }, + "auth_context_request_multiple" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "sign multiple requests from “%1$(appName)@” using secret “%2$(secretName)@”" + } + } + } + }, "auth_context_request_signature_description" : { "comment" : "When the user performs a signature action using a secret, they are shown a prompt to approve the action. This is the description, showing which secret will be used, and where the request is coming from. The first placeholder is the name of the app requesting the operation. The second placeholder is the name of the secret.", "extractionState" : "manual", @@ -18843,6 +18854,9 @@ } } } + }, + "Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually." : { + }, "no_secure_storage_description" : { "extractionState" : "manual", diff --git a/Sources/Secretive/Views/Views/MultilineInfoView.swift b/Sources/Packages/Sources/Common/MultilineInfoView.swift similarity index 68% rename from Sources/Secretive/Views/Views/MultilineInfoView.swift rename to Sources/Packages/Sources/Common/MultilineInfoView.swift index 5bca064..cae84e8 100644 --- a/Sources/Secretive/Views/Views/MultilineInfoView.swift +++ b/Sources/Packages/Sources/Common/MultilineInfoView.swift @@ -1,57 +1,71 @@ import SwiftUI import UniformTypeIdentifiers -struct MultilineInfoView: View { +public struct MultilineInfoView: View { - struct Item { - let text: String - let action: (Image, () -> Void)? +// public struct Item { +// public let text: String +// public let action: (Image, () -> Void)? +// +// public init(text: String, action: (Image, () -> Void)?) { +// self.text = text +// self.action = action +// } +// +// } + + var titleView: TitleView + var items: [ItemView] + + public init(titleView: () -> TitleView, items: () -> [ItemView]) { + self.titleView = titleView() + self.items = items() } - var title: LocalizedStringResource - var image: Image - var items: [Item] - - init(title: LocalizedStringResource, image: Image, items: [Item]) { - self.title = title - self.image = image - self.items = items - } - - init(title: LocalizedStringResource, image: Image, items: [String]) { - self.title = title - self.image = image - self.items = items.map({ Item(text: $0, action: nil) }) - } - - @State private var interactionState: InteractionState = .normal - @State private var interactionStateIndex: Int? - - var body: some View { - VStack(alignment: .leading, spacing: 0) { + public init(title: LocalizedStringResource, subtitle: LocalizedStringResource, image: Image, items: [String]) where TitleView == HStack> , ItemView == Text { + self.init { HStack { image .renderingMode(.template) - .imageScale(.large) +// .imageScale(.large) .foregroundColor(primaryTextColor) Text(title) .font(.headline) .foregroundColor(primaryTextColor) Spacer() } + } items: { + [Text("Hello")] + } + +// self.init { +// } items: { +// ForEach(items) { item in +// return HStack { +// Text(item) +// Spacer() +// // if let (image, _) = $0.1 { +// // image +// // .foregroundStyle(.secondary) +// // } +// } +// } +// } + + } + + @State private var interactionState: InteractionState = .normal + @State private var interactionStateIndex: Int? + + public var body: some View { + VStack(alignment: .leading, spacing: 0) { + titleView .safeAreaPadding(20) ForEach(Array(items.enumerated()), id: \.offset) { item in Divider() .ignoresSafeArea() .opacity(item.offset == 0 ? 1 : 0.75) - HStack { - Text(item.element.text) - Spacer() - if let (image, _) = item.element.action { - image - .foregroundStyle(.secondary) - } - } + items.element .safeAreaPadding(20) .onHover { hovering in withAnimation { @@ -155,16 +169,16 @@ fileprivate struct BackgroundViewModifier: ViewModifier { } -#Preview { - MultilineInfoView(title: "Multiple", image: Image(systemName: "figure.wave"), items: [ - MultilineInfoView.Item(text: "hello", action: (Image(systemName: "chevron.forward"), {})), - MultilineInfoView.Item(text: "World", action: (Image(systemName: "chevron.forward"), {})), - ]) - .padding() -} - - -#Preview { - MultilineInfoView(title: "One", image: Image(systemName: "figure.wave"), items: ["Hello world."]) - .padding() -} +//#Preview { +// MultilineInfoView(title: "Multiple", image: Image(systemName: "figure.wave"), items: [ +// MultilineInfoView.Item(text: "hello", action: (Image(systemName: "chevron.forward"), {})), +// MultilineInfoView.Item(text: "World", action: (Image(systemName: "chevron.forward"), {})), +// ]) +// .padding() +//} +// +// +//#Preview { +// MultilineInfoView(title: "One", image: Image(systemName: "figure.wave"), items: ["Hello world."]) +// .padding() +//} diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index ca4bc7f..edf3b01 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -133,20 +133,6 @@ extension Agent { } func signWithRequiredAuthentication(data: Data, store: AnySecretStore, secret: AnySecret, provenance: SigningRequestProvenance) async throws -> Data { -// let context: any AuthenticationContextProtocol -// let offerPersistence: Bool -// if let existing = await authenticationHandler.existingAuthenticationContextProtocol(for: SignatureRequest(secret: secret, provenance: provenance)) { -// context = existing -// offerPersistence = false -// logger.debug("Using existing auth context") -// } else { -// context = authenticationHandler.createAuthenticationContext(for: SignatureRequest(secret: secret, provenance: provenance)) -// offerPersistence = secret.authenticationRequirement.required -// logger.debug("Creating fresh auth context") -// } - - - let context = try await authenticationHandler.waitForAuthentication(for: SignatureRequest(secret: secret, provenance: provenance)) let result = try await store.sign(data: data, with: secret, for: provenance, context: context.laContext) let signedData = signatureWriter.data(secret: secret, signature: result) diff --git a/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift b/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift index 7c4d0c9..bfa152f 100644 --- a/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift +++ b/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift @@ -57,20 +57,28 @@ public final class AuthenticationContext: AuthenticationContextProtocol { } -public actor AuthenticationHandler { +@MainActor public protocol AuthenticationHandlerProtocol: Observable { + func setBatchAuthHandler(_ handler: @escaping () async throws -> Void) + func waitForAuthentication(for request: SignatureRequest) async throws -> any AuthenticationContextProtocol + var batchableRequests: [[SignatureRequest]] { get } + func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws + func requestAuthentication(for requests: Set) async throws +} + +@Observable @MainActor public class AuthenticationHandler: AuthenticationHandlerProtocol { private var persistedContexts: [AnySecret: AuthenticationContext] = [:] private var holdingRequests: Set = [] private var activeTask: Task? private var lastBatchAuthPresentation: Set? - private var presentBatchAuth: (([[SignatureRequest]], @escaping @Sendable (Set) async throws -> Void) async throws -> Void)? + private var presentBatchAuth: (() async throws -> Void)? private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent") public init() { } - public func setBatchAuthHandler(_ handler: @escaping (@Sendable ([[SignatureRequest]], @escaping @Sendable (Set) async throws -> Void) async throws -> Void)) { + public func setBatchAuthHandler(_ handler: @escaping () async throws -> Void) { self.presentBatchAuth = handler } @@ -92,7 +100,7 @@ public actor AuthenticationHandler { activeTask?.cancel() lastBatchAuthPresentation = holdingRequests logger.log("Requesting batch auth presentation") - try await presentBatchAuth?(batchableRequests, persistAuthentication(for:)) + try await presentBatchAuth?() logger.log("Requested batch auth presentation") } if let preauthorized = existingAuthenticationContext(for: request) { @@ -110,11 +118,10 @@ public actor AuthenticationHandler { activeTask = Task { logger.log("Beginning individual auth prompt") - try await Task.sleep(for: .seconds(1000)) -// _ = try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason) + _ = try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason) logger.log("Ended individual auth prompt") } - _ = try await activeTask?.value + _ = try? await activeTask?.value // TODO: Check something beyond cancellation? id? // Is this okay? Do we always assume that a cancelled task will be the proceeded on? if activeTask?.isCancelled ?? false { @@ -131,7 +138,7 @@ public actor AuthenticationHandler { return context } - private var batchableRequests: [[SignatureRequest]] { + public var batchableRequests: [[SignatureRequest]] { holdingRequests.reduce(into: [:]) { partialResult, next in partialResult[next.batchID, default: []].append(next) } @@ -167,14 +174,19 @@ public actor AuthenticationHandler { persistedContexts[AnySecret(secret)] = context } - private func persistAuthentication(for requests: Set) async throws { + public func requestAuthentication(for requests: Set) async throws { activeTask?.cancel() guard let first = requests.first else { return } let newContext = LAContext() newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton) - newContext.localizedReason = String("Multiple") -// newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString)) + let appNames = Set(requests.map(\.provenance.origin.displayName)).joined(separator: ", ") + let secretNames = Set(requests.map(\.secret.name)).joined(separator: ", ") + if requests.count > 1 { + newContext.localizedReason = String(localized: .authContextRequestMultiple(appName: appNames, secretName: secretNames)) + } else { + newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: appNames, secretName: secretNames)) + } let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason) guard success else { return } let context = AuthenticationContext(secret: first.secret, context: newContext, requestIDs: Set(requests.map(\.id))) diff --git a/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift b/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift index 1a2226f..a4b5cb7 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift @@ -44,6 +44,13 @@ extension SigningRequestTracer { let pathPointer = UnsafeMutablePointer.allocate(capacity: Int(MAXPATHLEN)) _ = unsafe proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN)) + +// let bufferLength = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0) + let x = UnsafeMutablePointer.allocate(capacity: Int(MAXPATHLEN)) + _ = unsafe proc_pidinfo(pid, PROC_PIDLISTFDS, 0, x, Int32(MAXPATHLEN)) + let buffer = unsafe UnsafeBufferPointer(start: x, count: Int(MAXPATHLEN)/64) + unsafe print(buffer) + let path = unsafe String(cString: pathPointer) var secCode: Unmanaged! let flags: SecCSFlags = [.considerExpiration, .enforceRevocationChecks] @@ -80,10 +87,6 @@ extension SigningRequestTracer { } -// from libproc.h -@_silgen_name("proc_pidpath") -@discardableResult func proc_pidpath(_ pid: Int32, _ buffer: UnsafeMutableRawPointer!, _ buffersize: UInt32) -> Int32 - //// from SecTask.h @_silgen_name("SecCodeCreateWithPID") @discardableResult func SecCodeCreateWithPID(_: Int32, _: SecCSFlags, _: UnsafeMutablePointer?>!) -> OSStatus diff --git a/Sources/SecretAgent/App.swift b/Sources/SecretAgent/App.swift index 2b7e9c5..143141e 100644 --- a/Sources/SecretAgent/App.swift +++ b/Sources/SecretAgent/App.swift @@ -29,7 +29,6 @@ struct SecretAgent: App { private let authenticationHandler = AuthenticationHandler() private let publicKeyFileStoreController = PublicKeyFileStoreController(publicKeysURL: URL.publicKeyDirectory, certificatesURL: URL.certificatesDirectory) - @State var pending: ([[SignatureRequest]], (Set) async throws -> Void)? @Environment(\.openWindow) var openWindow private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "App") @@ -100,8 +99,7 @@ struct SecretAgent: App { } } .task { - await authenticationHandler.setBatchAuthHandler { @MainActor pending, authorize in - self.pending = (pending, authorize) + authenticationHandler.setBatchAuthHandler { @MainActor in openWindow(id: String(describing: BatchedRequestsView.self)) } @@ -129,10 +127,10 @@ struct SecretAgent: App { @ViewBuilder var pendingView: some View { - if let (requests, authorize) = pending { - BatchedRequestsView(pending: requests, review: authorize) + if !authenticationHandler.batchableRequests.isEmpty { + BatchedRequestsView(authenticationHandler: authenticationHandler) } } - } + diff --git a/Sources/SecretAgent/BatchedRequestsView.swift b/Sources/SecretAgent/BatchedRequestsView.swift index 3effe21..e843900 100644 --- a/Sources/SecretAgent/BatchedRequestsView.swift +++ b/Sources/SecretAgent/BatchedRequestsView.swift @@ -2,23 +2,21 @@ import SwiftUI import SecretKit import SecretAgentKit import SmartCardSecretKit +import Common struct BatchedRequestsView: View { - let pending: [[SignatureRequest]] - let review: (Set) async throws -> Void + private let authenticationHandler: any AuthenticationHandlerProtocol - init(pending: [[SignatureRequest]], review: @escaping (Set) async throws -> Void) { - self.pending = pending - self.review = review + init(authenticationHandler: some AuthenticationHandlerProtocol) { + self.authenticationHandler = authenticationHandler } var body: some View { VStack(alignment: .leading) { -// .padding() Form { -// Text("Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually.") - ForEach(Array(pending.enumerated()), id: \.offset) { group in + Text("Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually.") + ForEach(Array(authenticationHandler.batchableRequests.enumerated()), id: \.offset) { group in Section { ForEach(Array(group.element.enumerated()), id: \.offset) { pending in HStack { @@ -31,7 +29,7 @@ struct BatchedRequestsView: View { Spacer() Button("Review") { Task { - try? await review([pending.element]) + try? await authenticationHandler.requestAuthentication(for: [pending.element]) } } } @@ -42,7 +40,7 @@ struct BatchedRequestsView: View { Spacer() Button("Review All") { Task { - try? await review(Set(group.element)) + try? await authenticationHandler.requestAuthentication(for: Set(group.element)) } } @@ -56,3 +54,65 @@ struct BatchedRequestsView: View { } } + +private struct TestHandler: AuthenticationHandlerProtocol { + + var batchableRequests: [[SignatureRequest]] = [] + + func requestAuthentication(for requests: Set) async throws { + + } + + func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws where SecretType : Secret { + + } + + func setBatchAuthHandler(_ handler: @escaping () async throws -> Void) { + + } + + func waitForAuthentication(for request: SignatureRequest) async throws -> any AuthenticationContextProtocol { + fatalError() + } + +} +// +//#Preview { +// ScrollView { +// MultilineInfoView(title: "GitHub", subtitle: "Ghostty", image: Image(systemName: "lock"), items: [ +// " +// ]) +//// Section { +//// ForEach(0..<2) { _ in +//// VStack(alignment: .leading) { +//// Text("Ghostty") +//// .font(.headline) +//// Text("zsh 􀯻 git 􀯻 zsh") +//// .font(.footnote) +//// Text("4:05 PM") +//// } +//// } +//// } header: { +//// Text("GitHub") +//// } +//// Section { +//// ForEach(0..<2) { _ in +//// VStack(alignment: .leading) { +//// Text("Ghostty") +//// .font(.headline) +//// Text("zsh 􀯻 git") +//// .font(.footnote.monospaced()) +//// Text("Git Signature") +//// .font(.footnote) +//// Text("4:05 PM") +//// .font(.caption) +//// } +//// } +//// } header: { +//// Text("GitHub Signing Key") +//// } +// } +// .padding() +// .formStyle(.grouped) +// .frame(minHeight: 700) +//} diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 6bcc374..269dfbf 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -75,7 +75,6 @@ 50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; }; 50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204E82FA9D12700402380 /* CertificateDetailView.swift */; }; 50E204ED2FAA997F00402380 /* CertificateListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204EC2FAA997F00402380 /* CertificateListItemView.swift */; }; - 50E204EF2FAA9C1400402380 /* MultilineInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */; }; 50E2051D2FAAB81C00402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 50E205282FAAB82700402380 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E205242FAAB82700402380 /* main.swift */; }; 50E2052C2FAAB85000402380 /* SecretiveCertificateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2052B2FAAB85000402380 /* SecretiveCertificateParser.swift */; }; @@ -276,7 +275,6 @@ 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = ""; }; 50E204E82FA9D12700402380 /* CertificateDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateDetailView.swift; sourceTree = ""; }; 50E204EC2FAA997F00402380 /* CertificateListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateListItemView.swift; sourceTree = ""; }; - 50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineInfoView.swift; sourceTree = ""; }; 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveCertificateParser.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 50E205232FAAB82700402380 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50E205242FAAB82700402380 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; @@ -404,7 +402,6 @@ 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */, 50617D8423FCE48E0099B055 /* ContentView.swift */, 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, - 50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */, 50153E1F250AFCB200525160 /* UpdateView.swift */, ); path = Views; @@ -814,7 +811,6 @@ 50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, 504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */, - 50E204EF2FAA9C1400402380 /* MultilineInfoView.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, @@ -868,7 +864,7 @@ buildActionMask = 2147483647; files = ( 50E205802FAB291E00402380 /* CertificateMigrator.swift in Sources */, - 50020BB024064869003D4025 /* AppDelegate.swift in Sources */, + 50020BB024064869003D4025 /* App.swift in Sources */, 5018F54F24064786002EB505 /* Notifier.swift in Sources */, 503647482F870B7800977A23 /* BatchedRequestsView.swift in Sources */, 501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */, diff --git a/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/Secretive.xcscheme b/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/Secretive.xcscheme index b7eccb7..77ddc38 100644 --- a/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/Secretive.xcscheme +++ b/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/Secretive.xcscheme @@ -87,6 +87,9 @@ ReferencedContainer = "container:Secretive.xcodeproj"> + + : View { image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret) ) + Spacer() + .frame(height: 20) + CopyableView( + title: .secretDetailPublicKeyLabel, + image: Image(systemName: "key"), + text: keyWriter.openSSHString(secret: secret) + ) Spacer() .frame(height: 20) CopyableView(