Package and updater

This commit is contained in:
Max Goedjen
2024-08-15 16:50:03 -07:00
parent 6f4226f97a
commit 56a662a9dd
12 changed files with 126 additions and 58 deletions

View File

@@ -1,12 +1,14 @@
// 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 secretiveDefaults: [PackageDescription.SwiftSetting]? = [.swiftLanguageMode(.v6), .unsafeFlags(["-warnings-as-errors"])]
let package = Package(
name: "SecretivePackages",
platforms: [
.macOS(.v12)
.macOS(.v13)
],
products: [
.library(
@@ -34,27 +36,27 @@ let package = Package(
.target(
name: "SecretKit",
dependencies: [],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency"), .unsafeFlags(["-warnings-as-errors"])]
swiftSettings: secretiveDefaults
),
.testTarget(
name: "SecretKitTests",
dependencies: ["SecretKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency"), .unsafeFlags(["-warnings-as-errors"])]
swiftSettings: secretiveDefaults
),
.target(
name: "SecureEnclaveSecretKit",
dependencies: ["SecretKit"],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency"), .unsafeFlags(["-warnings-as-errors"])]
swiftSettings: secretiveDefaults
),
.target(
name: "SmartCardSecretKit",
dependencies: ["SecretKit"],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency"), .unsafeFlags(["-warnings-as-errors"])]
swiftSettings: secretiveDefaults
),
.target(
name: "SecretAgentKit",
dependencies: ["SecretKit", "SecretAgentKitHeaders"],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency"), .unsafeFlags(["-warnings-as-errors"])]
swiftSettings: secretiveDefaults
),
.systemLibrary(
name: "SecretAgentKitHeaders"
@@ -73,3 +75,4 @@ let package = Package(
),
]
)

View File

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

View File

@@ -2,7 +2,7 @@ import Foundation
import Combine
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
public final class Updater: ObservableObject, UpdaterProtocol {
@MainActor public final class Updater: ObservableObject, UpdaterProtocol, Sendable {
@Published public var update: Release?
public let testBuild: Bool
@@ -18,27 +18,27 @@ 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: Duration = .seconds(24*60*60), 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 {
if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
await checkForUpdates()
}
while true {
try await Task.sleep(for: checkFrequency, tolerance: .seconds(60*60))
await checkForUpdates()
}
}
let timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in
self.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 }
evaluate(releases: releases)
}
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.

View File

@@ -5,7 +5,7 @@ import Combine
public protocol UpdaterProtocol: ObservableObject {
/// 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 }

View File

@@ -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 actor Agent {
private let storeList: SecretStoreList
private let witness: SigningWitness?
@@ -35,7 +35,7 @@ extension Agent {
/// - writer: A ``FileHandleWriter`` to write the response to.
/// - Return value:
/// - Boolean if data could be read
@discardableResult @Sendable public func handle(reader: FileHandleReader, writer: FileHandleWriter) async -> Bool {
@discardableResult public func handle(reader: FileHandleReader, writer: FileHandleWriter) async -> Bool {
logger.debug("Agent handling new data")
let data = Data(reader.availableData)
guard data.count > 4 else { return false}

View File

@@ -1,7 +1,7 @@
import Foundation
/// Type eraser for Secret.
public struct AnySecret: Secret {
public struct AnySecret: Secret, @unchecked Sendable {
let base: Any
private let hashable: AnyHashable

View File

@@ -1,7 +1,7 @@
import Foundation
/// The base protocol for describing a Secret
public protocol Secret: Identifiable, Hashable {
public protocol Secret: Identifiable, Hashable, Sendable {
/// A user-facing string identifying the Secret.
var name: String { get }
@@ -17,7 +17,7 @@ public protocol Secret: Identifiable, Hashable {
}
/// The type of algorithm the Secret uses. Currently, only elliptic curve algorithms are supported.
public enum Algorithm: Hashable {
public enum Algorithm: Hashable, Sendable {
case ellipticCurve
case rsa

View File

@@ -8,7 +8,7 @@ import SecretKit
extension SecureEnclave {
/// An implementation of Store backed by the Secure Enclave.
public final class Store: SecretStoreModifiable {
public final class Store: SecretStoreModifiable, Sendable {
public var isAvailable: Bool {
// For some reason, as of build time, CryptoKit.SecureEnclave.isAvailable always returns false
@@ -18,14 +18,18 @@ extension SecureEnclave {
}
public let id = UUID()
public let name = String(localized: "secure_enclave")
@Published public private(set) var secrets: [Secret] = []
@MainActor public private(set) var secrets: [Secret] = [] {
willSet {
self.objectWillChange.send()
}
}
private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
@MainActor private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
/// Initializes a Store.
public init() {
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { [reload = reloadSecretsInternal(notifyAgent:)] _ in
reload(false)
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
self.reloadSecretsInternal(notifyAgent: false)
}
loadSecrets()
}
@@ -211,7 +215,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.
@Sendable private func reloadSecretsInternal(notifyAgent: Bool = true) {
private func reloadSecretsInternal(notifyAgent: Bool = true) {
let before = secrets
secrets.removeAll()
loadSecrets()
@@ -304,8 +308,8 @@ 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 = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)!
static let keyType = kSecAttrKeyTypeECSECPrimeRandom as String
static let unauthenticatedThreshold: TimeInterval = 0.05
}

View File

@@ -20,13 +20,13 @@ extension SmartCard {
/// Initializes a Store.
public init() {
tokenID = watcher.nonSecureEnclaveTokens.first
watcher.setInsertionHandler { [reload = reloadSecretsInternal] string in
watcher.setInsertionHandler { string in
guard self.tokenID == nil else { return }
guard !string.contains("setoken") else { return }
self.tokenID = string
DispatchQueue.main.async {
reload()
// reload()
}
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
}
@@ -117,7 +117,7 @@ extension SmartCard {
extension SmartCard.Store {
@Sendable private func reloadSecretsInternal() {
private func reloadSecretsInternal() {
self.isAvailable = self.tokenID != nil
let before = self.secrets
self.secrets.removeAll()