From 0fc4bf46f229e1137687ab2c575f2998cfc46bfd Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 9 Nov 2025 13:07:03 -0800 Subject: [PATCH] Consolidating persistence --- .../PersistentAuthenticationHandler.swift | 69 ++++++++++++++++++ .../PersistentAuthenticationHandler.swift | 70 ------------------- .../SecureEnclaveStore.swift | 2 +- .../PersistentAuthenticationHandler.swift | 68 ------------------ .../SmartCardSecretKit/SmartCardStore.swift | 2 +- 5 files changed, 71 insertions(+), 140 deletions(-) create mode 100644 Sources/Packages/Sources/SecretKit/Convenience/PersistentAuthenticationHandler.swift delete mode 100644 Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift delete mode 100644 Sources/Packages/Sources/SmartCardSecretKit/PersistentAuthenticationHandler.swift diff --git a/Sources/Packages/Sources/SecretKit/Convenience/PersistentAuthenticationHandler.swift b/Sources/Packages/Sources/SecretKit/Convenience/PersistentAuthenticationHandler.swift new file mode 100644 index 0000000..db9f039 --- /dev/null +++ b/Sources/Packages/Sources/SecretKit/Convenience/PersistentAuthenticationHandler.swift @@ -0,0 +1,69 @@ +import LocalAuthentication + +/// A context describing a persisted authentication. +package final class PersistentAuthenticationContext: 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) + } +} + +package actor PersistentAuthenticationHandler: Sendable { + + private var persistedAuthenticationContexts: [SecretType: PersistentAuthenticationContext] = [:] + + package init() { + } + + package func existingPersistedAuthenticationContext(secret: SecretType) -> PersistentAuthenticationContext? { + guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil } + return persisted + } + + 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) + persistedAuthenticationContexts[secret] = context + } + +} + diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift deleted file mode 100644 index 4934c77..0000000 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift +++ /dev/null @@ -1,70 +0,0 @@ -import LocalAuthentication -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 - 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. - 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] = [:] - - func existingPersistedAuthenticationContext(secret: Secret) -> PersistentAuthenticationContext? { - guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil } - return persisted - } - - func persistAuthentication(secret: Secret, 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) - persistedAuthenticationContexts[secret] = context - } - - } - -} diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index 7f2fc55..61fc722 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -17,7 +17,7 @@ extension SecureEnclave { } public let id = UUID() public let name = String(localized: .secureEnclave) - private let persistentAuthenticationHandler = PersistentAuthenticationHandler() + private let persistentAuthenticationHandler = PersistentAuthenticationHandler() /// Initializes a Store. @MainActor public init() { diff --git a/Sources/Packages/Sources/SmartCardSecretKit/PersistentAuthenticationHandler.swift b/Sources/Packages/Sources/SmartCardSecretKit/PersistentAuthenticationHandler.swift deleted file mode 100644 index 0b71dc3..0000000 --- a/Sources/Packages/Sources/SmartCardSecretKit/PersistentAuthenticationHandler.swift +++ /dev/null @@ -1,68 +0,0 @@ -import LocalAuthentication -import SecretKit - -extension SmartCard { - - /// 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 - 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. - 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] = [:] - - func existingPersistedAuthenticationContext(secret: Secret) -> PersistentAuthenticationContext? { - guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil } - return persisted - } - - func persistAuthentication(secret: Secret, 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) - persistedAuthenticationContexts[secret] = context - } - } -} \ No newline at end of file diff --git a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift index cd2b741..77ed5e2 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift @@ -34,7 +34,7 @@ extension SmartCard { public var secrets: [Secret] { state.secrets } - private let persistentAuthenticationHandler = PersistentAuthenticationHandler() + private let persistentAuthenticationHandler = PersistentAuthenticationHandler() /// Initializes a Store. public init() {