This commit is contained in:
Max Goedjen
2026-06-11 12:40:57 -07:00
parent 3ad23b0506
commit a66db7fe2e
12 changed files with 66 additions and 142 deletions

View File

@@ -365,6 +365,16 @@
}, },
"shouldTranslate" : false "shouldTranslate" : false
}, },
"%@ - %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ - %2$@"
}
}
}
},
"about_build_log_button" : { "about_build_log_button" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -19967,6 +19977,12 @@
} }
} }
} }
},
"Review" : {
},
"Review All" : {
}, },
"secret_detail_certificate_path_label" : { "secret_detail_certificate_path_label" : {
"extractionState" : "manual", "extractionState" : "manual",

View File

@@ -35,6 +35,7 @@ import XPCWrappers
self.osVersion = osVersion self.osVersion = osVersion
self.currentVersion = currentVersion self.currentVersion = currentVersion
Task { Task {
do {
if checkOnLaunch { if checkOnLaunch {
try await checkForUpdates() try await checkForUpdates()
} }
@@ -42,6 +43,7 @@ import XPCWrappers
try? await Task.sleep(for: .seconds(Int(checkFrequency))) try? await Task.sleep(for: .seconds(Int(checkFrequency)))
try await checkForUpdates() try await checkForUpdates()
} }
} catch {}
} }
} }

View File

@@ -10,11 +10,8 @@ import SSHProtocolKit
public final class Agent: Sendable { public final class Agent: Sendable {
private let storeList: SecretStoreList private let storeList: SecretStoreList
<<<<<<< HEAD
private let authenticationHandler: AuthenticationHandler private let authenticationHandler: AuthenticationHandler
=======
private let certificateStore: CertificateStore private let certificateStore: CertificateStore
>>>>>>> main
private let witness: SigningWitness? private let witness: SigningWitness?
private let publicKeyWriter = OpenSSHPublicKeyWriter() private let publicKeyWriter = OpenSSHPublicKeyWriter()
private let signatureWriter = OpenSSHSignatureWriter() private let signatureWriter = OpenSSHSignatureWriter()
@@ -24,17 +21,16 @@ public final class Agent: Sendable {
/// - Parameters: /// - Parameters:
/// - storeList: The `SecretStoreList` to make available. /// - storeList: The `SecretStoreList` to make available.
/// - witness: A witness to notify of requests. /// - witness: A witness to notify of requests.
<<<<<<< HEAD public init(
public init(storeList: SecretStoreList, authenticationHandler: AuthenticationHandler, witness: SigningWitness? = nil) { storeList: SecretStoreList,
logger.debug("Agent is running") certificateStore: CertificateStore,
self.storeList = storeList authenticationHandler: AuthenticationHandler,
self.authenticationHandler = authenticationHandler witness: SigningWitness? = nil
======= ) {
public init(storeList: SecretStoreList, certificateStore: CertificateStore, witness: SigningWitness? = nil) {
logger.debug("Agent is running") logger.debug("Agent is running")
self.storeList = storeList self.storeList = storeList
self.certificateStore = certificateStore self.certificateStore = certificateStore
>>>>>>> main self.authenticationHandler = authenticationHandler
self.witness = witness self.witness = witness
} }

View File

@@ -75,6 +75,7 @@ public actor AuthenticationHandler {
} }
public func waitForAuthentication(for request: SignatureRequest) async throws -> any AuthenticationContextProtocol { public func waitForAuthentication(for request: SignatureRequest) async throws -> any AuthenticationContextProtocol {
logger.log("Entering waitForAuthentication for \(request.id)")
if let existing = existingAuthenticationContext(for: request) { if let existing = existingAuthenticationContext(for: request) {
logger.log("Short circuiting wait, existing valid context already exists.") logger.log("Short circuiting wait, existing valid context already exists.")
return existing return existing

View File

@@ -94,7 +94,7 @@ extension SocketController {
guard !data.isEmpty else { guard !data.isEmpty else {
logger.debug("Socket controller received empty data, ending continuation.") logger.debug("Socket controller received empty data, ending continuation.")
messagesContinuation.finish() messagesContinuation.finish()
try fileHandle.close() try? fileHandle.close()
return return
} }
messagesContinuation.yield(data) messagesContinuation.yield(data)
@@ -126,8 +126,8 @@ private extension SocketPort {
convenience init(path: String) { convenience init(path: String) {
var addr = sockaddr_un() var addr = sockaddr_un()
let length = unsafe withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in let length = withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
unsafe path.withCString { cstring in path.withCString { cstring in
let len = unsafe strlen(cstring) let len = unsafe strlen(cstring)
unsafe strncpy(pointer, cstring, len) unsafe strncpy(pointer, cstring, len)
return len return len

View File

@@ -8,6 +8,7 @@ import Brief
import Observation import Observation
import Common import Common
import SwiftUI import SwiftUI
import CertificateKit
@main @main
struct SecretAgent: App { struct SecretAgent: App {
@@ -21,10 +22,12 @@ struct SecretAgent: App {
list.add(store: SmartCard.Store()) list.add(store: SmartCard.Store())
return list return list
}() }()
@MainActor private let certificateStore: CertificateStore = CertificateStore()
private let updater = Updater(checkOnLaunch: true) private let updater = Updater(checkOnLaunch: true)
private let notifier = Notifier() private let notifier = Notifier()
private let authenticationHandler = AuthenticationHandler() private let authenticationHandler = AuthenticationHandler()
private let publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory) private let publicKeyFileStoreController = PublicKeyFileStoreController(publicKeysURL: URL.publicKeyDirectory, certificatesURL: URL.certificatesDirectory)
@State var pending: ([[SignatureRequest]], (Set<SignatureRequest>) async throws -> Void)? @State var pending: ([[SignatureRequest]], (Set<SignatureRequest>) async throws -> Void)?
@Environment(\.openWindow) var openWindow @Environment(\.openWindow) var openWindow
@@ -42,18 +45,23 @@ struct SecretAgent: App {
} }
.task { .task {
let socketController = SocketController(path: URL.socketPath) let socketController = SocketController(path: URL.socketPath)
let agent = Agent(storeList: storeList, authenticationHandler: authenticationHandler, witness: notifier) let agent = Agent(
storeList: storeList,
certificateStore: certificateStore,
authenticationHandler: authenticationHandler,
witness: notifier
)
for await session in socketController.sessions { for await session in socketController.sessions {
Task { Task {
let inputParser = try await XPCAgentInputParser()
do { do {
let inputParser = try await XPCAgentInputParser()
for await message in session.messages { for await message in session.messages {
let request = try await inputParser.parse(data: message) let request = try await inputParser.parse(data: message)
let agentResponse = await agent.handle(request: request, provenance: session.provenance) let agentResponse = await agent.handle(request: request, provenance: session.provenance)
try session.write(agentResponse) try session.write(agentResponse)
} }
} catch { } catch {
try session.close() try? session.close()
} }
} }
} }
@@ -78,10 +86,19 @@ struct SecretAgent: App {
// } // }
// } // }
.task { .task {
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) { for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true) try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
} }
} }
.task {
let certsMigrator = CertificateMigrator(homeDirectory: URL.homeDirectory, certificateStore: certificateStore)
try? certsMigrator.migrate()
try? publicKeyFileStoreController.generateCertificates(for: certificateStore.certificates, clear: true)
for await _ in NotificationCenter.default.notifications(named: .certificateStoreReloaded) {
try? publicKeyFileStoreController.generateCertificates(for: certificateStore.certificates, clear: true)
}
}
.task { .task {
await authenticationHandler.setBatchAuthHandler { @MainActor pending, authorize in await authenticationHandler.setBatchAuthHandler { @MainActor pending, authorize in
self.pending = (pending, authorize) self.pending = (pending, authorize)
@@ -90,7 +107,6 @@ struct SecretAgent: App {
} }
.task { .task {
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
notifier.prompt() notifier.prompt()
_ = withObservationTracking { _ = withObservationTracking {
updater.update updater.update

View File

@@ -1,94 +0,0 @@
import Cocoa
import OSLog
import SecretKit
import SecureEnclaveSecretKit
import SmartCardSecretKit
import SecretAgentKit
import Brief
import Observation
import SSHProtocolKit
import CertificateKit
import Common
import SwiftUI
extension EnvironmentValues {
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
@MainActor var certificateStore: CertificateStore {
EnvironmentValues._certificateStore
}
}
@main
class AppDelegate: NSObject, NSApplicationDelegate {
@MainActor private let storeList: SecretStoreList = {
let list = SecretStoreList()
let cryptoKit = SecureEnclave.Store()
let migrator = SecureEnclave.CryptoKitMigrator()
try? migrator.migrate(to: cryptoKit)
list.add(store: cryptoKit)
list.add(store: SmartCard.Store())
let certsMigrator = CertificateMigrator(homeDirectory: URL.homeDirectory, certificateStore: EnvironmentValues._certificateStore)
try? certsMigrator.migrate()
return list
}()
private let updater = Updater(checkOnLaunch: true)
private let notifier = Notifier()
private let publicKeyFileStoreController = PublicKeyFileStoreController(publicKeysURL: URL.publicKeyDirectory, certificatesURL: URL.certificatesDirectory)
@MainActor private lazy var agent: Agent = {
Agent(storeList: storeList, certificateStore: EnvironmentValues._certificateStore, witness: notifier)
}()
private lazy var socketController: SocketController = {
let path = URL.socketPath as String
return SocketController(path: path)
}()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
func applicationDidFinishLaunching(_ aNotification: Notification) {
logger.debug("SecretAgent finished launching")
Task {
for await session in socketController.sessions {
Task {
let inputParser = try await XPCAgentInputParser()
do {
for await message in session.messages {
let request = try await inputParser.parse(data: message)
let agentResponse = await agent.handle(request: request, provenance: session.provenance)
try session.write(agentResponse)
}
} catch {
try session.close()
}
}
}
}
Task {
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
}
}
Task {
for await _ in NotificationCenter.default.notifications(named: .certificateStoreReloaded) {
try? publicKeyFileStoreController.generateCertificates(for: EnvironmentValues._certificateStore.certificates, clear: true)
}
}
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
try? publicKeyFileStoreController.generateCertificates(for: EnvironmentValues._certificateStore.certificates, clear: true)
notifier.prompt()
_ = withObservationTracking {
updater.update
} onChange: { [updater, notifier] in
Task {
guard !updater.currentVersion.isTestBuild else { return }
await notifier.notify(update: updater.update!) { release in
await updater.ignore(release: release)
}
}
}
}
}

View File

@@ -31,7 +31,7 @@ struct BatchedRequestsView: View {
Spacer() Spacer()
Button("Review") { Button("Review") {
Task { Task {
try await review([pending.element]) try? await review([pending.element])
} }
} }
} }
@@ -42,7 +42,7 @@ struct BatchedRequestsView: View {
Spacer() Spacer()
Button("Review All") { Button("Review All") {
Task { Task {
try await review(Set(group.element)) try? await review(Set(group.element))
} }
} }

View File

@@ -25,7 +25,7 @@ struct Secretive: App {
guard !agentLaunchController.developmentBuild else { return } guard !agentLaunchController.developmentBuild else { return }
if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running { if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running {
// Relaunch the agent, since it'll be running from earlier update still // Relaunch the agent, since it'll be running from earlier update still
try await agentLaunchController.forceLaunch() try? await agentLaunchController.forceLaunch()
} }
} }
} }

View File

@@ -1,5 +1,6 @@
import Foundation import Foundation
import SecretKit import SecretKit
import LocalAuthentication
enum Preview {} enum Preview {}
@@ -38,17 +39,10 @@ extension Preview {
self.init(secrets: new) self.init(secrets: new)
} }
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, context: AuthenticationContextProtocol?) throws -> Data { func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data {
return data return data
} }
func existingAuthenticationContextProtocol(secret: Preview.Secret) -> AuthenticationContextProtocol? {
nil
}
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
}
func reloadSecrets() { func reloadSecrets() {
} }
@@ -82,16 +76,10 @@ extension Preview {
self.init(secrets: new) self.init(secrets: new)
} }
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, context: AuthenticationContextProtocol?) throws -> Data { func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data {
return data return data
} }
func existingAuthenticationContextProtocol(secret: Preview.Secret) -> AuthenticationContextProtocol? {
nil
}
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
}
func reloadSecrets() { func reloadSecrets() {
} }

View File

@@ -101,7 +101,7 @@ struct AgentNotRunningView: View {
guard !loading else { return } guard !loading else { return }
loading = true loading = true
Task { Task {
try await agentLaunchController.forceLaunch() try? await agentLaunchController.forceLaunch()
loading = false loading = false
if !agentLaunchController.running { if !agentLaunchController.running {

View File

@@ -163,10 +163,9 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
} else { } else {
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
content content
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15)) .glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15))
.mask(RoundedRectangle(cornerRadius: 15)) .mask(RoundedRectangle(cornerRadius: 15))
.contentShape(RoundedRectangle(cornerRadius: 15))
.shadow(color: .black.opacity(0.1), radius: 5) .shadow(color: .black.opacity(0.1), radius: 5)
} else { } else {
content content
@@ -182,7 +181,7 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1) let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
switch interactionState { switch interactionState {
case .normal: case .normal:
return base return base.mix(with: .accentColor, by: 0)
case .hovering: case .hovering:
return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1) return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1)
case .clicking, .dragging: case .clicking, .dragging: