mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-09-20 11:20:57 +00:00
.
This commit is contained in:
parent
52e61735c9
commit
e9a5729e07
@ -3,6 +3,41 @@ import SecretKit
|
|||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actor PersistentAuthenticationHandler: Sendable {
|
actor PersistentAuthenticationHandler: Sendable {
|
||||||
|
|
||||||
private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
|
private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
|
||||||
|
@ -19,9 +19,9 @@ extension SecureEnclave {
|
|||||||
private let persistentAuthenticationHandler = PersistentAuthenticationHandler()
|
private let persistentAuthenticationHandler = PersistentAuthenticationHandler()
|
||||||
|
|
||||||
/// Initializes a Store.
|
/// Initializes a Store.
|
||||||
public init() {
|
@MainActor public init() {
|
||||||
|
loadSecrets()
|
||||||
Task {
|
Task {
|
||||||
await loadSecrets()
|
|
||||||
for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
|
for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
|
||||||
await reloadSecretsInternal(notifyAgent: false)
|
await reloadSecretsInternal(notifyAgent: false)
|
||||||
}
|
}
|
||||||
@ -194,7 +194,7 @@ extension SecureEnclave.Store {
|
|||||||
@MainActor private func reloadSecretsInternal(notifyAgent: Bool = true) async {
|
@MainActor private func reloadSecretsInternal(notifyAgent: Bool = true) async {
|
||||||
let before = secrets
|
let before = secrets
|
||||||
secrets.removeAll()
|
secrets.removeAll()
|
||||||
await loadSecrets()
|
loadSecrets()
|
||||||
if secrets != before {
|
if secrets != before {
|
||||||
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
|
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
|
||||||
if notifyAgent {
|
if notifyAgent {
|
||||||
@ -204,7 +204,7 @@ extension SecureEnclave.Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Loads all secrets from the store.
|
/// Loads all secrets from the store.
|
||||||
private func loadSecrets() async {
|
@MainActor private func loadSecrets() {
|
||||||
let publicAttributes = KeychainDictionary([
|
let publicAttributes = KeychainDictionary([
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrKeyType: SecureEnclave.Constants.keyType,
|
kSecAttrKeyType: SecureEnclave.Constants.keyType,
|
||||||
@ -255,10 +255,8 @@ extension SecureEnclave.Store {
|
|||||||
}
|
}
|
||||||
return SecureEnclave.Secret(id: id, name: name, requiresAuthentication: requiresAuth, publicKey: publicKey)
|
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.
|
/// Saves a public key.
|
||||||
/// - Parameters:
|
/// - 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
|
import Brief
|
||||||
|
|
||||||
extension EnvironmentValues {
|
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()
|
let list = SecretStoreList()
|
||||||
Task { @MainActor in
|
|
||||||
list.add(store: SecureEnclave.Store())
|
list.add(store: SecureEnclave.Store())
|
||||||
list.add(store: SmartCard.Store())
|
list.add(store: SmartCard.Store())
|
||||||
}
|
|
||||||
return list
|
return list
|
||||||
}()
|
}()
|
||||||
@Entry var secretStoreList = _secretStoreList
|
|
||||||
private static let _agentStatusChecker = AgentStatusChecker()
|
private static let _agentStatusChecker = AgentStatusChecker()
|
||||||
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
|
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
|
||||||
private static let _updater: any UpdaterProtocol = Updater(checkOnLaunch: true)
|
private static let _updater: any UpdaterProtocol = Updater(checkOnLaunch: true)
|
||||||
@Entry var updater: any UpdaterProtocol = _updater
|
@Entry var updater: any UpdaterProtocol = _updater
|
||||||
|
|
||||||
|
@MainActor var secretStoreList: SecretStoreList {
|
||||||
|
EnvironmentValues._secretStoreList
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@ -33,8 +37,7 @@ struct Secretive: App {
|
|||||||
@SceneBuilder var body: some Scene {
|
@SceneBuilder var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
||||||
// This one is explicitly injected via environment to support hasRunSetup.
|
.environment(EnvironmentValues._secretStoreList)
|
||||||
// .environment(Updater(checkOnLaunch: hasRunSetup))
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if !hasRunSetup {
|
if !hasRunSetup {
|
||||||
showingSetup = true
|
showingSetup = true
|
||||||
|
@ -13,7 +13,7 @@ struct ContentView: View {
|
|||||||
@State var activeSecret: AnySecret?
|
@State var activeSecret: AnySecret?
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
@Environment(\.secretStoreList) private var storeList: SecretStoreList
|
@Environment(\.secretStoreList) private var storeList
|
||||||
@Environment(\.updater) private var updater: any UpdaterProtocol
|
@Environment(\.updater) private var updater: any UpdaterProtocol
|
||||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ struct StoreListView: View {
|
|||||||
|
|
||||||
@Binding var activeSecret: AnySecret?
|
@Binding var activeSecret: AnySecret?
|
||||||
|
|
||||||
@Environment(\.secretStoreList) private var storeList: SecretStoreList
|
@Environment(\.secretStoreList) private var storeList
|
||||||
|
|
||||||
private func secretDeleted(secret: AnySecret) {
|
private func secretDeleted(secret: AnySecret) {
|
||||||
activeSecret = nextDefaultSecret
|
activeSecret = nextDefaultSecret
|
||||||
@ -22,9 +22,6 @@ struct StoreListView: View {
|
|||||||
ForEach(storeList.stores) { store in
|
ForEach(storeList.stores) { store in
|
||||||
if store.isAvailable {
|
if store.isAvailable {
|
||||||
Section(header: Text(store.name)) {
|
Section(header: Text(store.name)) {
|
||||||
if store.secrets.isEmpty {
|
|
||||||
EmptyStoreView(store: store)
|
|
||||||
} else {
|
|
||||||
ForEach(store.secrets) { secret in
|
ForEach(store.secrets) { secret in
|
||||||
SecretListItemView(
|
SecretListItemView(
|
||||||
store: store,
|
store: store,
|
||||||
@ -37,24 +34,21 @@ struct StoreListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} detail: {
|
} detail: {
|
||||||
if let activeSecret {
|
if let activeSecret {
|
||||||
SecretDetailView(secret: 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 {
|
} else {
|
||||||
EmptyStoreView(store: storeList.modifiableStore ?? storeList.stores.first)
|
EmptyStoreView(store: storeList.modifiableStore ?? storeList.stores.first)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationSplitViewStyle(.balanced)
|
.navigationSplitViewStyle(.balanced)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
withObservationTracking {
|
|
||||||
_ = nextDefaultSecret
|
|
||||||
} onChange: {
|
|
||||||
Task { @MainActor in
|
|
||||||
activeSecret = nextDefaultSecret
|
activeSecret = nextDefaultSecret
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(minWidth: 100, idealWidth: 240)
|
.frame(minWidth: 100, idealWidth: 240)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user