This commit is contained in:
Max Goedjen 2024-12-25 18:25:01 -05:00
parent 8ea8f0510c
commit 2dc317d398
No known key found for this signature in database
26 changed files with 208 additions and 188 deletions

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "SecretivePackages", name: "SecretivePackages",
platforms: [ platforms: [
.macOS(.v12) .macOS(.v15)
], ],
products: [ products: [
.library( .library(

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// A release is a representation of a downloadable update. /// 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" /// The user-facing name of the release. Typically "Secretive 1.2.3"
public let name: String public let name: String

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// A representation of a Semantic Version. /// A representation of a Semantic Version.
public struct SemVer { public struct SemVer: Sendable {
/// The SemVer broken into an array of integers. /// The SemVer broken into an array of integers.
let versionNumbers: [Int] let versionNumbers: [Int]

View File

@ -1,10 +1,14 @@
import Foundation import Foundation
import Combine import Observation
import Synchronization
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version. /// 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 public let testBuild: Bool
/// The current OS version. /// The current OS version.
@ -46,8 +50,10 @@ public final class Updater: ObservableObject, UpdaterProtocol {
public func ignore(release: Release) { public func ignore(release: Release) {
guard !release.critical else { return } guard !release.critical else { return }
defaults.set(true, forKey: release.name) defaults.set(true, forKey: release.name)
DispatchQueue.main.async { Task { @MainActor in
self.update = nil _update.withLock { value in
value = nil
}
} }
} }
@ -67,8 +73,10 @@ extension Updater {
guard !release.prerelease else { return } guard !release.prerelease else { return }
let latestVersion = SemVer(release.name) let latestVersion = SemVer(release.name)
if latestVersion > currentVersion { if latestVersion > currentVersion {
DispatchQueue.main.async { Task { @MainActor in
self.update = release _update.withLock { value in
value = release
}
} }
} }
} }

View File

@ -1,5 +1,5 @@
import Foundation import Foundation
import Combine import Synchronization
/// A protocol for retreiving the latest available version of an app. /// A protocol for retreiving the latest available version of an app.
public protocol UpdaterProtocol: ObservableObject { public protocol UpdaterProtocol: ObservableObject {

View File

@ -54,7 +54,7 @@ extension Agent {
func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) async -> Data { 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 // 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() var response = Data()
do { do {
switch requestType { switch requestType {
@ -65,7 +65,7 @@ extension Agent {
case .signRequest: case .signRequest:
let provenance = requestTracer.provenance(from: reader) let provenance = requestTracer.provenance(from: reader)
response.append(SSHAgent.ResponseType.agentSignResponse.data) 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)") logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
} }
} catch { } catch {
@ -112,7 +112,7 @@ extension Agent {
/// - data: The data to sign. /// - data: The data to sign.
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request. /// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
/// - Returns: An OpenSSH formatted Data payload containing the signed data response. /// - 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 reader = OpenSSHReader(data: data)
let payloadHash = reader.readNextChunk() let payloadHash = reader.readNextChunk()
let hash: Data let hash: Data
@ -129,11 +129,11 @@ extension Agent {
} }
if let witness = witness { 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 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 derSignature = signed
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)! let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
@ -175,7 +175,7 @@ extension Agent {
signedData.append(writer.lengthAndData(of: sub)) signedData.append(writer.lengthAndData(of: sub))
if let witness = witness { 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") logger.debug("Agent signed request")
@ -188,11 +188,11 @@ extension Agent {
extension Agent { extension Agent {
/// Gives any store with no loaded secrets a chance to reload. /// Gives any store with no loaded secrets a chance to reload.
func reloadSecretsIfNeccessary() { func reloadSecretsIfNeccessary() async {
for store in storeList.stores { for store in storeList.stores {
if store.secrets.isEmpty { if store.secrets.isEmpty {
logger.debug("Store \(store.name, privacy: .public) has no loaded secrets. Reloading.") logger.debug("Store \(store.name, privacy: .public) has no loaded secrets. Reloading.")
store.reloadSecrets() await store.reloadSecrets()
} }
} }
} }

View File

@ -10,13 +10,13 @@ public protocol SigningWitness {
/// - store: The `Store` being asked to sign the request.. /// - store: The `Store` being asked to sign the request..
/// - provenance: A `SigningRequestProvenance` object describing the origin of 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. /// - 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. /// Notifies the callee that a signing operation has been performed for a given secret.
/// - Parameters: /// - Parameters:
/// - secret: The `Secret` that will was used to sign the request. /// - secret: The `Secret` that will was used to sign the request.
/// - store: The `Store` that signed the request.. /// - store: The `Store` that signed the request..
/// - provenance: A `SigningRequestProvenance` object describing the origin of 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
} }

View File

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

View File

@ -9,13 +9,11 @@ public class AnySecretStore: SecretStore {
private let _id: () -> UUID private let _id: () -> UUID
private let _name: () -> String private let _name: () -> String
private let _secrets: () -> [AnySecret] private let _secrets: () -> [AnySecret]
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data private let _sign: (Data, AnySecret, SigningRequestProvenance) async throws -> Data
private let _verify: (Data, Data, AnySecret) throws -> Bool private let _verify: (Data, Data, AnySecret) async throws -> Bool
private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext? private let _existingPersistedAuthenticationContext: (AnySecret) async -> PersistedAuthenticationContext?
private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void private let _persistAuthentication: (AnySecret, TimeInterval) async throws -> Void
private let _reloadSecrets: () -> Void private let _reloadSecrets: () async -> Void
private var sink: AnyCancellable?
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore { public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
base = secretStore base = secretStore
@ -23,14 +21,11 @@ public class AnySecretStore: SecretStore {
_name = { secretStore.name } _name = { secretStore.name }
_id = { secretStore.id } _id = { secretStore.id }
_secrets = { secretStore.secrets.map { AnySecret($0) } } _secrets = { secretStore.secrets.map { AnySecret($0) } }
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) } _sign = { try await 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) } _verify = { try await secretStore.verify(signature: $0, for: $1, with: $2.base as! SecretStoreType.SecretType) }
_existingPersistedAuthenticationContext = { secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) } _existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
_persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) } _persistAuthentication = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
_reloadSecrets = { secretStore.reloadSecrets() } _reloadSecrets = { await secretStore.reloadSecrets() }
sink = secretStore.objectWillChange.sink { _ in
self.objectWillChange.send()
}
} }
public var isAvailable: Bool { public var isAvailable: Bool {
@ -49,51 +44,51 @@ public class AnySecretStore: SecretStore {
return _secrets() return _secrets()
} }
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) throws -> Data { public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) async throws -> Data {
try _sign(data, secret, provenance) try await _sign(data, secret, provenance)
} }
public func verify(signature: Data, for data: Data, with secret: AnySecret) throws -> Bool { public func verify(signature: Data, for data: Data, with secret: AnySecret) async throws -> Bool {
try _verify(signature, data, secret) try await _verify(signature, data, secret)
} }
public func existingPersistedAuthenticationContext(secret: AnySecret) -> PersistedAuthenticationContext? { public func existingPersistedAuthenticationContext(secret: AnySecret) async -> PersistedAuthenticationContext? {
_existingPersistedAuthenticationContext(secret) await _existingPersistedAuthenticationContext(secret)
} }
public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) throws { public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) async throws {
try _persistAuthentication(secret, duration) try await _persistAuthentication(secret, duration)
} }
public func reloadSecrets() { public func reloadSecrets() async {
_reloadSecrets() await _reloadSecrets()
} }
} }
public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable { public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
private let _create: (String, Bool) throws -> Void private let _create: (String, Bool) async throws -> Void
private let _delete: (AnySecret) throws -> Void private let _delete: (AnySecret) async throws -> Void
private let _update: (AnySecret, String) throws -> Void private let _update: (AnySecret, String) async throws -> Void
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable { public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
_create = { try secretStore.create(name: $0, requiresAuthentication: $1) } _create = { try await secretStore.create(name: $0, requiresAuthentication: $1) }
_delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) } _delete = { try await secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
_update = { try secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) } _update = { try await secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) }
super.init(secretStore) super.init(secretStore)
} }
public func create(name: String, requiresAuthentication: Bool) throws { public func create(name: String, requiresAuthentication: Bool) async throws {
try _create(name, requiresAuthentication) try await _create(name, requiresAuthentication)
} }
public func delete(secret: AnySecret) throws { public func delete(secret: AnySecret) async throws {
try _delete(secret) try await _delete(secret)
} }
public func update(secret: AnySecret, name: String) throws { public func update(secret: AnySecret, name: String) async throws {
try _update(secret, name) try await _update(secret, name)
} }
} }

View File

@ -1,14 +1,13 @@
import Foundation import Foundation
import Combine import Observation
/// A "Store Store," which holds a list of type-erased stores. /// A "Store Store," which holds a list of type-erased stores.
public final class SecretStoreList: ObservableObject { @Observable public final class SecretStoreList: ObservableObject {
/// The Stores managed by the SecretStoreList. /// The Stores managed by the SecretStoreList.
@Published public var stores: [AnySecretStore] = [] public var stores: [AnySecretStore] = []
/// A modifiable store, if one is available. /// A modifiable store, if one is available.
@Published public var modifiableStore: AnySecretStoreModifiable? public var modifiableStore: AnySecretStoreModifiable?
private var cancellables: Set<AnyCancellable> = []
/// Initializes a SecretStoreList. /// Initializes a SecretStoreList.
public init() { public init() {
@ -41,9 +40,9 @@ extension SecretStoreList {
private func addInternal(store: AnySecretStore) { private func addInternal(store: AnySecretStore) {
stores.append(store) stores.append(store)
store.objectWillChange.sink { // store.objectWillChange.sink {
self.objectWillChange.send() // self.objectWillChange.send()
}.store(in: &cancellables) // }.store(in: &cancellables)
} }
} }

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// The base protocol for describing a Secret /// 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. /// A user-facing string identifying the Secret.
var name: String { get } 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. /// 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 ellipticCurve
case rsa case rsa

View File

@ -2,7 +2,7 @@ import Foundation
import Combine import Combine
/// Manages access to Secrets, and performs signature operations on data using those Secrets. /// Manages access to Secrets, and performs signature operations on data using those Secrets.
public protocol SecretStore: ObservableObject, Identifiable { public protocol SecretStore: Identifiable {
associatedtype SecretType: Secret associatedtype SecretType: Secret
@ -21,7 +21,7 @@ public protocol SecretStore: ObservableObject, Identifiable {
/// - secret: The ``Secret`` to sign with. /// - secret: The ``Secret`` to sign with.
/// - provenance: A ``SigningRequestProvenance`` describing where the request came from. /// - provenance: A ``SigningRequestProvenance`` describing where the request came from.
/// - Returns: The signed data. /// - 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. /// Verifies that a signature is valid over a specified payload.
/// - Parameters: /// - Parameters:
@ -29,23 +29,23 @@ public protocol SecretStore: ObservableObject, Identifiable {
/// - data: The data to verify the signature of. /// - data: The data to verify the signature of.
/// - secret: The secret whose signature to verify. /// - secret: The secret whose signature to verify.
/// - Returns: Whether the signature was verified. /// - 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. /// Checks to see if there is currently a valid persisted authentication for a given secret.
/// - Parameters: /// - Parameters:
/// - secret: The ``Secret`` to check if there is a persisted authentication for. /// - secret: The ``Secret`` to check if there is a persisted authentication for.
/// - Returns: A persisted authentication context, if a valid one exists. /// - 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. /// Persists user authorization for access to a secret.
/// - Parameters: /// - Parameters:
/// - secret: The ``Secret`` to persist the authorization for. /// - secret: The ``Secret`` to persist the authorization for.
/// - duration: The duration that the authorization should persist 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. /// - 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. /// 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: /// - Parameters:
/// - name: The user-facing name for the ``Secret``. /// - 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. /// - 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. /// Deletes a Secret in the store.
/// - Parameters: /// - Parameters:
/// - secret: The ``Secret`` to delete. /// - 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. /// Updates the name of a Secret in the store.
/// - Parameters: /// - Parameters:
/// - secret: The ``Secret`` to update. /// - secret: The ``Secret`` to update.
/// - name: The new name for the Secret. /// - name: The new name for the Secret.
func update(secret: SecretType, name: String) throws func update(secret: SecretType, name: String) async throws
} }

View File

@ -1,38 +1,42 @@
import Foundation import Foundation
import Combine import Observation
import Security import Security
import CryptoTokenKit import CryptoKit
import LocalAuthentication import LocalAuthentication
import SecretKit import SecretKit
import Synchronization
extension SecureEnclave { extension SecureEnclave {
/// An implementation of Store backed by the Secure Enclave. /// An implementation of Store backed by the Secure Enclave.
public final class Store: SecretStoreModifiable { @Observable public final class Store: SecretStoreModifiable {
public var isAvailable: Bool { public var isAvailable: Bool {
// For some reason, as of build time, CryptoKit.SecureEnclave.isAvailable always returns false CryptoKit.SecureEnclave.isAvailable
// error msg "Received error sending GET UNIQUE DEVICE command"
// Verify it with TKTokenWatcher manually.
TKTokenWatcher().tokenIDs.contains("com.apple.setoken")
} }
public let id = UUID() public let id = UUID()
public let name = String(localized: "secure_enclave") public let name = String(localized: "secure_enclave")
@Published public private(set) var secrets: [Secret] = [] public var secrets: [Secret] {
_secrets.withLock { $0 }
}
private let _secrets: Mutex<[Secret]> = .init([])
private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:] private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
/// Initializes a Store. /// Initializes a Store.
public init() { public init() {
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { [reload = reloadSecretsInternal(notifyAgent:)] _ in // FIXME: THIS
reload(false) // Task {
} // for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
// await reloadSecretsInternal(notifyAgent: false)
// }
// }
loadSecrets() loadSecrets()
} }
// MARK: Public API // MARK: Public API
public func create(name: String, requiresAuthentication: Bool) throws { public func create(name: String, requiresAuthentication: Bool) async throws {
var accessError: SecurityError? var accessError: SecurityError?
let flags: SecAccessControlCreateFlags let flags: SecAccessControlCreateFlags
if requiresAuthentication { if requiresAuthentication {
@ -69,10 +73,10 @@ extension SecureEnclave {
throw KeychainError(statusCode: nil) throw KeychainError(statusCode: nil)
} }
try savePublicKey(publicKey, name: name) try savePublicKey(publicKey, name: name)
reloadSecretsInternal() await reloadSecretsInternal()
} }
public func delete(secret: Secret) throws { public func delete(secret: Secret) async throws {
let deleteAttributes = KeychainDictionary([ let deleteAttributes = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData kSecAttrApplicationLabel: secret.id as CFData
@ -81,10 +85,10 @@ extension SecureEnclave {
if status != errSecSuccess { if status != errSecSuccess {
throw KeychainError(statusCode: status) 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([ let updateQuery = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData kSecAttrApplicationLabel: secret.id as CFData
@ -98,7 +102,7 @@ extension SecureEnclave {
if status != errSecSuccess { if status != errSecSuccess {
throw KeychainError(statusCode: status) 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) throws -> Data {
@ -199,8 +203,8 @@ extension SecureEnclave {
} }
} }
public func reloadSecrets() { public func reloadSecrets() async {
reloadSecretsInternal(notifyAgent: false) await reloadSecretsInternal(notifyAgent: false)
} }
} }
@ -211,9 +215,11 @@ extension SecureEnclave.Store {
/// Reloads all secrets from the 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. /// - 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) { private func reloadSecretsInternal(notifyAgent: Bool = true) async {
let before = secrets let before = secrets
secrets.removeAll() _secrets.withLock {
$0.removeAll()
}
loadSecrets() loadSecrets()
if secrets != before { if secrets != before {
NotificationCenter.default.post(name: .secretStoreReloaded, object: self) NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
@ -275,7 +281,9 @@ extension SecureEnclave.Store {
} }
return SecureEnclave.Secret(id: id, name: name, requiresAuthentication: requiresAuth, publicKey: publicKey) return SecureEnclave.Secret(id: id, name: name, requiresAuthentication: requiresAuth, publicKey: publicKey)
} }
secrets.append(contentsOf: wrapped) _secrets.withLock {
$0.append(contentsOf: wrapped)
}
} }
/// Saves a public key. /// Saves a public key.
@ -304,8 +312,8 @@ extension SecureEnclave.Store {
extension SecureEnclave { extension SecureEnclave {
enum Constants { enum Constants {
static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8)
static let keyType = kSecAttrKeyTypeECSECPrimeRandom static let keyType = kSecAttrKeyTypeECSECPrimeRandom as String
static let unauthenticatedThreshold: TimeInterval = 0.05 static let unauthenticatedThreshold: TimeInterval = 0.05
} }

View File

@ -20,14 +20,15 @@ extension SmartCard {
/// Initializes a Store. /// Initializes a Store.
public init() { public init() {
tokenID = watcher.nonSecureEnclaveTokens.first tokenID = watcher.nonSecureEnclaveTokens.first
watcher.setInsertionHandler { [reload = reloadSecretsInternal] string in // FIXME: THIS
watcher.setInsertionHandler { string in
guard self.tokenID == nil else { return } guard self.tokenID == nil else { return }
guard !string.contains("setoken") else { return } guard !string.contains("setoken") else { return }
self.tokenID = string self.tokenID = string
DispatchQueue.main.async { // DispatchQueue.main.async {
reload() // reload()
} // }
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string) self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
} }
if let tokenID = tokenID { if let tokenID = tokenID {

View File

@ -7,7 +7,7 @@ import SmartCardSecretKit
import SecretAgentKit import SecretAgentKit
import Brief import Brief
@NSApplicationMain @main
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
private let storeList: SecretStoreList = { private let storeList: SecretStoreList = {
@ -31,18 +31,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
logger.debug("SecretAgent finished launching") logger.debug("SecretAgent finished launching")
DispatchQueue.main.async { // DispatchQueue.main.async {
self.socketController.handler = self.agent.handle(reader:writer:) // self.socketController.handler = self.agent.handle(reader:writer:)
} // }
NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in // NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) // try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
} // }
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
notifier.prompt() notifier.prompt()
updateSink = updater.$update.sink { update in // updateSink = updater.$update.sink { update in
guard let update = update else { return } // guard let update = update else { return }
self.notifier.notify(update: update, ignore: self.updater.ignore(release:)) // self.notifier.notify(update: update, ignore: self.updater.ignore(release:))
} // }
} }
} }

View File

@ -47,7 +47,7 @@ class Notifier {
notificationDelegate.persistAuthentication = { secret, store, duration in notificationDelegate.persistAuthentication = { secret, store, duration in
guard let duration = duration else { return } guard let duration = duration else { return }
try? store.persistAuthentication(secret: secret, forDuration: duration) try? await store.persistAuthentication(secret: secret, forDuration: duration)
} }
} }
@ -57,7 +57,7 @@ class Notifier {
notificationCenter.requestAuthorization(options: .alert) { _, _ in } notificationCenter.requestAuthorization(options: .alert) { _, _ in }
} }
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) { func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async {
notificationDelegate.pendingPersistableSecrets[secret.id.description] = secret notificationDelegate.pendingPersistableSecrets[secret.id.description] = secret
notificationDelegate.pendingPersistableStores[store.id.description] = store notificationDelegate.pendingPersistableStores[store.id.description] = store
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
@ -67,7 +67,7 @@ class Notifier {
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
notificationContent.interruptionLevel = .timeSensitive notificationContent.interruptionLevel = .timeSensitive
if secret.requiresAuthentication && store.existingPersistedAuthenticationContext(secret: secret) == nil { if await store.existingPersistedAuthenticationContext(secret: secret) == nil && secret.requiresAuthentication {
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
} }
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) { if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
@ -99,11 +99,11 @@ class Notifier {
extension Notifier: SigningWitness { 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 { func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
notify(accessTo: secret, from: store, by: provenance) await notify(accessTo: secret, from: store, by: provenance)
} }
} }
@ -133,7 +133,7 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
fileprivate var release: Release? fileprivate var release: Release?
fileprivate var ignore: ((Release) -> Void)? fileprivate var ignore: ((Release) -> Void)?
fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) -> Void)? fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) async -> Void)?
fileprivate var persistOptions: [String: TimeInterval] = [:] fileprivate var persistOptions: [String: TimeInterval] = [:]
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:] fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:] fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
@ -142,18 +142,16 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
} }
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 let category = response.notification.request.content.categoryIdentifier
switch category { switch category {
case Notifier.Constants.updateCategoryIdentitifier: case Notifier.Constants.updateCategoryIdentitifier:
handleUpdateResponse(response: response) handleUpdateResponse(response: response)
case Notifier.Constants.persistAuthenticationCategoryIdentitifier: case Notifier.Constants.persistAuthenticationCategoryIdentitifier:
handlePersistAuthenticationResponse(response: response) await handlePersistAuthenticationResponse(response: response)
default: default:
break break
} }
completionHandler()
} }
func handleUpdateResponse(response: UNNotificationResponse) { func handleUpdateResponse(response: UNNotificationResponse) {
@ -168,12 +166,12 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
} }
} }
func handlePersistAuthenticationResponse(response: UNNotificationResponse) { func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String, let secret = pendingPersistableSecrets[secretID], 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] let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String, let store = pendingPersistableStores[storeID]
else { return } else { return }
pendingPersistableSecrets[secretID] = nil pendingPersistableSecrets[secretID] = nil
persistAuthentication?(secret, store, persistOptions[response.actionIdentifier]) await persistAuthentication?(secret, store, persistOptions[response.actionIdentifier])
} }
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

View File

@ -610,7 +610,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
@ -671,7 +671,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = ""; OTHER_SWIFT_FLAGS = "";
@ -704,7 +704,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -731,7 +730,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -831,7 +829,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
@ -862,7 +860,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -904,7 +901,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -927,7 +923,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -951,7 +946,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";

View File

@ -69,24 +69,26 @@ struct Secretive: App {
extension Secretive { extension Secretive {
private func reinstallAgent() { private func reinstallAgent() {
justUpdatedChecker.check() // justUpdatedChecker.check()
LaunchAgentController().install { // FIXME: THIS
// Wait a second for launchd to kick in (next runloop isn't enough). // LaunchAgentController().install {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // // Wait a second for launchd to kick in (next runloop isn't enough).
agentStatusChecker.check() // DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if !agentStatusChecker.running { // agentStatusChecker.check()
forceLaunchAgent() // if !agentStatusChecker.running {
} // forceLaunchAgent()
} // }
} // }
// }
} }
private func forceLaunchAgent() { private func forceLaunchAgent() {
// We've run setup, we didn't just update, launchd is just not doing it's thing. // We've run setup, we didn't just update, launchd is just not doing it's thing.
// Force a launch directly. // Force a launch directly.
LaunchAgentController().forceLaunch { _ in // FIXME: THIS
agentStatusChecker.check() // LaunchAgentController().forceLaunch { _ in
} // agentStatusChecker.check()
// }
} }
} }

View File

@ -8,37 +8,35 @@ struct LaunchAgentController {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController") private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
func install(completion: (() -> Void)? = nil) { func install() async {
logger.debug("Installing agent") logger.debug("Installing agent")
_ = setEnabled(false) _ = setEnabled(false)
// This is definitely a bit of a "seems to work better" thing but: // 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 // Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new? // and start new?
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { try? await Task.sleep(for: .seconds(1))
await MainActor.run {
_ = setEnabled(true) _ = setEnabled(true)
completion?()
} }
} }
func forceLaunch(completion: ((Bool) -> Void)?) { func forceLaunch() async -> Bool {
logger.debug("Agent is not running, attempting to force launch") logger.debug("Agent is not running, attempting to force launch")
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app") let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
let config = NSWorkspace.OpenConfiguration() let config = NSWorkspace.OpenConfiguration()
config.activates = false config.activates = false
NSWorkspace.shared.openApplication(at: url, configuration: config) { app, error in do {
DispatchQueue.main.async { let app = try await NSWorkspace.shared.openApplication(at: url, configuration: config)
completion?(error == nil) logger.debug("Agent force launched")
} return true
if let error = error { } catch {
logger.error("Error force launching \(error.localizedDescription)") logger.error("Error force launching \(error.localizedDescription)")
} else { return false
logger.debug("Agent force launched")
}
} }
} }
private func setEnabled(_ enabled: Bool) -> Bool { private func setEnabled(_ enabled: Bool) -> Bool {
// FIXME: THIS
SMLoginItemSetEnabled(Bundle.main.agentBundleID as CFString, enabled) SMLoginItemSetEnabled(Bundle.main.agentBundleID as CFString, enabled)
} }

View File

@ -1,20 +1,32 @@
import Foundation import Foundation
import Combine import Synchronization
import Observation
import Brief import Brief
class PreviewUpdater: UpdaterProtocol { @Observable class PreviewUpdater: UpdaterProtocol {
var update: Release? {
_update.withLock { $0 }
}
let _update: Mutex<Release?> = .init(nil)
let update: Release?
let testBuild = false let testBuild = false
init(update: Update = .none) { init(update: Update = .none) {
switch update { switch update {
case .none: case .none:
self.update = nil _update.withLock {
$0 = nil
}
case .advisory: case .advisory:
self.update = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update") _update.withLock {
$0 = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update")
}
case .critical: case .critical:
self.update = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update") _update.withLock {
$0 = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
}
} }
} }

View File

@ -3,7 +3,7 @@ import SecretKit
struct CreateSecretView<StoreType: SecretStoreModifiable>: View { struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@ObservedObject var store: StoreType @State var store: StoreType
@Binding var showing: Bool @Binding var showing: Bool
@State private var name = "" @State private var name = ""
@ -45,7 +45,8 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
} }
func save() { func save() {
try! store.create(name: name, requiresAuthentication: requiresAuthentication) // FIXME: THIS
// try! store.create(name: name, requiresAuthentication: requiresAuthentication)
showing = false showing = false
} }
@ -93,14 +94,14 @@ struct ThumbnailPickerView<ValueType: Hashable>: View {
extension ThumbnailPickerView { extension ThumbnailPickerView {
struct Item<ValueType: Hashable>: Identifiable { struct Item<InnerValueType: Hashable>: Identifiable {
let id = UUID() let id = UUID()
let value: ValueType let value: InnerValueType
let name: LocalizedStringKey let name: LocalizedStringKey
let description: LocalizedStringKey let description: LocalizedStringKey
let thumbnail: AnyView let thumbnail: AnyView
init<ViewType: View>(value: ValueType, name: LocalizedStringKey, description: LocalizedStringKey, thumbnail: ViewType) { init<ViewType: View>(value: InnerValueType, name: LocalizedStringKey, description: LocalizedStringKey, thumbnail: ViewType) {
self.value = value self.value = value
self.name = name self.name = name
self.description = description self.description = description

View File

@ -3,7 +3,7 @@ import SecretKit
struct DeleteSecretView<StoreType: SecretStoreModifiable>: View { struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
@ObservedObject var store: StoreType @State var store: StoreType
let secret: StoreType.SecretType let secret: StoreType.SecretType
var dismissalBlock: (Bool) -> () var dismissalBlock: (Bool) -> ()
@ -49,7 +49,8 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
} }
func delete() { func delete() {
try! store.delete(secret: secret) // FIXME: THIS
// try! store.delete(secret: secret)
dismissalBlock(true) dismissalBlock(true)
} }

View File

@ -3,7 +3,7 @@ import SecretKit
struct EmptyStoreView: View { struct EmptyStoreView: View {
@ObservedObject var store: AnySecretStore @State var store: AnySecretStore
@Binding var activeSecret: AnySecret.ID? @Binding var activeSecret: AnySecret.ID?
var body: some View { var body: some View {
@ -22,8 +22,8 @@ struct EmptyStoreView: View {
extension EmptyStoreView { extension EmptyStoreView {
enum Constants { enum Constants {
static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag" static let emptyStoreModifiableTag = "emptyStoreModifiableTag"
static let emptyStoreTag: AnyHashable = "emptyStoreTag" static let emptyStoreTag = "emptyStoreTag"
} }
} }

View File

@ -3,7 +3,7 @@ import SecretKit
struct RenameSecretView<StoreType: SecretStoreModifiable>: View { struct RenameSecretView<StoreType: SecretStoreModifiable>: View {
@ObservedObject var store: StoreType @State var store: StoreType
let secret: StoreType.SecretType let secret: StoreType.SecretType
var dismissalBlock: (_ renamed: Bool) -> () var dismissalBlock: (_ renamed: Bool) -> ()
@ -44,7 +44,8 @@ struct RenameSecretView<StoreType: SecretStoreModifiable>: View {
} }
func rename() { func rename() {
try? store.update(secret: secret, name: newName) // FIXME: THIS
// try? await store.update(secret: secret, name: newName)
dismissalBlock(true) dismissalBlock(true)
} }
} }

View File

@ -3,7 +3,7 @@ import SecretKit
struct SecretListItemView: View { struct SecretListItemView: View {
@ObservedObject var store: AnySecretStore @State var store: AnySecretStore
var secret: AnySecret var secret: AnySecret
@Binding var activeSecret: AnySecret.ID? @Binding var activeSecret: AnySecret.ID?

View File

@ -156,8 +156,10 @@ struct SecretAgentSetupView: View {
} }
func install() { func install() {
LaunchAgentController().install() Task {
buttonAction() await LaunchAgentController().install()
buttonAction()
}
} }
} }