mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-08-19 11:40:56 +00:00
Merge branch 'swift6' into xcode_26
This commit is contained in:
commit
374da84128
@ -6,7 +6,7 @@ import PackageDescription
|
|||||||
let package = Package(
|
let package = Package(
|
||||||
name: "SecretivePackages",
|
name: "SecretivePackages",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v15)
|
.macOS(.v14)
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
@ -27,13 +27,16 @@ let package = Package(
|
|||||||
.library(
|
.library(
|
||||||
name: "Brief",
|
name: "Brief",
|
||||||
targets: ["Brief"]),
|
targets: ["Brief"]),
|
||||||
|
.library(
|
||||||
|
name: "Common",
|
||||||
|
targets: ["Common"]),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "SecretKit",
|
name: "SecretKit",
|
||||||
dependencies: [],
|
dependencies: ["Common"],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
@ -43,17 +46,17 @@ let package = Package(
|
|||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SecureEnclaveSecretKit",
|
name: "SecureEnclaveSecretKit",
|
||||||
dependencies: ["SecretKit"],
|
dependencies: ["Common", "SecretKit"],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SmartCardSecretKit",
|
name: "SmartCardSecretKit",
|
||||||
dependencies: ["SecretKit"],
|
dependencies: ["Common", "SecretKit"],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SecretAgentKit",
|
name: "SecretAgentKit",
|
||||||
dependencies: ["SecretKit", "SecretAgentKitHeaders"],
|
dependencies: ["Common", "SecretKit", "SecretAgentKitHeaders"],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
.systemLibrary(
|
.systemLibrary(
|
||||||
@ -65,13 +68,18 @@ let package = Package(
|
|||||||
,
|
,
|
||||||
.target(
|
.target(
|
||||||
name: "Brief",
|
name: "Brief",
|
||||||
dependencies: [],
|
dependencies: ["Common"],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "BriefTests",
|
name: "BriefTests",
|
||||||
dependencies: ["Brief"]
|
dependencies: ["Brief"]
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "Common",
|
||||||
|
dependencies: [],
|
||||||
|
swiftSettings: swiftSettings
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
import Synchronization
|
import os
|
||||||
|
import Common
|
||||||
|
|
||||||
/// 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.
|
||||||
@Observable public final class Updater: UpdaterProtocol, ObservableObject, Sendable {
|
@Observable public final class Updater: UpdaterProtocol, ObservableObject, Sendable {
|
||||||
|
|
||||||
public var update: Release? {
|
public var update: Release? {
|
||||||
_update.withLock { $0 }
|
_update.lockedValue
|
||||||
}
|
}
|
||||||
private let _update: Mutex<Release?> = .init(nil)
|
private let _update: OSAllocatedUnfairLock<Release?> = .init(uncheckedState: nil)
|
||||||
public let testBuild: Bool
|
public let testBuild: Bool
|
||||||
|
|
||||||
/// The current OS version.
|
/// The current OS version.
|
||||||
@ -53,9 +54,7 @@ import Synchronization
|
|||||||
guard !release.critical else { return }
|
guard !release.critical else { return }
|
||||||
defaults.set(true, forKey: release.name)
|
defaults.set(true, forKey: release.name)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
_update.withLock { value in
|
_update.lockedValue = nil
|
||||||
value = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,9 +75,7 @@ extension Updater {
|
|||||||
let latestVersion = SemVer(release.name)
|
let latestVersion = SemVer(release.name)
|
||||||
if latestVersion > currentVersion {
|
if latestVersion > currentVersion {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
_update.withLock { value in
|
_update.lockedValue = release
|
||||||
value = release
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Synchronization
|
import os
|
||||||
|
|
||||||
/// 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: Observable {
|
public protocol UpdaterProtocol: Observable {
|
||||||
|
14
Sources/Packages/Sources/Common/Locks.swift
Normal file
14
Sources/Packages/Sources/Common/Locks.swift
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
public extension OSAllocatedUnfairLock where State: Sendable {
|
||||||
|
|
||||||
|
var lockedValue: State {
|
||||||
|
get {
|
||||||
|
withLock { $0 }
|
||||||
|
}
|
||||||
|
nonmutating set {
|
||||||
|
withLock { $0 = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
import Synchronization
|
import os
|
||||||
|
|
||||||
/// Manages storage and lookup for OpenSSH certificates.
|
/// Manages storage and lookup for OpenSSH certificates.
|
||||||
public final class OpenSSHCertificateHandler: Sendable {
|
public final class OpenSSHCertificateHandler: Sendable {
|
||||||
@ -8,7 +8,7 @@ public final class OpenSSHCertificateHandler: Sendable {
|
|||||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
|
||||||
private let writer = OpenSSHKeyWriter()
|
private let writer = OpenSSHKeyWriter()
|
||||||
private let keyBlobsAndNames: Mutex<[AnySecret: (Data, Data)]> = .init([:])
|
private let keyBlobsAndNames: OSAllocatedUnfairLock<[AnySecret: (Data, Data)]> = .init(uncheckedState: [:])
|
||||||
|
|
||||||
/// Initializes an OpenSSHCertificateHandler.
|
/// Initializes an OpenSSHCertificateHandler.
|
||||||
public init() {
|
public init() {
|
||||||
@ -32,10 +32,7 @@ public final class OpenSSHCertificateHandler: Sendable {
|
|||||||
/// - Parameter secret: The secret to check for a certificate.
|
/// - 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
|
/// - Returns: A boolean describing whether or not the certificate handler has a certifiicate associated with a given secret
|
||||||
public func hasCertificate<SecretType: Secret>(for secret: SecretType) -> Bool {
|
public func hasCertificate<SecretType: Secret>(for secret: SecretType) -> Bool {
|
||||||
keyBlobsAndNames.withLock {
|
keyBlobsAndNames.lockedValue[AnySecret(secret)] != nil
|
||||||
$0[AnySecret(secret)] != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -67,9 +64,7 @@ public final class OpenSSHCertificateHandler: Sendable {
|
|||||||
/// - Parameter secret: The secret to search for a certificate with
|
/// - Parameter secret: The secret to search for a certificate with
|
||||||
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
|
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
|
||||||
public func keyBlobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
|
public func keyBlobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
|
||||||
keyBlobsAndNames.withLock {
|
keyBlobsAndNames.lockedValue[AnySecret(secret)]
|
||||||
$0[AnySecret(secret)]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
import Synchronization
|
import os
|
||||||
|
import Common
|
||||||
|
|
||||||
/// A "Store Store," which holds a list of type-erased stores.
|
/// A "Store Store," which holds a list of type-erased stores.
|
||||||
@Observable public final class SecretStoreList: Sendable {
|
@Observable public final class SecretStoreList: Sendable {
|
||||||
|
|
||||||
/// The Stores managed by the SecretStoreList.
|
/// The Stores managed by the SecretStoreList.
|
||||||
public var stores: [AnySecretStore] {
|
public var stores: [AnySecretStore] {
|
||||||
__stores.withLock { $0 }
|
__stores.lockedValue
|
||||||
}
|
}
|
||||||
private let __stores: Mutex<[AnySecretStore]> = .init([])
|
private let __stores: OSAllocatedUnfairLock<[AnySecretStore]> = .init(uncheckedState: [])
|
||||||
|
|
||||||
/// A modifiable store, if one is available.
|
/// A modifiable store, if one is available.
|
||||||
public var modifiableStore: AnySecretStoreModifiable? {
|
public var modifiableStore: AnySecretStoreModifiable? {
|
||||||
__modifiableStore.withLock { $0 }
|
__modifiableStore.withLock { $0 }
|
||||||
}
|
}
|
||||||
private let __modifiableStore: Mutex<AnySecretStoreModifiable?> = .init(nil)
|
private let __modifiableStore: OSAllocatedUnfairLock<AnySecretStoreModifiable?> = .init(uncheckedState: nil)
|
||||||
|
|
||||||
/// Initializes a SecretStoreList.
|
/// Initializes a SecretStoreList.
|
||||||
public init() {
|
public init() {
|
||||||
@ -31,9 +32,7 @@ import Synchronization
|
|||||||
/// Adds a non-type-erased modifiable SecretStore.
|
/// Adds a non-type-erased modifiable SecretStore.
|
||||||
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
||||||
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
||||||
__modifiableStore.withLock {
|
__modifiableStore.lockedValue = modifiable
|
||||||
$0 = modifiable
|
|
||||||
}
|
|
||||||
__stores.withLock {
|
__stores.withLock {
|
||||||
$0.append(modifiable)
|
$0.append(modifiable)
|
||||||
}
|
}
|
||||||
@ -41,15 +40,11 @@ import Synchronization
|
|||||||
|
|
||||||
/// A boolean describing whether there are any Stores available.
|
/// A boolean describing whether there are any Stores available.
|
||||||
public var anyAvailable: Bool {
|
public var anyAvailable: Bool {
|
||||||
__stores.withLock {
|
__stores.lockedValue.contains(where: \.isAvailable)
|
||||||
$0.reduce(false, { $0 || $1.isAvailable })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public var allSecrets: [AnySecret] {
|
public var allSecrets: [AnySecret] {
|
||||||
__stores.withLock {
|
__stores.lockedValue.flatMap(\.secrets)
|
||||||
$0.flatMap(\.secrets)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import Foundation
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
/// Describes the chain of applications that requested a signature operation.
|
/// 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.
|
/// 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`
|
/// - 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 {
|
extension SigningRequestProvenance {
|
||||||
|
|
||||||
/// Describes a process in a `SigningRequestProvenance` chain.
|
/// Describes a process in a `SigningRequestProvenance` chain.
|
||||||
public struct Process: Equatable {
|
public struct Process: Equatable, Sendable {
|
||||||
|
|
||||||
/// The pid of the process.
|
/// The pid of the process.
|
||||||
public let pid: Int32
|
public let pid: Int32
|
||||||
|
@ -4,7 +4,8 @@ import Security
|
|||||||
import CryptoKit
|
import CryptoKit
|
||||||
@preconcurrency import LocalAuthentication
|
@preconcurrency import LocalAuthentication
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import Synchronization
|
import os
|
||||||
|
import Common
|
||||||
|
|
||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
|
|
||||||
@ -17,11 +18,11 @@ extension SecureEnclave {
|
|||||||
public let id = UUID()
|
public let id = UUID()
|
||||||
public let name = String(localized: "secure_enclave")
|
public let name = String(localized: "secure_enclave")
|
||||||
public var secrets: [Secret] {
|
public var secrets: [Secret] {
|
||||||
_secrets.withLock { $0 }
|
_secrets.lockedValue
|
||||||
}
|
}
|
||||||
private let _secrets: Mutex<[Secret]> = .init([])
|
private let _secrets: OSAllocatedUnfairLock<[Secret]> = .init(uncheckedState: [])
|
||||||
|
|
||||||
private let persistedAuthenticationContexts: Mutex<[Secret: PersistentAuthenticationContext]> = .init([:])
|
private let persistedAuthenticationContexts: OSAllocatedUnfairLock<[Secret: PersistentAuthenticationContext]> = .init(uncheckedState: [:])
|
||||||
|
|
||||||
/// Initializes a Store.
|
/// Initializes a Store.
|
||||||
public init() {
|
public init() {
|
||||||
@ -105,15 +106,14 @@ extension SecureEnclave {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
let context: Mutex<LAContext>
|
var context: LAContext
|
||||||
// if let existing = persistedAuthenticationContexts.withLock({ $0 })[secret], existing.valid {
|
if let existing = persistedAuthenticationContexts.lockedValue[secret], existing.valid {
|
||||||
// context = existing.context
|
context = existing.context
|
||||||
// } else {
|
} else {
|
||||||
let newContext = LAContext()
|
let newContext = LAContext()
|
||||||
newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
||||||
context = .init(newContext)
|
context = newContext
|
||||||
// }
|
}
|
||||||
return try context.withLock { context in
|
|
||||||
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
|
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
|
||||||
let attributes = KeychainDictionary([
|
let attributes = KeychainDictionary([
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
@ -141,7 +141,6 @@ extension SecureEnclave {
|
|||||||
}
|
}
|
||||||
return signature as Data
|
return signature as Data
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
|
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
|
||||||
let context = LAContext()
|
let context = LAContext()
|
||||||
@ -179,7 +178,7 @@ extension SecureEnclave {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
|
||||||
guard let persisted = persistedAuthenticationContexts.withLock({ $0 })[secret], persisted.valid else { return nil }
|
guard let persisted = persistedAuthenticationContexts.lockedValue[secret], persisted.valid else { return nil }
|
||||||
return persisted
|
return persisted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Synchronization
|
import os
|
||||||
import Observation
|
import Observation
|
||||||
import Security
|
import Security
|
||||||
import CryptoTokenKit
|
import CryptoTokenKit
|
||||||
@ -19,7 +19,7 @@ extension SmartCard {
|
|||||||
/// An implementation of Store backed by a Smart Card.
|
/// An implementation of Store backed by a Smart Card.
|
||||||
@Observable public final class Store: SecretStore {
|
@Observable public final class Store: SecretStore {
|
||||||
|
|
||||||
private let state: Mutex<State> = .init(.init())
|
private let state: OSAllocatedUnfairLock<State> = .init(uncheckedState: .init())
|
||||||
public var isAvailable: Bool {
|
public var isAvailable: Bool {
|
||||||
state.withLock { $0.isAvailable }
|
state.withLock { $0.isAvailable }
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import Testing
|
|||||||
import Foundation
|
import Foundation
|
||||||
@testable import Brief
|
@testable import Brief
|
||||||
|
|
||||||
|
|
||||||
@Suite struct ReleaseParsingTests {
|
@Suite struct ReleaseParsingTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import os
|
||||||
import Testing
|
import Testing
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
@testable import SecretKit
|
@testable import SecretKit
|
||||||
@testable import SecretAgentKit
|
@testable import SecretAgentKit
|
||||||
|
import Common
|
||||||
|
|
||||||
@Suite struct AgentTests {
|
@Suite struct AgentTests {
|
||||||
|
|
||||||
@ -90,34 +92,35 @@ import CryptoKit
|
|||||||
@Test func witnessSignature() async {
|
@Test func witnessSignature() async {
|
||||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
||||||
var witnessed = false
|
let witnessed: OSAllocatedUnfairLock<Bool> = .init(uncheckedState: false)
|
||||||
let witness = StubWitness(speakNow: { _, trace in
|
let witness = StubWitness(speakNow: { _, trace in
|
||||||
return false
|
return false
|
||||||
}, witness: { _, trace in
|
}, witness: { _, trace in
|
||||||
witnessed = true
|
witnessed.lockedValue = true
|
||||||
})
|
})
|
||||||
let agent = Agent(storeList: list, witness: witness)
|
let agent = Agent(storeList: list, witness: witness)
|
||||||
await agent.handle(reader: stubReader, writer: stubWriter)
|
await agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
#expect(witnessed)
|
let value = witnessed.lockedValue
|
||||||
|
#expect(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func requestTracing() async {
|
@Test func requestTracing() async {
|
||||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
||||||
var speakNowTrace: SigningRequestProvenance! = nil
|
let speakNowTrace: OSAllocatedUnfairLock<SigningRequestProvenance?> = .init(uncheckedState: nil)
|
||||||
var witnessTrace: SigningRequestProvenance! = nil
|
let witnessTrace: OSAllocatedUnfairLock<SigningRequestProvenance?> = .init(uncheckedState: nil)
|
||||||
let witness = StubWitness(speakNow: { _, trace in
|
let witness = StubWitness(speakNow: { _, trace in
|
||||||
speakNowTrace = trace
|
speakNowTrace.lockedValue = trace
|
||||||
return false
|
return false
|
||||||
}, witness: { _, trace in
|
}, witness: { _, trace in
|
||||||
witnessTrace = trace
|
witnessTrace.lockedValue = trace
|
||||||
})
|
})
|
||||||
let agent = Agent(storeList: list, witness: witness)
|
let agent = Agent(storeList: list, witness: witness)
|
||||||
await agent.handle(reader: stubReader, writer: stubWriter)
|
await agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
#expect(witnessTrace == speakNowTrace)
|
#expect(witnessTrace.lockedValue == speakNowTrace.lockedValue)
|
||||||
#expect(witnessTrace.origin.displayName == "Finder")
|
#expect(witnessTrace.lockedValue?.origin.displayName == "Finder")
|
||||||
#expect(witnessTrace.origin.validSignature == true)
|
#expect(witnessTrace.lockedValue?.origin.validSignature == true)
|
||||||
#expect(witnessTrace.origin.parentPID == 1)
|
#expect(witnessTrace.lockedValue?.origin.parentPID == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Exception Handling
|
// MARK: Exception Handling
|
||||||
|
@ -3,8 +3,8 @@ import SecretAgentKit
|
|||||||
|
|
||||||
struct StubWitness {
|
struct StubWitness {
|
||||||
|
|
||||||
let speakNow: (AnySecret, SigningRequestProvenance) -> Bool
|
let speakNow: @Sendable (AnySecret, SigningRequestProvenance) -> Bool
|
||||||
let witness: (AnySecret, SigningRequestProvenance) -> ()
|
let witness: @Sendable (AnySecret, SigningRequestProvenance) -> ()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import AppKit
|
|||||||
import SecretKit
|
import SecretKit
|
||||||
import SecretAgentKit
|
import SecretAgentKit
|
||||||
import Brief
|
import Brief
|
||||||
import Synchronization
|
import os
|
||||||
|
|
||||||
final class Notifier: Sendable {
|
final class Notifier: Sendable {
|
||||||
|
|
||||||
@ -84,10 +84,10 @@ final class Notifier: Sendable {
|
|||||||
try? await notificationCenter.add(request)
|
try? await notificationCenter.add(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func notify(update: Release, ignore: ((Release) -> Void)?) {
|
func notify(update: Release, ignore: (@Sendable (Release) -> Void)?) {
|
||||||
notificationDelegate.state.withLock { [update] state in
|
notificationDelegate.state.withLock { [update] state in
|
||||||
state.release = update
|
state.release = update
|
||||||
// state.ignore = ignore
|
state.ignore = ignore
|
||||||
}
|
}
|
||||||
let notificationCenter = UNUserNotificationCenter.current()
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
let notificationContent = UNMutableNotificationContent()
|
let notificationContent = UNMutableNotificationContent()
|
||||||
@ -141,7 +141,7 @@ extension Notifier {
|
|||||||
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
|
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
typealias PersistAuthentication = ((AnySecret, AnySecretStore, TimeInterval?) async -> Void)
|
typealias PersistAuthentication = (@Sendable (AnySecret, AnySecretStore, TimeInterval?) async -> Void)
|
||||||
typealias Ignore = ((Release) -> Void)
|
typealias Ignore = ((Release) -> Void)
|
||||||
fileprivate var release: Release?
|
fileprivate var release: Release?
|
||||||
fileprivate var ignore: Ignore?
|
fileprivate var ignore: Ignore?
|
||||||
@ -151,7 +151,7 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Se
|
|||||||
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
|
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate let state: Mutex<State> = .init(.init())
|
fileprivate let state: OSAllocatedUnfairLock<State> = .init(uncheckedState: .init())
|
||||||
|
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
||||||
|
|
||||||
@ -170,9 +170,10 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Se
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateResponse(response: UNNotificationResponse) {
|
func handleUpdateResponse(response: UNNotificationResponse) {
|
||||||
|
let id = response.actionIdentifier
|
||||||
state.withLock { state in
|
state.withLock { state in
|
||||||
guard let update = state.release else { return }
|
guard let update = state.release else { return }
|
||||||
switch response.actionIdentifier {
|
switch id {
|
||||||
case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
|
case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
|
||||||
NSWorkspace.shared.open(update.html_url)
|
NSWorkspace.shared.open(update.html_url)
|
||||||
case Notifier.Constants.ignoreActionIdentitifier:
|
case Notifier.Constants.ignoreActionIdentitifier:
|
||||||
@ -184,15 +185,21 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Se
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
|
func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
|
||||||
// let (secret, store, persistOptions, callback): (AnySecret?, AnySecretStore?, TimeInterval?, State.PersistAuthentication?) = state.withLock { state in
|
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String,
|
||||||
// guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String, let secret = state.pendingPersistableSecrets[secretID],
|
let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String else {
|
||||||
// let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String, let store = state.pendingPersistableStores[storeID]
|
return
|
||||||
// else { return (nil, nil, nil, nil) }
|
}
|
||||||
// state.pendingPersistableSecrets[secretID] = nil
|
let id = response.actionIdentifier
|
||||||
// return (secret, store, state.persistOptions[response.actionIdentifier], state.persistAuthentication)
|
|
||||||
// }
|
let (secret, store, persistOptions, callback): (AnySecret?, AnySecretStore?, TimeInterval?, State.PersistAuthentication?) = state.withLock { state in
|
||||||
// guard let secret, let store, let persistOptions else { return }
|
guard let secret = state.pendingPersistableSecrets[secretID],
|
||||||
// await callback?(secret, store, persistOptions)
|
let store = state.pendingPersistableStores[storeID]
|
||||||
|
else { return (nil, nil, nil, nil) }
|
||||||
|
state.pendingPersistableSecrets[secretID] = nil
|
||||||
|
return (secret, store, state.persistOptions[id], state.persistAuthentication)
|
||||||
|
}
|
||||||
|
guard let secret, let store, let persistOptions else { return }
|
||||||
|
await callback?(secret, store, persistOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -646,6 +646,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
@ -675,6 +676,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
@ -773,6 +775,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
@ -796,6 +799,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
@ -821,6 +825,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
@ -847,6 +852,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
|
@ -1,32 +1,25 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Synchronization
|
import os
|
||||||
import Observation
|
import Observation
|
||||||
import Brief
|
import Brief
|
||||||
|
|
||||||
@Observable class PreviewUpdater: UpdaterProtocol {
|
@Observable class PreviewUpdater: UpdaterProtocol {
|
||||||
|
|
||||||
var update: Release? {
|
var update: Release? {
|
||||||
_update.withLock { $0 }
|
_update.lockedValue
|
||||||
}
|
}
|
||||||
let _update: Mutex<Release?> = .init(nil)
|
let _update: OSAllocatedUnfairLock<Release?> = .init(uncheckedState: nil)
|
||||||
|
|
||||||
let testBuild = false
|
let testBuild = false
|
||||||
|
|
||||||
init(update: Update = .none) {
|
init(update: Update = .none) {
|
||||||
switch update {
|
switch update {
|
||||||
case .none:
|
case .none:
|
||||||
_update.withLock {
|
_update.lockedValue = nil
|
||||||
$0 = nil
|
|
||||||
}
|
|
||||||
case .advisory:
|
case .advisory:
|
||||||
_update.withLock {
|
_update.lockedValue = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update")
|
||||||
$0 = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update")
|
|
||||||
}
|
|
||||||
case .critical:
|
case .critical:
|
||||||
_update.withLock {
|
_update.lockedValue = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
|
||||||
$0 = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user