diff --git a/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift b/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift index 64419cb..b5df7d5 100644 --- a/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift +++ b/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift @@ -8,7 +8,7 @@ public final class AuthenticationContext: AuthenticationContextProtocol { /// The Secret to persist authentication for. public let secret: AnySecret /// The LAContext used to authorize the persistent context. - public let laContext: LAContext + public let laContext: LAContext? enum Validity { /// - Note - Monotonic time instead of Date() to prevent people setting the clock back. @@ -55,12 +55,21 @@ public final class AuthenticationContext: AuthenticationContextProtocol { } } + public func evaluate() async throws -> Bool { + guard let laContext else { return false } + return try await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason) + } + + public func cancel() async { + laContext?.invalidate() + } + } @MainActor public protocol AuthenticationHandlerProtocol: Observable { + var batchableRequests: [[SignatureRequest]] { get } 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 } @@ -70,7 +79,7 @@ public final class AuthenticationContext: AuthenticationContextProtocol { private var persistedContexts: [AnySecret: AuthenticationContext] = [:] private var holdingRequests: Set = [] private var activeTask: Task? - private var activeContext: LAContext? + private var activeContext: (any AuthenticationContextProtocol)? private var lastBatchAuthPresentation: Set? private var presentBatchAuth: (() async throws -> Void)? @@ -102,7 +111,7 @@ public final class AuthenticationContext: AuthenticationContextProtocol { lastBatchAuthPresentation = holdingRequests logger.log("Requesting batch auth presentation") try await presentBatchAuth?() - activeContext?.invalidate() + await activeContext?.cancel() logger.log("Requested batch auth presentation") } if let preauthorized = existingAuthenticationContext(for: request) { @@ -117,11 +126,11 @@ public final class AuthenticationContext: AuthenticationContextProtocol { laContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: request.provenance.origin.displayName, secretName: request.secret.name)) laContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton) let context = AuthenticationContext(secret: request.secret, context: laContext, requestID: request.id) - activeContext = laContext + activeContext = context activeTask = Task { logger.log("Beginning individual auth prompt") - let result = (try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason)) ?? false + let result = (try? await context.evaluate()) ?? false logger.log("Ended individual auth prompt") return result } diff --git a/Sources/Packages/Sources/SecretKit/Types/AuthenticationContext.swift b/Sources/Packages/Sources/SecretKit/Types/AuthenticationContext.swift index 9252ae1..e1204e7 100644 --- a/Sources/Packages/Sources/SecretKit/Types/AuthenticationContext.swift +++ b/Sources/Packages/Sources/SecretKit/Types/AuthenticationContext.swift @@ -3,14 +3,11 @@ import LocalAuthentication /// Protocol describing an authentication context. This is an authorization that can be reused for multiple access to a secret that requires authentication for a specific period of time. public protocol AuthenticationContextProtocol: Sendable, Identifiable { - /// Whether the context remains valid. - var secret: AnySecret { get } - - var laContext: LAContext { get } - func valid(for request: SignatureRequest) -> Bool - + var laContext: LAContext? { get } + func evaluate() async throws -> Bool + func cancel() async } public struct SignatureRequest: Identifiable, Hashable, Sendable, Comparable { diff --git a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift index 43ae399..c1a939e 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift @@ -11,7 +11,7 @@ import CertificateKit // MARK: Identity Listing @Test func emptyStores() async throws { - let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore()) + let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler()) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities) let response = await agent.handle(request: request, provenance: .test, hosts: nil) #expect(response == Constants.Responses.requestIdentitiesEmpty) @@ -19,7 +19,7 @@ import CertificateKit @Test func identitiesList() async throws { let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) - let agent = Agent(storeList: list, certificateStore: CertificateStore()) + let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler()) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities) let response = await agent.handle(request: request, provenance: .test, hosts: nil) @@ -33,7 +33,7 @@ import CertificateKit @Test func noMatchingIdentities() async throws { let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) - let agent = Agent(storeList: list, certificateStore: CertificateStore()) + let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler()) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignatureWithNoneMatching) let response = await agent.handle(request: request, provenance: .test, hosts: nil) #expect(response == Constants.Responses.requestFailure) @@ -43,7 +43,7 @@ import CertificateKit let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature) guard case SSHAgent.Request.signRequest(let context) = request else { return } let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) - let agent = Agent(storeList: list, certificateStore: CertificateStore()) + let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler()) let response = await agent.handle(request: request, provenance: .test, hosts: nil) let responseReader = OpenSSHReader(data: response) let length = try responseReader.readNextBytes(as: UInt32.self) @@ -78,7 +78,7 @@ import CertificateKit let witness = StubWitness(speakNow: { _,_ in return true }, witness: { _, _ in }) - let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness) + let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler(), witness: witness) let response = await agent.handle(request: .signRequest(.empty), provenance: .test, hosts: nil) #expect(response == Constants.Responses.requestFailure) } @@ -91,7 +91,7 @@ import CertificateKit }, witness: { _, trace in witnessed = true }) - let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness) + let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler(), witness: witness) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature) _ = await agent.handle(request: request, provenance: .test, hosts: nil) #expect(witnessed) @@ -107,7 +107,7 @@ import CertificateKit }, witness: { _, trace in witnessTrace = trace }) - let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness) + let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler(), witness: witness) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature) _ = await agent.handle(request: request, provenance: .test, hosts: nil) #expect(witnessTrace == speakNowTrace) @@ -120,7 +120,7 @@ import CertificateKit let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let store = list.stores.first?.base as! Stub.Store store.shouldThrow = true - let agent = Agent(storeList: list, certificateStore: CertificateStore()) + let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler()) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature) let response = await agent.handle(request: request, provenance: .test, hosts: nil) #expect(response == Constants.Responses.requestFailure) @@ -129,7 +129,7 @@ import CertificateKit // MARK: Unsupported @Test func unhandledAdd() async throws { - let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore()) + let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler()) let response = await agent.handle(request: .addIdentity, provenance: .test, hosts: nil) #expect(response == Constants.Responses.requestFailure) } diff --git a/Sources/Packages/Tests/SecretAgentKitTests/AuthenticationHandlerTests.swift b/Sources/Packages/Tests/SecretAgentKitTests/AuthenticationHandlerTests.swift new file mode 100644 index 0000000..c9ef71d --- /dev/null +++ b/Sources/Packages/Tests/SecretAgentKitTests/AuthenticationHandlerTests.swift @@ -0,0 +1,13 @@ +import Testing +import SecretAgentKit + +@Suite @MainActor struct AuthenticationHandlerTests { + + @Test func singleImmediatelyRequests() async throws { + } + + @Test func authRequiredDoesntBlockNoAuthRequired() async throws { + } + + +} diff --git a/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift index 9f31a8d..72de7a7 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift @@ -17,7 +17,7 @@ extension StubWitness: SigningWitness { } } -func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?) throws { + func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?, offerPersistence: Bool) async throws { witness(secret, provenance) } diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index ba645c0..820474d 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -78,7 +78,6 @@ 50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */; }; 50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; - 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; }; 50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; }; 50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; }; 50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204E82FA9D12700402380 /* CertificateDetailView.swift */; }; @@ -294,7 +293,6 @@ 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = ""; }; 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = ""; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = ""; }; - 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 = ""; }; 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveCertificateParser.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -385,7 +383,6 @@ 504788ED2E681EB200B4556F /* Modifiers */ = { isa = PBXGroup; children = ( - 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */, 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */, 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */, 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */, @@ -912,7 +909,6 @@ 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */, - 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */, 50E204ED2FAA997F00402380 /* CertificateListItemView.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, diff --git a/Sources/Secretive/Views/Modifiers/ActionButtonStyle.swift b/Sources/Secretive/Views/Modifiers/ActionButtonStyle.swift deleted file mode 100644 index 70ab463..0000000 --- a/Sources/Secretive/Views/Modifiers/ActionButtonStyle.swift +++ /dev/null @@ -1,94 +0,0 @@ -import SwiftUI - -struct PrimaryButtonModifier: ViewModifier { - - @Environment(\.colorScheme) var colorScheme - @Environment(\.isEnabled) var isEnabled - - func body(content: Content) -> some View { - // Tinted glass prominent is really hard to read on 26.0. - if #available(macOS 26.0, *), colorScheme == .dark, isEnabled { - content.buttonStyle(.glassProminent) - } else { - content.buttonStyle(.borderedProminent) - } - } - -} - -extension View { - - func primaryButton() -> some View { - modifier(PrimaryButtonModifier()) - } - -} - -struct ToolbarCircleButtonModifier: ViewModifier { - - func body(content: Content) -> some View { - if #available(macOS 26.0, *) { - content - .glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle) - } else { - content - .buttonStyle(.borderless) - } - } - -} - -extension View { - - func toolbarCircleButton() -> some View { - modifier(ToolbarCircleButtonModifier()) - } - -} - -struct NormalButtonModifier: ViewModifier { - - func body(content: Content) -> some View { - if #available(macOS 26.0, *) { - content.buttonStyle(.glass) - } else { - content.buttonStyle(.bordered) - } - } - -} - -extension View { - - func normalButton() -> some View { - modifier(NormalButtonModifier()) - } - -} - -struct DangerButtonModifier: ViewModifier { - - @Environment(\.colorScheme) var colorScheme - - func body(content: Content) -> some View { - // Tinted glass prominent is really hard to read on 26.0. - if #available(macOS 26.0, *), colorScheme == .dark { - content.buttonStyle(.glassProminent) - .tint(.red) - .foregroundStyle(.white) - } else { - content.buttonStyle(.borderedProminent) - .tint(.red) - .foregroundStyle(.white) - } - } - -} - -extension View { - - func danger() -> some View { - modifier(DangerButtonModifier()) - } - -}