From 2dc317d3988247b93a1d4d9d6d00b720a15b5496 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Wed, 25 Dec 2024 18:25:01 -0500 Subject: [PATCH] WIP --- Sources/Packages/Package.swift | 2 +- Sources/Packages/Sources/Brief/Release.swift | 2 +- Sources/Packages/Sources/Brief/SemVer.swift | 2 +- Sources/Packages/Sources/Brief/Updater.swift | 22 ++++-- .../Sources/Brief/UpdaterProtocol.swift | 2 +- .../Sources/SecretAgentKit/Agent.swift | 16 ++--- .../SecretAgentKit/SigningWitness.swift | 4 +- .../Sources/SecretKit/Erasers/AnySecret.swift | 2 +- .../SecretKit/Erasers/AnySecretStore.swift | 69 +++++++++---------- .../Sources/SecretKit/SecretStoreList.swift | 15 ++-- .../Sources/SecretKit/Types/Secret.swift | 4 +- .../Sources/SecretKit/Types/SecretStore.swift | 18 ++--- .../SecureEnclaveStore.swift | 56 ++++++++------- .../SmartCardSecretKit/SmartCardStore.swift | 9 +-- Sources/SecretAgent/AppDelegate.swift | 22 +++--- Sources/SecretAgent/Notifier.swift | 26 ++++--- Sources/Secretive.xcodeproj/project.pbxproj | 12 +--- Sources/Secretive/App.swift | 28 ++++---- .../Controllers/LaunchAgentController.swift | 26 ++++--- .../Preview Content/PreviewUpdater.swift | 24 +++++-- .../Secretive/Views/CreateSecretView.swift | 11 +-- .../Secretive/Views/DeleteSecretView.swift | 5 +- Sources/Secretive/Views/EmptyStoreView.swift | 6 +- .../Secretive/Views/RenameSecretView.swift | 5 +- .../Secretive/Views/SecretListItemView.swift | 2 +- Sources/Secretive/Views/SetupView.swift | 6 +- 26 files changed, 208 insertions(+), 188 deletions(-) diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 4c38227..1594580 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "SecretivePackages", platforms: [ - .macOS(.v12) + .macOS(.v15) ], products: [ .library( diff --git a/Sources/Packages/Sources/Brief/Release.swift b/Sources/Packages/Sources/Brief/Release.swift index 847dffe..ffc3293 100644 --- a/Sources/Packages/Sources/Brief/Release.swift +++ b/Sources/Packages/Sources/Brief/Release.swift @@ -1,7 +1,7 @@ import Foundation /// A release is a representation of a downloadable update. -public struct Release: Codable { +public struct Release: Codable, Sendable { /// The user-facing name of the release. Typically "Secretive 1.2.3" public let name: String diff --git a/Sources/Packages/Sources/Brief/SemVer.swift b/Sources/Packages/Sources/Brief/SemVer.swift index 8308521..8df8c4a 100644 --- a/Sources/Packages/Sources/Brief/SemVer.swift +++ b/Sources/Packages/Sources/Brief/SemVer.swift @@ -1,7 +1,7 @@ import Foundation /// A representation of a Semantic Version. -public struct SemVer { +public struct SemVer: Sendable { /// The SemVer broken into an array of integers. let versionNumbers: [Int] diff --git a/Sources/Packages/Sources/Brief/Updater.swift b/Sources/Packages/Sources/Brief/Updater.swift index 6c88d82..d64bf40 100644 --- a/Sources/Packages/Sources/Brief/Updater.swift +++ b/Sources/Packages/Sources/Brief/Updater.swift @@ -1,10 +1,14 @@ import Foundation -import Combine +import Observation +import Synchronization /// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version. -public final class Updater: ObservableObject, UpdaterProtocol { +@Observable public final class Updater: UpdaterProtocol, ObservableObject, Sendable { - @Published public var update: Release? + public var update: Release? { + _update.withLock { $0 } + } + private let _update: Mutex = .init(nil) public let testBuild: Bool /// The current OS version. @@ -46,8 +50,10 @@ public final class Updater: ObservableObject, UpdaterProtocol { public func ignore(release: Release) { guard !release.critical else { return } defaults.set(true, forKey: release.name) - DispatchQueue.main.async { - self.update = nil + Task { @MainActor in + _update.withLock { value in + value = nil + } } } @@ -67,8 +73,10 @@ extension Updater { guard !release.prerelease else { return } let latestVersion = SemVer(release.name) if latestVersion > currentVersion { - DispatchQueue.main.async { - self.update = release + Task { @MainActor in + _update.withLock { value in + value = release + } } } } diff --git a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift index a5c5edc..c4da349 100644 --- a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift +++ b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift @@ -1,5 +1,5 @@ import Foundation -import Combine +import Synchronization /// A protocol for retreiving the latest available version of an app. public protocol UpdaterProtocol: ObservableObject { diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index 7209635..c2b0044 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -54,7 +54,7 @@ extension Agent { func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) async -> Data { // Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting - reloadSecretsIfNeccessary() + await reloadSecretsIfNeccessary() var response = Data() do { switch requestType { @@ -65,7 +65,7 @@ extension Agent { case .signRequest: let provenance = requestTracer.provenance(from: reader) response.append(SSHAgent.ResponseType.agentSignResponse.data) - response.append(try sign(data: data, provenance: provenance)) + response.append(try await sign(data: data, provenance: provenance)) logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)") } } catch { @@ -112,7 +112,7 @@ extension Agent { /// - data: The data to sign. /// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request. /// - Returns: An OpenSSH formatted Data payload containing the signed data response. - func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data { + func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data { let reader = OpenSSHReader(data: data) let payloadHash = reader.readNextChunk() let hash: Data @@ -129,11 +129,11 @@ extension Agent { } if let witness = witness { - try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance) + try await witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance) } let dataToSign = reader.readNextChunk() - let signed = try store.sign(data: dataToSign, with: secret, for: provenance) + let signed = try await store.sign(data: dataToSign, with: secret, for: provenance) let derSignature = signed let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)! @@ -175,7 +175,7 @@ extension Agent { signedData.append(writer.lengthAndData(of: sub)) if let witness = witness { - try witness.witness(accessTo: secret, from: store, by: provenance) + try await witness.witness(accessTo: secret, from: store, by: provenance) } logger.debug("Agent signed request") @@ -188,11 +188,11 @@ extension Agent { extension Agent { /// Gives any store with no loaded secrets a chance to reload. - func reloadSecretsIfNeccessary() { + func reloadSecretsIfNeccessary() async { for store in storeList.stores { if store.secrets.isEmpty { logger.debug("Store \(store.name, privacy: .public) has no loaded secrets. Reloading.") - store.reloadSecrets() + await store.reloadSecrets() } } } diff --git a/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift b/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift index b090bd3..2527fef 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift @@ -10,13 +10,13 @@ public protocol SigningWitness { /// - store: The `Store` being asked to sign the request.. /// - provenance: A `SigningRequestProvenance` object describing the origin of the request. /// - Note: This method being called does not imply that the requst has been authorized. If a secret requires authentication, authentication will still need to be performed by the user before the request will be performed. If the user declines or fails to authenticate, the request will fail. - func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws + func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws /// Notifies the callee that a signing operation has been performed for a given secret. /// - Parameters: /// - secret: The `Secret` that will was used to sign the request. /// - store: The `Store` that signed the request.. /// - provenance: A `SigningRequestProvenance` object describing the origin of the request. - func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws + func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws } diff --git a/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift b/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift index f6e8bef..88991dc 100644 --- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift +++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift @@ -1,7 +1,7 @@ import Foundation /// Type eraser for Secret. -public struct AnySecret: Secret { +public struct AnySecret: Secret, @unchecked Sendable { let base: Any private let hashable: AnyHashable diff --git a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift index bf5a74d..3e831b0 100644 --- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift @@ -9,13 +9,11 @@ public class AnySecretStore: SecretStore { private let _id: () -> UUID private let _name: () -> String private let _secrets: () -> [AnySecret] - private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data - private let _verify: (Data, Data, AnySecret) throws -> Bool - private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext? - private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void - private let _reloadSecrets: () -> Void - - private var sink: AnyCancellable? + private let _sign: (Data, AnySecret, SigningRequestProvenance) async throws -> Data + private let _verify: (Data, Data, AnySecret) async throws -> Bool + private let _existingPersistedAuthenticationContext: (AnySecret) async -> PersistedAuthenticationContext? + private let _persistAuthentication: (AnySecret, TimeInterval) async throws -> Void + private let _reloadSecrets: () async -> Void public init(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore { base = secretStore @@ -23,14 +21,11 @@ public class AnySecretStore: SecretStore { _name = { secretStore.name } _id = { secretStore.id } _secrets = { secretStore.secrets.map { AnySecret($0) } } - _sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) } - _verify = { try secretStore.verify(signature: $0, for: $1, with: $2.base as! SecretStoreType.SecretType) } - _existingPersistedAuthenticationContext = { secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) } - _persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) } - _reloadSecrets = { secretStore.reloadSecrets() } - sink = secretStore.objectWillChange.sink { _ in - self.objectWillChange.send() - } + _sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) } + _verify = { try await secretStore.verify(signature: $0, for: $1, with: $2.base as! SecretStoreType.SecretType) } + _existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) } + _persistAuthentication = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) } + _reloadSecrets = { await secretStore.reloadSecrets() } } public var isAvailable: Bool { @@ -49,51 +44,51 @@ public class AnySecretStore: SecretStore { return _secrets() } - public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) throws -> Data { - try _sign(data, secret, provenance) + public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) async throws -> Data { + try await _sign(data, secret, provenance) } - public func verify(signature: Data, for data: Data, with secret: AnySecret) throws -> Bool { - try _verify(signature, data, secret) + public func verify(signature: Data, for data: Data, with secret: AnySecret) async throws -> Bool { + try await _verify(signature, data, secret) } - public func existingPersistedAuthenticationContext(secret: AnySecret) -> PersistedAuthenticationContext? { - _existingPersistedAuthenticationContext(secret) + public func existingPersistedAuthenticationContext(secret: AnySecret) async -> PersistedAuthenticationContext? { + await _existingPersistedAuthenticationContext(secret) } - public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) throws { - try _persistAuthentication(secret, duration) + public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) async throws { + try await _persistAuthentication(secret, duration) } - public func reloadSecrets() { - _reloadSecrets() + public func reloadSecrets() async { + await _reloadSecrets() } } public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable { - private let _create: (String, Bool) throws -> Void - private let _delete: (AnySecret) throws -> Void - private let _update: (AnySecret, String) throws -> Void + private let _create: (String, Bool) async throws -> Void + private let _delete: (AnySecret) async throws -> Void + private let _update: (AnySecret, String) async throws -> Void public init(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable { - _create = { try secretStore.create(name: $0, requiresAuthentication: $1) } - _delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) } - _update = { try secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) } + _create = { try await secretStore.create(name: $0, requiresAuthentication: $1) } + _delete = { try await secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) } + _update = { try await secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) } super.init(secretStore) } - public func create(name: String, requiresAuthentication: Bool) throws { - try _create(name, requiresAuthentication) + public func create(name: String, requiresAuthentication: Bool) async throws { + try await _create(name, requiresAuthentication) } - public func delete(secret: AnySecret) throws { - try _delete(secret) + public func delete(secret: AnySecret) async throws { + try await _delete(secret) } - public func update(secret: AnySecret, name: String) throws { - try _update(secret, name) + public func update(secret: AnySecret, name: String) async throws { + try await _update(secret, name) } } diff --git a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift index eb8456f..0f1da09 100644 --- a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift +++ b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift @@ -1,14 +1,13 @@ import Foundation -import Combine +import Observation /// A "Store Store," which holds a list of type-erased stores. -public final class SecretStoreList: ObservableObject { +@Observable public final class SecretStoreList: ObservableObject { /// The Stores managed by the SecretStoreList. - @Published public var stores: [AnySecretStore] = [] + public var stores: [AnySecretStore] = [] /// A modifiable store, if one is available. - @Published public var modifiableStore: AnySecretStoreModifiable? - private var cancellables: Set = [] + public var modifiableStore: AnySecretStoreModifiable? /// Initializes a SecretStoreList. public init() { @@ -41,9 +40,9 @@ extension SecretStoreList { private func addInternal(store: AnySecretStore) { stores.append(store) - store.objectWillChange.sink { - self.objectWillChange.send() - }.store(in: &cancellables) +// store.objectWillChange.sink { +// self.objectWillChange.send() +// }.store(in: &cancellables) } } diff --git a/Sources/Packages/Sources/SecretKit/Types/Secret.swift b/Sources/Packages/Sources/SecretKit/Types/Secret.swift index 8f9656c..e4cdb22 100644 --- a/Sources/Packages/Sources/SecretKit/Types/Secret.swift +++ b/Sources/Packages/Sources/SecretKit/Types/Secret.swift @@ -1,7 +1,7 @@ import Foundation /// The base protocol for describing a Secret -public protocol Secret: Identifiable, Hashable { +public protocol Secret: Identifiable, Hashable, Sendable { /// A user-facing string identifying the Secret. var name: String { get } @@ -17,7 +17,7 @@ public protocol Secret: Identifiable, Hashable { } /// The type of algorithm the Secret uses. Currently, only elliptic curve algorithms are supported. -public enum Algorithm: Hashable { +public enum Algorithm: Hashable, Sendable { case ellipticCurve case rsa diff --git a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift index f780201..f0a465b 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift @@ -2,7 +2,7 @@ import Foundation import Combine /// Manages access to Secrets, and performs signature operations on data using those Secrets. -public protocol SecretStore: ObservableObject, Identifiable { +public protocol SecretStore: Identifiable { associatedtype SecretType: Secret @@ -21,7 +21,7 @@ public protocol SecretStore: ObservableObject, Identifiable { /// - secret: The ``Secret`` to sign with. /// - provenance: A ``SigningRequestProvenance`` describing where the request came from. /// - Returns: The signed data. - func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data + func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) async throws -> Data /// Verifies that a signature is valid over a specified payload. /// - Parameters: @@ -29,23 +29,23 @@ public protocol SecretStore: ObservableObject, Identifiable { /// - data: The data to verify the signature of. /// - secret: The secret whose signature to verify. /// - Returns: Whether the signature was verified. - func verify(signature: Data, for data: Data, with secret: SecretType) throws -> Bool + func verify(signature: Data, for data: Data, with secret: SecretType) async throws -> Bool /// Checks to see if there is currently a valid persisted authentication for a given secret. /// - Parameters: /// - secret: The ``Secret`` to check if there is a persisted authentication for. /// - Returns: A persisted authentication context, if a valid one exists. - func existingPersistedAuthenticationContext(secret: SecretType) -> PersistedAuthenticationContext? + func existingPersistedAuthenticationContext(secret: SecretType) async -> PersistedAuthenticationContext? /// Persists user authorization for access to a secret. /// - Parameters: /// - secret: The ``Secret`` to persist the authorization for. /// - duration: The duration that the authorization should persist for. /// - Note: This is used for temporarily unlocking access to a secret which would otherwise require authentication every single use. This is useful for situations where the user anticipates several rapid accesses to a authorization-guarded secret. - func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) throws + func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws /// Requests that the store reload secrets from any backing store, if neccessary. - func reloadSecrets() + func reloadSecrets() async } @@ -56,18 +56,18 @@ public protocol SecretStoreModifiable: SecretStore { /// - Parameters: /// - name: The user-facing name for the ``Secret``. /// - requiresAuthentication: A boolean indicating whether or not the user will be required to authenticate before performing signature operations with the secret. - func create(name: String, requiresAuthentication: Bool) throws + func create(name: String, requiresAuthentication: Bool) async throws /// Deletes a Secret in the store. /// - Parameters: /// - secret: The ``Secret`` to delete. - func delete(secret: SecretType) throws + func delete(secret: SecretType) async throws /// Updates the name of a Secret in the store. /// - Parameters: /// - secret: The ``Secret`` to update. /// - name: The new name for the Secret. - func update(secret: SecretType, name: String) throws + func update(secret: SecretType, name: String) async throws } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index 19b6168..216033b 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -1,38 +1,42 @@ import Foundation -import Combine +import Observation import Security -import CryptoTokenKit +import CryptoKit import LocalAuthentication import SecretKit +import Synchronization extension SecureEnclave { /// An implementation of Store backed by the Secure Enclave. - public final class Store: SecretStoreModifiable { + @Observable public final class Store: SecretStoreModifiable { public var isAvailable: Bool { - // For some reason, as of build time, CryptoKit.SecureEnclave.isAvailable always returns false - // error msg "Received error sending GET UNIQUE DEVICE command" - // Verify it with TKTokenWatcher manually. - TKTokenWatcher().tokenIDs.contains("com.apple.setoken") + CryptoKit.SecureEnclave.isAvailable } public let id = UUID() public let name = String(localized: "secure_enclave") - @Published public private(set) var secrets: [Secret] = [] + public var secrets: [Secret] { + _secrets.withLock { $0 } + } + private let _secrets: Mutex<[Secret]> = .init([]) private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:] /// Initializes a Store. public init() { - DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { [reload = reloadSecretsInternal(notifyAgent:)] _ in - reload(false) - } + // FIXME: THIS +// Task { +// for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { +// await reloadSecretsInternal(notifyAgent: false) +// } +// } loadSecrets() } // MARK: Public API - public func create(name: String, requiresAuthentication: Bool) throws { + public func create(name: String, requiresAuthentication: Bool) async throws { var accessError: SecurityError? let flags: SecAccessControlCreateFlags if requiresAuthentication { @@ -69,10 +73,10 @@ extension SecureEnclave { throw KeychainError(statusCode: nil) } try savePublicKey(publicKey, name: name) - reloadSecretsInternal() + await reloadSecretsInternal() } - public func delete(secret: Secret) throws { + public func delete(secret: Secret) async throws { let deleteAttributes = KeychainDictionary([ kSecClass: kSecClassKey, kSecAttrApplicationLabel: secret.id as CFData @@ -81,10 +85,10 @@ extension SecureEnclave { if status != errSecSuccess { throw KeychainError(statusCode: status) } - reloadSecretsInternal() + await reloadSecretsInternal() } - public func update(secret: Secret, name: String) throws { + public func update(secret: Secret, name: String) async throws { let updateQuery = KeychainDictionary([ kSecClass: kSecClassKey, kSecAttrApplicationLabel: secret.id as CFData @@ -98,7 +102,7 @@ extension SecureEnclave { if status != errSecSuccess { throw KeychainError(statusCode: status) } - reloadSecretsInternal() + await reloadSecretsInternal() } public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data { @@ -199,8 +203,8 @@ extension SecureEnclave { } } - public func reloadSecrets() { - reloadSecretsInternal(notifyAgent: false) + public func reloadSecrets() async { + await reloadSecretsInternal(notifyAgent: false) } } @@ -211,9 +215,11 @@ extension SecureEnclave.Store { /// Reloads all secrets from the store. /// - Parameter notifyAgent: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well. - private func reloadSecretsInternal(notifyAgent: Bool = true) { + private func reloadSecretsInternal(notifyAgent: Bool = true) async { let before = secrets - secrets.removeAll() + _secrets.withLock { + $0.removeAll() + } loadSecrets() if secrets != before { NotificationCenter.default.post(name: .secretStoreReloaded, object: self) @@ -275,7 +281,9 @@ extension SecureEnclave.Store { } return SecureEnclave.Secret(id: id, name: name, requiresAuthentication: requiresAuth, publicKey: publicKey) } - secrets.append(contentsOf: wrapped) + _secrets.withLock { + $0.append(contentsOf: wrapped) + } } /// Saves a public key. @@ -304,8 +312,8 @@ extension SecureEnclave.Store { extension SecureEnclave { enum Constants { - static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData - static let keyType = kSecAttrKeyTypeECSECPrimeRandom + static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8) + static let keyType = kSecAttrKeyTypeECSECPrimeRandom as String static let unauthenticatedThreshold: TimeInterval = 0.05 } diff --git a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift index c8c3281..1280508 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift @@ -20,14 +20,15 @@ extension SmartCard { /// Initializes a Store. public init() { tokenID = watcher.nonSecureEnclaveTokens.first - watcher.setInsertionHandler { [reload = reloadSecretsInternal] string in + // FIXME: THIS + watcher.setInsertionHandler { string in guard self.tokenID == nil else { return } guard !string.contains("setoken") else { return } self.tokenID = string - DispatchQueue.main.async { - reload() - } +// DispatchQueue.main.async { +// reload() +// } self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string) } if let tokenID = tokenID { diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index ab6a3cd..13b0a9b 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -7,7 +7,7 @@ import SmartCardSecretKit import SecretAgentKit import Brief -@NSApplicationMain +@main class AppDelegate: NSObject, NSApplicationDelegate { private let storeList: SecretStoreList = { @@ -31,18 +31,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { logger.debug("SecretAgent finished launching") - DispatchQueue.main.async { - self.socketController.handler = self.agent.handle(reader:writer:) - } - NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in - try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) - } +// DispatchQueue.main.async { +// self.socketController.handler = self.agent.handle(reader:writer:) +// } +// NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in +// try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) +// } try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) notifier.prompt() - updateSink = updater.$update.sink { update in - guard let update = update else { return } - self.notifier.notify(update: update, ignore: self.updater.ignore(release:)) - } +// updateSink = updater.$update.sink { update in +// guard let update = update else { return } +// self.notifier.notify(update: update, ignore: self.updater.ignore(release:)) +// } } } diff --git a/Sources/SecretAgent/Notifier.swift b/Sources/SecretAgent/Notifier.swift index 69b29bb..5b1fe44 100644 --- a/Sources/SecretAgent/Notifier.swift +++ b/Sources/SecretAgent/Notifier.swift @@ -47,7 +47,7 @@ class Notifier { notificationDelegate.persistAuthentication = { secret, store, duration in guard let duration = duration else { return } - try? store.persistAuthentication(secret: secret, forDuration: duration) + try? await store.persistAuthentication(secret: secret, forDuration: duration) } } @@ -57,7 +57,7 @@ class Notifier { notificationCenter.requestAuthorization(options: .alert) { _, _ in } } - func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) { + func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async { notificationDelegate.pendingPersistableSecrets[secret.id.description] = secret notificationDelegate.pendingPersistableStores[store.id.description] = store let notificationCenter = UNUserNotificationCenter.current() @@ -67,7 +67,7 @@ class Notifier { notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description notificationContent.interruptionLevel = .timeSensitive - if secret.requiresAuthentication && store.existingPersistedAuthenticationContext(secret: secret) == nil { + if await store.existingPersistedAuthenticationContext(secret: secret) == nil && secret.requiresAuthentication { notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier } if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) { @@ -99,11 +99,11 @@ class Notifier { extension Notifier: SigningWitness { - func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws { + func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws { } - func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws { - notify(accessTo: secret, from: store, by: provenance) + func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws { + await notify(accessTo: secret, from: store, by: provenance) } } @@ -133,7 +133,7 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { fileprivate var release: Release? fileprivate var ignore: ((Release) -> Void)? - fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) -> Void)? + fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) async -> Void)? fileprivate var persistOptions: [String: TimeInterval] = [:] fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:] fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:] @@ -141,19 +141,17 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { } - - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { let category = response.notification.request.content.categoryIdentifier switch category { case Notifier.Constants.updateCategoryIdentitifier: handleUpdateResponse(response: response) case Notifier.Constants.persistAuthenticationCategoryIdentitifier: - handlePersistAuthenticationResponse(response: response) + await handlePersistAuthenticationResponse(response: response) default: break } - - completionHandler() } func handleUpdateResponse(response: UNNotificationResponse) { @@ -168,12 +166,12 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { } } - func handlePersistAuthenticationResponse(response: UNNotificationResponse) { + func handlePersistAuthenticationResponse(response: UNNotificationResponse) async { guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String, let secret = pendingPersistableSecrets[secretID], let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String, let store = pendingPersistableStores[storeID] else { return } pendingPersistableSecrets[secretID] = nil - persistAuthentication?(secret, store, persistOptions[response.actionIdentifier]) + await persistAuthentication?(secret, store, persistOptions[response.actionIdentifier]) } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 72a1b3a..61fd17b 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -610,7 +610,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -671,7 +671,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = ""; @@ -704,7 +704,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -731,7 +730,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -831,7 +829,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -862,7 +860,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -904,7 +901,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -927,7 +923,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -951,7 +946,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 81555ab..fbd7e5e 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -69,24 +69,26 @@ struct Secretive: App { extension Secretive { private func reinstallAgent() { - justUpdatedChecker.check() - LaunchAgentController().install { - // Wait a second for launchd to kick in (next runloop isn't enough). - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - agentStatusChecker.check() - if !agentStatusChecker.running { - forceLaunchAgent() - } - } - } +// justUpdatedChecker.check() + // FIXME: THIS +// LaunchAgentController().install { +// // Wait a second for launchd to kick in (next runloop isn't enough). +// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { +// agentStatusChecker.check() +// if !agentStatusChecker.running { +// forceLaunchAgent() +// } +// } +// } } private func forceLaunchAgent() { // We've run setup, we didn't just update, launchd is just not doing it's thing. // Force a launch directly. - LaunchAgentController().forceLaunch { _ in - agentStatusChecker.check() - } + // FIXME: THIS +// LaunchAgentController().forceLaunch { _ in +// agentStatusChecker.check() +// } } } diff --git a/Sources/Secretive/Controllers/LaunchAgentController.swift b/Sources/Secretive/Controllers/LaunchAgentController.swift index 7f512aa..2cf72e2 100644 --- a/Sources/Secretive/Controllers/LaunchAgentController.swift +++ b/Sources/Secretive/Controllers/LaunchAgentController.swift @@ -8,37 +8,35 @@ struct LaunchAgentController { private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController") - func install(completion: (() -> Void)? = nil) { + func install() async { logger.debug("Installing agent") _ = setEnabled(false) // This is definitely a bit of a "seems to work better" thing but: // Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old // and start new? - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + try? await Task.sleep(for: .seconds(1)) + await MainActor.run { _ = setEnabled(true) - completion?() } - } - func forceLaunch(completion: ((Bool) -> Void)?) { + func forceLaunch() async -> Bool { logger.debug("Agent is not running, attempting to force launch") let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app") let config = NSWorkspace.OpenConfiguration() config.activates = false - NSWorkspace.shared.openApplication(at: url, configuration: config) { app, error in - DispatchQueue.main.async { - completion?(error == nil) - } - if let error = error { - logger.error("Error force launching \(error.localizedDescription)") - } else { - logger.debug("Agent force launched") - } + do { + let app = try await NSWorkspace.shared.openApplication(at: url, configuration: config) + logger.debug("Agent force launched") + return true + } catch { + logger.error("Error force launching \(error.localizedDescription)") + return false } } private func setEnabled(_ enabled: Bool) -> Bool { + // FIXME: THIS SMLoginItemSetEnabled(Bundle.main.agentBundleID as CFString, enabled) } diff --git a/Sources/Secretive/Preview Content/PreviewUpdater.swift b/Sources/Secretive/Preview Content/PreviewUpdater.swift index a993d87..6979615 100644 --- a/Sources/Secretive/Preview Content/PreviewUpdater.swift +++ b/Sources/Secretive/Preview Content/PreviewUpdater.swift @@ -1,20 +1,32 @@ import Foundation -import Combine +import Synchronization +import Observation import Brief -class PreviewUpdater: UpdaterProtocol { +@Observable class PreviewUpdater: UpdaterProtocol { + + var update: Release? { + _update.withLock { $0 } + } + let _update: Mutex = .init(nil) - let update: Release? let testBuild = false init(update: Update = .none) { switch update { case .none: - self.update = nil + _update.withLock { + $0 = nil + } case .advisory: - self.update = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update") + _update.withLock { + $0 = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update") + } case .critical: - self.update = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update") + _update.withLock { + $0 = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update") + + } } } diff --git a/Sources/Secretive/Views/CreateSecretView.swift b/Sources/Secretive/Views/CreateSecretView.swift index accd8be..4190c99 100644 --- a/Sources/Secretive/Views/CreateSecretView.swift +++ b/Sources/Secretive/Views/CreateSecretView.swift @@ -3,7 +3,7 @@ import SecretKit struct CreateSecretView: View { - @ObservedObject var store: StoreType + @State var store: StoreType @Binding var showing: Bool @State private var name = "" @@ -45,7 +45,8 @@ struct CreateSecretView: View { } func save() { - try! store.create(name: name, requiresAuthentication: requiresAuthentication) + // FIXME: THIS +// try! store.create(name: name, requiresAuthentication: requiresAuthentication) showing = false } @@ -93,14 +94,14 @@ struct ThumbnailPickerView: View { extension ThumbnailPickerView { - struct Item: Identifiable { + struct Item: Identifiable { let id = UUID() - let value: ValueType + let value: InnerValueType let name: LocalizedStringKey let description: LocalizedStringKey let thumbnail: AnyView - init(value: ValueType, name: LocalizedStringKey, description: LocalizedStringKey, thumbnail: ViewType) { + init(value: InnerValueType, name: LocalizedStringKey, description: LocalizedStringKey, thumbnail: ViewType) { self.value = value self.name = name self.description = description diff --git a/Sources/Secretive/Views/DeleteSecretView.swift b/Sources/Secretive/Views/DeleteSecretView.swift index 5e3a6f9..d5c4e19 100644 --- a/Sources/Secretive/Views/DeleteSecretView.swift +++ b/Sources/Secretive/Views/DeleteSecretView.swift @@ -3,7 +3,7 @@ import SecretKit struct DeleteSecretView: View { - @ObservedObject var store: StoreType + @State var store: StoreType let secret: StoreType.SecretType var dismissalBlock: (Bool) -> () @@ -49,7 +49,8 @@ struct DeleteSecretView: View { } func delete() { - try! store.delete(secret: secret) + // FIXME: THIS +// try! store.delete(secret: secret) dismissalBlock(true) } diff --git a/Sources/Secretive/Views/EmptyStoreView.swift b/Sources/Secretive/Views/EmptyStoreView.swift index 6a88c2b..1bd3eae 100644 --- a/Sources/Secretive/Views/EmptyStoreView.swift +++ b/Sources/Secretive/Views/EmptyStoreView.swift @@ -3,7 +3,7 @@ import SecretKit struct EmptyStoreView: View { - @ObservedObject var store: AnySecretStore + @State var store: AnySecretStore @Binding var activeSecret: AnySecret.ID? var body: some View { @@ -22,8 +22,8 @@ struct EmptyStoreView: View { extension EmptyStoreView { enum Constants { - static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag" - static let emptyStoreTag: AnyHashable = "emptyStoreTag" + static let emptyStoreModifiableTag = "emptyStoreModifiableTag" + static let emptyStoreTag = "emptyStoreTag" } } diff --git a/Sources/Secretive/Views/RenameSecretView.swift b/Sources/Secretive/Views/RenameSecretView.swift index 915b2b2..1a2b664 100644 --- a/Sources/Secretive/Views/RenameSecretView.swift +++ b/Sources/Secretive/Views/RenameSecretView.swift @@ -3,7 +3,7 @@ import SecretKit struct RenameSecretView: View { - @ObservedObject var store: StoreType + @State var store: StoreType let secret: StoreType.SecretType var dismissalBlock: (_ renamed: Bool) -> () @@ -44,7 +44,8 @@ struct RenameSecretView: View { } func rename() { - try? store.update(secret: secret, name: newName) + // FIXME: THIS +// try? await store.update(secret: secret, name: newName) dismissalBlock(true) } } diff --git a/Sources/Secretive/Views/SecretListItemView.swift b/Sources/Secretive/Views/SecretListItemView.swift index 8f6bbf4..c498eaa 100644 --- a/Sources/Secretive/Views/SecretListItemView.swift +++ b/Sources/Secretive/Views/SecretListItemView.swift @@ -3,7 +3,7 @@ import SecretKit struct SecretListItemView: View { - @ObservedObject var store: AnySecretStore + @State var store: AnySecretStore var secret: AnySecret @Binding var activeSecret: AnySecret.ID? diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index ffd142c..b212455 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -156,8 +156,10 @@ struct SecretAgentSetupView: View { } func install() { - LaunchAgentController().install() - buttonAction() + Task { + await LaunchAgentController().install() + buttonAction() + } } }