This commit is contained in:
Max Goedjen
2026-05-04 15:20:25 -07:00
parent bc19a37acf
commit 438b4c6658
10 changed files with 272 additions and 105 deletions

View File

@@ -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",

View File

@@ -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<Void, any Error>?
private var lastBatchAuthPresentation: Set<SignatureRequest>?
private var presentBatchAuth: ((Set<SignatureRequest>, @Sendable (Set<SignatureRequest>) async throws -> Void) async throws -> Void)?
private var presentBatchAuth: (([[SignatureRequest]], @escaping @Sendable (Set<SignatureRequest>) async throws -> Void) async throws -> Void)?
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
public init(presentBatchAuth: ((Set<SignatureRequest>, @Sendable (Set<SignatureRequest>) async throws -> Void) async throws -> Void)?) {
self.presentBatchAuth = presentBatchAuth
public init() {
}
public func setBatchAuthHandler(_ handler: @escaping (@Sendable ([[SignatureRequest]], @escaping @Sendable (Set<SignatureRequest>) 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)? {

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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
}