More cleanup

This commit is contained in:
Max Goedjen 2025-08-10 23:08:17 -07:00
parent 1196530e27
commit 8b428e6c64
No known key found for this signature in database
12 changed files with 125 additions and 123 deletions

View File

@ -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
),
]
)

View File

@ -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<Release?> = .init(nil)
private let _update: OSAllocatedUnfairLock<Release?> = .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
}
}
}

View File

@ -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 {

View File

@ -0,0 +1,14 @@
import os
public extension OSAllocatedUnfairLock where State: Sendable {
var lockedValue: State {
get {
withLock { $0 }
}
nonmutating set {
withLock { $0 = newValue }
}
}
}

View File

@ -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<SecretType: Secret>(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<SecretType: Secret>(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``

View File

@ -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<AnySecretStoreModifiable?> = .init(nil)
private let __modifiableStore: OSAllocatedUnfairLock<AnySecretStoreModifiable?> = .init(uncheckedState: nil)
/// Initializes a SecretStoreList.
public init() {
@ -31,9 +32,7 @@ import Synchronization
/// Adds a non-type-erased modifiable SecretStore.
public func add<SecretStoreType: SecretStoreModifiable>(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)
}
}

View File

@ -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,15 +106,14 @@ extension SecureEnclave {
}
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
let context: Mutex<LAContext>
// 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 = newContext
}
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
let attributes = KeychainDictionary([
kSecClass: kSecClassKey,
@ -141,7 +141,6 @@ extension SecureEnclave {
}
return signature as Data
}
}
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
let context = LAContext()
@ -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
}

View File

@ -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<State> = .init(.init())
private let state: OSAllocatedUnfairLock<State> = .init(uncheckedState: .init())
public var isAvailable: Bool {
state.withLock { $0.isAvailable }
}

View File

@ -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<Bool> = .init(false)
let witnessed: OSAllocatedUnfairLock<Bool> = .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<SigningRequestProvenance?> = .init(nil)
let witnessTrace: Mutex<SigningRequestProvenance?> = .init(nil)
let speakNowTrace: OSAllocatedUnfairLock<SigningRequestProvenance?> = .init(uncheckedState: nil)
let witnessTrace: OSAllocatedUnfairLock<SigningRequestProvenance?> = .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 {

View File

@ -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<State> = .init(.init())
fileprivate let state: OSAllocatedUnfairLock<State> = .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)
}

View File

@ -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)";

View File

@ -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<Release?> = .init(nil)
let _update: OSAllocatedUnfairLock<Release?> = .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")
}
}