Compare commits

..

6 Commits

Author SHA1 Message Date
Max Goedjen
438b4c6658 WIP 2026-05-04 15:20:25 -07:00
Max Goedjen
bc19a37acf Restrict connectionAcceptedNotifications to creating filehandle 2026-04-22 16:48:56 -07:00
Max Goedjen
c6c4ea60be Messy auth batching infra WIP 2026-04-11 00:46:39 -07:00
Max Goedjen
8696a2c9c0 Split out auth/nonauth paths 2026-04-10 13:36:34 -07:00
Max Goedjen
2b12d6df1e Add uncommitted authhandler rename 2026-04-09 20:43:32 -07:00
Max Goedjen
b68c82ae69 Splitting out auth context stuff in preparation for batch 2026-04-09 20:43:18 -07:00
23 changed files with 533 additions and 464 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

@@ -9,20 +9,21 @@ import SSHProtocolKit
public final class Agent: Sendable {
private let storeList: SecretStoreList
private let authenticationHandler: AuthenticationHandler
private let witness: SigningWitness?
private let publicKeyWriter = OpenSSHPublicKeyWriter()
private let signatureWriter = OpenSSHSignatureWriter()
private let certificateHandler = OpenSSHCertificateHandler()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
private let authorizationCoordinator = AuthorizationCoordinator()
/// Initializes an agent with a store list and a witness.
/// - Parameters:
/// - storeList: The `SecretStoreList` to make available.
/// - witness: A witness to notify of requests.
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
public init(storeList: SecretStoreList, authenticationHandler: AuthenticationHandler, witness: SigningWitness? = nil) {
logger.debug("Agent is running")
self.storeList = storeList
self.authenticationHandler = authenticationHandler
self.witness = witness
Task { @MainActor in
await certificateHandler.reloadCertificates(for: storeList.allSecrets)
@@ -103,26 +104,52 @@ extension Agent {
throw NoMatchingKeyError()
}
let decision = try await authorizationCoordinator.waitForAccessIfNeeded(to: secret, provenance: provenance)
switch decision {
case .proceed:
break
case .promptForSharedAuth:
do {
try await store.persistAuthentication(secret: secret, forProvenance: provenance)
await authorizationCoordinator.completedPersistence(secret: secret, forProvenance: provenance)
} catch {
await authorizationCoordinator.didNotCompletePersistence(secret: secret, forProvenance: provenance)
}
logger.debug("Agent offering witness chance to object")
do {
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
} catch {
logger.debug("Witness objected")
throw error
}
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance)
logger.debug("Witness did not object")
if secret.authenticationRequirement.required {
// Slow path, may block or suggest batching.
return try await signWithRequiredAuthentication(data: data, store: store, secret: secret, provenance: provenance)
} else {
// Fast path, no blocking/enqueing required
return try await signWithoutRequiredAuthentication(data: data, store: store, secret: secret, provenance: provenance)
}
}
func signWithoutRequiredAuthentication(data: Data, store: AnySecretStore, secret: AnySecret, provenance: SigningRequestProvenance) async throws -> Data {
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance, context: nil)
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
try await witness?.witness(accessTo: secret, from: store, by: provenance)
try await witness?.witness(accessTo: secret, from: store, by: provenance, offerPersistence: false)
logger.debug("Agent signed request")
return signedData
}
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)
try await witness?.witness(accessTo: secret, from: store, by: provenance, offerPersistence: false) // FIXME: THIS
logger.debug("Agent signed request")
return signedData
}

View File

@@ -0,0 +1,184 @@
@unsafe @preconcurrency import LocalAuthentication
import SecretKit
import OSLog
/// A context describing a persisted authentication.
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
enum Validity {
/// - Note - Monotonic time instead of Date() to prevent people setting the clock back.
case time(monotonicExpiration: UInt64)
case requestIDs(Set<UUID>)
case exclusive(UUID)
}
let validity: Validity
/// Initializes a context.
/// - Parameters:
/// - secret: The Secret to persist authentication for.
/// - context: The LAContext used to authorize the persistent context.
/// - duration: The duration of the authorization context, in seconds.
init<SecretType: Secret>(secret: SecretType, context: LAContext, duration: TimeInterval) {
self.secret = AnySecret(secret)
self.laContext = context
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value
self.validity = .time(monotonicExpiration: clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds))
}
init<SecretType: Secret>(secret: SecretType, context: LAContext, requestIDs: Set<UUID>) {
self.secret = AnySecret(secret)
self.laContext = context
self.validity = .requestIDs(requestIDs)
}
init<SecretType: Secret>(secret: SecretType, context: LAContext, requestID: UUID) {
self.secret = AnySecret(secret)
self.laContext = context
self.validity = .exclusive(requestID)
}
/// A boolean describing whether or not the context is still valid.
public func valid(for request: SignatureRequest) -> Bool {
switch validity {
case .time(let monotonicExpiration):
clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration
case .requestIDs(let set):
set.contains(request.id)
case .exclusive(let id):
id == request.id
}
}
}
public actor AuthenticationHandler {
private var persistedContexts: [AnySecret: AuthenticationContext] = [:]
private var holdingRequests: Set<SignatureRequest> = []
private var activeTask: Task<Void, any Error>?
private var lastBatchAuthPresentation: Set<SignatureRequest>?
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() {
}
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) {
logger.log("Short circuiting wait, existing valid context already exists.")
return existing
}
holdingRequests.insert(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
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))
}
let laContext = LAContext()
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)
activeTask = Task {
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))
}
}
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 }
return batchableRequests.count < holdingRequests.count
}
private func existingAuthenticationContext(for request: SignatureRequest) -> (any AuthenticationContextProtocol)? {
guard let persisted = persistedContexts[request.secret], persisted.valid(for: request) else { return nil }
return persisted
}
public func persistAuthentication<SecretType: Secret>(secret: SecretType, forDuration duration: TimeInterval) async throws {
let newContext = LAContext()
newContext.touchIDAuthenticationAllowableReuseDuration = duration
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day]
let durationString = formatter.string(from: duration)!
newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
guard success else { return }
let context = AuthenticationContext(secret: secret, context: newContext, duration: duration)
persistedContexts[AnySecret(secret)] = context
}
private func persistAuthentication(for requests: Set<SignatureRequest>) 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 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)))
persistedContexts[AnySecret(first.secret)] = context
}
}

View File

@@ -1,105 +0,0 @@
import Foundation
import SecretKit
import os
import LocalAuthentication
struct PendingRequest: Identifiable, Hashable, CustomStringConvertible {
let id: UUID = UUID()
let secret: AnySecret
let provenance: SigningRequestProvenance
var description: String {
"\(id.uuidString) - \(secret.name) \(provenance.origin.displayName)"
}
func batchable(with request: PendingRequest) -> Bool {
secret == request.secret &&
provenance.isSameProvenance(as: request.provenance)
}
}
enum Decision {
case proceed
case promptForSharedAuth
}
actor RequestHolder {
var pending: [PendingRequest] = []
var authorizing: PendingRequest?
var preauthorized: PendingRequest?
func addPending(_ request: PendingRequest) {
pending.append(request)
}
func advanceIfIdle() {
}
func shouldBlock(_ request: PendingRequest) -> Bool {
guard request != authorizing else { return false }
if let preauthorized, preauthorized.batchable(with: request) {
print("Batching: \(request)")
pending.removeAll(where: { $0 == request })
return false
}
return authorizing == nil && authorizing.
}
func clear() {
if let preauthorized, allBatchable(with: preauthorized).isEmpty {
self.preauthorized = nil
}
}
func allBatchable(with request: PendingRequest) -> [PendingRequest] {
pending.filter { $0.batchable(with: request) }
}
func completedPersistence(secret: AnySecret, forProvenance provenance: SigningRequestProvenance) {
self.preauthorized = PendingRequest(secret: secret, provenance: provenance)
}
func didNotCompletePersistence(secret: AnySecret, forProvenance provenance: SigningRequestProvenance) {
self.preauthorized = nil
}
}
final class AuthorizationCoordinator: Sendable {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AuthorizationCoordinator")
private let holder = RequestHolder()
public func waitForAccessIfNeeded(to secret: AnySecret, provenance: SigningRequestProvenance) async throws -> Decision {
// Block on unknown, since we don't really have any way to check.
if secret.authenticationRequirement == .unknown {
logger.warning("\(secret.name) has unknown authentication requirement.")
}
guard secret.authenticationRequirement != .notRequired else {
logger.debug("\(secret.name) does not require authentication, continuing.")
return .proceed
}
logger.debug("\(secret.name) requires authentication.")
let pending = PendingRequest(secret: secret, provenance: provenance)
await holder.addPending(pending)
while await holder.shouldBlock(pending) {
logger.debug("\(pending) waiting.")
try await Task.sleep(for: .milliseconds(100))
}
if await holder.preauthorized == nil, await holder.allBatchable(with: pending).count > 0 {
logger.debug("\(pending) batch suggestion.")
return .promptForSharedAuth
}
logger.debug("\(pending) continuing")
return .proceed
}
func completedPersistence(secret: AnySecret, forProvenance provenance: SigningRequestProvenance) async {
await holder.completedPersistence(secret: secret, forProvenance: provenance)
}
func didNotCompletePersistence(secret: AnySecret, forProvenance provenance: SigningRequestProvenance) async {
await holder.didNotCompletePersistence(secret: secret, forProvenance: provenance)
}
}

View File

@@ -17,6 +17,6 @@ public protocol SigningWitness: Sendable {
/// - secret: The `Secret` that will was used to sign the request.
/// - store: The `Store` that signed the request..
/// - provenance: A `SigningRequestProvenance` object describing the origin of the request.
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, offerPersistence: Bool) async throws
}

View File

@@ -39,7 +39,7 @@ public struct SocketController {
Task { @MainActor [fileHandle, sessionsContinuation, logger] in
// Create the sequence before triggering the notification to
// ensure it will not be missed.
let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted)
let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted, object: fileHandle)
fileHandle.acceptConnectionInBackgroundAndNotify()

View File

@@ -1,95 +0,0 @@
import LocalAuthentication
/// A context describing a persisted authentication.
package struct PersistentAuthenticationContext<SecretType: Secret>: PersistedAuthenticationContext {
/// The Secret to persist authentication for.
let secret: SecretType
/// The LAContext used to authorize the persistent context.
package nonisolated(unsafe) let context: LAContext
/// An expiration date for the context.
/// - Note - Monotonic time instead of Date() to prevent people setting the clock back.
let monotonicExpiration: UInt64
/// Initializes a context.
/// - Parameters:
/// - secret: The Secret to persist authentication for.
/// - context: The LAContext used to authorize the persistent context.
/// - duration: The duration of the authorization context, in seconds.
init(secret: SecretType, context: LAContext, duration: TimeInterval) {
self.secret = secret
unsafe self.context = context
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value
self.monotonicExpiration = clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds)
}
/// A boolean describing whether or not the context is still valid.
package var valid: Bool {
clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration
}
package var expiration: Date {
let remainingNanoseconds = monotonicExpiration - clock_gettime_nsec_np(CLOCK_MONOTONIC)
let remainingInSeconds = Measurement(value: Double(remainingNanoseconds), unit: UnitDuration.nanoseconds).converted(to: .seconds).value
return Date(timeIntervalSinceNow: remainingInSeconds)
}
}
struct ScopedPersistentAuthenticationContext<SecretType: Secret>: Hashable {
let provenance: SigningRequestProvenance
let secret: SecretType
}
package actor PersistentAuthenticationHandler<SecretType: Secret>: Sendable {
private var unscopedPersistedAuthenticationContexts: [SecretType: PersistentAuthenticationContext<SecretType>] = [:]
private var scopedPersistedAuthenticationContexts: [ScopedPersistentAuthenticationContext<SecretType>: PersistentAuthenticationContext<SecretType>] = [:]
package init() {
}
package func existingPersistedAuthenticationContext(secret: SecretType, provenance: SigningRequestProvenance) -> PersistentAuthenticationContext<SecretType>? {
if let unscopedPersistence = unscopedPersistedAuthenticationContexts[secret], unscopedPersistence.valid {
return unscopedPersistence
}
if let scopedPersistence = scopedPersistedAuthenticationContexts[.init(provenance: provenance, secret: secret)], scopedPersistence.valid {
return scopedPersistence
}
return nil
}
package func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws {
let newContext = LAContext()
newContext.touchIDAuthenticationAllowableReuseDuration = duration
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day]
let durationString = formatter.string(from: duration)!
newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
guard success else { return }
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
unscopedPersistedAuthenticationContexts[secret] = context
}
package func persistAuthentication(secret: SecretType, provenance: SigningRequestProvenance) async throws {
let newContext = LAContext()
// FIXME: TEMPORARY
let duration: TimeInterval = 10000
newContext.touchIDAuthenticationAllowableReuseDuration = duration
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
newContext.localizedReason = "Batch requests"
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
guard success else { return }
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
scopedPersistedAuthenticationContexts[.init(provenance: provenance, secret: secret)] = context
}
}

View File

@@ -31,7 +31,7 @@ SecretKit is a collection of protocols describing secrets and stores.
### Authentication Persistence
- ``PersistedAuthenticationContext``
- ``AuthenticationContextProtocol``
### Errors

View File

@@ -1,4 +1,5 @@
import Foundation
import LocalAuthentication
/// Type eraser for SecretStore.
open class AnySecretStore: SecretStore, @unchecked Sendable {
@@ -8,10 +9,7 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
private let _id: @Sendable () -> UUID
private let _name: @MainActor @Sendable () -> String
private let _secrets: @MainActor @Sendable () -> [AnySecret]
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance) async throws -> Data
private let _existingPersistedAuthenticationContext: @Sendable (AnySecret, SigningRequestProvenance) async -> PersistedAuthenticationContext?
private let _persistAuthenticationForDuration: @Sendable (AnySecret, TimeInterval) async throws -> Void
private let _persistAuthenticationForProvenance: @Sendable (AnySecret, SigningRequestProvenance) async throws -> Void
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance, LAContext?) async throws -> Data
private let _reloadSecrets: @Sendable () async -> Void
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
@@ -20,10 +18,7 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
_name = { secretStore.name }
_id = { secretStore.id }
_secrets = { secretStore.secrets.map { AnySecret($0) } }
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
_existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType, provenance: $1) }
_persistAuthenticationForDuration = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
_persistAuthenticationForProvenance = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forProvenance: $1) }
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2, context: $3) }
_reloadSecrets = { await secretStore.reloadSecrets() }
}
@@ -43,20 +38,8 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
return _secrets()
}
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) async throws -> Data {
try await _sign(data, secret, provenance)
}
public func existingPersistedAuthenticationContext(secret: AnySecret, provenance: SigningRequestProvenance) async -> PersistedAuthenticationContext? {
await _existingPersistedAuthenticationContext(secret, provenance)
}
public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) async throws {
try await _persistAuthenticationForDuration(secret, duration)
}
public func persistAuthentication(secret: AnySecret, forProvenance provenance: SigningRequestProvenance) async throws {
try await _persistAuthenticationForProvenance(secret, provenance)
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data {
try await _sign(data, secret, provenance, context)
}
public func reloadSecrets() async {

View File

@@ -0,0 +1,41 @@
import Foundation
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
}
public struct SignatureRequest: Identifiable, Hashable, Sendable, Comparable {
public let id: UUID
public let date: Date
public let secret: AnySecret
public let provenance: SigningRequestProvenance
public init(secret: AnySecret, provenance: SigningRequestProvenance) {
self.id = UUID()
self.date = Date()
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

@@ -1,9 +0,0 @@
import Foundation
/// Protocol describing a persisted 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 PersistedAuthenticationContext: Sendable {
/// Whether the context remains valid.
var valid: Bool { get }
/// The date at which the authorization expires and the context becomes invalid.
var expiration: Date { get }
}

View File

@@ -1,4 +1,5 @@
import Foundation
import LocalAuthentication
/// Manages access to Secrets, and performs signature operations on data using those Secrets.
public protocol SecretStore<SecretType>: Identifiable, Sendable {
@@ -20,22 +21,7 @@ public protocol SecretStore<SecretType>: Identifiable, Sendable {
/// - secret: The ``Secret`` to sign with.
/// - provenance: A ``SigningRequestProvenance`` describing where the request came from.
/// - Returns: The signed data.
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) async throws -> Data
/// Checks to see if there is currently a valid persisted authentication for a given secret.
/// - Parameters:
/// - secret: The ``Secret`` to check if there is a persisted authentication for.
/// - Returns: A persisted authentication context, if a valid one exists.
func existingPersistedAuthenticationContext(secret: SecretType, provenance: SigningRequestProvenance) async -> PersistedAuthenticationContext?
/// Persists user authorization for access to a secret.
/// - Parameters:
/// - secret: The ``Secret`` to persist the authorization for.
/// - duration: The duration that the authorization should persist for.
/// - Note: This is used for temporarily unlocking access to a secret which would otherwise require authentication every single use. This is useful for situations where the user anticipates several rapid accesses to a authorization-guarded secret.
func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws
func persistAuthentication(secret: SecretType, forProvenance provenance: SigningRequestProvenance) async throws
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data
/// Requests that the store reload secrets from any backing store, if neccessary.
func reloadSecrets() async

View File

@@ -2,13 +2,23 @@ import Foundation
import AppKit
/// Describes the chain of applications that requested a signature operation.
public struct SigningRequestProvenance: Equatable, Sendable, Hashable {
public struct SigningRequestProvenance: Hashable, Sendable {
/// A list of processes involved in the request.
/// - Note: A chain will typically consist of many elements even for a simple request. For example, running `git fetch` in Terminal.app would generate a request chain of `ssh` -> `git` -> `zsh` -> `login` -> `Terminal.app`
public var chain: [Process]
public init(root: Process) {
public var date: Date
public init(root: Process, date: Date = .now) {
self.chain = [root]
self.date = date
}
public var batchID: Int {
var hasher = Hasher()
chain.map(\.path).hash(into: &hasher)
return hasher.finalize()
}
}
@@ -25,16 +35,12 @@ extension SigningRequestProvenance {
chain.allSatisfy { $0.validSignature }
}
public func isSameProvenance(as other: SigningRequestProvenance) -> Bool {
zip(chain, other.chain).allSatisfy { $0.isSameProcess(as: $1) }
}
}
extension SigningRequestProvenance {
/// Describes a process in a `SigningRequestProvenance` chain.
public struct Process: Equatable, Sendable, Hashable {
public struct Process: Hashable, Sendable {
/// The pid of the process.
public let pid: Int32
@@ -75,15 +81,6 @@ extension SigningRequestProvenance {
appName ?? processName
}
// Whether the
public func isSameProcess(as other: Process) -> Bool {
processName == other.processName &&
appName == other.appName &&
iconURL == other.iconURL &&
path == other.path &&
validSignature == other.validSignature
}
}
}

View File

@@ -17,7 +17,6 @@ extension SecureEnclave {
}
public let id = UUID()
public let name = String(localized: .secureEnclave)
private let persistentAuthenticationHandler = PersistentAuthenticationHandler<Secret>()
/// Initializes a Store.
@MainActor public init() {
@@ -37,16 +36,7 @@ extension SecureEnclave {
// MARK: SecretStore
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
var context: LAContext
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret, provenance: provenance) {
context = unsafe existing.context
} else {
let newContext = LAContext()
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
context = newContext
}
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data {
let queryAttributes = KeychainDictionary([
kSecClass: Constants.keyClass,
@@ -88,18 +78,6 @@ extension SecureEnclave {
}
public func existingPersistedAuthenticationContext(secret: Secret, provenance: SigningRequestProvenance) async -> PersistedAuthenticationContext? {
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret, provenance: provenance)
}
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
}
public func persistAuthentication(secret: SecureEnclave.Secret, forProvenance provenance: SigningRequestProvenance) async throws {
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, provenance: provenance)
}
@MainActor public func reloadSecrets() {
let before = secrets
secrets.removeAll()

View File

@@ -34,7 +34,6 @@ extension SmartCard {
public var secrets: [Secret] {
state.secrets
}
private let persistentAuthenticationHandler = PersistentAuthenticationHandler<Secret>()
/// Initializes a Store.
public init() {
@@ -57,23 +56,15 @@ extension SmartCard {
// MARK: Public API
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
public func sign(data: Data, with secret: SmartCard.Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data {
guard let tokenID = await state.tokenID else { fatalError() }
var context: LAContext
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret, provenance: provenance) {
context = unsafe existing.context
} else {
let newContext = LAContext()
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
context = newContext
}
let attributes = KeychainDictionary([
kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecAttrApplicationLabel: secret.id as CFData,
kSecAttrTokenID: tokenID,
kSecUseAuthenticationContext: context,
kSecUseAuthenticationContext: context!, // FIXME: THIS
kSecReturnRef: true
])
var untyped: CFTypeRef?
@@ -93,18 +84,6 @@ extension SmartCard {
return signature as Data
}
public func existingPersistedAuthenticationContext(secret: Secret, provenance: SigningRequestProvenance) async -> PersistedAuthenticationContext? {
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret, provenance: provenance)
}
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
}
public func persistAuthentication(secret: Secret, forProvenance provenance: SigningRequestProvenance) async throws {
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, provenance: provenance)
}
/// Reloads all secrets from the store.
@MainActor public func reloadSecrets() {
reloadSecretsInternal()

View File

@@ -49,7 +49,7 @@ extension Stub {
print("Public Key OpenSSH: \(OpenSSHPublicKeyWriter().openSSHString(secret: secret))")
}
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, context: AuthenticationContextProtocol?) throws -> Data {
guard !shouldThrow else {
throw NSError(domain: "test", code: 0, userInfo: nil)
}
@@ -57,7 +57,7 @@ extension Stub {
return try privateKey.signature(for: data).rawRepresentation
}
public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? {
public func existingAuthenticationContextProtocol(secret: Stub.Secret) -> AuthenticationContextProtocol? {
nil
}

View File

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

View File

@@ -1,73 +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 publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory)
private lazy var agent: Agent = {
Agent(storeList: storeList, 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 {
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)
}
}
}
}
}

View File

@@ -0,0 +1,58 @@
import SwiftUI
import SecretKit
import SecretAgentKit
import SmartCardSecretKit
struct BatchedRequestsView: View {
let pending: [[SignatureRequest]]
let review: (Set<SignatureRequest>) async throws -> Void
init(pending: [[SignatureRequest]], review: @escaping (Set<SignatureRequest>) 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)
}
}

View File

@@ -5,6 +5,8 @@ import SecretKit
import SecretAgentKit
import Brief
typealias PersistAction = (@Sendable (AnySecret, TimeInterval) async throws -> Void)
final class Notifier: Sendable {
private let notificationDelegate = NotificationDelegate()
@@ -15,6 +17,12 @@ final class Notifier: Sendable {
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory])
UNUserNotificationCenter.current().delegate = notificationDelegate
}
func registerPersistenceHandler(action: @escaping PersistAction) async {
let rawDurations = [
Measurement(value: 1, unit: UnitDuration.minutes),
Measurement(value: 5, unit: UnitDuration.minutes),
@@ -24,11 +32,9 @@ final class Notifier: Sendable {
let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: .persistAuthenticationDeclineButton), options: [])
var allPersistenceActions = [doNotPersistAction]
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day]
var identifiers: [String: TimeInterval] = [:]
for duration in rawDurations {
let seconds = duration.converted(to: .seconds).value
@@ -43,16 +49,11 @@ final class Notifier: Sendable {
if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) {
persistAuthenticationCategory.setValue(String(localized: .persistAuthenticationAcceptButton), forKey: "_actionsMenuTitle")
}
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
UNUserNotificationCenter.current().delegate = notificationDelegate
Task {
await notificationDelegate.state.setPersistenceState(options: identifiers) { secret, store, duration in
guard let duration = duration else { return }
try? await store.persistAuthentication(secret: secret, forDuration: duration)
}
}
var categories = await UNUserNotificationCenter.current().notificationCategories()
categories.insert(persistAuthenticationCategory)
UNUserNotificationCenter.current().setNotificationCategories(categories)
await notificationDelegate.state.setPersistenceState(options: identifiers, action: action)
}
func prompt() {
@@ -60,7 +61,7 @@ final class Notifier: Sendable {
notificationCenter.requestAuthorization(options: .alert) { _, _ in }
}
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async {
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, offerPersistence: Bool) async {
await notificationDelegate.state.setPending(secret: secret, store: store)
let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent()
@@ -69,7 +70,7 @@ final class Notifier: Sendable {
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
notificationContent.interruptionLevel = .timeSensitive
if await store.existingPersistedAuthenticationContext(secret: secret, provenance: provenance) == nil && secret.authenticationRequirement.required {
if offerPersistence {
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
}
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
@@ -79,25 +80,6 @@ final class Notifier: Sendable {
try? await notificationCenter.add(request)
}
func notify(pendingAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async {
await notificationDelegate.state.setPending(secret: secret, store: store)
let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "pending" //String(localized: .signedNotificationTitle(appName: provenance.origin.displayName))
notificationContent.subtitle = "pending" //String(localized: .signedNotificationDescription(secretName: secret.name))
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
notificationContent.interruptionLevel = .timeSensitive
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
notificationContent.threadIdentifier = "\(secret.id)_\(provenance.hashValue)"
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
notificationContent.attachments = [attachment]
}
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
try? await notificationCenter.add(request)
}
func notify(update: Release, ignore: (@Sendable (Release) async -> Void)?) async {
await notificationDelegate.state.prepareForNotification(release: update, ignoreAction: ignore)
let notificationCenter = UNUserNotificationCenter.current()
@@ -122,12 +104,8 @@ extension Notifier: SigningWitness {
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
}
func witness(pendingAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
await notify(pendingAccessTo: secret, from: store, by: provenance)
}
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
await notify(accessTo: secret, from: store, by: provenance)
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, offerPersistence: Bool) async throws {
await notify(accessTo: secret, from: store, by: provenance, offerPersistence: offerPersistence)
}
}
@@ -156,28 +134,24 @@ extension Notifier {
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
fileprivate actor State {
typealias PersistAction = (@Sendable (AnySecret, AnySecretStore, TimeInterval?) async -> Void)
typealias IgnoreAction = (@Sendable (Release) async -> Void)
fileprivate var release: Release?
fileprivate var ignoreAction: IgnoreAction?
fileprivate var persistAction: PersistAction?
fileprivate var persistOptions: [String: TimeInterval] = [:]
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
func setPending(secret: AnySecret, store: AnySecretStore) {
pendingPersistableSecrets[secret.id.description] = secret
pendingPersistableStores[store.id.description] = store
}
func retrievePending(secretID: String, storeID: String, optionID: String) -> (AnySecret, AnySecretStore, TimeInterval)? {
func retrievePending(secretID: String, optionID: String) -> (AnySecret, TimeInterval)? {
guard let secret = pendingPersistableSecrets[secretID],
let store = pendingPersistableStores[storeID],
let options = persistOptions[optionID] else {
return nil
}
pendingPersistableSecrets.removeValue(forKey: secretID)
return (secret, store, options)
return (secret, options)
}
func setPersistenceState(options: [String: TimeInterval], action: @escaping PersistAction) {
@@ -225,13 +199,12 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Se
}
func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String,
let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String else {
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String else {
return
}
let optionID = response.actionIdentifier
guard let (secret, store, persistOptions) = await state.retrievePending(secretID: secretID, storeID: storeID, optionID: optionID) else { return }
await state.persistAction?(secret, store, persistOptions)
guard let (secret, persistOptions) = await state.retrievePending(secretID: secretID, optionID: optionID) else { return }
try? await state.persistAction?(secret, persistOptions)
}

View File

@@ -16,10 +16,10 @@
<string>1</string>
<key>com.apple.security.hardened-process.hardened-heap</key>
<true/>
<key>com.apple.security.smartcard</key>
<true/>
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
<string>2</string>
<key>com.apple.security.smartcard</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>

View File

@@ -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 = "<group>"; };
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
50020BAF24064869003D4025 /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
500666D02F04786900328939 /* SecretiveUpdater.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretiveUpdater.entitlements; sourceTree = "<group>"; };
500666D12F04787200328939 /* SecretAgentInputParser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgentInputParser.entitlements; sourceTree = "<group>"; };
@@ -190,6 +191,7 @@
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
503647472F870B7800977A23 /* BatchedRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchedRequestsView.swift; sourceTree = "<group>"; };
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
@@ -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;

View File

@@ -38,11 +38,11 @@ extension Preview {
self.init(secrets: new)
}
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data {
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, context: AuthenticationContextProtocol?) throws -> Data {
return data
}
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
func existingAuthenticationContextProtocol(secret: Preview.Secret) -> AuthenticationContextProtocol? {
nil
}
@@ -82,11 +82,11 @@ extension Preview {
self.init(secrets: new)
}
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data {
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, context: AuthenticationContextProtocol?) throws -> Data {
return data
}
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
func existingAuthenticationContextProtocol(secret: Preview.Secret) -> AuthenticationContextProtocol? {
nil
}