From e9a5729e074926f2ecbd5208ed6ec2394815771c Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 17 Aug 2025 11:56:35 -0500 Subject: [PATCH] . --- .../PersistentAuthenticationHandler.swift | 35 +++++++++++++ .../SecureEnclaveStore.swift | 51 ++----------------- Sources/Secretive/App.swift | 19 ++++--- Sources/Secretive/Views/ContentView.swift | 2 +- Sources/Secretive/Views/StoreListView.swift | 32 +++++------- 5 files changed, 65 insertions(+), 74 deletions(-) diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift index c07a102..d42e051 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift @@ -3,6 +3,41 @@ import SecretKit extension SecureEnclave { + /// A context describing a persisted authentication. + final class PersistentAuthenticationContext: PersistedAuthenticationContext { + + /// The Secret to persist authentication for. + let secret: Secret + /// The LAContext used to authorize the persistent context. + 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: Secret, context: LAContext, duration: TimeInterval) { + self.secret = secret + 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. + var valid: Bool { + clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration + } + + 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) + } + } + actor PersistentAuthenticationHandler: Sendable { private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:] diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index 61bf961..302988a 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -19,9 +19,9 @@ extension SecureEnclave { private let persistentAuthenticationHandler = PersistentAuthenticationHandler() /// Initializes a Store. - public init() { + @MainActor public init() { + loadSecrets() Task { - await loadSecrets() for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { await reloadSecretsInternal(notifyAgent: false) } @@ -194,7 +194,7 @@ extension SecureEnclave.Store { @MainActor private func reloadSecretsInternal(notifyAgent: Bool = true) async { let before = secrets secrets.removeAll() - await loadSecrets() + loadSecrets() if secrets != before { NotificationCenter.default.post(name: .secretStoreReloaded, object: self) if notifyAgent { @@ -204,7 +204,7 @@ extension SecureEnclave.Store { } /// Loads all secrets from the store. - private func loadSecrets() async { + @MainActor private func loadSecrets() { let publicAttributes = KeychainDictionary([ kSecClass: kSecClassKey, kSecAttrKeyType: SecureEnclave.Constants.keyType, @@ -255,9 +255,7 @@ extension SecureEnclave.Store { } return SecureEnclave.Secret(id: id, name: name, requiresAuthentication: requiresAuth, publicKey: publicKey) } - Task { @MainActor in - secrets.append(contentsOf: wrapped) - } + secrets.append(contentsOf: wrapped) } /// Saves a public key. @@ -292,42 +290,3 @@ extension SecureEnclave { } } - -extension SecureEnclave { - - /// A context describing a persisted authentication. - final class PersistentAuthenticationContext: PersistedAuthenticationContext { - - /// The Secret to persist authentication for. - let secret: Secret - /// The LAContext used to authorize the persistent context. - 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: Secret, context: LAContext, duration: TimeInterval) { - self.secret = secret - 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. - var valid: Bool { - clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration - } - - 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) - } - } - -} diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 3fb147f..7ebbcde 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -6,19 +6,23 @@ import SmartCardSecretKit import Brief extension EnvironmentValues { - private static let _secretStoreList: SecretStoreList = { + + // This is injected through .environment modifier below instead of @Entry for performance reasons (basially, restrictions around init/mainactor causing delay in loading secrets/"empty screen" blip). + @MainActor fileprivate static let _secretStoreList: SecretStoreList = { let list = SecretStoreList() - Task { @MainActor in - list.add(store: SecureEnclave.Store()) - list.add(store: SmartCard.Store()) - } + list.add(store: SecureEnclave.Store()) + list.add(store: SmartCard.Store()) return list }() - @Entry var secretStoreList = _secretStoreList + private static let _agentStatusChecker = AgentStatusChecker() @Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker private static let _updater: any UpdaterProtocol = Updater(checkOnLaunch: true) @Entry var updater: any UpdaterProtocol = _updater + + @MainActor var secretStoreList: SecretStoreList { + EnvironmentValues._secretStoreList + } } @main @@ -33,8 +37,7 @@ struct Secretive: App { @SceneBuilder var body: some Scene { WindowGroup { ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup) - // This one is explicitly injected via environment to support hasRunSetup. -// .environment(Updater(checkOnLaunch: hasRunSetup)) + .environment(EnvironmentValues._secretStoreList) .onAppear { if !hasRunSetup { showingSetup = true diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index 8bcbe0a..cb21882 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -13,7 +13,7 @@ struct ContentView: View { @State var activeSecret: AnySecret? @Environment(\.colorScheme) var colorScheme - @Environment(\.secretStoreList) private var storeList: SecretStoreList + @Environment(\.secretStoreList) private var storeList @Environment(\.updater) private var updater: any UpdaterProtocol @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol diff --git a/Sources/Secretive/Views/StoreListView.swift b/Sources/Secretive/Views/StoreListView.swift index ac36bc0..c0e6d3e 100644 --- a/Sources/Secretive/Views/StoreListView.swift +++ b/Sources/Secretive/Views/StoreListView.swift @@ -6,7 +6,7 @@ struct StoreListView: View { @Binding var activeSecret: AnySecret? - @Environment(\.secretStoreList) private var storeList: SecretStoreList + @Environment(\.secretStoreList) private var storeList private func secretDeleted(secret: AnySecret) { activeSecret = nextDefaultSecret @@ -22,17 +22,13 @@ struct StoreListView: View { ForEach(storeList.stores) { store in if store.isAvailable { Section(header: Text(store.name)) { - if store.secrets.isEmpty { - EmptyStoreView(store: store) - } else { - ForEach(store.secrets) { secret in - SecretListItemView( - store: store, - secret: secret, - deletedSecret: secretDeleted, - renamedSecret: secretRenamed - ) - } + ForEach(store.secrets) { secret in + SecretListItemView( + store: store, + secret: secret, + deletedSecret: secretDeleted, + renamedSecret: secretRenamed + ) } } } @@ -41,19 +37,17 @@ struct StoreListView: View { } detail: { if let activeSecret { SecretDetailView(secret: activeSecret) + } else if let nextDefaultSecret { + // This just means onAppear hasn't executed yet. + // Do this to avoid a blip. + SecretDetailView(secret: nextDefaultSecret) } else { EmptyStoreView(store: storeList.modifiableStore ?? storeList.stores.first) } } .navigationSplitViewStyle(.balanced) .onAppear { - withObservationTracking { - _ = nextDefaultSecret - } onChange: { - Task { @MainActor in - activeSecret = nextDefaultSecret - } - } + activeSecret = nextDefaultSecret } .frame(minWidth: 100, idealWidth: 240)