mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-09-20 03:10:57 +00:00
.
This commit is contained in:
parent
52e61735c9
commit
e9a5729e07
@ -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] = [:]
|
||||
|
@ -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,10 +255,8 @@ extension SecureEnclave.Store {
|
||||
}
|
||||
return SecureEnclave.Secret(id: id, name: name, requiresAuthentication: requiresAuth, publicKey: publicKey)
|
||||
}
|
||||
Task { @MainActor in
|
||||
secrets.append(contentsOf: wrapped)
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves a public key.
|
||||
/// - Parameters:
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,9 +22,6 @@ 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,
|
||||
@ -37,24 +34,21 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 100, idealWidth: 240)
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user