Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift 6 / Concurrency fixes #578

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
jobs:
build:
# runs-on: macOS-latest
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
Expand All @@ -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_15.4.app
run: sudo xcrun xcode-select -s /Applications/Xcode_16.2.app
- name: Update Build Number
env:
RUN_ID: ${{ github.run_id }}
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
jobs:
test:
# runs-on: macOS-latest
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
Expand All @@ -21,15 +21,15 @@ 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_15.4.app
run: sudo xcrun xcode-select -s /Applications/Xcode_16.2.app
- name: Test
run: |
pushd Sources/Packages
swift test
popd
build:
# runs-on: macOS-latest
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
Expand All @@ -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_15.4.app
run: sudo xcrun xcode-select -s /Applications/Xcode_16.2.app
- name: Update Build Number
env:
TAG_NAME: ${{ github.ref }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ on: [push, pull_request]
jobs:
test:
# runs-on: macOS-latest
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_15.4.app
run: sudo xcrun xcode-select -s /Applications/Xcode_16.2.app
- name: Test
run: |
pushd Sources/Packages
Expand Down
21 changes: 14 additions & 7 deletions Sources/Packages/Package.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// swift-tools-version:5.9
// swift-tools-version:6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "SecretivePackages",
platforms: [
.macOS(.v12)
.macOS(.v15)
],
products: [
.library(
Expand Down Expand Up @@ -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"
Expand All @@ -73,3 +73,10 @@ let package = Package(
),
]
)

var swiftSettings: [PackageDescription.SwiftSetting] {
[
.swiftLanguageMode(.v6),
.unsafeFlags(["-warnings-as-errors"])
]
}
2 changes: 1 addition & 1 deletion Sources/Packages/Sources/Brief/Release.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/Packages/Sources/Brief/SemVer.swift
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
48 changes: 29 additions & 19 deletions Sources/Packages/Sources/Brief/Updater.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import Foundation
import Combine
import Observation
import Synchronization

/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
public final class Updater: ObservableObject, UpdaterProtocol {
@Observable public final class Updater: UpdaterProtocol, ObservableObject, Sendable {

@Published public var update: Release?
public var update: Release? {
_update.withLock { $0 }
}
private let _update: Mutex<Release?> = .init(nil)
public let testBuild: Bool

/// The current OS version.
Expand All @@ -24,30 +28,34 @@ public final class Updater: ObservableObject, UpdaterProtocol {
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 {
_update.withLock { value in
value = nil
}
}
}

Expand All @@ -57,7 +65,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()
Expand All @@ -67,8 +75,10 @@ extension Updater {
guard !release.prerelease else { return }
let latestVersion = SemVer(release.name)
if latestVersion > currentVersion {
DispatchQueue.main.async {
self.update = release
await MainActor.run {
_update.withLock { value in
value = release
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Packages/Sources/Brief/UpdaterProtocol.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Foundation
import Combine
import Synchronization

/// A protocol for retreiving the latest available version of an app.
public protocol UpdaterProtocol: ObservableObject {
public protocol UpdaterProtocol: Observable {

/// The latest update
var update: Release? { get }
Expand Down
18 changes: 9 additions & 9 deletions Sources/Packages/Sources/SecretAgentKit/Agent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -54,7 +54,7 @@ extension Agent {

func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) async -> Data {
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
reloadSecretsIfNeccessary()
await reloadSecretsIfNeccessary()
var response = Data()
do {
switch requestType {
Expand All @@ -65,7 +65,7 @@ extension Agent {
case .signRequest:
let provenance = requestTracer.provenance(from: reader)
response.append(SSHAgent.ResponseType.agentSignResponse.data)
response.append(try sign(data: data, provenance: provenance))
response.append(try await sign(data: data, provenance: provenance))
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
}
} catch {
Expand Down Expand Up @@ -112,7 +112,7 @@ extension Agent {
/// - data: The data to sign.
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
let reader = OpenSSHReader(data: data)
let payloadHash = reader.readNextChunk()
let hash: Data
Expand All @@ -129,11 +129,11 @@ extension Agent {
}

if let witness = witness {
try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
try await witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
}

let dataToSign = reader.readNextChunk()
let signed = try store.sign(data: dataToSign, with: secret, for: provenance)
let signed = try await store.sign(data: dataToSign, with: secret, for: provenance)
let derSignature = signed

let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
Expand Down Expand Up @@ -175,7 +175,7 @@ extension Agent {
signedData.append(writer.lengthAndData(of: sub))

if let witness = witness {
try witness.witness(accessTo: secret, from: store, by: provenance)
try await witness.witness(accessTo: secret, from: store, by: provenance)
}

logger.debug("Agent signed request")
Expand All @@ -188,11 +188,11 @@ extension Agent {
extension Agent {

/// Gives any store with no loaded secrets a chance to reload.
func reloadSecretsIfNeccessary() {
func reloadSecretsIfNeccessary() async {
for store in storeList.stores {
if store.secrets.isEmpty {
logger.debug("Store \(store.name, privacy: .public) has no loaded secrets. Reloading.")
store.reloadSecrets()
await store.reloadSecrets()
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ 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:
/// - secret: The `Secret` that will be used to sign the request.
/// - 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

}
2 changes: 1 addition & 1 deletion Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading
Loading