diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ab92a74..555a43c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -20,7 +20,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_16.4.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app - name: Update Build Number env: RUN_ID: ${{ github.run_id }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be7e6d0..cb58539 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_16.4.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app - name: Test run: | pushd Sources/Packages @@ -43,7 +43,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_16.4.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app - name: Update Build Number env: TAG_NAME: ${{ github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da42eb3..20b4db8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_16.4.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app - name: Test run: | pushd Sources/Packages diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 9fa196a..220f960 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "SecretivePackages", platforms: [ - .macOS(.v12) + .macOS(.v14) ], products: [ .library( @@ -34,27 +34,27 @@ let package = Package( .target( name: "SecretKit", dependencies: [], - swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] + swiftSettings: swiftSettings ), .testTarget( name: "SecretKitTests", dependencies: ["SecretKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"], - swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] + swiftSettings: swiftSettings ), .target( name: "SecureEnclaveSecretKit", dependencies: ["SecretKit"], - swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] + swiftSettings: swiftSettings ), .target( name: "SmartCardSecretKit", dependencies: ["SecretKit"], - swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] + swiftSettings: swiftSettings ), .target( name: "SecretAgentKit", dependencies: ["SecretKit", "SecretAgentKitHeaders"], - swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] + swiftSettings: swiftSettings ), .systemLibrary( name: "SecretAgentKitHeaders" @@ -65,7 +65,8 @@ let package = Package( , .target( name: "Brief", - dependencies: [] + dependencies: [], + swiftSettings: swiftSettings ), .testTarget( name: "BriefTests", @@ -73,3 +74,10 @@ let package = Package( ), ] ) + +var swiftSettings: [PackageDescription.SwiftSetting] { + [ + .swiftLanguageMode(.v6), + .unsafeFlags(["-warnings-as-errors"]) + ] +} 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..f79effe 100644 --- a/Sources/Packages/Sources/Brief/Updater.swift +++ b/Sources/Packages/Sources/Brief/Updater.swift @@ -1,10 +1,18 @@ import Foundation -import Combine +import Observation /// 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, Sendable { + + private let state = State() + @MainActor @Observable public final class State { + var update: Release? = nil + nonisolated init() {} + } + public var update: Release? { + state.update + } - @Published public var update: Release? public let testBuild: Bool /// The current OS version. @@ -18,36 +26,43 @@ public final class Updater: ObservableObject, UpdaterProtocol { /// - checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour. /// - osVersion: The current OS version. /// - currentVersion: The current version of the app that is running. - public init(checkOnLaunch: Bool, checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) { + public init( + checkOnLaunch: Bool, + checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value, + osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), + currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0") + ) { self.osVersion = osVersion self.currentVersion = currentVersion testBuild = currentVersion == SemVer("0.0.0") if checkOnLaunch { // Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet. - checkForUpdates() + Task { + await checkForUpdates() + } } - let timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in - self.checkForUpdates() + Task { + while !Task.isCancelled { + try? await Task.sleep(for: .seconds(Int(checkFrequency))) + await checkForUpdates() + } } - timer.tolerance = 60*60 } /// Manually trigger an update check. - public func checkForUpdates() { - URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in - guard let data = data else { return } - guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return } - self.evaluate(releases: releases) - }.resume() + public func checkForUpdates() async { + guard let (data, _) = try? await URLSession.shared.data(from: Constants.updateURL) else { return } + guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return } + await evaluate(releases: releases) } /// Ignores a specified release. `update` will be nil if the user has ignored the latest available release. /// - Parameter release: The release to ignore. - public func ignore(release: Release) { + public func ignore(release: Release) async { guard !release.critical else { return } defaults.set(true, forKey: release.name) - DispatchQueue.main.async { - self.update = nil + await MainActor.run { + state.update = nil } } @@ -57,7 +72,7 @@ extension Updater { /// Evaluates the available downloadable releases, and selects the newest non-prerelease release that the user is able to run. /// - Parameter releases: An array of ``Release`` objects. - func evaluate(releases: [Release]) { + func evaluate(releases: [Release]) async { guard let release = releases .sorted() .reversed() @@ -67,8 +82,8 @@ extension Updater { guard !release.prerelease else { return } let latestVersion = SemVer(release.name) if latestVersion > currentVersion { - DispatchQueue.main.async { - self.update = release + await MainActor.run { + state.update = release } } } diff --git a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift index a5c5edc..b9df10a 100644 --- a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift +++ b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift @@ -1,13 +1,13 @@ import Foundation -import Combine /// A protocol for retreiving the latest available version of an app. -public protocol UpdaterProtocol: ObservableObject { +public protocol UpdaterProtocol: Observable, Sendable { /// The latest update - var update: Release? { get } + @MainActor var update: Release? { get } /// A boolean describing whether or not the current build of the app is a "test" build (ie, a debug build or otherwise special build) var testBuild: Bool { get } + func ignore(release: Release) async } diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index 7209635..4638e3b 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -5,7 +5,7 @@ import SecretKit import AppKit /// The `Agent` is an implementation of an SSH agent. It manages coordination and access between a socket, traces requests, notifies witnesses and passes requests to stores. -public final class Agent { +public final class Agent: Sendable { private let storeList: SecretStoreList private let witness: SigningWitness? @@ -22,7 +22,9 @@ public final class Agent { logger.debug("Agent is running") self.storeList = storeList self.witness = witness - certificateHandler.reloadCertificates(for: storeList.allSecrets) + Task { @MainActor in + await certificateHandler.reloadCertificates(for: storeList.allSecrets) + } } } @@ -54,18 +56,18 @@ 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 { case .requestIdentities: response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data) - response.append(identities()) + response.append(await identities()) logger.debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)") case .signRequest: let provenance = requestTracer.provenance(from: reader) 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 { @@ -83,9 +85,9 @@ extension Agent { /// Lists the identities available for signing operations /// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations. - func identities() -> Data { - let secrets = storeList.allSecrets - certificateHandler.reloadCertificates(for: secrets) + func identities() async -> Data { + let secrets = await storeList.allSecrets + await certificateHandler.reloadCertificates(for: secrets) var count = secrets.count var keyData = Data() @@ -95,7 +97,7 @@ extension Agent { keyData.append(writer.lengthAndData(of: keyBlob)) keyData.append(writer.lengthAndData(of: curveData)) - if let (certificateData, name) = try? certificateHandler.keyBlobAndName(for: secret) { + if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) { keyData.append(writer.lengthAndData(of: certificateData)) keyData.append(writer.lengthAndData(of: name)) count += 1 @@ -112,28 +114,28 @@ 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 // Check if hash is actually an openssh certificate and reconstruct the public key if it is - if let certificatePublicKey = certificateHandler.publicKeyHash(from: payloadHash) { + if let certificatePublicKey = await certificateHandler.publicKeyHash(from: payloadHash) { hash = certificatePublicKey } else { hash = payloadHash } - guard let (store, secret) = secret(matching: hash) else { + guard let (store, secret) = await secret(matching: hash) else { logger.debug("Agent did not have a key matching \(hash as NSData)") throw AgentError.noMatchingKey } 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 +177,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 +190,12 @@ extension Agent { extension Agent { /// Gives any store with no loaded secrets a chance to reload. - func reloadSecretsIfNeccessary() { - for store in storeList.stores { - if store.secrets.isEmpty { - logger.debug("Store \(store.name, privacy: .public) has no loaded secrets. Reloading.") - store.reloadSecrets() + func reloadSecretsIfNeccessary() async { + for store in await storeList.stores { + if await store.secrets.isEmpty { + let name = await store.name + logger.debug("Store \(name, privacy: .public) has no loaded secrets. Reloading.") + await store.reloadSecrets() } } } @@ -200,16 +203,16 @@ extension Agent { /// Finds a ``Secret`` matching a specified hash whos signature was requested. /// - Parameter hash: The hash to match against. /// - Returns: A ``Secret`` and the ``SecretStore`` containing it, if a match is found. - func secret(matching hash: Data) -> (AnySecretStore, AnySecret)? { - storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in - let allMatching = store.secrets.filter { secret in + func secret(matching hash: Data) async -> (AnySecretStore, AnySecret)? { + for store in await storeList.stores { + let allMatching = await store.secrets.filter { secret in hash == writer.data(secret: secret) } if let matching = allMatching.first { return (store, matching) } - return nil - }.first + } + return nil } } diff --git a/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift b/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift index b090bd3..2e6ab49 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift @@ -2,7 +2,7 @@ import Foundation import SecretKit /// A protocol that allows conformers to be notified of access to secrets, and optionally prevent access. -public protocol SigningWitness { +public protocol SigningWitness: Sendable { /// A ridiculously named method that notifies the callee that a signing operation is about to be performed using a secret. The callee may `throw` an `Error` to prevent access from occurring. /// - Parameters: @@ -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..f62bab1 100644 --- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift @@ -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(_ 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(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/OpenSSH/OpenSSHCertificateHandler.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift index 5066545..dbfcc7a 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift @@ -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(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. diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift index da8c4b1..cca64df 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift @@ -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() { diff --git a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift index 7c3f8da..736c0f8 100644 --- a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift +++ b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift @@ -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 diff --git a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift index eb8456f..d8d4074 100644 --- a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift +++ b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift @@ -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 = [] + public var modifiableStore: AnySecretStoreModifiable? = nil /// Initializes a SecretStoreList. - public init() { + public nonisolated init() { } /// Adds a non-type-erased SecretStore to the list. public func add(store: SecretStoreType) { - addInternal(store: AnySecretStore(store)) + stores.append(AnySecretStore(store)) } /// Adds a non-type-erased modifiable SecretStore. public func add(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) - } - -} diff --git a/Sources/Packages/Sources/SecretKit/Types/PersistedAuthenticationContext.swift b/Sources/Packages/Sources/SecretKit/Types/PersistedAuthenticationContext.swift index 65ceaf8..edd6dea 100644 --- a/Sources/Packages/Sources/SecretKit/Types/PersistedAuthenticationContext.swift +++ b/Sources/Packages/Sources/SecretKit/Types/PersistedAuthenticationContext.swift @@ -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. 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..c47c17e 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift @@ -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 } diff --git a/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift b/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift index a1095fd..2216f45 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift @@ -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 diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift new file mode 100644 index 0000000..d42e051 --- /dev/null +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/PersistentAuthenticationHandler.swift @@ -0,0 +1,72 @@ +import LocalAuthentication +import SecretKit + +extension SecureEnclave { + + /// A context describing a persisted authentication. + final class PersistentAuthenticationContext: PersistedAuthenticationContext { + + /// The Secret to persist authentication for. + let secret: Secret + /// The LAContext used to authorize the persistent context. + nonisolated(unsafe) let context: LAContext + /// An expiration date for the context. + /// - Note - Monotonic time instead of Date() to prevent people setting the clock back. + let monotonicExpiration: UInt64 + + /// Initializes a context. + /// - Parameters: + /// - secret: The Secret to persist authentication for. + /// - context: The LAContext used to authorize the persistent context. + /// - duration: The duration of the authorization context, in seconds. + init(secret: Secret, context: LAContext, duration: TimeInterval) { + self.secret = secret + self.context = context + let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value + self.monotonicExpiration = clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds) + } + + /// A boolean describing whether or not the context is still valid. + var valid: Bool { + clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration + } + + var expiration: Date { + let remainingNanoseconds = monotonicExpiration - clock_gettime_nsec_np(CLOCK_MONOTONIC) + let remainingInSeconds = Measurement(value: Double(remainingNanoseconds), unit: UnitDuration.nanoseconds).converted(to: .seconds).value + return Date(timeIntervalSinceNow: remainingInSeconds) + } + } + + actor PersistentAuthenticationHandler: Sendable { + + private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:] + + func existingPersistedAuthenticationContext(secret: Secret) -> PersistentAuthenticationContext? { + guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil } + return persisted + } + + func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws { + let newContext = LAContext() + newContext.touchIDAuthenticationAllowableReuseDuration = duration + newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button") + + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .spellOut + formatter.allowedUnits = [.hour, .minute, .day] + + if let durationString = formatter.string(from: duration) { + newContext.localizedReason = String(localized: "auth_context_persist_for_duration_\(secret.name)_\(durationString)") + } else { + newContext.localizedReason = String(localized: "auth_context_persist_for_duration_unknown_\(secret.name)") + } + let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason) + guard success else { return } + let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration) + persistedAuthenticationContexts[secret] = context + } + + } + +} diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index 19b6168..2c756be 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -1,38 +1,36 @@ import Foundation -import Combine +import Observation import Security -import CryptoTokenKit +import CryptoKit import LocalAuthentication import SecretKit extension SecureEnclave { /// An implementation of Store backed by the Secure Enclave. - public final class Store: SecretStoreModifiable { + @Observable public final class Store: SecretStoreModifiable { + @MainActor public var secrets: [Secret] = [] 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] = [] - - private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:] + private let persistentAuthenticationHandler = PersistentAuthenticationHandler() /// Initializes a Store. - public init() { - DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { [reload = reloadSecretsInternal(notifyAgent:)] _ in - reload(false) - } + @MainActor public init() { loadSecrets() + Task { + for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { + await reloadSecretsInternal(notifyAgent: false) + } + } } // 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 +67,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 +79,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,12 +96,12 @@ 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 { + public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data { let context: LAContext - if let existing = persistedAuthenticationContexts[secret], existing.valid { + if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) { context = existing.context } else { let newContext = LAContext() @@ -120,7 +118,7 @@ extension SecureEnclave { kSecAttrApplicationTag: Constants.keyTag, kSecUseAuthenticationContext: context, kSecReturnRef: true - ]) + ]) var untyped: CFTypeRef? let status = SecItemCopyMatching(attributes, &untyped) if status != errSecSuccess { @@ -173,34 +171,16 @@ extension SecureEnclave { return verified } - public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? { - guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil } - return persisted + public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? { + await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) } - public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) throws { - let newContext = LAContext() - newContext.touchIDAuthenticationAllowableReuseDuration = duration - newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button") - - let formatter = DateComponentsFormatter() - formatter.unitsStyle = .spellOut - formatter.allowedUnits = [.hour, .minute, .day] - - if let durationString = formatter.string(from: duration) { - newContext.localizedReason = String(localized: "auth_context_persist_for_duration_\(secret.name)_\(durationString)") - } else { - newContext.localizedReason = String(localized: "auth_context_persist_for_duration_unknown_\(secret.name)") - } - newContext.evaluatePolicy(LAPolicy.deviceOwnerAuthentication, localizedReason: newContext.localizedReason) { [weak self] success, _ in - guard success else { return } - let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration) - self?.persistedAuthenticationContexts[secret] = context - } + public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws { + try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration) } - public func reloadSecrets() { - reloadSecretsInternal(notifyAgent: false) + public func reloadSecrets() async { + await reloadSecretsInternal(notifyAgent: false) } } @@ -211,7 +191,7 @@ 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) { + @MainActor private func reloadSecretsInternal(notifyAgent: Bool = true) async { let before = secrets secrets.removeAll() loadSecrets() @@ -224,7 +204,7 @@ extension SecureEnclave.Store { } /// Loads all secrets from the store. - private func loadSecrets() { + @MainActor private func loadSecrets() { let publicAttributes = KeychainDictionary([ kSecClass: kSecClassKey, kSecAttrKeyType: SecureEnclave.Constants.keyType, @@ -304,48 +284,9 @@ 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 } } - -extension SecureEnclave { - - /// A context describing a persisted authentication. - private struct PersistentAuthenticationContext: PersistedAuthenticationContext { - - /// The Secret to persist authentication for. - let secret: Secret - /// The LAContext used to authorize the persistent context. - let context: LAContext - /// An expiration date for the context. - /// - Note - Monotonic time instead of Date() to prevent people setting the clock back. - let monotonicExpiration: UInt64 - - /// Initializes a context. - /// - Parameters: - /// - secret: The Secret to persist authentication for. - /// - context: The LAContext used to authorize the persistent context. - /// - duration: The duration of the authorization context, in seconds. - init(secret: Secret, context: LAContext, duration: TimeInterval) { - self.secret = secret - self.context = context - let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value - self.monotonicExpiration = clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds) - } - - /// A boolean describing whether or not the context is still valid. - var valid: Bool { - clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration - } - - var expiration: Date { - let remainingNanoseconds = monotonicExpiration - clock_gettime_nsec_np(CLOCK_MONOTONIC) - let remainingInSeconds = Measurement(value: Double(remainingNanoseconds), unit: UnitDuration.nanoseconds).converted(to: .seconds).value - return Date(timeIntervalSinceNow: remainingInSeconds) - } - } - -} diff --git a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift index c8c3281..f9ed8f8 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift @@ -1,40 +1,53 @@ import Foundation -import Combine +import Observation import Security import CryptoTokenKit import LocalAuthentication import SecretKit extension SmartCard { + + @MainActor @Observable fileprivate final class State { + var isAvailable = false + var name = String(localized: "smart_card") + var secrets: [Secret] = [] + let watcher = TKTokenWatcher() + var tokenID: String? = nil + nonisolated init() {} + } /// An implementation of Store backed by a Smart Card. - public final class Store: SecretStore { + @Observable public final class Store: SecretStore { + + private let state = State() + public var isAvailable: Bool { + state.isAvailable + } - @Published public var isAvailable: Bool = false public let id = UUID() - public private(set) var name = String(localized: "smart_card") - @Published public private(set) var secrets: [Secret] = [] - private let watcher = TKTokenWatcher() - private var tokenID: String? + @MainActor public var name: String { + state.name + } + public var secrets: [Secret] { + state.secrets + } /// Initializes a Store. public init() { - tokenID = watcher.nonSecureEnclaveTokens.first - watcher.setInsertionHandler { [reload = reloadSecretsInternal] string in - guard self.tokenID == nil else { return } - guard !string.contains("setoken") else { return } - - self.tokenID = string - DispatchQueue.main.async { - reload() + Task { @MainActor in + if let tokenID = state.tokenID { + state.isAvailable = true + state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID) + } + loadSecrets() + state.watcher.setInsertionHandler { id in + // Setting insertion handler will cause it to be called immediately. + // Make a thread jump so we don't hit a recursive lock attempt. + Task { + self.smartcardInserted(for: id) + } } - self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string) } - if let tokenID = tokenID { - self.isAvailable = true - self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID) - } - loadSecrets() } // MARK: Public API @@ -47,8 +60,8 @@ extension SmartCard { fatalError("Keys must be deleted on the smart card.") } - public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data { - guard let tokenID = tokenID else { fatalError() } + public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data { + guard let tokenID = await state.tokenID else { fatalError() } let context = LAContext() context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)") context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") @@ -107,7 +120,7 @@ extension SmartCard { } /// Reloads all secrets from the store. - public func reloadSecrets() { + @MainActor public func reloadSecrets() { reloadSecretsInternal() } @@ -117,32 +130,43 @@ extension SmartCard { extension SmartCard.Store { - private func reloadSecretsInternal() { - self.isAvailable = self.tokenID != nil - let before = self.secrets - self.secrets.removeAll() - self.loadSecrets() + @MainActor private func reloadSecretsInternal() { + let before = state.secrets + state.isAvailable = state.tokenID != nil + state.secrets.removeAll() + loadSecrets() if self.secrets != before { NotificationCenter.default.post(name: .secretStoreReloaded, object: self) } } + /// Resets the token ID and reloads secrets. + /// - Parameter tokenID: The ID of the token that was inserted. + @MainActor private func smartcardInserted(for tokenID: String? = nil) { + guard let string = state.watcher.nonSecureEnclaveTokens.first else { return } + guard state.tokenID == nil else { return } + guard !string.contains("setoken") else { return } + state.tokenID = string + state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string) + state.tokenID = string + } + /// Resets the token ID and reloads secrets. /// - Parameter tokenID: The ID of the token that was removed. - private func smartcardRemoved(for tokenID: String? = nil) { - self.tokenID = nil + @MainActor private func smartcardRemoved(for tokenID: String? = nil) { + state.tokenID = nil reloadSecrets() } /// Loads all secrets from the store. - private func loadSecrets() { - guard let tokenID = tokenID else { return } + @MainActor private func loadSecrets() { + guard let tokenID = state.tokenID else { return } let fallbackName = String(localized: "smart_card") - if let driverName = watcher.tokenInfo(forTokenID: tokenID)?.driverName { - name = driverName + if let driverName = state.watcher.tokenInfo(forTokenID: tokenID)?.driverName { + state.name = driverName } else { - name = fallbackName + state.name = fallbackName } let attributes = KeychainDictionary([ @@ -166,7 +190,7 @@ extension SmartCard.Store { let publicKey = publicKeyAttributes[kSecValueData] as! Data return SmartCard.Secret(id: tokenID, name: name, algorithm: algorithm, keySize: keySize, publicKey: publicKey) } - secrets.append(contentsOf: wrapped) + state.secrets.append(contentsOf: wrapped) } } @@ -209,8 +233,8 @@ extension SmartCard.Store { /// - secret: The secret to decrypt with. /// - Returns: The decrypted data. /// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged. - public func decrypt(data: Data, with secret: SecretType) throws -> Data { - guard let tokenID = tokenID else { fatalError() } + public func decrypt(data: Data, with secret: SecretType) async throws -> Data { + guard let tokenID = await state.tokenID else { fatalError() } let context = LAContext() context.localizedReason = String(localized: "auth_context_request_decrypt_description_\(secret.name)") context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") diff --git a/Sources/Packages/Tests/BriefTests/ReleaseParsingTests.swift b/Sources/Packages/Tests/BriefTests/ReleaseParsingTests.swift index 74af9ee..a75c2ad 100644 --- a/Sources/Packages/Tests/BriefTests/ReleaseParsingTests.swift +++ b/Sources/Packages/Tests/BriefTests/ReleaseParsingTests.swift @@ -1,54 +1,65 @@ -import XCTest +import Testing +import Foundation @testable import Brief -class ReleaseParsingTests: XCTestCase { +@Suite struct ReleaseParsingTests { - func testNonCritical() { + @Test + func nonCritical() { let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Initial release") - XCTAssert(release.critical == false) + #expect(release.critical == false) } - func testCritical() { + @Test + func critical() { let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update") - XCTAssert(release.critical == true) + #expect(release.critical == true) } - func testOSMissing() { + @Test + func osMissing() { let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update") - XCTAssert(release.minimumOSVersion == SemVer("11.0.0")) + #expect(release.minimumOSVersion == SemVer("11.0.0")) } - func testOSPresentWithContentBelow() { + @Test + func osPresentWithContentBelow() { let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update ##Minimum macOS Version\n1.2.3\nBuild info") - XCTAssert(release.minimumOSVersion == SemVer("1.2.3")) + #expect(release.minimumOSVersion == SemVer("1.2.3")) } - func testOSPresentAtEnd() { + @Test + func osPresentAtEnd() { let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 1.2.3") - XCTAssert(release.minimumOSVersion == SemVer("1.2.3")) + #expect(release.minimumOSVersion == SemVer("1.2.3")) } - func testOSWithMacOSPrefix() { + @Test + func osWithMacOSPrefix() { let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: macOS 1.2.3") - XCTAssert(release.minimumOSVersion == SemVer("1.2.3")) + #expect(release.minimumOSVersion == SemVer("1.2.3")) } - func testOSGreaterThanMinimum() { + @Test + func osGreaterThanMinimum() { let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 1.2.3") - XCTAssert(release.minimumOSVersion < SemVer("11.0.0")) + #expect(release.minimumOSVersion < SemVer("11.0.0")) } - func testOSEqualToMinimum() { + @Test + func osEqualToMinimum() { let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 11.2.3") - XCTAssert(release.minimumOSVersion <= SemVer("11.2.3")) + #expect(release.minimumOSVersion <= SemVer("11.2.3")) } - func testOSLessThanMinimum() { + @Test + func osLessThanMinimum() { let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 1.2.3") - XCTAssert(release.minimumOSVersion > SemVer("1.0.0")) + #expect(release.minimumOSVersion > SemVer("1.0.0")) } - func testGreatestSelectedIfOldPatchIsPublishedLater() { + @Test + @MainActor func greatestSelectedIfOldPatchIsPublishedLater() async throws { // If 2.x.x series has been published, and a patch for 1.x.x is issued // 2.x.x should still be selected if user can run it. let updater = Updater(checkOnLaunch: false, osVersion: SemVer("2.2.3"), currentVersion: SemVer("1.0.0")) @@ -60,16 +71,13 @@ class ReleaseParsingTests: XCTestCase { Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch! Minimum macOS Version: 1.2.3"), ] - let expectation = XCTestExpectation() - updater.evaluate(releases: releases) - DispatchQueue.main.async { - XCTAssert(updater.update == two) - expectation.fulfill() - } - wait(for: [expectation], timeout: 1) + await updater.evaluate(releases: releases) + try await Task.sleep(nanoseconds: 1) + #expect(updater.update == two) } - func testLatestVersionIsRunnable() { + @Test + @MainActor func latestVersionIsRunnable() async throws { // If the 2.x.x series has been published but the user can't run it // the last version the user can run should be selected. let updater = Updater(checkOnLaunch: false, osVersion: SemVer("1.2.3"), currentVersion: SemVer("1.0.0")) @@ -80,16 +88,13 @@ class ReleaseParsingTests: XCTestCase { Release(name: "2.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "2.0 available! Minimum macOS Version: 2.2.3"), Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch! Minimum macOS Version: 1.2.3"), ] - let expectation = XCTestExpectation() - updater.evaluate(releases: releases) - DispatchQueue.main.async { - XCTAssert(updater.update == oneOhTwo) - expectation.fulfill() - } - wait(for: [expectation], timeout: 1) + await updater.evaluate(releases: releases) + try await Task.sleep(nanoseconds: 1) + #expect(updater.update == oneOhTwo) } - func testSorting() { + @Test + func sorting() { let two = Release(name: "2.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "2.0 available!") let releases = [ Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Initial release"), @@ -98,7 +103,7 @@ class ReleaseParsingTests: XCTestCase { Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch!"), ] let sorted = releases.sorted().reversed().first - XCTAssert(sorted == two) + #expect(sorted == two) } } diff --git a/Sources/Packages/Tests/BriefTests/SemVerTests.swift b/Sources/Packages/Tests/BriefTests/SemVerTests.swift index f7ee332..5cd5738 100644 --- a/Sources/Packages/Tests/BriefTests/SemVerTests.swift +++ b/Sources/Packages/Tests/BriefTests/SemVerTests.swift @@ -1,51 +1,52 @@ -import XCTest +import Testing +import Foundation @testable import Brief -class SemVerTests: XCTestCase { +@Suite struct SemVerTests { - func testEqual() { + @Test func equal() { let current = SemVer("1.0.2") let old = SemVer("1.0.2") - XCTAssert(!(current > old)) + #expect(!(current > old)) } - func testPatchGreaterButMinorLess() { + @Test func patchGreaterButMinorLess() { let current = SemVer("1.1.0") let old = SemVer("1.0.2") - XCTAssert(current > old) + #expect(current > old) } - func testMajorSameMinorGreater() { + @Test func majorSameMinorGreater() { let current = SemVer("1.0.2") let new = SemVer("1.0.3") - XCTAssert(current < new) + #expect(current < new) } - func testMajorGreaterMinorLesser() { + @Test func majorGreaterMinorLesser() { let current = SemVer("1.0.2") let new = SemVer("2.0.0") - XCTAssert(current < new) + #expect(current < new) } - func testRegularParsing() { + @Test func regularParsing() { let current = SemVer("1.0.2") - XCTAssert(current.versionNumbers == [1, 0, 2]) + #expect(current.versionNumbers == [1, 0, 2]) } - func testNoPatch() { + @Test func noPatch() { let current = SemVer("1.1") - XCTAssert(current.versionNumbers == [1, 1, 0]) + #expect(current.versionNumbers == [1, 1, 0]) } - func testGarbage() { + @Test func garbage() { let current = SemVer("Test") - XCTAssert(current.versionNumbers == [0, 0, 0]) + #expect(current.versionNumbers == [0, 0, 0]) } - func testBeta() { + @Test func beta() { let current = SemVer("1.0.2") let new = SemVer("1.1.0_beta1") - XCTAssert(current < new) + #expect(current < new) } } diff --git a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift index d5ffd37..63734ef 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift @@ -1,46 +1,46 @@ import Foundation -import XCTest +import Testing import CryptoKit @testable import SecretKit @testable import SecretAgentKit -class AgentTests: XCTestCase { +@Suite struct AgentTests { let stubWriter = StubFileHandleWriter() // MARK: Identity Listing - func testEmptyStores() async { + @Test func emptyStores() async { let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestIdentities) let agent = Agent(storeList: SecretStoreList()) await agent.handle(reader: stubReader, writer: stubWriter) - XCTAssertEqual(stubWriter.data, Constants.Responses.requestIdentitiesEmpty) + #expect(stubWriter.data == Constants.Responses.requestIdentitiesEmpty) } - func testIdentitiesList() async { + @Test func identitiesList() async { let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestIdentities) - let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) + let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let agent = Agent(storeList: list) await agent.handle(reader: stubReader, writer: stubWriter) - XCTAssertEqual(stubWriter.data, Constants.Responses.requestIdentitiesMultiple) + #expect(stubWriter.data == Constants.Responses.requestIdentitiesMultiple) } // MARK: Signatures - func testNoMatchingIdentities() async { + @Test func noMatchingIdentities() async { let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignatureWithNoneMatching) - let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) + let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let agent = Agent(storeList: list) await agent.handle(reader: stubReader, writer: stubWriter) -// XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure) + #expect(stubWriter.data == Constants.Responses.requestFailure) } - func testSignature() async { + @Test func signature() async throws { let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) let requestReader = OpenSSHReader(data: Constants.Requests.requestSignature[5...]) _ = requestReader.readNextChunk() let dataToSign = requestReader.readNextChunk() - let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) + let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let agent = Agent(storeList: list) await agent.handle(reader: stubReader, writer: stubWriter) let outer = OpenSSHReader(data: stubWriter.data[5...]) @@ -62,35 +62,35 @@ class AgentTests: XCTestCase { rs.append(s) let signature = try! P256.Signing.ECDSASignature(rawRepresentation: rs) let referenceValid = try! P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey).isValidSignature(signature, for: dataToSign) - let store = list.stores.first! - let derVerifies = try! store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret)) - let invalidRandomSignature = try? store.verify(signature: "invalid".data(using: .utf8)!, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret)) - let invalidRandomData = try? store.verify(signature: signature.derRepresentation, for: "invalid".data(using: .utf8)!, with: AnySecret(Constants.Secrets.ecdsa256Secret)) - let invalidWrongKey = try? store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa384Secret)) - XCTAssertTrue(referenceValid) - XCTAssertTrue(derVerifies) - XCTAssert(invalidRandomSignature == false) - XCTAssert(invalidRandomData == false) - XCTAssert(invalidWrongKey == false) + let store = await list.stores.first! + let derVerifies = try await store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret)) + let invalidRandomSignature = try await store.verify(signature: "invalid".data(using: .utf8)!, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret)) + let invalidRandomData = try await store.verify(signature: signature.derRepresentation, for: "invalid".data(using: .utf8)!, with: AnySecret(Constants.Secrets.ecdsa256Secret)) + let invalidWrongKey = try await store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa384Secret)) + #expect(referenceValid) + #expect(derVerifies) + #expect(invalidRandomSignature == false) + #expect(invalidRandomData == false) + #expect(invalidWrongKey == false) } // MARK: Witness protocol - func testWitnessObjectionStopsRequest() async { + @Test func witnessObjectionStopsRequest() async { let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) - let list = storeList(with: [Constants.Secrets.ecdsa256Secret]) + let list = await storeList(with: [Constants.Secrets.ecdsa256Secret]) let witness = StubWitness(speakNow: { _,_ in return true }, witness: { _, _ in }) let agent = Agent(storeList: list, witness: witness) await agent.handle(reader: stubReader, writer: stubWriter) - XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure) + #expect(stubWriter.data == Constants.Responses.requestFailure) } - func testWitnessSignature() async { + @Test func witnessSignature() async { let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) - let list = storeList(with: [Constants.Secrets.ecdsa256Secret]) - var witnessed = false + let list = await storeList(with: [Constants.Secrets.ecdsa256Secret]) + nonisolated(unsafe) var witnessed = false let witness = StubWitness(speakNow: { _, trace in return false }, witness: { _, trace in @@ -98,14 +98,14 @@ class AgentTests: XCTestCase { }) let agent = Agent(storeList: list, witness: witness) await agent.handle(reader: stubReader, writer: stubWriter) - XCTAssertTrue(witnessed) + #expect(witnessed) } - func testRequestTracing() async { + @Test func requestTracing() async { let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) - let list = storeList(with: [Constants.Secrets.ecdsa256Secret]) - var speakNowTrace: SigningRequestProvenance! = nil - var witnessTrace: SigningRequestProvenance! = nil + let list = await storeList(with: [Constants.Secrets.ecdsa256Secret]) + nonisolated(unsafe) var speakNowTrace: SigningRequestProvenance? + nonisolated(unsafe) var witnessTrace: SigningRequestProvenance? let witness = StubWitness(speakNow: { _, trace in speakNowTrace = trace return false @@ -114,38 +114,38 @@ class AgentTests: XCTestCase { }) let agent = Agent(storeList: list, witness: witness) await agent.handle(reader: stubReader, writer: stubWriter) - XCTAssertEqual(witnessTrace, speakNowTrace) - XCTAssertEqual(witnessTrace.origin.displayName, "Finder") - XCTAssertEqual(witnessTrace.origin.validSignature, true) - XCTAssertEqual(witnessTrace.origin.parentPID, 1) + #expect(witnessTrace == speakNowTrace) + #expect(witnessTrace?.origin.displayName == "Finder") + #expect(witnessTrace?.origin.validSignature == true) + #expect(witnessTrace?.origin.parentPID == 1) } // MARK: Exception Handling - func testSignatureException() async { + @Test func signatureException() async { let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) - let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) - let store = list.stores.first?.base as! Stub.Store + let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) + let store = await list.stores.first?.base as! Stub.Store store.shouldThrow = true let agent = Agent(storeList: list) await agent.handle(reader: stubReader, writer: stubWriter) - XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure) + #expect(stubWriter.data == Constants.Responses.requestFailure) } // MARK: Unsupported - func testUnhandledAdd() async { + @Test func unhandledAdd() async { let stubReader = StubFileHandleReader(availableData: Constants.Requests.addIdentity) let agent = Agent(storeList: SecretStoreList()) await agent.handle(reader: stubReader, writer: stubWriter) - XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure) + #expect(stubWriter.data == Constants.Responses.requestFailure) } } extension AgentTests { - func storeList(with secrets: [Stub.Secret]) -> SecretStoreList { + @MainActor func storeList(with secrets: [Stub.Secret]) async -> SecretStoreList { let store = Stub.Store() store.secrets.append(contentsOf: secrets) let storeList = SecretStoreList() diff --git a/Sources/Packages/Tests/SecretAgentKitTests/StubFileHandleWriter.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubFileHandleWriter.swift index 5b35aef..798a7e2 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubFileHandleWriter.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubFileHandleWriter.swift @@ -1,7 +1,7 @@ import Foundation import SecretAgentKit -class StubFileHandleWriter: FileHandleWriter { +class StubFileHandleWriter: FileHandleWriter, @unchecked Sendable { var data = Data() diff --git a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift index f990f97..a390e25 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift @@ -6,7 +6,7 @@ struct Stub {} extension Stub { - public final class Store: SecretStore { + public final class Store: SecretStore, @unchecked Sendable { public let isAvailable = true public let id = UUID() diff --git a/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift index 87e0fd9..2fa1af8 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift @@ -3,8 +3,8 @@ import SecretAgentKit struct StubWitness { - let speakNow: (AnySecret, SigningRequestProvenance) -> Bool - let witness: (AnySecret, SigningRequestProvenance) -> () + let speakNow: @Sendable (AnySecret, SigningRequestProvenance) -> Bool + let witness: @Sendable (AnySecret, SigningRequestProvenance) -> () } diff --git a/Sources/Packages/Tests/SecretKitTests/AnySecretTests.swift b/Sources/Packages/Tests/SecretKitTests/AnySecretTests.swift index ee2646b..f8229bd 100644 --- a/Sources/Packages/Tests/SecretKitTests/AnySecretTests.swift +++ b/Sources/Packages/Tests/SecretKitTests/AnySecretTests.swift @@ -1,19 +1,19 @@ import Foundation -import XCTest +import Testing @testable import SecretKit @testable import SecureEnclaveSecretKit @testable import SmartCardSecretKit -class AnySecretTests: XCTestCase { +@Suite struct AnySecretTests { - func testEraser() { + @Test func eraser() { let secret = SmartCard.Secret(id: UUID().uuidString.data(using: .utf8)!, name: "Name", algorithm: .ellipticCurve, keySize: 256, publicKey: UUID().uuidString.data(using: .utf8)!) let erased = AnySecret(secret) - XCTAssert(erased.id == secret.id as AnyHashable) - XCTAssert(erased.name == secret.name) - XCTAssert(erased.algorithm == secret.algorithm) - XCTAssert(erased.keySize == secret.keySize) - XCTAssert(erased.publicKey == secret.publicKey) + #expect(erased.id == secret.id as AnyHashable) + #expect(erased.name == secret.name) + #expect(erased.algorithm == secret.algorithm) + #expect(erased.keySize == secret.keySize) + #expect(erased.publicKey == secret.publicKey) } } diff --git a/Sources/Packages/Tests/SecretKitTests/OpenSSHReaderTests.swift b/Sources/Packages/Tests/SecretKitTests/OpenSSHReaderTests.swift index 5ad7494..39c3029 100644 --- a/Sources/Packages/Tests/SecretKitTests/OpenSSHReaderTests.swift +++ b/Sources/Packages/Tests/SecretKitTests/OpenSSHReaderTests.swift @@ -1,19 +1,19 @@ import Foundation -import XCTest +import Testing @testable import SecretKit @testable import SecureEnclaveSecretKit @testable import SmartCardSecretKit -class OpenSSHReaderTests: XCTestCase { +@Suite struct OpenSSHReaderTests { - func testSignatureRequest() { + @Test func signatureRequest() { let reader = OpenSSHReader(data: Constants.signatureRequest) let hash = reader.readNextChunk() - XCTAssert(hash == Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQ==")) + #expect(hash == Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQ==")) let dataToSign = reader.readNextChunk() - XCTAssert(dataToSign == Data(base64Encoded: "AAAAICi5xf1ixOestUlxdjvt/BDcM+rzhwy7Vo8cW5YcxA8+MgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRKgm5CWzh8uUtcFQmiaCj5qUtHoDOH6zKpZYJ5UUUP1L55+hFUvKFdN3DwNgc9BHREskSgP7G5p9GgbYIyMBhC8buOVFxFtUxM+k8cktLt77iKkd1qNQq0FkQ55Kxv6QU=")) + #expect(dataToSign == Data(base64Encoded: "AAAAICi5xf1ixOestUlxdjvt/BDcM+rzhwy7Vo8cW5YcxA8+MgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRKgm5CWzh8uUtcFQmiaCj5qUtHoDOH6zKpZYJ5UUUP1L55+hFUvKFdN3DwNgc9BHREskSgP7G5p9GgbYIyMBhC8buOVFxFtUxM+k8cktLt77iKkd1qNQq0FkQ55Kxv6QU=")) let empty = reader.readNextChunk() - XCTAssert(empty.isEmpty) + #expect(empty.isEmpty) } } diff --git a/Sources/Packages/Tests/SecretKitTests/OpenSSHWriterTests.swift b/Sources/Packages/Tests/SecretKitTests/OpenSSHWriterTests.swift index 6e9718f..e471992 100644 --- a/Sources/Packages/Tests/SecretKitTests/OpenSSHWriterTests.swift +++ b/Sources/Packages/Tests/SecretKitTests/OpenSSHWriterTests.swift @@ -1,45 +1,45 @@ import Foundation -import XCTest +import Testing @testable import SecretKit @testable import SecureEnclaveSecretKit @testable import SmartCardSecretKit -class OpenSSHWriterTests: XCTestCase { +@Suite struct OpenSSHWriterTests { let writer = OpenSSHKeyWriter() - func testECDSA256MD5Fingerprint() { - XCTAssertEqual(writer.openSSHMD5Fingerprint(secret: Constants.ecdsa256Secret), "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5") + @Test func ecdsa256MD5Fingerprint() { + #expect(writer.openSSHMD5Fingerprint(secret: Constants.ecdsa256Secret) == "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5") } - func testECDSA256SHA256Fingerprint() { - XCTAssertEqual(writer.openSSHSHA256Fingerprint(secret: Constants.ecdsa256Secret), "SHA256:/VQFeGyM8qKA8rB6WGMuZZxZLJln2UgXLk3F0uTF650") + @Test func ecdsa256SHA256Fingerprint() { + #expect(writer.openSSHSHA256Fingerprint(secret: Constants.ecdsa256Secret) == "SHA256:/VQFeGyM8qKA8rB6WGMuZZxZLJln2UgXLk3F0uTF650") } - func testECDSA256PublicKey() { - XCTAssertEqual(writer.openSSHString(secret: Constants.ecdsa256Secret), + @Test func ecdsa256PublicKey() { + #expect(writer.openSSHString(secret: Constants.ecdsa256Secret) == "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=") } - func testECDSA256Hash() { - XCTAssertEqual(writer.data(secret: Constants.ecdsa256Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")) + @Test func ecdsa256Hash() { + #expect(writer.data(secret: Constants.ecdsa256Secret) == Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")) } - func testECDSA384MD5Fingerprint() { - XCTAssertEqual(writer.openSSHMD5Fingerprint(secret: Constants.ecdsa384Secret), "66:e0:66:d7:41:ed:19:8e:e2:20:df:ce:ac:7e:2b:6e") + @Test func ecdsa384MD5Fingerprint() { + #expect(writer.openSSHMD5Fingerprint(secret: Constants.ecdsa384Secret) == "66:e0:66:d7:41:ed:19:8e:e2:20:df:ce:ac:7e:2b:6e") } - func testECDSA384SHA256Fingerprint() { - XCTAssertEqual(writer.openSSHSHA256Fingerprint(secret: Constants.ecdsa384Secret), "SHA256:GJUEymQNL9ymaMRRJCMGY4rWIJHu/Lm8Yhao/PAiz1I") + @Test func ecdsa384SHA256Fingerprint() { + #expect(writer.openSSHSHA256Fingerprint(secret: Constants.ecdsa384Secret) == "SHA256:GJUEymQNL9ymaMRRJCMGY4rWIJHu/Lm8Yhao/PAiz1I") } - func testECDSA384PublicKey() { - XCTAssertEqual(writer.openSSHString(secret: Constants.ecdsa384Secret), + @Test func ecdsa384PublicKey() { + #expect(writer.openSSHString(secret: Constants.ecdsa384Secret) == "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==") } - func testECDSA384Hash() { - XCTAssertEqual(writer.data(secret: Constants.ecdsa384Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")) + @Test func ecdsa384Hash() { + #expect(writer.data(secret: Constants.ecdsa384Secret) == Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")) } } diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index ab6a3cd..6bf0ec2 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -6,17 +6,18 @@ import SecureEnclaveSecretKit import SmartCardSecretKit import SecretAgentKit import Brief +import Observation -@NSApplicationMain +@main class AppDelegate: NSObject, NSApplicationDelegate { - private let storeList: SecretStoreList = { + @MainActor private let storeList: SecretStoreList = { let list = SecretStoreList() list.add(store: SecureEnclave.Store()) list.add(store: SmartCard.Store()) return list }() - private let updater = Updater(checkOnLaunch: false) + private let updater = Updater(checkOnLaunch: true) private let notifier = Notifier() private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) private lazy var agent: Agent = { @@ -31,17 +32,26 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { logger.debug("SecretAgent finished launching") - DispatchQueue.main.async { - self.socketController.handler = self.agent.handle(reader:writer:) + Task { @MainActor in + socketController.handler = { [agent] reader, writer in + await agent.handle(reader: reader, writer: writer) + } } - NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in - try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) + Task { + for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) { + 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:)) + _ = withObservationTracking { + updater.update + } onChange: { [updater, notifier] in + Task { + await notifier.notify(update: updater.update!) { release in + await updater.ignore(release: release) + } + } } } diff --git a/Sources/SecretAgent/Notifier.swift b/Sources/SecretAgent/Notifier.swift index 69b29bb..2e53260 100644 --- a/Sources/SecretAgent/Notifier.swift +++ b/Sources/SecretAgent/Notifier.swift @@ -5,7 +5,7 @@ import SecretKit import SecretAgentKit import Brief -class Notifier { +final class Notifier: Sendable { private let notificationDelegate = NotificationDelegate() @@ -29,12 +29,13 @@ class Notifier { formatter.unitsStyle = .spellOut formatter.allowedUnits = [.hour, .minute, .day] + var identifiers: [String: TimeInterval] = [:] for duration in rawDurations { let seconds = duration.converted(to: .seconds).value guard let string = formatter.string(from: seconds)?.capitalized else { continue } let identifier = Constants.persistAuthenticationCategoryIdentitifier.appending("\(seconds)") let action = UNNotificationAction(identifier: identifier, title: string, options: []) - notificationDelegate.persistOptions[identifier] = seconds + identifiers[identifier] = seconds allPersistenceActions.append(action) } @@ -45,9 +46,11 @@ class Notifier { UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory]) UNUserNotificationCenter.current().delegate = notificationDelegate - notificationDelegate.persistAuthentication = { secret, store, duration in - guard let duration = duration else { return } - try? store.persistAuthentication(secret: secret, forDuration: duration) + Task { + await notificationDelegate.state.setPersistenceState(options: identifiers) { secret, store, duration in + guard let duration = duration else { return } + try? await store.persistAuthentication(secret: secret, forDuration: duration) + } } } @@ -57,9 +60,8 @@ class Notifier { notificationCenter.requestAuthorization(options: .alert) { _, _ in } } - func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) { - notificationDelegate.pendingPersistableSecrets[secret.id.description] = secret - notificationDelegate.pendingPersistableStores[store.id.description] = store + func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async { + await notificationDelegate.state.setPending(secret: secret, store: store) let notificationCenter = UNUserNotificationCenter.current() let notificationContent = UNMutableNotificationContent() notificationContent.title = String(localized: "signed_notification_title_\(provenance.origin.displayName)") @@ -67,19 +69,18 @@ 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) { notificationContent.attachments = [attachment] } let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil) - notificationCenter.add(request, withCompletionHandler: nil) + try? await notificationCenter.add(request) } - func notify(update: Release, ignore: ((Release) -> Void)?) { - notificationDelegate.release = update - notificationDelegate.ignore = ignore + func notify(update: Release, ignore: (@Sendable (Release) async -> Void)?) async { + await notificationDelegate.state.prepareForNotification(release: update, ignoreAction: ignore) let notificationCenter = UNUserNotificationCenter.current() let notificationContent = UNMutableNotificationContent() if update.critical { @@ -92,18 +93,18 @@ class Notifier { notificationContent.body = update.body notificationContent.categoryIdentifier = update.critical ? Constants.criticalUpdateCategoryIdentitifier : Constants.updateCategoryIdentitifier let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil) - notificationCenter.add(request, withCompletionHandler: nil) + try? await notificationCenter.add(request) } } 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) } } @@ -129,55 +130,91 @@ extension Notifier { } -class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { +final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable { - fileprivate var release: Release? - fileprivate var ignore: ((Release) -> Void)? - fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) -> Void)? - fileprivate var persistOptions: [String: TimeInterval] = [:] - fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:] - fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:] + fileprivate actor State { + typealias PersistAction = (@Sendable (AnySecret, AnySecretStore, TimeInterval?) async -> Void) + typealias IgnoreAction = (@Sendable (Release) async -> Void) + fileprivate var release: Release? + fileprivate var ignoreAction: IgnoreAction? + fileprivate var persistAction: PersistAction? + fileprivate var persistOptions: [String: TimeInterval] = [:] + fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:] + fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:] + + func setPending(secret: AnySecret, store: AnySecretStore) { + pendingPersistableSecrets[secret.id.description] = secret + pendingPersistableStores[store.id.description] = store + } + + func retrievePending(secretID: String, storeID: String, optionID: String) -> (AnySecret, AnySecretStore, TimeInterval)? { + guard let secret = pendingPersistableSecrets[secretID], + let store = pendingPersistableStores[storeID], + let options = persistOptions[optionID] else { + return nil + } + pendingPersistableSecrets.removeValue(forKey: secretID) + return (secret, store, options) + } + + func setPersistenceState(options: [String: TimeInterval], action: @escaping PersistAction) { + self.persistOptions = options + self.persistAction = action + } + + func prepareForNotification(release: Release, ignoreAction: IgnoreAction?) { + self.release = release + self.ignoreAction = ignoreAction + } + + + } + + fileprivate let state = State() 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) + await handleUpdateResponse(response: response) case Notifier.Constants.persistAuthenticationCategoryIdentitifier: - handlePersistAuthenticationResponse(response: response) + await handlePersistAuthenticationResponse(response: response) default: break } - - completionHandler() } - func handleUpdateResponse(response: UNNotificationResponse) { - guard let update = release else { return } - switch response.actionIdentifier { + func handleUpdateResponse(response: UNNotificationResponse) async { + let id = response.actionIdentifier + guard let update = await state.release else { return } + switch id { case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier: NSWorkspace.shared.open(update.html_url) case Notifier.Constants.ignoreActionIdentitifier: - ignore?(update) + await state.ignoreAction?(update) default: fatalError() } } - func handlePersistAuthenticationResponse(response: UNNotificationResponse) { - 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]) + func handlePersistAuthenticationResponse(response: UNNotificationResponse) async { + 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 optionID = response.actionIdentifier + guard let (secret, store, persistOptions) = await state.retrievePending(secretID: secretID, storeID: storeID, optionID: optionID) else { return } + await state.persistAction?(secret, store, persistOptions) } - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.list, .banner]) + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + [.list, .banner] } } + diff --git a/Sources/SecretAgent/SecretAgent.entitlements b/Sources/SecretAgent/SecretAgent.entitlements index 895fc77..c9423c4 100644 --- a/Sources/SecretAgent/SecretAgent.entitlements +++ b/Sources/SecretAgent/SecretAgent.entitlements @@ -2,10 +2,6 @@ - com.apple.security.app-sandbox - - com.apple.security.network.client - com.apple.security.smartcard keychain-access-groups diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 1e757c1..d9b954e 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -30,7 +30,6 @@ 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; }; 50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; }; 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; }; - 50617D9923FCE48E0099B055 /* SecretiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D9823FCE48E0099B055 /* SecretiveTests.swift */; }; 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; }; 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; }; 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; }; @@ -63,13 +62,6 @@ remoteGlobalIDString = 50A3B78924026B7500D209EA; remoteInfo = SecretAgent; }; - 50617D9523FCE48E0099B055 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 50617D7723FCE48D0099B055 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 50617D7E23FCE48D0099B055; - remoteInfo = Secretive; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -123,9 +115,6 @@ 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 50617D8E23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50617D8F23FCE48E0099B055 /* Secretive.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Secretive.entitlements; sourceTree = ""; }; - 50617D9423FCE48E0099B055 /* SecretiveTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecretiveTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 50617D9823FCE48E0099B055 /* SecretiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretiveTests.swift; sourceTree = ""; }; - 50617D9A23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = ""; }; 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = ""; }; 5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; @@ -166,13 +155,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 50617D9123FCE48E0099B055 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 50A3B78724026B7500D209EA /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -201,7 +183,6 @@ children = ( 5003EF39278005C800DF2006 /* Packages */, 50617D8123FCE48E0099B055 /* Secretive */, - 50617D9723FCE48E0099B055 /* SecretiveTests */, 50A3B78B24026B7500D209EA /* SecretAgent */, 508A58AF241E144C0069DC07 /* Config */, 50617D8023FCE48E0099B055 /* Products */, @@ -213,7 +194,6 @@ isa = PBXGroup; children = ( 50617D7F23FCE48E0099B055 /* Secretive.app */, - 50617D9423FCE48E0099B055 /* SecretiveTests.xctest */, 50A3B78A24026B7500D209EA /* SecretAgent.app */, ); name = Products; @@ -248,15 +228,6 @@ path = "Preview Content"; sourceTree = ""; }; - 50617D9723FCE48E0099B055 /* SecretiveTests */ = { - isa = PBXGroup; - children = ( - 50617D9823FCE48E0099B055 /* SecretiveTests.swift */, - 50617D9A23FCE48E0099B055 /* Info.plist */, - ); - path = SecretiveTests; - sourceTree = ""; - }; 508A58AF241E144C0069DC07 /* Config */ = { isa = PBXGroup; children = ( @@ -357,24 +328,6 @@ productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */; productType = "com.apple.product-type.application"; }; - 50617D9323FCE48E0099B055 /* SecretiveTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 50617DA023FCE48E0099B055 /* Build configuration list for PBXNativeTarget "SecretiveTests" */; - buildPhases = ( - 50617D9023FCE48E0099B055 /* Sources */, - 50617D9123FCE48E0099B055 /* Frameworks */, - 50617D9223FCE48E0099B055 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 50617D9623FCE48E0099B055 /* PBXTargetDependency */, - ); - name = SecretiveTests; - productName = SecretiveTests; - productReference = 50617D9423FCE48E0099B055 /* SecretiveTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 50A3B78924026B7500D209EA /* SecretAgent */ = { isa = PBXNativeTarget; buildConfigurationList = 50A3B79A24026B7600D209EA /* Build configuration list for PBXNativeTarget "SecretAgent" */; @@ -406,17 +359,14 @@ 50617D7723FCE48D0099B055 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1220; - LastUpgradeCheck = 1320; + LastUpgradeCheck = 2600; ORGANIZATIONNAME = "Max Goedjen"; TargetAttributes = { 50617D7E23FCE48D0099B055 = { CreatedOnToolsVersion = 11.3; }; - 50617D9323FCE48E0099B055 = { - CreatedOnToolsVersion = 11.3; - TestTargetID = 50617D7E23FCE48D0099B055; - }; 50A3B78924026B7500D209EA = { CreatedOnToolsVersion = 11.4; }; @@ -445,7 +395,6 @@ projectRoot = ""; targets = ( 50617D7E23FCE48D0099B055 /* Secretive */, - 50617D9323FCE48E0099B055 /* SecretiveTests */, 50A3B78924026B7500D209EA /* SecretAgent */, ); }; @@ -464,13 +413,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 50617D9223FCE48E0099B055 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 50A3B78824026B7500D209EA /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -516,14 +458,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 50617D9023FCE48E0099B055 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 50617D9923FCE48E0099B055 /* SecretiveTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 50A3B78624026B7500D209EA /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -541,11 +475,6 @@ target = 50A3B78924026B7500D209EA /* SecretAgent */; targetProxy = 50142166278126B500BBAA70 /* PBXContainerItemProxy */; }; - 50617D9623FCE48E0099B055 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 50617D7E23FCE48D0099B055 /* Secretive */; - targetProxy = 50617D9523FCE48E0099B055 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -565,6 +494,8 @@ baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -595,9 +526,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -612,17 +545,19 @@ 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; OTHER_SWIFT_FLAGS = ""; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; STRIP_INSTALLED_PRODUCT = NO; STRIP_SWIFT_SYMBOLS = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -631,6 +566,8 @@ baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -661,9 +598,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -672,114 +611,78 @@ 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 = ""; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; STRIP_INSTALLED_PRODUCT = NO; STRIP_SWIFT_SYMBOLS = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 6.0; }; name = Release; }; 50617D9E23FCE48E0099B055 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Secretive/Secretive.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\""; DEVELOPMENT_TEAM = Z72PRUAWF6; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readwrite; INFOPLIST_FILE = Secretive/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; }; name = Debug; }; 50617D9F23FCE48E0099B055 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Secretive/Secretive.entitlements; CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\""; DEVELOPMENT_TEAM = Z72PRUAWF6; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readwrite; INFOPLIST_FILE = Secretive/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Secretive - Host"; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 50617DA123FCE48E0099B055 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = Z72PRUAWF6; - INFOPLIST_FILE = SecretiveTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Secretive.app/Contents/MacOS/Secretive"; - }; - name = Debug; - }; - 50617DA223FCE48E0099B055 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = Z72PRUAWF6; - INFOPLIST_FILE = SecretiveTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Secretive.app/Contents/MacOS/Secretive"; }; name = Release; }; @@ -788,6 +691,8 @@ baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -818,9 +723,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -835,63 +742,45 @@ 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; OTHER_SWIFT_FLAGS = ""; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; STRIP_INSTALLED_PRODUCT = NO; STRIP_SWIFT_SYMBOLS = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; }; name = Test; }; 508A5915241EF1A00069DC07 /* Test */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\""; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readwrite; INFOPLIST_FILE = Secretive/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - }; - name = Test; - }; - 508A5916241EF1A00069DC07 /* Test */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_IDENTITY = "-"; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = SecretiveTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Secretive.app/Contents/MacOS/Secretive"; }; name = Test; }; @@ -901,19 +790,21 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = SecretAgent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; }; name = Test; }; @@ -924,20 +815,22 @@ CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; DEVELOPMENT_TEAM = Z72PRUAWF6; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = SecretAgent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -949,21 +842,23 @@ CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; DEVELOPMENT_TEAM = Z72PRUAWF6; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = SecretAgent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Secretive - Secret Agent"; - SWIFT_VERSION = 5.0; }; name = Release; }; @@ -990,16 +885,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 50617DA023FCE48E0099B055 /* Build configuration list for PBXNativeTarget "SecretiveTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 50617DA123FCE48E0099B055 /* Debug */, - 508A5916241EF1A00069DC07 /* Test */, - 50617DA223FCE48E0099B055 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 50A3B79A24026B7600D209EA /* Build configuration list for PBXNativeTarget "SecretAgent" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/SecretAgent.xcscheme b/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/SecretAgent.xcscheme index 647184f..4c9211b 100644 --- a/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/SecretAgent.xcscheme +++ b/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/SecretAgent.xcscheme @@ -1,6 +1,6 @@ (showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup) - .environmentObject(storeList) - .environmentObject(Updater(checkOnLaunch: hasRunSetup)) - .environmentObject(agentStatusChecker) + ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup) + .environment(EnvironmentValues._secretStoreList) .onAppear { if !hasRunSetup { showingSetup = true @@ -70,13 +84,12 @@ 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() - } + Task { + await LaunchAgentController().install() + try? await Task.sleep(for: .seconds(1)) + agentStatusChecker.check() + if !agentStatusChecker.running { + forceLaunchAgent() } } } @@ -84,7 +97,8 @@ extension Secretive { 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 + Task { + _ = await LaunchAgentController().forceLaunch() agentStatusChecker.check() } } diff --git a/Sources/Secretive/Controllers/AgentStatusChecker.swift b/Sources/Secretive/Controllers/AgentStatusChecker.swift index 8f0602e..8ff234a 100644 --- a/Sources/Secretive/Controllers/AgentStatusChecker.swift +++ b/Sources/Secretive/Controllers/AgentStatusChecker.swift @@ -2,18 +2,22 @@ import Foundation import Combine import AppKit import SecretKit +import Observation -protocol AgentStatusCheckerProtocol: ObservableObject { +@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable { var running: Bool { get } var developmentBuild: Bool { get } + func check() } -class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol { +@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol { - @Published var running: Bool = false + var running: Bool = false - init() { - check() + nonisolated init() { + Task { @MainActor in + check() + } } func check() { diff --git a/Sources/Secretive/Controllers/JustUpdatedChecker.swift b/Sources/Secretive/Controllers/JustUpdatedChecker.swift index 4c86f68..4e8ea72 100644 --- a/Sources/Secretive/Controllers/JustUpdatedChecker.swift +++ b/Sources/Secretive/Controllers/JustUpdatedChecker.swift @@ -2,13 +2,13 @@ import Foundation import Combine import AppKit -protocol JustUpdatedCheckerProtocol: ObservableObject { +protocol JustUpdatedCheckerProtocol: Observable { var justUpdated: Bool { get } } -class JustUpdatedChecker: ObservableObject, JustUpdatedCheckerProtocol { +@Observable class JustUpdatedChecker: JustUpdatedCheckerProtocol { - @Published var justUpdated: Bool = false + var justUpdated: Bool = false init() { check() diff --git a/Sources/Secretive/Controllers/LaunchAgentController.swift b/Sources/Secretive/Controllers/LaunchAgentController.swift index 7f512aa..a65f8b0 100644 --- a/Sources/Secretive/Controllers/LaunchAgentController.swift +++ b/Sources/Secretive/Controllers/LaunchAgentController.swift @@ -8,38 +8,45 @@ 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 { + 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 { - SMLoginItemSetEnabled(Bundle.main.agentBundleID as CFString, enabled) + let service = SMAppService.loginItem(identifier: Bundle.main.agentBundleID) + do { + if enabled { + try service.register() + } else { + try service.unregister() + } + return true + } catch { + return false + } } } diff --git a/Sources/Secretive/Localizable.xcstrings b/Sources/Secretive/Localizable.xcstrings index 045feeb..063d2fc 100644 --- a/Sources/Secretive/Localizable.xcstrings +++ b/Sources/Secretive/Localizable.xcstrings @@ -2676,6 +2676,7 @@ } }, "empty_store_modifiable_title" : { + "extractionState" : "stale", "localizations" : { "ca" : { "stringUnit" : { diff --git a/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift b/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift index e893155..5fce50d 100644 --- a/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift +++ b/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift @@ -10,4 +10,7 @@ class PreviewAgentStatusChecker: AgentStatusCheckerProtocol { self.running = running } + func check() { + } + } diff --git a/Sources/Secretive/Preview Content/PreviewStore.swift b/Sources/Secretive/Preview Content/PreviewStore.swift index 9480c88..c0bdd85 100644 --- a/Sources/Secretive/Preview Content/PreviewStore.swift +++ b/Sources/Secretive/Preview Content/PreviewStore.swift @@ -20,20 +20,20 @@ extension Preview { extension Preview { - class Store: SecretStore, ObservableObject { + @Observable final class Store: SecretStore { let isAvailable = true let id = UUID() var name: String { "Preview Store" } - @Published var secrets: [Secret] = [] + let secrets: [Secret] init(secrets: [Secret]) { - self.secrets.append(contentsOf: secrets) + self.secrets = secrets } - init(numberOfRandomSecrets: Int = 5) { + convenience init(numberOfRandomSecrets: Int = 5) { let new = (0.. Data { @@ -56,8 +56,40 @@ extension Preview { } - class StoreModifiable: Store, SecretStoreModifiable { - override var name: String { "Modifiable Preview Store" } + final class StoreModifiable: SecretStoreModifiable { + + let isAvailable = true + let id = UUID() + var name: String { "Modifiable Preview Store" } + let secrets: [Secret] + + init(secrets: [Secret]) { + self.secrets = secrets + } + + convenience init(numberOfRandomSecrets: Int = 5) { + let new = (0.. Data { + return data + } + + func verify(signature data: Data, for signature: Data, with secret: Preview.Secret) throws -> Bool { + true + } + + func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? { + nil + } + + func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws { + } + + func reloadSecrets() { + } + func create(name: String, requiresAuthentication: Bool) throws { } @@ -72,7 +104,7 @@ extension Preview { extension Preview { - static func storeList(stores: [Store] = [], modifiableStores: [StoreModifiable] = []) -> SecretStoreList { + @MainActor static func storeList(stores: [Store] = [], modifiableStores: [StoreModifiable] = []) -> SecretStoreList { let list = SecretStoreList() for store in stores { list.add(store: store) diff --git a/Sources/Secretive/Preview Content/PreviewUpdater.swift b/Sources/Secretive/Preview Content/PreviewUpdater.swift index a993d87..77fbeea 100644 --- a/Sources/Secretive/Preview Content/PreviewUpdater.swift +++ b/Sources/Secretive/Preview Content/PreviewUpdater.swift @@ -1,10 +1,11 @@ import Foundation -import Combine +import Observation import Brief -class PreviewUpdater: UpdaterProtocol { +@Observable @MainActor final class PreviewUpdater: UpdaterProtocol { + + var update: Release? = nil - let update: Release? let testBuild = false init(update: Update = .none) { @@ -18,6 +19,9 @@ class PreviewUpdater: UpdaterProtocol { } } + func ignore(release: Release) async { + } + } extension PreviewUpdater { diff --git a/Sources/Secretive/Secretive.entitlements b/Sources/Secretive/Secretive.entitlements index c1bb5e0..c9423c4 100644 --- a/Sources/Secretive/Secretive.entitlements +++ b/Sources/Secretive/Secretive.entitlements @@ -2,12 +2,6 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - com.apple.security.smartcard keychain-access-groups diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index c48991c..cb21882 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -4,18 +4,18 @@ import SecureEnclaveSecretKit import SmartCardSecretKit import Brief -struct ContentView: View { +struct ContentView: View { @Binding var showingCreation: Bool @Binding var runningSetup: Bool @Binding var hasRunSetup: Bool @State var showingAgentInfo = false - @State var activeSecret: AnySecret.ID? + @State var activeSecret: AnySecret? @Environment(\.colorScheme) var colorScheme - @EnvironmentObject private var storeList: SecretStoreList - @EnvironmentObject private var updater: UpdaterType - @EnvironmentObject private var agentStatusChecker: AgentStatusCheckerType + @Environment(\.secretStoreList) private var storeList + @Environment(\.updater) private var updater: any UpdaterProtocol + @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol @State private var selectedUpdate: Release? @State private var showingAppPathNotice = false @@ -106,7 +106,7 @@ extension ContentView { if let modifiable = storeList.modifiableStore { CreateSecretView(store: modifiable, showing: $showingCreation) .onDisappear { - guard let newest = modifiable.secrets.last?.id else { return } + guard let newest = modifiable.secrets.last else { return } activeSecret = newest } } @@ -197,34 +197,18 @@ extension ContentView { struct ContentView_Previews: PreviewProvider { - private static let storeList: SecretStoreList = { - let list = SecretStoreList() - list.add(store: SecureEnclave.Store()) - list.add(store: SmartCard.Store()) - return list - }() - private static let agentStatusChecker = AgentStatusChecker() - private static let justUpdatedChecker = JustUpdatedChecker() - - @State var hasRunSetup = false - @State private var showingSetup = false - @State private var showingCreation = false - static var previews: some View { Group { // Empty on modifiable and nonmodifiable - ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) - .environmentObject(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)])) - .environmentObject(PreviewUpdater()) - .environmentObject(agentStatusChecker) + ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) + .environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)])) + .environment(PreviewUpdater()) // 5 items on modifiable and nonmodifiable - ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) - .environmentObject(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()])) - .environmentObject(PreviewUpdater()) - .environmentObject(agentStatusChecker) + ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) + .environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()])) + .environment(PreviewUpdater()) } - .environmentObject(agentStatusChecker) } } diff --git a/Sources/Secretive/Views/CreateSecretView.swift b/Sources/Secretive/Views/CreateSecretView.swift index accd8be..9bddcd2 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,8 +45,10 @@ struct CreateSecretView: View { } func save() { - try! store.create(name: name, requiresAuthentication: requiresAuthentication) - showing = false + Task { + try! await store.create(name: name, requiresAuthentication: requiresAuthentication) + showing = false + } } } @@ -93,14 +95,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 @@ -110,10 +112,10 @@ extension ThumbnailPickerView { } -@MainActor class SystemBackground: ObservableObject { +@MainActor @Observable class SystemBackground { static let shared = SystemBackground() - @Published var image: NSImage? + var image: NSImage? private init() { if let mainScreen = NSScreen.main, let imageURL = NSWorkspace.shared.desktopImageURL(for: mainScreen) { diff --git a/Sources/Secretive/Views/DeleteSecretView.swift b/Sources/Secretive/Views/DeleteSecretView.swift index 5e3a6f9..3be07c2 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,8 +49,10 @@ struct DeleteSecretView: View { } func delete() { - try! store.delete(secret: secret) - dismissalBlock(true) + Task { + try! await store.delete(secret: secret) + dismissalBlock(true) + } } } diff --git a/Sources/Secretive/Views/EmptyStoreView.swift b/Sources/Secretive/Views/EmptyStoreView.swift index 6a88c2b..b6593c0 100644 --- a/Sources/Secretive/Views/EmptyStoreView.swift +++ b/Sources/Secretive/Views/EmptyStoreView.swift @@ -3,31 +3,17 @@ import SecretKit struct EmptyStoreView: View { - @ObservedObject var store: AnySecretStore - @Binding var activeSecret: AnySecret.ID? - + @State var store: AnySecretStore? + var body: some View { if store is AnySecretStoreModifiable { - NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: $activeSecret) { - Text("empty_store_modifiable_title") - } + EmptyStoreModifiableView() } else { - NavigationLink(destination: EmptyStoreImmutableView(), tag: Constants.emptyStoreTag, selection: $activeSecret) { - Text("empty_store_nonmodifiable_title") - } + EmptyStoreImmutableView() } } } -extension EmptyStoreView { - - enum Constants { - static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag" - static let emptyStoreTag: AnyHashable = "emptyStoreTag" - } - -} - struct EmptyStoreImmutableView: View { var body: some View { diff --git a/Sources/Secretive/Views/RenameSecretView.swift b/Sources/Secretive/Views/RenameSecretView.swift index 915b2b2..0a417cb 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,9 @@ struct RenameSecretView: View { } func rename() { - try? store.update(secret: secret, name: newName) - dismissalBlock(true) + Task { + try? await store.update(secret: secret, name: newName) + dismissalBlock(true) + } } } diff --git a/Sources/Secretive/Views/SecretDetailView.swift b/Sources/Secretive/Views/SecretDetailView.swift index aefe49d..75dde47 100644 --- a/Sources/Secretive/Views/SecretDetailView.swift +++ b/Sources/Secretive/Views/SecretDetailView.swift @@ -3,7 +3,7 @@ import SecretKit struct SecretDetailView: View { - @State var secret: SecretType + let secret: SecretType private let keyWriter = OpenSSHKeyWriter() private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID)) diff --git a/Sources/Secretive/Views/SecretListItemView.swift b/Sources/Secretive/Views/SecretListItemView.swift index 8f6bbf4..38a6f36 100644 --- a/Sources/Secretive/Views/SecretListItemView.swift +++ b/Sources/Secretive/Views/SecretListItemView.swift @@ -2,24 +2,30 @@ import SwiftUI import SecretKit struct SecretListItemView: View { - - @ObservedObject var store: AnySecretStore + + @State var store: AnySecretStore var secret: AnySecret - @Binding var activeSecret: AnySecret.ID? - + @State var isDeleting: Bool = false @State var isRenaming: Bool = false - + var deletedSecret: (AnySecret) -> Void var renamedSecret: (AnySecret) -> Void - - var body: some View { - let showingPopupWrapped = Binding( + + private var showingPopup: Binding { + Binding( get: { isDeleting || isRenaming }, - set: { if $0 == false { isDeleting = false; isRenaming = false } } + set: { + if $0 == false { + isDeleting = false + isRenaming = false + } + } ) - - return NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: $activeSecret) { + } + + var body: some View { + NavigationLink(value: secret) { if secret.requiresAuthentication { HStack { Text(secret.name) @@ -40,7 +46,7 @@ struct SecretListItemView: View { } } } - .popover(isPresented: showingPopupWrapped) { + .popover(isPresented: showingPopup) { if let modifiable = store as? AnySecretStoreModifiable { if isDeleting { DeleteSecretView(store: modifiable, secret: secret) { deleted in diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index ffd142c..8f71bc0 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -55,7 +55,7 @@ struct StepView: View { .foregroundColor(.green) .frame(width: max(0, ((width - (Constants.padding * 2)) / Double(numberOfSteps - 1)) * Double(currentStep) - (Constants.circleWidth / 2)), height: 5) HStack { - ForEach(0.. index { Circle() @@ -156,8 +156,10 @@ struct SecretAgentSetupView: View { } func install() { - LaunchAgentController().install() - buttonAction() + Task { + await LaunchAgentController().install() + buttonAction() + } } } diff --git a/Sources/Secretive/Views/StoreListView.swift b/Sources/Secretive/Views/StoreListView.swift index c2b64e2..c0e6d3e 100644 --- a/Sources/Secretive/Views/StoreListView.swift +++ b/Sources/Secretive/Views/StoreListView.swift @@ -4,60 +4,60 @@ import SecretKit struct StoreListView: View { - @Binding var activeSecret: AnySecret.ID? - - @EnvironmentObject private var storeList: SecretStoreList + @Binding var activeSecret: AnySecret? + + @Environment(\.secretStoreList) private var storeList private func secretDeleted(secret: AnySecret) { activeSecret = nextDefaultSecret } private func secretRenamed(secret: AnySecret) { - activeSecret = secret.id + activeSecret = secret } var body: some View { - NavigationView { + NavigationSplitView { List(selection: $activeSecret) { ForEach(storeList.stores) { store in if store.isAvailable { Section(header: Text(store.name)) { - if store.secrets.isEmpty { - EmptyStoreView(store: store, activeSecret: $activeSecret) - } else { - ForEach(store.secrets) { secret in - SecretListItemView( - store: store, - secret: secret, - activeSecret: $activeSecret, - deletedSecret: self.secretDeleted, - renamedSecret: self.secretRenamed - ) - } + ForEach(store.secrets) { secret in + SecretListItemView( + store: store, + secret: secret, + deletedSecret: secretDeleted, + renamedSecret: secretRenamed + ) } } } } } - .listStyle(SidebarListStyle()) - .onAppear { - activeSecret = nextDefaultSecret + } detail: { + if let activeSecret { + SecretDetailView(secret: activeSecret) + } else if let nextDefaultSecret { + // This just means onAppear hasn't executed yet. + // Do this to avoid a blip. + SecretDetailView(secret: nextDefaultSecret) + } else { + EmptyStoreView(store: storeList.modifiableStore ?? storeList.stores.first) } - .frame(minWidth: 100, idealWidth: 240) } + .navigationSplitViewStyle(.balanced) + .onAppear { + activeSecret = nextDefaultSecret + } + .frame(minWidth: 100, idealWidth: 240) + } } extension StoreListView { - var nextDefaultSecret: AnyHashable? { - let fallback: AnyHashable - if storeList.modifiableStore?.isAvailable ?? false { - fallback = EmptyStoreView.Constants.emptyStoreModifiableTag - } else { - fallback = EmptyStoreView.Constants.emptyStoreTag - } - return storeList.stores.compactMap(\.secrets.first).first?.id ?? fallback + private var nextDefaultSecret: AnySecret? { + return storeList.stores.first(where: { !$0.secrets.isEmpty })?.secrets.first } } diff --git a/Sources/Secretive/Views/UpdateView.swift b/Sources/Secretive/Views/UpdateView.swift index a997d3a..bebbcc5 100644 --- a/Sources/Secretive/Views/UpdateView.swift +++ b/Sources/Secretive/Views/UpdateView.swift @@ -1,9 +1,9 @@ import SwiftUI import Brief -struct UpdateDetailView: View { +struct UpdateDetailView: View { - @EnvironmentObject var updater: UpdaterType + @Environment(\.updater) var updater: any UpdaterProtocol let update: Release @@ -18,7 +18,9 @@ struct UpdateDetailView: View { HStack { if !update.critical { Button("update_ignore_button") { - updater.ignore(release: update) + Task { + await updater.ignore(release: update) + } } Spacer() } diff --git a/Sources/SecretiveTests/Info.plist b/Sources/SecretiveTests/Info.plist deleted file mode 100644 index 64d65ca..0000000 --- a/Sources/SecretiveTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Sources/SecretiveTests/SecretiveTests.swift b/Sources/SecretiveTests/SecretiveTests.swift deleted file mode 100644 index d596a41..0000000 --- a/Sources/SecretiveTests/SecretiveTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -import XCTest -@testable import Secretive - -class SecretiveTests: XCTestCase { - - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - -}