mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-03-06 01:37:22 +01:00
Swift 6 / Concurrency fixes (#578)
* Enable language mode
* WIP
* WIP
* Fix concurrency issues in SmartCardStore
* Switch to SMAppService
* Bump runners
* Base
* Finish Testing migration
* Tweak async for updater
* More
* Backport mutex
* Revert "Backport mutex"
This reverts commit 9b02afb20c.
* WIP
* Reenable
* Fix preview.
* Update package.
* Bump to latest public macOS and Xcode
* Bump back down to 6.1
* Update to Xcode 26.
* Fixed tests.
* More cleanup
* Env fixes
* var->let
* Cleanup
* Persist auth async
* Whitespace.
* Whitespace.
* Cleanup.
* Cleanup
* Redoing locks in actors bc of observable
* Actors.
* .
* Specify b5
* Update package to 6.2
* Fix disabled updater
* Remove preconcurrency warning
* Move updater init
This commit is contained in:
@@ -6,17 +6,18 @@ import SecureEnclaveSecretKit
|
||||
import SmartCardSecretKit
|
||||
import SecretAgentKit
|
||||
import Brief
|
||||
import Observation
|
||||
|
||||
@NSApplicationMain
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
private let storeList: SecretStoreList = {
|
||||
@MainActor private let storeList: SecretStoreList = {
|
||||
let list = SecretStoreList()
|
||||
list.add(store: SecureEnclave.Store())
|
||||
list.add(store: SmartCard.Store())
|
||||
return list
|
||||
}()
|
||||
private let updater = Updater(checkOnLaunch: false)
|
||||
private let updater = Updater(checkOnLaunch: true)
|
||||
private let notifier = Notifier()
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||
private lazy var agent: Agent = {
|
||||
@@ -31,17 +32,26 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
logger.debug("SecretAgent finished launching")
|
||||
DispatchQueue.main.async {
|
||||
self.socketController.handler = self.agent.handle(reader:writer:)
|
||||
Task { @MainActor in
|
||||
socketController.handler = { [agent] reader, writer in
|
||||
await agent.handle(reader: reader, writer: writer)
|
||||
}
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in
|
||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||
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
|
||||
Task {
|
||||
await notifier.notify(update: updater.update!) { release in
|
||||
await updater.ignore(release: release)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import SecretKit
|
||||
import SecretAgentKit
|
||||
import Brief
|
||||
|
||||
class Notifier {
|
||||
final class Notifier: Sendable {
|
||||
|
||||
private let notificationDelegate = NotificationDelegate()
|
||||
|
||||
@@ -29,12 +29,13 @@ class Notifier {
|
||||
formatter.unitsStyle = .spellOut
|
||||
formatter.allowedUnits = [.hour, .minute, .day]
|
||||
|
||||
var identifiers: [String: TimeInterval] = [:]
|
||||
for duration in rawDurations {
|
||||
let seconds = duration.converted(to: .seconds).value
|
||||
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
|
||||
identifiers[identifier] = seconds
|
||||
allPersistenceActions.append(action)
|
||||
}
|
||||
|
||||
@@ -45,9 +46,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? store.persistAuthentication(secret: secret, forDuration: duration)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -57,9 +60,8 @@ class Notifier {
|
||||
notificationCenter.requestAuthorization(options: .alert) { _, _ in }
|
||||
}
|
||||
|
||||
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) {
|
||||
notificationDelegate.pendingPersistableSecrets[secret.id.description] = secret
|
||||
notificationDelegate.pendingPersistableStores[store.id.description] = store
|
||||
func notify(accessTo 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 = String(localized: "signed_notification_title_\(provenance.origin.displayName)")
|
||||
@@ -67,19 +69,18 @@ class Notifier {
|
||||
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
|
||||
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
|
||||
notificationContent.interruptionLevel = .timeSensitive
|
||||
if secret.requiresAuthentication && store.existingPersistedAuthenticationContext(secret: secret) == nil {
|
||||
if await store.existingPersistedAuthenticationContext(secret: secret) == nil && secret.requiresAuthentication {
|
||||
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
|
||||
}
|
||||
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)
|
||||
notificationCenter.add(request, withCompletionHandler: nil)
|
||||
try? await notificationCenter.add(request)
|
||||
}
|
||||
|
||||
func notify(update: Release, ignore: ((Release) -> Void)?) {
|
||||
notificationDelegate.release = update
|
||||
notificationDelegate.ignore = ignore
|
||||
func notify(update: Release, ignore: (@Sendable (Release) async -> Void)?) async {
|
||||
await notificationDelegate.state.prepareForNotification(release: update, ignoreAction: ignore)
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
let notificationContent = UNMutableNotificationContent()
|
||||
if update.critical {
|
||||
@@ -92,18 +93,18 @@ class Notifier {
|
||||
notificationContent.body = update.body
|
||||
notificationContent.categoryIdentifier = update.critical ? Constants.criticalUpdateCategoryIdentitifier : Constants.updateCategoryIdentitifier
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
|
||||
notificationCenter.add(request, withCompletionHandler: nil)
|
||||
try? await notificationCenter.add(request)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Notifier: SigningWitness {
|
||||
|
||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws {
|
||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
|
||||
}
|
||||
|
||||
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws {
|
||||
notify(accessTo: 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -129,55 +130,91 @@ extension Notifier {
|
||||
|
||||
}
|
||||
|
||||
class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
||||
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
|
||||
|
||||
fileprivate var release: Release?
|
||||
fileprivate var ignore: ((Release) -> Void)?
|
||||
fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) -> Void)?
|
||||
fileprivate var persistOptions: [String: TimeInterval] = [:]
|
||||
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
|
||||
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
|
||||
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)? {
|
||||
guard let secret = pendingPersistableSecrets[secretID],
|
||||
let store = pendingPersistableStores[storeID],
|
||||
let options = persistOptions[optionID] else {
|
||||
return nil
|
||||
}
|
||||
pendingPersistableSecrets.removeValue(forKey: secretID)
|
||||
return (secret, store, options)
|
||||
}
|
||||
|
||||
func setPersistenceState(options: [String: TimeInterval], action: @escaping PersistAction) {
|
||||
self.persistOptions = options
|
||||
self.persistAction = action
|
||||
}
|
||||
|
||||
func prepareForNotification(release: Release, ignoreAction: IgnoreAction?) {
|
||||
self.release = release
|
||||
self.ignoreAction = ignoreAction
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fileprivate let state = State()
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
||||
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
|
||||
let category = response.notification.request.content.categoryIdentifier
|
||||
switch category {
|
||||
case Notifier.Constants.updateCategoryIdentitifier:
|
||||
handleUpdateResponse(response: response)
|
||||
await handleUpdateResponse(response: response)
|
||||
case Notifier.Constants.persistAuthenticationCategoryIdentitifier:
|
||||
handlePersistAuthenticationResponse(response: response)
|
||||
await handlePersistAuthenticationResponse(response: response)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
func handleUpdateResponse(response: UNNotificationResponse) {
|
||||
guard let update = release else { return }
|
||||
switch response.actionIdentifier {
|
||||
func handleUpdateResponse(response: UNNotificationResponse) async {
|
||||
let id = response.actionIdentifier
|
||||
guard let update = await state.release else { return }
|
||||
switch id {
|
||||
case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
|
||||
NSWorkspace.shared.open(update.html_url)
|
||||
case Notifier.Constants.ignoreActionIdentitifier:
|
||||
ignore?(update)
|
||||
await state.ignoreAction?(update)
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
func handlePersistAuthenticationResponse(response: UNNotificationResponse) {
|
||||
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
|
||||
persistAuthentication?(secret, store, persistOptions[response.actionIdentifier])
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.smartcard</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
|
||||
Reference in New Issue
Block a user