diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index fb2574d..b1fcdea 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -27,16 +27,13 @@ let package = Package( .library( name: "Brief", targets: ["Brief"]), - .library( - name: "Common", - targets: ["Common"]), ], dependencies: [ ], targets: [ .target( name: "SecretKit", - dependencies: ["Common"], + dependencies: [], swiftSettings: swiftSettings ), .testTarget( @@ -46,17 +43,17 @@ let package = Package( ), .target( name: "SecureEnclaveSecretKit", - dependencies: ["Common", "SecretKit"], + dependencies: ["SecretKit"], swiftSettings: swiftSettings ), .target( name: "SmartCardSecretKit", - dependencies: ["Common", "SecretKit"], + dependencies: ["SecretKit"], swiftSettings: swiftSettings ), .target( name: "SecretAgentKit", - dependencies: ["Common", "SecretKit", "SecretAgentKitHeaders"], + dependencies: ["SecretKit", "SecretAgentKitHeaders"], swiftSettings: swiftSettings ), .systemLibrary( @@ -68,18 +65,13 @@ let package = Package( , .target( name: "Brief", - dependencies: ["Common"], + dependencies: [], swiftSettings: swiftSettings ), .testTarget( name: "BriefTests", dependencies: ["Brief"] ), - .target( - name: "Common", - dependencies: [], - swiftSettings: swiftSettings - ), ] ) diff --git a/Sources/Packages/Sources/Brief/Updater.swift b/Sources/Packages/Sources/Brief/Updater.swift index 0b30327..4c0c6c5 100644 --- a/Sources/Packages/Sources/Brief/Updater.swift +++ b/Sources/Packages/Sources/Brief/Updater.swift @@ -1,15 +1,18 @@ import Foundation import Observation -import os -import Common /// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version. @Observable public final class Updater: UpdaterProtocol, Sendable { - public var update: Release? { - _update.lockedValue + private let state = State() + @MainActor @Observable public final class State { + var update: Release? = nil + nonisolated init() {} } - private let _update: OSAllocatedUnfairLock = .init(uncheckedState: nil) + public var update: Release? { + state.update + } + public let testBuild: Bool /// The current OS version. @@ -23,7 +26,12 @@ import Common /// - checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour. /// - osVersion: The current OS version. /// - currentVersion: The current version of the app that is running. - public init(checkOnLaunch: Bool, checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) { + public init( + checkOnLaunch: Bool, + checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value, + osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), + currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0") + ) { self.osVersion = osVersion self.currentVersion = currentVersion testBuild = currentVersion == SemVer("0.0.0") @@ -54,7 +62,7 @@ import Common guard !release.critical else { return } defaults.set(true, forKey: release.name) await MainActor.run { - _update.lockedValue = nil + state.update = nil } } @@ -75,7 +83,8 @@ extension Updater { let latestVersion = SemVer(release.name) if latestVersion > currentVersion { await MainActor.run { - _update.lockedValue = release + print("SET \(release)") + state.update = release } } } diff --git a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift index 04f96d7..1077e84 100644 --- a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift +++ b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift @@ -5,7 +5,7 @@ import os public protocol UpdaterProtocol: Observable, Sendable { /// The latest update - var update: Release? { get } + @MainActor var update: Release? { get } /// A boolean describing whether or not the current build of the app is a "test" build (ie, a debug build or otherwise special build) var testBuild: Bool { get } diff --git a/Sources/Packages/Sources/Common/Locks.swift b/Sources/Packages/Sources/Common/Locks.swift deleted file mode 100644 index e3384aa..0000000 --- a/Sources/Packages/Sources/Common/Locks.swift +++ /dev/null @@ -1,14 +0,0 @@ -import os - -public extension OSAllocatedUnfairLock where State: Sendable { - - var lockedValue: State { - get { - withLock { $0 } - } - nonmutating set { - withLock { $0 = newValue } - } - } - -} diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index cf3fa4f..968d22d 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -22,7 +22,9 @@ public final class Agent: Sendable { logger.debug("Agent is running") self.storeList = storeList self.witness = witness - certificateHandler.reloadCertificates(for: storeList.allSecrets) + Task { @MainActor in + certificateHandler.reloadCertificates(for: storeList.allSecrets) + } } } @@ -60,7 +62,7 @@ extension Agent { switch requestType { case .requestIdentities: response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data) - response.append(identities()) + response.append(await identities()) logger.debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)") case .signRequest: let provenance = requestTracer.provenance(from: reader) @@ -83,8 +85,8 @@ extension Agent { /// Lists the identities available for signing operations /// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations. - func identities() -> Data { - let secrets = storeList.allSecrets + func identities() async -> Data { + let secrets = await storeList.allSecrets certificateHandler.reloadCertificates(for: secrets) var count = secrets.count var keyData = Data() @@ -123,7 +125,7 @@ extension Agent { hash = payloadHash } - guard let (store, secret) = secret(matching: hash) else { + guard let (store, secret) = await secret(matching: hash) else { logger.debug("Agent did not have a key matching \(hash as NSData)") throw AgentError.noMatchingKey } @@ -189,7 +191,7 @@ extension Agent { /// Gives any store with no loaded secrets a chance to reload. func reloadSecretsIfNeccessary() async { - for store in storeList.stores { + for store in await storeList.stores { if store.secrets.isEmpty { logger.debug("Store \(store.name, privacy: .public) has no loaded secrets. Reloading.") await store.reloadSecrets() @@ -200,8 +202,8 @@ extension Agent { /// Finds a ``Secret`` matching a specified hash whos signature was requested. /// - Parameter hash: The hash to match against. /// - Returns: A ``Secret`` and the ``SecretStore`` containing it, if a match is found. - func secret(matching hash: Data) -> (AnySecretStore, AnySecret)? { - storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in + func secret(matching hash: Data) async -> (AnySecretStore, AnySecret)? { + await storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in let allMatching = store.secrets.filter { secret in hash == writer.data(secret: secret) } diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift index 86699ea..de304c0 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift @@ -32,7 +32,7 @@ public final class OpenSSHCertificateHandler: Sendable { /// - Parameter secret: The secret to check for a certificate. /// - Returns: A boolean describing whether or not the certificate handler has a certifiicate associated with a given secret public func hasCertificate(for secret: SecretType) -> Bool { - keyBlobsAndNames.lockedValue[AnySecret(secret)] != nil + keyBlobsAndNames.withLock { $0[AnySecret(secret)] != nil } } @@ -64,7 +64,7 @@ public final class OpenSSHCertificateHandler: Sendable { /// - Parameter secret: The secret to search for a certificate with /// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively. public func keyBlobAndName(for secret: SecretType) throws -> (Data, Data)? { - keyBlobsAndNames.lockedValue[AnySecret(secret)] + keyBlobsAndNames.withLock { $0[AnySecret(secret)] } } /// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret`` diff --git a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift index baa19e6..d8d4074 100644 --- a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift +++ b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift @@ -1,50 +1,39 @@ import Foundation import Observation -import os -import Common /// A "Store Store," which holds a list of type-erased stores. -@Observable public final class SecretStoreList: Sendable { +@Observable @MainActor public final class SecretStoreList: Sendable { /// The Stores managed by the SecretStoreList. - public var stores: [AnySecretStore] { - __stores.lockedValue - } - private let __stores: OSAllocatedUnfairLock<[AnySecretStore]> = .init(uncheckedState: []) - + public var stores: [AnySecretStore] = [] /// A modifiable store, if one is available. - public var modifiableStore: AnySecretStoreModifiable? { - __modifiableStore.withLock { $0 } - } - private let __modifiableStore: OSAllocatedUnfairLock = .init(uncheckedState: nil) + public var modifiableStore: AnySecretStoreModifiable? = nil /// Initializes a SecretStoreList. - public init() { + public nonisolated init() { } /// Adds a non-type-erased SecretStore to the list. public func add(store: SecretStoreType) { - __stores.withLock { - $0.append(AnySecretStore(store)) - } + stores.append(AnySecretStore(store)) } /// Adds a non-type-erased modifiable SecretStore. public func add(store: SecretStoreType) { let modifiable = AnySecretStoreModifiable(modifiable: store) - __modifiableStore.lockedValue = modifiable - __stores.withLock { - $0.append(modifiable) + if modifiableStore == nil { + modifiableStore = modifiable } + stores.append(modifiable) } /// A boolean describing whether there are any Stores available. public var anyAvailable: Bool { - __stores.lockedValue.contains(where: \.isAvailable) + stores.contains(where: \.isAvailable) } public var allSecrets: [AnySecret] { - __stores.lockedValue.flatMap(\.secrets) + stores.flatMap(\.secrets) } } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index bcce342..f31d576 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -5,7 +5,20 @@ import CryptoKit @preconcurrency import LocalAuthentication import SecretKit import os -import Common + +public extension OSAllocatedUnfairLock where State: Sendable { + + var lockedValue: State { + get { + withLock { $0 } + } + nonmutating set { + withLock { $0 = newValue } + } + } + +} + extension SecureEnclave { diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index ee3f223..dbc2d15 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -11,13 +11,13 @@ import Observation @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 = { @@ -44,15 +44,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { } try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) notifier.prompt() - _ = withObservationTracking { - updater.update - } onChange: { [updater, notifier] in - notifier.notify(update: updater.update!) { release in - Task { - await updater.ignore(release: release) - } - } - } +// _ = withObservationTracking { +// updater.update +// } onChange: { [updater, notifier] in +// notifier.notify(update: updater.update!) { release in +// Task { +// await updater.ignore(release: release) +// } +// } +// } } } diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 0c3ba5f..3fb147f 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -6,14 +6,19 @@ import SmartCardSecretKit import Brief extension EnvironmentValues { - @Entry var secretStoreList: SecretStoreList = { + private static let _secretStoreList: SecretStoreList = { let list = SecretStoreList() - list.add(store: SecureEnclave.Store()) - list.add(store: SmartCard.Store()) + Task { @MainActor in + list.add(store: SecureEnclave.Store()) + list.add(store: SmartCard.Store()) + } return list }() - @Entry var agentStatusChecker: any AgentStatusCheckerProtocol = AgentStatusChecker() - @Entry var updater: any UpdaterProtocol = Updater(checkOnLaunch: false) + @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 } @main @@ -29,7 +34,7 @@ struct Secretive: App { WindowGroup { ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup) // This one is explicitly injected via environment to support hasRunSetup. - .environment(Updater(checkOnLaunch: hasRunSetup)) +// .environment(Updater(checkOnLaunch: hasRunSetup)) .onAppear { if !hasRunSetup { showingSetup = true diff --git a/Sources/Secretive/Controllers/AgentStatusChecker.swift b/Sources/Secretive/Controllers/AgentStatusChecker.swift index acbe4ad..8ff234a 100644 --- a/Sources/Secretive/Controllers/AgentStatusChecker.swift +++ b/Sources/Secretive/Controllers/AgentStatusChecker.swift @@ -4,18 +4,20 @@ import AppKit import SecretKit import Observation -protocol AgentStatusCheckerProtocol: Observable { +@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable { var running: Bool { get } var developmentBuild: Bool { get } func check() } -@Observable class AgentStatusChecker: AgentStatusCheckerProtocol { +@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol { var running: Bool = false - init() { - check() + nonisolated init() { + Task { @MainActor in + check() + } } func check() { diff --git a/Sources/Secretive/Localizable.xcstrings b/Sources/Secretive/Localizable.xcstrings index 3dcedc8..4302219 100644 --- a/Sources/Secretive/Localizable.xcstrings +++ b/Sources/Secretive/Localizable.xcstrings @@ -2511,6 +2511,9 @@ } } } + }, + "No Update: %@" : { + }, "no_secure_storage_description" : { "localizations" : { diff --git a/Sources/Secretive/Preview Content/PreviewStore.swift b/Sources/Secretive/Preview Content/PreviewStore.swift index 8318505..c0bdd85 100644 --- a/Sources/Secretive/Preview Content/PreviewStore.swift +++ b/Sources/Secretive/Preview Content/PreviewStore.swift @@ -104,7 +104,7 @@ extension Preview { extension Preview { - static func storeList(stores: [Store] = [], modifiableStores: [StoreModifiable] = []) -> SecretStoreList { + @MainActor static func storeList(stores: [Store] = [], modifiableStores: [StoreModifiable] = []) -> SecretStoreList { let list = SecretStoreList() for store in stores { list.add(store: store) diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index 8bcbe0a..0e9dbc9 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -91,6 +91,8 @@ extension ContentView { .popover(item: $selectedUpdate, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { update in UpdateDetailView(update: update) } + } else { + Text("No Update: \(updater.update as Any)") } }