From 576e625b8f425ef1eb83bba9a3ce6fc4eae3dd4e Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 5 Jan 2025 16:07:11 -0800 Subject: [PATCH] More --- .../Sources/Brief/UpdaterProtocol.swift | 2 +- .../Sources/SecretAgentKit/Agent.swift | 2 +- .../SecretAgentKit/SigningWitness.swift | 2 +- .../OpenSSH/OpenSSHCertificateHandler.swift | 20 +++-- .../SecretKit/OpenSSH/OpenSSHKeyWriter.swift | 2 +- .../PublicKeyStandinFileController.swift | 2 +- .../Sources/SecretKit/SecretStoreList.swift | 34 +++++-- Sources/SecretAgent/AppDelegate.swift | 32 ++++--- Sources/SecretAgent/Notifier.swift | 88 ++++++++++++------- Sources/Secretive/App.swift | 21 +++-- .../Controllers/AgentStatusChecker.swift | 7 +- Sources/Secretive/Views/ContentView.swift | 8 +- Sources/Secretive/Views/StoreListView.swift | 2 +- Sources/Secretive/Views/UpdateView.swift | 2 +- 14 files changed, 147 insertions(+), 77 deletions(-) diff --git a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift index c4da349..acbf5b9 100644 --- a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift +++ b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift @@ -2,7 +2,7 @@ import Foundation import Synchronization /// A protocol for retreiving the latest available version of an app. -public protocol UpdaterProtocol: ObservableObject { +public protocol UpdaterProtocol: Observable { /// The latest update var update: Release? { get } diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index c2b0044..cf3fa4f 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -5,7 +5,7 @@ import SecretKit import AppKit /// The `Agent` is an implementation of an SSH agent. It manages coordination and access between a socket, traces requests, notifies witnesses and passes requests to stores. -public final class Agent { +public final class Agent: Sendable { private let storeList: SecretStoreList private let witness: SigningWitness? diff --git a/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift b/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift index 2527fef..2e6ab49 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift @@ -2,7 +2,7 @@ import Foundation import SecretKit /// A protocol that allows conformers to be notified of access to secrets, and optionally prevent access. -public protocol SigningWitness { +public protocol SigningWitness: Sendable { /// A ridiculously named method that notifies the callee that a signing operation is about to be performed using a secret. The callee may `throw` an `Error` to prevent access from occurring. /// - Parameters: diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift index 5066545..d8345ba 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift @@ -1,13 +1,14 @@ import Foundation import OSLog +import Synchronization /// Manages storage and lookup for OpenSSH certificates. -public final class OpenSSHCertificateHandler { +public final class OpenSSHCertificateHandler: Sendable { private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler") private let writer = OpenSSHKeyWriter() - private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:] + private let keyBlobsAndNames: Mutex<[AnySecret: (Data, Data)]> = .init([:]) /// Initializes an OpenSSHCertificateHandler. public init() { @@ -20,8 +21,10 @@ public final class OpenSSHCertificateHandler { logger.log("No certificates, short circuiting") return } - keyBlobsAndNames = secrets.reduce(into: [:]) { partialResult, next in - partialResult[next] = try? loadKeyblobAndName(for: next) + keyBlobsAndNames.withLock { + $0 = secrets.reduce(into: [:]) { partialResult, next in + partialResult[next] = try? loadKeyblobAndName(for: next) + } } } @@ -29,7 +32,10 @@ public final class OpenSSHCertificateHandler { /// - Parameter secret: The secret to check for a certificate. /// - Returns: A boolean describing whether or not the certificate handler has a certifiicate associated with a given secret public func hasCertificate(for secret: SecretType) -> Bool { - keyBlobsAndNames[AnySecret(secret)] != nil + keyBlobsAndNames.withLock { + $0[AnySecret(secret)] != nil + } + } @@ -61,7 +67,9 @@ public final class OpenSSHCertificateHandler { /// - Parameter secret: The secret to search for a certificate with /// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively. public func keyBlobAndName(for secret: SecretType) throws -> (Data, Data)? { - keyBlobsAndNames[AnySecret(secret)] + keyBlobsAndNames.withLock { + $0[AnySecret(secret)] + } } /// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret`` diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift index da8c4b1..cca64df 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift @@ -2,7 +2,7 @@ import Foundation import CryptoKit /// Generates OpenSSH representations of Secrets. -public struct OpenSSHKeyWriter { +public struct OpenSSHKeyWriter: Sendable { /// Initializes the writer. public init() { diff --git a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift index 7c3f8da..736c0f8 100644 --- a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift +++ b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift @@ -2,7 +2,7 @@ import Foundation import OSLog /// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts. -public final class PublicKeyFileStoreController { +public final class PublicKeyFileStoreController: Sendable { private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController") private let directory: String diff --git a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift index af0dc9e..b2ef3b2 100644 --- a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift +++ b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift @@ -1,13 +1,21 @@ import Foundation import Observation +import Synchronization /// A "Store Store," which holds a list of type-erased stores. -@Observable public final class SecretStoreList: ObservableObject { +@Observable public final class SecretStoreList: Sendable { /// The Stores managed by the SecretStoreList. - public var stores: [AnySecretStore] = [] + public var stores: [AnySecretStore] { + __stores.withLock { $0 } + } + private let __stores: Mutex<[AnySecretStore]> = .init([]) + /// A modifiable store, if one is available. - public var modifiableStore: AnySecretStoreModifiable? + public var modifiableStore: AnySecretStoreModifiable? { + __modifiableStore.withLock { $0 } + } + private let __modifiableStore: Mutex = .init(nil) /// Initializes a SecretStoreList. public init() { @@ -15,23 +23,33 @@ import Observation /// Adds a non-type-erased SecretStore to the list. public func add(store: SecretStoreType) { - stores.append(AnySecretStore(store)) + __stores.withLock { + $0.append(AnySecretStore(store)) + } } /// Adds a non-type-erased modifiable SecretStore. public func add(store: SecretStoreType) { let modifiable = AnySecretStoreModifiable(modifiable: store) - modifiableStore = modifiable - stores.append(modifiable) + __modifiableStore.withLock { + $0 = modifiable + } + __stores.withLock { + $0.append(modifiable) + } } /// A boolean describing whether there are any Stores available. public var anyAvailable: Bool { - stores.reduce(false, { $0 || $1.isAvailable }) + __stores.withLock { + $0.reduce(false, { $0 || $1.isAvailable }) + } } public var allSecrets: [AnySecret] { - stores.flatMap(\.secrets) + __stores.withLock { + $0.flatMap(\.secrets) + } } } diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index 13b0a9b..abc0543 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -6,6 +6,7 @@ import SecureEnclaveSecretKit import SmartCardSecretKit import SecretAgentKit import Brief +import Observation @main class AppDelegate: NSObject, NSApplicationDelegate { @@ -31,19 +32,28 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { logger.debug("SecretAgent finished launching") -// DispatchQueue.main.async { -// self.socketController.handler = self.agent.handle(reader:writer:) -// } -// NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in -// try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) -// } + Task { @MainActor in + socketController.handler = { [agent] reader, writer in + await agent.handle(reader: reader, writer: writer) + } + } + 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() -// updateSink = updater.$update.sink { update in -// guard let update = update else { return } -// self.notifier.notify(update: update, ignore: self.updater.ignore(release:)) -// } + _ = withObservationTracking { + updater.update + } onChange: { [updater, notifier] in + notifier.notify(update: updater.update!) { release in + Task { + await updater.ignore(release: release) + } + } + } } - + } diff --git a/Sources/SecretAgent/Notifier.swift b/Sources/SecretAgent/Notifier.swift index 5b1fe44..2f0e15a 100644 --- a/Sources/SecretAgent/Notifier.swift +++ b/Sources/SecretAgent/Notifier.swift @@ -4,8 +4,9 @@ import AppKit import SecretKit import SecretAgentKit import Brief +import Synchronization -class Notifier { +final class Notifier: Sendable { private let notificationDelegate = NotificationDelegate() @@ -34,7 +35,9 @@ class Notifier { guard let string = formatter.string(from: seconds)?.capitalized else { continue } let identifier = Constants.persistAuthenticationCategoryIdentitifier.appending("\(seconds)") let action = UNNotificationAction(identifier: identifier, title: string, options: []) - notificationDelegate.persistOptions[identifier] = seconds + notificationDelegate.state.withLock { state in + state.persistOptions[identifier] = seconds + } allPersistenceActions.append(action) } @@ -45,9 +48,11 @@ class Notifier { UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory]) UNUserNotificationCenter.current().delegate = notificationDelegate - notificationDelegate.persistAuthentication = { secret, store, duration in - guard let duration = duration else { return } - try? await store.persistAuthentication(secret: secret, forDuration: duration) + notificationDelegate.state.withLock { state in + state.persistAuthentication = { secret, store, duration in + guard let duration = duration else { return } + try? await store.persistAuthentication(secret: secret, forDuration: duration) + } } } @@ -58,8 +63,10 @@ class Notifier { } func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async { - notificationDelegate.pendingPersistableSecrets[secret.id.description] = secret - notificationDelegate.pendingPersistableStores[store.id.description] = store + notificationDelegate.state.withLock { state in + state.pendingPersistableSecrets[secret.id.description] = secret + state.pendingPersistableStores[store.id.description] = store + } let notificationCenter = UNUserNotificationCenter.current() let notificationContent = UNMutableNotificationContent() notificationContent.title = String(localized: "signed_notification_title_\(provenance.origin.displayName)") @@ -74,12 +81,14 @@ class Notifier { notificationContent.attachments = [attachment] } let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil) - notificationCenter.add(request, withCompletionHandler: nil) + try? await notificationCenter.add(request) } func notify(update: Release, ignore: ((Release) -> Void)?) { - notificationDelegate.release = update - notificationDelegate.ignore = ignore + notificationDelegate.state.withLock { [update] state in + state.release = update +// state.ignore = ignore + } let notificationCenter = UNUserNotificationCenter.current() let notificationContent = UNMutableNotificationContent() if update.critical { @@ -129,15 +138,21 @@ extension Notifier { } -class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { - - fileprivate var release: Release? - fileprivate var ignore: ((Release) -> Void)? - fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) async -> Void)? - fileprivate var persistOptions: [String: TimeInterval] = [:] - fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:] - fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:] +final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable { + struct State { + typealias PersistAuthentication = ((AnySecret, AnySecretStore, TimeInterval?) async -> Void) + typealias Ignore = ((Release) -> Void) + fileprivate var release: Release? + fileprivate var ignore: Ignore? + fileprivate var persistAuthentication: PersistAuthentication? + fileprivate var persistOptions: [String: TimeInterval] = [:] + fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:] + fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:] + } + + fileprivate let state: Mutex = .init(.init()) + func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { } @@ -155,27 +170,34 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { } func handleUpdateResponse(response: UNNotificationResponse) { - guard let update = release else { return } - switch response.actionIdentifier { - case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier: - NSWorkspace.shared.open(update.html_url) - case Notifier.Constants.ignoreActionIdentitifier: - ignore?(update) - default: - fatalError() + state.withLock { state in + guard let update = state.release else { return } + switch response.actionIdentifier { + case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier: + NSWorkspace.shared.open(update.html_url) + case Notifier.Constants.ignoreActionIdentitifier: + state.ignore?(update) + default: + fatalError() + } } } func handlePersistAuthenticationResponse(response: UNNotificationResponse) async { - guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String, let secret = pendingPersistableSecrets[secretID], - let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String, let store = pendingPersistableStores[storeID] - else { return } - pendingPersistableSecrets[secretID] = nil - await persistAuthentication?(secret, store, persistOptions[response.actionIdentifier]) +// let (secret, store, persistOptions, callback): (AnySecret?, AnySecretStore?, TimeInterval?, State.PersistAuthentication?) = state.withLock { state in +// guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String, let secret = state.pendingPersistableSecrets[secretID], +// let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String, let store = state.pendingPersistableStores[storeID] +// else { return (nil, nil, nil, nil) } +// state.pendingPersistableSecrets[secretID] = nil +// return (secret, store, state.persistOptions[response.actionIdentifier], state.persistAuthentication) +// } +// guard let secret, let store, let persistOptions else { return } +// await callback?(secret, store, persistOptions) } - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.list, .banner]) + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + [.list, .banner] } } diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 8d6ed8b..18dad10 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -5,9 +5,20 @@ import SecureEnclaveSecretKit import SmartCardSecretKit import Brief +extension EnvironmentValues { + @Entry var secretStoreList: SecretStoreList = { + let list = SecretStoreList() + list.add(store: SecureEnclave.Store()) + list.add(store: SmartCard.Store()) + return list + }() + @Entry var agentStatusChecker: any AgentStatusCheckerProtocol = AgentStatusChecker() + @Entry var updater: any UpdaterProtocol = Updater(checkOnLaunch: false) +} + @main struct Secretive: App { - + private let storeList: SecretStoreList = { let list = SecretStoreList() list.add(store: SecureEnclave.Store()) @@ -23,10 +34,10 @@ struct Secretive: App { @SceneBuilder var body: some Scene { WindowGroup { - ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup) - .environmentObject(storeList) - .environmentObject(Updater(checkOnLaunch: hasRunSetup)) - .environmentObject(agentStatusChecker) + ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup) + .environment(storeList) + .environment(Updater(checkOnLaunch: hasRunSetup)) + .environment(agentStatusChecker) .onAppear { if !hasRunSetup { showingSetup = true diff --git a/Sources/Secretive/Controllers/AgentStatusChecker.swift b/Sources/Secretive/Controllers/AgentStatusChecker.swift index 8f0602e..daad478 100644 --- a/Sources/Secretive/Controllers/AgentStatusChecker.swift +++ b/Sources/Secretive/Controllers/AgentStatusChecker.swift @@ -2,15 +2,16 @@ import Foundation import Combine import AppKit import SecretKit +import Observation -protocol AgentStatusCheckerProtocol: ObservableObject { +protocol AgentStatusCheckerProtocol: Observable { var running: Bool { get } var developmentBuild: Bool { get } } -class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol { +@Observable class AgentStatusChecker: AgentStatusCheckerProtocol { - @Published var running: Bool = false + var running: Bool = false init() { check() diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index c556e64..77ee96e 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -4,7 +4,7 @@ import SecureEnclaveSecretKit import SmartCardSecretKit import Brief -struct ContentView: View { +struct ContentView: View { @Binding var showingCreation: Bool @Binding var runningSetup: Bool @@ -13,9 +13,9 @@ struct ContentView: View { - @EnvironmentObject var updater: UpdaterType + @Environment(UpdaterType.self) var updater: UpdaterType let update: Release