From 438b4c665860572f0acf247e004ab7072139307b Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 4 May 2026 15:20:25 -0700 Subject: [PATCH] WIP --- .../Packages/Resources/Localizable.xcstrings | 16 +++ .../AuthenticationHandler.swift | 53 ++++++-- .../Types/AuthenticationContext.swift | 15 ++- .../Types/SigningRequestProvenance.swift | 6 + .../Packages/Sources/XPCWrappers/TeamID.swift | 2 +- Sources/SecretAgent/App.swift | 122 ++++++++++++++++++ Sources/SecretAgent/AppDelegate.swift | 86 ------------ Sources/SecretAgent/BatchedRequestsView.swift | 58 +++++++++ Sources/SecretAgent/SecretAgent.entitlements | 4 +- Sources/Secretive.xcodeproj/project.pbxproj | 15 ++- 10 files changed, 272 insertions(+), 105 deletions(-) create mode 100644 Sources/SecretAgent/App.swift delete mode 100644 Sources/SecretAgent/AppDelegate.swift create mode 100644 Sources/SecretAgent/BatchedRequestsView.swift diff --git a/Sources/Packages/Resources/Localizable.xcstrings b/Sources/Packages/Resources/Localizable.xcstrings index 3513013..b0b6615 100644 --- a/Sources/Packages/Resources/Localizable.xcstrings +++ b/Sources/Packages/Resources/Localizable.xcstrings @@ -365,6 +365,16 @@ }, "shouldTranslate" : false }, + "%@ - %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ - %2$@" + } + } + } + }, "about_build_log_button" : { "extractionState" : "manual", "localizations" : { @@ -19821,6 +19831,12 @@ } } } + }, + "Review" : { + + }, + "Review All" : { + }, "secret_detail_md5_fingerprint_label" : { "extractionState" : "manual", diff --git a/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift b/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift index e814dda..61daaf0 100644 --- a/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift +++ b/Sources/Packages/Sources/SecretAgentKit/AuthenticationHandler.swift @@ -1,5 +1,6 @@ @unsafe @preconcurrency import LocalAuthentication import SecretKit +import OSLog /// A context describing a persisted authentication. public final class AuthenticationContext: AuthenticationContextProtocol { @@ -63,26 +64,41 @@ public actor AuthenticationHandler { private var activeTask: Task? private var lastBatchAuthPresentation: Set? - private var presentBatchAuth: ((Set, @Sendable (Set) async throws -> Void) async throws -> Void)? + private var presentBatchAuth: (([[SignatureRequest]], @escaping @Sendable (Set) async throws -> Void) async throws -> Void)? + private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent") - public init(presentBatchAuth: ((Set, @Sendable (Set) async throws -> Void) async throws -> Void)?) { - self.presentBatchAuth = presentBatchAuth + public init() { + } + + public func setBatchAuthHandler(_ handler: @escaping (@Sendable ([[SignatureRequest]], @escaping @Sendable (Set) async throws -> Void) async throws -> Void)) { + self.presentBatchAuth = handler } public func waitForAuthentication(for request: SignatureRequest) async throws -> any AuthenticationContextProtocol { - if let existing = existingAuthenticationContext(for: request) { return existing } + if let existing = existingAuthenticationContext(for: request) { + logger.log("Short circuiting wait, existing valid context already exists.") + return existing + } holdingRequests.insert(request) - defer { holdingRequests.remove(request) } + logger.log("Waiting for authentication for \(request.id)") + defer { + logger.log("Removed hold for \(request.id)") + holdingRequests.remove(request) + } while holdingRequests.count > 1 { if hasBatchableRequests, holdingRequests != lastBatchAuthPresentation { + logger.log("Batchable requests exist, cancelling existing auth prompt") activeTask?.cancel() lastBatchAuthPresentation = holdingRequests - try await presentBatchAuth?(holdingRequests) { - try await persistAuthentication(for: $0) - } + logger.log("Requesting batch auth presentation") + try await presentBatchAuth?(batchableRequests, persistAuthentication(for:)) + logger.log("Requested batch auth presentation") } if let preauthorized = existingAuthenticationContext(for: request) { + logger.log("Batch auth context found, proceededing with preauthorized context") return preauthorized + } else { + logger.log("Waiting for batch request handling") } try await Task.sleep(for: .milliseconds(100)) } @@ -92,12 +108,20 @@ public actor AuthenticationHandler { let context = AuthenticationContext(secret: request.secret, context: laContext, requestID: request.id) activeTask = Task { - _ = try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason) + logger.log("Beginning individual auth prompt") + try await Task.sleep(for: .seconds(1000)) +// _ = try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason) + logger.log("Ended individual auth prompt") } _ = 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 { + logger.log("Auth prompt was cancelled, waiting for explicit auth") + // If we explicitly cancelled the task, hang on until we auth it. while true { if let preauthorized = existingAuthenticationContext(for: request) { + logger.log("Explicit auth context found") return preauthorized } try await Task.sleep(for: .milliseconds(100)) @@ -106,10 +130,17 @@ public actor AuthenticationHandler { return context } + private var batchableRequests: [[SignatureRequest]] { + holdingRequests.reduce(into: [:]) { partialResult, next in + partialResult[next.batchID, default: []].append(next) + } + .values + .map { $0.sorted() } + } + private var hasBatchableRequests: Bool { guard presentBatchAuth != nil else { return false } - // FIXME: THIS - return holdingRequests.count > 1 + return batchableRequests.count < holdingRequests.count } private func existingAuthenticationContext(for request: SignatureRequest) -> (any AuthenticationContextProtocol)? { diff --git a/Sources/Packages/Sources/SecretKit/Types/AuthenticationContext.swift b/Sources/Packages/Sources/SecretKit/Types/AuthenticationContext.swift index c22dec0..03117d3 100644 --- a/Sources/Packages/Sources/SecretKit/Types/AuthenticationContext.swift +++ b/Sources/Packages/Sources/SecretKit/Types/AuthenticationContext.swift @@ -13,7 +13,8 @@ public protocol AuthenticationContextProtocol: Sendable, Identifiable { } -public struct SignatureRequest: Identifiable, Hashable, Sendable { +public struct SignatureRequest: Identifiable, Hashable, Sendable, Comparable { + public let id: UUID public let date: Date public let secret: AnySecret @@ -25,4 +26,16 @@ public struct SignatureRequest: Identifiable, Hashable, Sendable { self.secret = secret self.provenance = provenance } + + public var batchID: Int { + var hasher = Hasher() + provenance.batchID.hash(into: &hasher) + secret.id.hash(into: &hasher) + return hasher.finalize() + } + + public static func < (lhs: SignatureRequest, rhs: SignatureRequest) -> Bool { + lhs.date < rhs.date + } + } diff --git a/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift b/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift index 320fe58..86b763a 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift @@ -15,6 +15,12 @@ public struct SigningRequestProvenance: Hashable, Sendable { self.date = date } + public var batchID: Int { + var hasher = Hasher() + chain.map(\.path).hash(into: &hasher) + return hasher.finalize() + } + } extension SigningRequestProvenance { diff --git a/Sources/Packages/Sources/XPCWrappers/TeamID.swift b/Sources/Packages/Sources/XPCWrappers/TeamID.swift index 56848a1..22d715e 100644 --- a/Sources/Packages/Sources/XPCWrappers/TeamID.swift +++ b/Sources/Packages/Sources/XPCWrappers/TeamID.swift @@ -11,7 +11,7 @@ extension ProcessInfo { } guard let value = SecTaskCopyValueForEntitlement(task, "com.apple.developer.team-identifier" as CFString, nil) as? String else { - assertionFailure("SecTaskCopyValueForEntitlement(com.apple.developer.team-identifier) failed") +// assertionFailure("SecTaskCopyValueForEntitlement(com.apple.developer.team-identifier) failed") return fallbackTeamID } diff --git a/Sources/SecretAgent/App.swift b/Sources/SecretAgent/App.swift new file mode 100644 index 0000000..e46ed16 --- /dev/null +++ b/Sources/SecretAgent/App.swift @@ -0,0 +1,122 @@ +import Cocoa +import OSLog +import SecretKit +import SecureEnclaveSecretKit +import SmartCardSecretKit +import SecretAgentKit +import Brief +import Observation +import Common +import SwiftUI + +@main +struct SecretAgent: App { + + @MainActor private let storeList: SecretStoreList = { + let list = SecretStoreList() + let cryptoKit = SecureEnclave.Store() + let migrator = SecureEnclave.CryptoKitMigrator() + try? migrator.migrate(to: cryptoKit) + list.add(store: cryptoKit) + list.add(store: SmartCard.Store()) + return list + }() + private let updater = Updater(checkOnLaunch: true) + private let notifier = Notifier() + private let authenticationHandler = AuthenticationHandler() + private let publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory) + + @State var pending: ([[SignatureRequest]], (Set) async throws -> Void)? + @Environment(\.openWindow) var openWindow + + private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "App") + @SceneBuilder var body: some Scene { + MenuBarExtra(isInserted: .constant(false)) { + EmptyView() + } label: { + Image(systemName: "lock") + .task { + await notifier.registerPersistenceHandler { + try await authenticationHandler.persistAuthentication(secret: $0, forDuration: $1) + } + } + .task { + let socketController = SocketController(path: URL.socketPath) + let agent = Agent(storeList: storeList, authenticationHandler: authenticationHandler, witness: notifier) + for await session in socketController.sessions { + Task { + let inputParser = try await XPCAgentInputParser() + do { + for await message in session.messages { + let request = try await inputParser.parse(data: message) + let agentResponse = await agent.handle(request: request, provenance: session.provenance) + try session.write(agentResponse) + } + } catch { + try session.close() + } + } + } + } +// .task { +// let socketController = SocketController(path: URL.agentHomeURL.appendingPathComponent("socket-two.ssh").path()) +// let socketController = SocketController(path: "/Users/max/Downloads/test.ssh") +// let agent = Agent(storeList: storeList, authenticationHandler: authenticationHandler, witness: notifier) +// for await session in socketController.sessions { +// Task { +// let inputParser = try await XPCAgentInputParser() +// do { +// for await message in session.messages { +// let request = try await inputParser.parse(data: message) +// let agentResponse = await agent.handle(request: request, provenance: session.provenance) +// try session.write(agentResponse) +// } +// } catch { +// try session.close() +// } +// } +// } +// } + .task { + for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) { + try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) + } + } + .task { + await authenticationHandler.setBatchAuthHandler { @MainActor pending, authorize in + self.pending = (pending, authorize) + openWindow(id: String(describing: BatchedRequestsView.self)) + } + + } + .task { + try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) + notifier.prompt() + _ = withObservationTracking { + updater.update + } onChange: { [updater, notifier] in + Task { + guard !updater.currentVersion.isTestBuild else { return } + await notifier.notify(update: updater.update!) { release in + await updater.ignore(release: release) + } + } + } + } + } + WindowGroup(id: String(describing: BatchedRequestsView.self)) { + pendingView + } + .windowStyle(.hiddenTitleBar) + .windowResizability(.contentSize) + } + + @ViewBuilder + var pendingView: some View { + if let (requests, authorize) = pending { + BatchedRequestsView(pending: requests, review: authorize) + } + } + + +} diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift deleted file mode 100644 index 14e7c35..0000000 --- a/Sources/SecretAgent/AppDelegate.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Cocoa -import OSLog -import SecretKit -import SecureEnclaveSecretKit -import SmartCardSecretKit -import SecretAgentKit -import Brief -import Observation -import Common - -@main -class AppDelegate: NSObject, NSApplicationDelegate { - - @MainActor private let storeList: SecretStoreList = { - let list = SecretStoreList() - let cryptoKit = SecureEnclave.Store() - let migrator = SecureEnclave.CryptoKitMigrator() - try? migrator.migrate(to: cryptoKit) - list.add(store: cryptoKit) - list.add(store: SmartCard.Store()) - return list - }() - private let updater = Updater(checkOnLaunch: true) - private let notifier = Notifier() - private let authenticationHandler = AuthenticationHandler { pending, authorize in - print(pending) - print("Waiting") -// Task { - try await Task.sleep(for: .seconds(3)) - try await authorize(pending) -// } - } - private let publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory) - private lazy var agent: Agent = { - Agent(storeList: storeList, authenticationHandler: authenticationHandler, witness: notifier) - }() - private lazy var socketController: SocketController = { - let path = URL.socketPath as String - return SocketController(path: path) - }() - private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate") - - func applicationDidFinishLaunching(_ aNotification: Notification) { - logger.debug("SecretAgent finished launching") - Task { - for await session in socketController.sessions { - Task { - let inputParser = try await XPCAgentInputParser() - do { - for await message in session.messages { - let request = try await inputParser.parse(data: message) - let agentResponse = await agent.handle(request: request, provenance: session.provenance) - try session.write(agentResponse) - } - } catch { - try session.close() - } - } - } - } - Task { [notifier, authenticationHandler] in - await notifier.registerPersistenceHandler { - try await authenticationHandler.persistAuthentication(secret: $0, forDuration: $1) - } - } - Task { - for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) { - try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) - } - } - try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) - notifier.prompt() - _ = withObservationTracking { - updater.update - } onChange: { [updater, notifier] in - Task { - guard !updater.currentVersion.isTestBuild else { return } - await notifier.notify(update: updater.update!) { release in - await updater.ignore(release: release) - } - } - } - } - -} - diff --git a/Sources/SecretAgent/BatchedRequestsView.swift b/Sources/SecretAgent/BatchedRequestsView.swift new file mode 100644 index 0000000..6508654 --- /dev/null +++ b/Sources/SecretAgent/BatchedRequestsView.swift @@ -0,0 +1,58 @@ +import SwiftUI +import SecretKit +import SecretAgentKit +import SmartCardSecretKit + +struct BatchedRequestsView: View { + + let pending: [[SignatureRequest]] + let review: (Set) async throws -> Void + + init(pending: [[SignatureRequest]], review: @escaping (Set) async throws -> Void) { + self.pending = pending + self.review = review + } + + 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 + Section { + ForEach(Array(group.element.enumerated()), id: \.offset) { pending in + HStack { + VStack(alignment: .leading) { + Text(pending.element.provenance.origin.displayName) + .font(.headline) + Text(pending.element.provenance.date.formatted()) + .font(.footnote) + } + Spacer() + Button("Review") { + Task { + try await review([pending.element]) + } + } + } + } + } header: { + HStack { + Text("\(group.element.first!.provenance.origin.displayName) - \(group.element.first!.secret.name)") + Spacer() + Button("Review All") { + Task { + try await review(Set(group.element)) + } + + } + } + } + } + } + .formStyle(.grouped) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + +} diff --git a/Sources/SecretAgent/SecretAgent.entitlements b/Sources/SecretAgent/SecretAgent.entitlements index 35188de..28f4467 100644 --- a/Sources/SecretAgent/SecretAgent.entitlements +++ b/Sources/SecretAgent/SecretAgent.entitlements @@ -16,10 +16,10 @@ 1 com.apple.security.hardened-process.hardened-heap - com.apple.security.smartcard - com.apple.security.hardened-process.platform-restrictions-string 2 + com.apple.security.smartcard + keychain-access-groups $(AppIdentifierPrefix)com.maxgoedjen.Secretive diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index d7a7aea..5ed6ecd 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; }; - 50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; }; + 50020BB024064869003D4025 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* App.swift */; }; 5002C3AB2EEF483300FFAD22 /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 5002C3AA2EEF483300FFAD22 /* XPCWrappers */; }; 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; }; 5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; }; @@ -26,6 +26,7 @@ 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; + 503647482F870B7800977A23 /* BatchedRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503647472F870B7800977A23 /* BatchedRequestsView.swift */; }; 504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; }; 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; }; 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; }; @@ -181,7 +182,7 @@ /* Begin PBXFileReference section */ 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSecretView.swift; sourceTree = ""; }; - 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 50020BAF24064869003D4025 /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = ""; }; 500666D02F04786900328939 /* SecretiveUpdater.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretiveUpdater.entitlements; sourceTree = ""; }; 500666D12F04787200328939 /* SecretAgentInputParser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgentInputParser.entitlements; sourceTree = ""; }; @@ -190,6 +191,7 @@ 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = ""; }; 501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = ""; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; + 503647472F870B7800977A23 /* BatchedRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchedRequestsView.swift; sourceTree = ""; }; 504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = ""; }; 504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = ""; }; 504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = ""; }; @@ -459,9 +461,10 @@ 50A3B78B24026B7500D209EA /* SecretAgent */ = { isa = PBXGroup; children = ( - 50020BAF24064869003D4025 /* AppDelegate.swift */, + 50020BAF24064869003D4025 /* App.swift */, 5018F54E24064786002EB505 /* Notifier.swift */, 501578122E6C0479004A37D0 /* XPCInputParser.swift */, + 503647472F870B7800977A23 /* BatchedRequestsView.swift */, 50A3B79524026B7600D209EA /* Main.storyboard */, 50A3B79824026B7600D209EA /* Info.plist */, 508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */, @@ -740,8 +743,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 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 */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1378,6 +1382,7 @@ DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; ENABLE_APP_SANDBOX = YES; ENABLE_ENHANCED_SECURITY = YES; + ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; ENABLE_HARDENED_RUNTIME = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO; @@ -1415,6 +1420,7 @@ DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)"; ENABLE_APP_SANDBOX = YES; ENABLE_ENHANCED_SECURITY = YES; + ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; ENABLE_HARDENED_RUNTIME = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO; @@ -1453,6 +1459,7 @@ DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)"; ENABLE_APP_SANDBOX = YES; ENABLE_ENHANCED_SECURITY = YES; + ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; ENABLE_HARDENED_RUNTIME = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;