diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 09742e6..fb2574d 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "SecretivePackages", platforms: [ - .macOS(.v15) + .macOS(.v14) ], products: [ .library( @@ -27,13 +27,16 @@ let package = Package( .library( name: "Brief", targets: ["Brief"]), + .library( + name: "Common", + targets: ["Common"]), ], dependencies: [ ], targets: [ .target( name: "SecretKit", - dependencies: [], + dependencies: ["Common"], swiftSettings: swiftSettings ), .testTarget( @@ -43,17 +46,17 @@ let package = Package( ), .target( name: "SecureEnclaveSecretKit", - dependencies: ["SecretKit"], + dependencies: ["Common", "SecretKit"], swiftSettings: swiftSettings ), .target( name: "SmartCardSecretKit", - dependencies: ["SecretKit"], + dependencies: ["Common", "SecretKit"], swiftSettings: swiftSettings ), .target( name: "SecretAgentKit", - dependencies: ["SecretKit", "SecretAgentKitHeaders"], + dependencies: ["Common", "SecretKit", "SecretAgentKitHeaders"], swiftSettings: swiftSettings ), .systemLibrary( @@ -65,13 +68,18 @@ let package = Package( , .target( name: "Brief", - dependencies: [], + dependencies: ["Common"], 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 334689d..ded9996 100644 --- a/Sources/Packages/Sources/Brief/Updater.swift +++ b/Sources/Packages/Sources/Brief/Updater.swift @@ -1,14 +1,15 @@ import Foundation import Observation -import Synchronization +import os +import Common /// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version. @Observable public final class Updater: UpdaterProtocol, ObservableObject, Sendable { public var update: Release? { - _update.withLock { $0 } + _update.lockedValue } - private let _update: Mutex = .init(nil) + private let _update: OSAllocatedUnfairLock = .init(uncheckedState: nil) public let testBuild: Bool /// The current OS version. @@ -53,9 +54,7 @@ import Synchronization guard !release.critical else { return } defaults.set(true, forKey: release.name) await MainActor.run { - _update.withLock { value in - value = nil - } + _update.lockedValue = nil } } @@ -76,9 +75,7 @@ extension Updater { let latestVersion = SemVer(release.name) if latestVersion > currentVersion { await MainActor.run { - _update.withLock { value in - value = release - } + _update.lockedValue = release } } } diff --git a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift index acbf5b9..fcfd3bd 100644 --- a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift +++ b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift @@ -1,5 +1,5 @@ import Foundation -import Synchronization +import os /// A protocol for retreiving the latest available version of an app. public protocol UpdaterProtocol: Observable { diff --git a/Sources/Packages/Sources/Common/Locks.swift b/Sources/Packages/Sources/Common/Locks.swift new file mode 100644 index 0000000..e3384aa --- /dev/null +++ b/Sources/Packages/Sources/Common/Locks.swift @@ -0,0 +1,14 @@ +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/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift index d8345ba..86699ea 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift @@ -1,6 +1,6 @@ import Foundation import OSLog -import Synchronization +import os /// Manages storage and lookup for OpenSSH certificates. public final class OpenSSHCertificateHandler: Sendable { @@ -8,7 +8,7 @@ public final class OpenSSHCertificateHandler: Sendable { private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler") private let writer = OpenSSHKeyWriter() - private let keyBlobsAndNames: Mutex<[AnySecret: (Data, Data)]> = .init([:]) + private let keyBlobsAndNames: OSAllocatedUnfairLock<[AnySecret: (Data, Data)]> = .init(uncheckedState: [:]) /// Initializes an OpenSSHCertificateHandler. public init() { @@ -32,10 +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.withLock { - $0[AnySecret(secret)] != nil - } - + keyBlobsAndNames.lockedValue[AnySecret(secret)] != nil } @@ -67,9 +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.withLock { - $0[AnySecret(secret)] - } + keyBlobsAndNames.lockedValue[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 b2ef3b2..baa19e6 100644 --- a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift +++ b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift @@ -1,21 +1,22 @@ import Foundation import Observation -import Synchronization +import os +import Common /// A "Store Store," which holds a list of type-erased stores. @Observable public final class SecretStoreList: Sendable { /// The Stores managed by the SecretStoreList. public var stores: [AnySecretStore] { - __stores.withLock { $0 } + __stores.lockedValue } - private let __stores: Mutex<[AnySecretStore]> = .init([]) + private let __stores: OSAllocatedUnfairLock<[AnySecretStore]> = .init(uncheckedState: []) /// A modifiable store, if one is available. public var modifiableStore: AnySecretStoreModifiable? { __modifiableStore.withLock { $0 } } - private let __modifiableStore: Mutex = .init(nil) + private let __modifiableStore: OSAllocatedUnfairLock = .init(uncheckedState: nil) /// Initializes a SecretStoreList. public init() { @@ -31,9 +32,7 @@ import Synchronization /// Adds a non-type-erased modifiable SecretStore. public func add(store: SecretStoreType) { let modifiable = AnySecretStoreModifiable(modifiable: store) - __modifiableStore.withLock { - $0 = modifiable - } + __modifiableStore.lockedValue = modifiable __stores.withLock { $0.append(modifiable) } @@ -41,15 +40,11 @@ import Synchronization /// A boolean describing whether there are any Stores available. public var anyAvailable: Bool { - __stores.withLock { - $0.reduce(false, { $0 || $1.isAvailable }) - } + __stores.lockedValue.contains(where: \.isAvailable) } public var allSecrets: [AnySecret] { - __stores.withLock { - $0.flatMap(\.secrets) - } + __stores.lockedValue.flatMap(\.secrets) } } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index 1259b42..0d5396a 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -4,7 +4,8 @@ import Security import CryptoKit @preconcurrency import LocalAuthentication import SecretKit -import Synchronization +import os +import Common extension SecureEnclave { @@ -17,11 +18,11 @@ extension SecureEnclave { public let id = UUID() public let name = String(localized: "secure_enclave") public var secrets: [Secret] { - _secrets.withLock { $0 } + _secrets.lockedValue } - private let _secrets: Mutex<[Secret]> = .init([]) + private let _secrets: OSAllocatedUnfairLock<[Secret]> = .init(uncheckedState: []) - private let persistedAuthenticationContexts: Mutex<[Secret: PersistentAuthenticationContext]> = .init([:]) + private let persistedAuthenticationContexts: OSAllocatedUnfairLock<[Secret: PersistentAuthenticationContext]> = .init(uncheckedState: [:]) /// Initializes a Store. public init() { @@ -105,42 +106,40 @@ extension SecureEnclave { } public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data { - let context: Mutex -// if let existing = persistedAuthenticationContexts.withLock({ $0 })[secret], existing.valid { -// context = existing.context -// } else { + var context: LAContext + if let existing = persistedAuthenticationContexts.lockedValue[secret], existing.valid { + context = existing.context + } else { let newContext = LAContext() newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button") - context = .init(newContext) -// } - return try context.withLock { context in - context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)") - let attributes = KeychainDictionary([ - kSecClass: kSecClassKey, - kSecAttrKeyClass: kSecAttrKeyClassPrivate, - kSecAttrApplicationLabel: secret.id as CFData, - kSecAttrKeyType: Constants.keyType, - kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, - kSecAttrApplicationTag: Constants.keyTag, - kSecUseAuthenticationContext: context, - kSecReturnRef: true - ]) - var untyped: CFTypeRef? - let status = SecItemCopyMatching(attributes, &untyped) - if status != errSecSuccess { - throw KeychainError(statusCode: status) - } - guard let untypedSafe = untyped else { - throw KeychainError(statusCode: errSecSuccess) - } - let key = untypedSafe as! SecKey - var signError: SecurityError? - - guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else { - throw SigningError(error: signError) - } - return signature as Data + context = newContext } + context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)") + let attributes = KeychainDictionary([ + kSecClass: kSecClassKey, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + kSecAttrApplicationLabel: secret.id as CFData, + kSecAttrKeyType: Constants.keyType, + kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, + kSecAttrApplicationTag: Constants.keyTag, + kSecUseAuthenticationContext: context, + kSecReturnRef: true + ]) + var untyped: CFTypeRef? + let status = SecItemCopyMatching(attributes, &untyped) + if status != errSecSuccess { + throw KeychainError(statusCode: status) + } + guard let untypedSafe = untyped else { + throw KeychainError(statusCode: errSecSuccess) + } + let key = untypedSafe as! SecKey + var signError: SecurityError? + + guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else { + throw SigningError(error: signError) + } + return signature as Data } public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool { @@ -179,7 +178,7 @@ extension SecureEnclave { } public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? { - guard let persisted = persistedAuthenticationContexts.withLock({ $0 })[secret], persisted.valid else { return nil } + guard let persisted = persistedAuthenticationContexts.lockedValue[secret], persisted.valid else { return nil } return persisted } diff --git a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift index 199e4cd..56f802d 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift @@ -1,5 +1,5 @@ import Foundation -import Synchronization +import os import Observation import Security import CryptoTokenKit @@ -19,7 +19,7 @@ extension SmartCard { /// An implementation of Store backed by a Smart Card. @Observable public final class Store: SecretStore { - private let state: Mutex = .init(.init()) + private let state: OSAllocatedUnfairLock = .init(uncheckedState: .init()) public var isAvailable: Bool { state.withLock { $0.isAvailable } } diff --git a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift index 6cc464e..ef55363 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift @@ -1,9 +1,10 @@ import Foundation +import os import Testing import CryptoKit -import Synchronization @testable import SecretKit @testable import SecretAgentKit +import Common @Suite struct AgentTests { @@ -91,7 +92,7 @@ import Synchronization @Test func witnessSignature() async { let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) let list = storeList(with: [Constants.Secrets.ecdsa256Secret]) - let witnessed: Mutex = .init(false) + let witnessed: OSAllocatedUnfairLock = .init(uncheckedState: false) let witness = StubWitness(speakNow: { _, trace in return false }, witness: { _, trace in @@ -106,8 +107,8 @@ import Synchronization @Test func requestTracing() async { let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) let list = storeList(with: [Constants.Secrets.ecdsa256Secret]) - let speakNowTrace: Mutex = .init(nil) - let witnessTrace: Mutex = .init(nil) + let speakNowTrace: OSAllocatedUnfairLock = .init(uncheckedState: nil) + let witnessTrace: OSAllocatedUnfairLock = .init(uncheckedState: nil) let witness = StubWitness(speakNow: { _, trace in speakNowTrace.lockedValue = trace return false @@ -145,19 +146,6 @@ import Synchronization } -extension Mutex where Value: Sendable { - - var lockedValue: Value { - get { - withLock { $0 } - } - nonmutating set { - withLock { $0 = newValue } - } - } - -} - extension AgentTests { func storeList(with secrets: [Stub.Secret]) -> SecretStoreList { diff --git a/Sources/SecretAgent/Notifier.swift b/Sources/SecretAgent/Notifier.swift index 2f0e15a..1c86ce8 100644 --- a/Sources/SecretAgent/Notifier.swift +++ b/Sources/SecretAgent/Notifier.swift @@ -4,7 +4,7 @@ import AppKit import SecretKit import SecretAgentKit import Brief -import Synchronization +import os final class Notifier: Sendable { @@ -84,10 +84,10 @@ final class Notifier: Sendable { try? await notificationCenter.add(request) } - func notify(update: Release, ignore: ((Release) -> Void)?) { + func notify(update: Release, ignore: (@Sendable (Release) -> Void)?) { notificationDelegate.state.withLock { [update] state in state.release = update -// state.ignore = ignore + state.ignore = ignore } let notificationCenter = UNUserNotificationCenter.current() let notificationContent = UNMutableNotificationContent() @@ -141,7 +141,7 @@ extension Notifier { final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable { struct State { - typealias PersistAuthentication = ((AnySecret, AnySecretStore, TimeInterval?) async -> Void) + typealias PersistAuthentication = (@Sendable (AnySecret, AnySecretStore, TimeInterval?) async -> Void) typealias Ignore = ((Release) -> Void) fileprivate var release: Release? fileprivate var ignore: Ignore? @@ -151,7 +151,7 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Se fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:] } - fileprivate let state: Mutex = .init(.init()) + fileprivate let state: OSAllocatedUnfairLock = .init(uncheckedState: .init()) func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { @@ -170,9 +170,10 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Se } func handleUpdateResponse(response: UNNotificationResponse) { + let id = response.actionIdentifier state.withLock { state in guard let update = state.release else { return } - switch response.actionIdentifier { + switch id { case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier: NSWorkspace.shared.open(update.html_url) case Notifier.Constants.ignoreActionIdentitifier: @@ -184,15 +185,21 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Se } func handlePersistAuthenticationResponse(response: UNNotificationResponse) async { -// let (secret, store, persistOptions, callback): (AnySecret?, AnySecretStore?, TimeInterval?, State.PersistAuthentication?) = state.withLock { state in -// guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String, let secret = state.pendingPersistableSecrets[secretID], -// let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String, let store = state.pendingPersistableStores[storeID] -// else { return (nil, nil, nil, nil) } -// state.pendingPersistableSecrets[secretID] = nil -// return (secret, store, state.persistOptions[response.actionIdentifier], state.persistAuthentication) -// } -// guard let secret, let store, let persistOptions else { return } -// await callback?(secret, store, persistOptions) + guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String, + let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String else { + return + } + let id = response.actionIdentifier + + let (secret, store, persistOptions, callback): (AnySecret?, AnySecretStore?, TimeInterval?, State.PersistAuthentication?) = state.withLock { state in + guard let secret = state.pendingPersistableSecrets[secretID], + let store = state.pendingPersistableStores[storeID] + else { return (nil, nil, nil, nil) } + state.pendingPersistableSecrets[secretID] = nil + return (secret, store, state.persistOptions[id], state.persistAuthentication) + } + guard let secret, let store, let persistOptions else { return } + await callback?(secret, store, persistOptions) } diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 09a050d..b05a128 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -646,6 +646,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -675,6 +676,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -773,6 +775,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -796,6 +799,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -821,6 +825,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -847,6 +852,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Sources/Secretive/Preview Content/PreviewUpdater.swift b/Sources/Secretive/Preview Content/PreviewUpdater.swift index 6979615..5ad2585 100644 --- a/Sources/Secretive/Preview Content/PreviewUpdater.swift +++ b/Sources/Secretive/Preview Content/PreviewUpdater.swift @@ -1,32 +1,25 @@ import Foundation -import Synchronization +import os import Observation import Brief @Observable class PreviewUpdater: UpdaterProtocol { var update: Release? { - _update.withLock { $0 } + _update.lockedValue } - let _update: Mutex = .init(nil) + let _update: OSAllocatedUnfairLock = .init(uncheckedState: nil) let testBuild = false init(update: Update = .none) { switch update { case .none: - _update.withLock { - $0 = nil - } + _update.lockedValue = nil case .advisory: - _update.withLock { - $0 = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update") - } + _update.lockedValue = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update") case .critical: - _update.withLock { - $0 = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update") - - } + _update.lockedValue = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update") } }