Swift 6 / Concurrency fixes (#578)

* Enable language mode

* WIP

* WIP

* Fix concurrency issues in SmartCardStore

* Switch to SMAppService

* Bump runners

* Base

* Finish Testing migration

* Tweak async for updater

* More

* Backport mutex

* Revert "Backport mutex"

This reverts commit 9b02afb20c.

* WIP

* Reenable

* Fix preview.

* Update package.

* Bump to latest public macOS and Xcode

* Bump back down to 6.1

* Update to Xcode 26.

* Fixed tests.

* More cleanup

* Env fixes

* var->let

* Cleanup

* Persist auth async

* Whitespace.

* Whitespace.

* Cleanup.

* Cleanup

* Redoing locks in actors bc of observable

* Actors.

* .

* Specify b5

* Update package to 6.2

* Fix disabled updater

* Remove preconcurrency warning

* Move updater init
This commit is contained in:
Max Goedjen
2025-08-17 12:38:18 -05:00
committed by GitHub
parent 5cc62b628a
commit 0e6b218f1f
59 changed files with 834 additions and 857 deletions

View File

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

View File

@@ -2,20 +2,18 @@ import Foundation
import Combine
/// Type eraser for SecretStore.
public class AnySecretStore: SecretStore {
public class AnySecretStore: SecretStore, @unchecked Sendable {
let base: Any
private let _isAvailable: () -> Bool
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?
let base: any Sendable
private let _isAvailable: @MainActor @Sendable () -> Bool
private let _id: @Sendable () -> UUID
private let _name: @MainActor @Sendable () -> String
private let _secrets: @MainActor @Sendable () -> [AnySecret]
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance) async throws -> Data
private let _verify: @Sendable (Data, Data, AnySecret) async throws -> Bool
private let _existingPersistedAuthenticationContext: @Sendable (AnySecret) async -> PersistedAuthenticationContext?
private let _persistAuthentication: @Sendable (AnySecret, TimeInterval) async throws -> Void
private let _reloadSecrets: @Sendable () async -> Void
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
base = secretStore
@@ -23,17 +21,14 @@ 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 {
@MainActor public var isAvailable: Bool {
return _isAvailable()
}
@@ -41,59 +36,59 @@ public class AnySecretStore: SecretStore {
return _id()
}
public var name: String {
@MainActor public var name: String {
return _name()
}
public var secrets: [AnySecret] {
@MainActor public var secrets: [AnySecret] {
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 {
public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable, @unchecked Sendable {
private let _create: (String, Bool) throws -> Void
private let _delete: (AnySecret) throws -> Void
private let _update: (AnySecret, String) throws -> Void
private let _create: @Sendable (String, Bool) async throws -> Void
private let _delete: @Sendable (AnySecret) async throws -> Void
private let _update: @Sendable (AnySecret, String) async throws -> Void
public init<SecretStoreType>(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)
}
}

View File

@@ -2,7 +2,7 @@ import Foundation
import OSLog
/// Manages storage and lookup for OpenSSH certificates.
public final class OpenSSHCertificateHandler {
public actor OpenSSHCertificateHandler: Sendable {
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
@@ -25,14 +25,6 @@ public final class OpenSSHCertificateHandler {
}
}
/// Whether or not the certificate handler has a certifiicate associated with a given secret.
/// - 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[AnySecret(secret)] != nil
}
/// Reconstructs a public key from a ``Data``, if that ``Data`` contains an OpenSSH certificate hash. Currently only ecdsa certificates are supported
/// - Parameter certBlock: The openssh certificate to extract the public key from
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.

View File

@@ -2,7 +2,7 @@ import Foundation
import CryptoKit
/// Generates OpenSSH representations of Secrets.
public struct OpenSSHKeyWriter {
public struct OpenSSHKeyWriter: Sendable {
/// Initializes the writer.
public init() {

View File

@@ -2,7 +2,7 @@ import Foundation
import OSLog
/// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts.
public final class PublicKeyFileStoreController {
public final class PublicKeyFileStoreController: Sendable {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
private let directory: String

View File

@@ -1,34 +1,35 @@
import Foundation
import Combine
import Observation
/// A "Store Store," which holds a list of type-erased stores.
public final class SecretStoreList: ObservableObject {
@Observable @MainActor public final class SecretStoreList: Sendable {
/// 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<AnyCancellable> = []
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<SecretStoreType: SecretStore>(store: SecretStoreType) {
addInternal(store: AnySecretStore(store))
stores.append(AnySecretStore(store))
}
/// Adds a non-type-erased modifiable SecretStore.
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
let modifiable = AnySecretStoreModifiable(modifiable: store)
modifiableStore = modifiable
addInternal(store: modifiable)
if modifiableStore == nil {
modifiableStore = modifiable
}
stores.append(modifiable)
}
/// A boolean describing whether there are any Stores available.
public var anyAvailable: Bool {
stores.reduce(false, { $0 || $1.isAvailable })
stores.contains(where: \.isAvailable)
}
public var allSecrets: [AnySecret] {
@@ -36,14 +37,3 @@ public final class SecretStoreList: ObservableObject {
}
}
extension SecretStoreList {
private func addInternal(store: AnySecretStore) {
stores.append(store)
store.objectWillChange.sink {
self.objectWillChange.send()
}.store(in: &cancellables)
}
}

View File

@@ -1,7 +1,7 @@
import Foundation
/// Protocol describing a persisted authentication context. This is an authorization that can be reused for multiple access to a secret that requires authentication for a specific period of time.
public protocol PersistedAuthenticationContext {
public protocol PersistedAuthenticationContext: Sendable {
/// Whether the context remains valid.
var valid: Bool { get }
/// The date at which the authorization expires and the context becomes invalid.

View File

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

View File

@@ -2,18 +2,18 @@ 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, Sendable {
associatedtype SecretType: Secret
/// A boolean indicating whether or not the store is available.
var isAvailable: Bool { get }
@MainActor var isAvailable: Bool { get }
/// A unique identifier for the store.
var id: UUID { get }
/// A user-facing name for the store.
var name: String { get }
@MainActor var name: String { get }
/// The secrets the store manages.
var secrets: [SecretType] { get }
@MainActor var secrets: [SecretType] { get }
/// Signs a data payload with a specified Secret.
/// - Parameters:
@@ -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
}

View File

@@ -2,7 +2,7 @@ import Foundation
import AppKit
/// Describes the chain of applications that requested a signature operation.
public struct SigningRequestProvenance: Equatable {
public struct SigningRequestProvenance: Equatable, Sendable {
/// A list of processes involved in the request.
/// - Note: A chain will typically consist of many elements even for a simple request. For example, running `git fetch` in Terminal.app would generate a request chain of `ssh` -> `git` -> `zsh` -> `login` -> `Terminal.app`
@@ -30,7 +30,7 @@ extension SigningRequestProvenance {
extension SigningRequestProvenance {
/// Describes a process in a `SigningRequestProvenance` chain.
public struct Process: Equatable {
public struct Process: Equatable, Sendable {
/// The pid of the process.
public let pid: Int32