mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-01-08 12:37:07 +00:00
More
This commit is contained in:
parent
304741e019
commit
576e625b8f
@ -2,7 +2,7 @@ import Foundation
|
||||
import Synchronization
|
||||
|
||||
/// A protocol for retreiving the latest available version of an app.
|
||||
public protocol UpdaterProtocol: ObservableObject {
|
||||
public protocol UpdaterProtocol: Observable {
|
||||
|
||||
/// The latest update
|
||||
var update: Release? { get }
|
||||
|
@ -5,7 +5,7 @@ import SecretKit
|
||||
import AppKit
|
||||
|
||||
/// The `Agent` is an implementation of an SSH agent. It manages coordination and access between a socket, traces requests, notifies witnesses and passes requests to stores.
|
||||
public final class Agent {
|
||||
public final class Agent: Sendable {
|
||||
|
||||
private let storeList: SecretStoreList
|
||||
private let witness: SigningWitness?
|
||||
|
@ -2,7 +2,7 @@ import Foundation
|
||||
import SecretKit
|
||||
|
||||
/// A protocol that allows conformers to be notified of access to secrets, and optionally prevent access.
|
||||
public protocol SigningWitness {
|
||||
public protocol SigningWitness: Sendable {
|
||||
|
||||
/// A ridiculously named method that notifies the callee that a signing operation is about to be performed using a secret. The callee may `throw` an `Error` to prevent access from occurring.
|
||||
/// - Parameters:
|
||||
|
@ -1,13 +1,14 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import Synchronization
|
||||
|
||||
/// Manages storage and lookup for OpenSSH certificates.
|
||||
public final class OpenSSHCertificateHandler {
|
||||
public final class OpenSSHCertificateHandler: Sendable {
|
||||
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
|
||||
private let writer = OpenSSHKeyWriter()
|
||||
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
|
||||
private let keyBlobsAndNames: Mutex<[AnySecret: (Data, Data)]> = .init([:])
|
||||
|
||||
/// Initializes an OpenSSHCertificateHandler.
|
||||
public init() {
|
||||
@ -20,16 +21,21 @@ public final class OpenSSHCertificateHandler {
|
||||
logger.log("No certificates, short circuiting")
|
||||
return
|
||||
}
|
||||
keyBlobsAndNames = secrets.reduce(into: [:]) { partialResult, next in
|
||||
keyBlobsAndNames.withLock {
|
||||
$0 = secrets.reduce(into: [:]) { partialResult, next in
|
||||
partialResult[next] = try? loadKeyblobAndName(for: next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not the certificate handler has a certifiicate associated with a given secret.
|
||||
/// - 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
|
||||
public func hasCertificate<SecretType: Secret>(for secret: SecretType) -> Bool {
|
||||
keyBlobsAndNames[AnySecret(secret)] != nil
|
||||
keyBlobsAndNames.withLock {
|
||||
$0[AnySecret(secret)] != nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -61,7 +67,9 @@ public final class OpenSSHCertificateHandler {
|
||||
/// - Parameter secret: The secret to search for a certificate with
|
||||
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
|
||||
public func keyBlobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
|
||||
keyBlobsAndNames[AnySecret(secret)]
|
||||
keyBlobsAndNames.withLock {
|
||||
$0[AnySecret(secret)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
||||
|
@ -2,7 +2,7 @@ import Foundation
|
||||
import CryptoKit
|
||||
|
||||
/// Generates OpenSSH representations of Secrets.
|
||||
public struct OpenSSHKeyWriter {
|
||||
public struct OpenSSHKeyWriter: Sendable {
|
||||
|
||||
/// Initializes the writer.
|
||||
public init() {
|
||||
|
@ -2,7 +2,7 @@ import Foundation
|
||||
import OSLog
|
||||
|
||||
/// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts.
|
||||
public final class PublicKeyFileStoreController {
|
||||
public final class PublicKeyFileStoreController: Sendable {
|
||||
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
|
||||
private let directory: String
|
||||
|
@ -1,13 +1,21 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import Synchronization
|
||||
|
||||
/// A "Store Store," which holds a list of type-erased stores.
|
||||
@Observable public final class SecretStoreList: ObservableObject {
|
||||
@Observable public final class SecretStoreList: Sendable {
|
||||
|
||||
/// The Stores managed by the SecretStoreList.
|
||||
public var stores: [AnySecretStore] = []
|
||||
public var stores: [AnySecretStore] {
|
||||
__stores.withLock { $0 }
|
||||
}
|
||||
private let __stores: Mutex<[AnySecretStore]> = .init([])
|
||||
|
||||
/// A modifiable store, if one is available.
|
||||
public var modifiableStore: AnySecretStoreModifiable?
|
||||
public var modifiableStore: AnySecretStoreModifiable? {
|
||||
__modifiableStore.withLock { $0 }
|
||||
}
|
||||
private let __modifiableStore: Mutex<AnySecretStoreModifiable?> = .init(nil)
|
||||
|
||||
/// Initializes a SecretStoreList.
|
||||
public init() {
|
||||
@ -15,23 +23,33 @@ import Observation
|
||||
|
||||
/// Adds a non-type-erased SecretStore to the list.
|
||||
public func add<SecretStoreType: SecretStore>(store: SecretStoreType) {
|
||||
stores.append(AnySecretStore(store))
|
||||
__stores.withLock {
|
||||
$0.append(AnySecretStore(store))
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a non-type-erased modifiable SecretStore.
|
||||
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
||||
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
||||
modifiableStore = modifiable
|
||||
stores.append(modifiable)
|
||||
__modifiableStore.withLock {
|
||||
$0 = modifiable
|
||||
}
|
||||
__stores.withLock {
|
||||
$0.append(modifiable)
|
||||
}
|
||||
}
|
||||
|
||||
/// A boolean describing whether there are any Stores available.
|
||||
public var anyAvailable: Bool {
|
||||
stores.reduce(false, { $0 || $1.isAvailable })
|
||||
__stores.withLock {
|
||||
$0.reduce(false, { $0 || $1.isAvailable })
|
||||
}
|
||||
}
|
||||
|
||||
public var allSecrets: [AnySecret] {
|
||||
stores.flatMap(\.secrets)
|
||||
__stores.withLock {
|
||||
$0.flatMap(\.secrets)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import SecureEnclaveSecretKit
|
||||
import SmartCardSecretKit
|
||||
import SecretAgentKit
|
||||
import Brief
|
||||
import Observation
|
||||
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@ -31,18 +32,27 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
logger.debug("SecretAgent finished launching")
|
||||
// DispatchQueue.main.async {
|
||||
// self.socketController.handler = self.agent.handle(reader:writer:)
|
||||
// }
|
||||
// NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in
|
||||
// try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||
// }
|
||||
Task { @MainActor in
|
||||
socketController.handler = { [agent] reader, writer in
|
||||
await agent.handle(reader: reader, writer: writer)
|
||||
}
|
||||
}
|
||||
Task {
|
||||
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
|
||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||
}
|
||||
}
|
||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||
notifier.prompt()
|
||||
// updateSink = updater.$update.sink { update in
|
||||
// guard let update = update else { return }
|
||||
// self.notifier.notify(update: update, ignore: self.updater.ignore(release:))
|
||||
// }
|
||||
_ = withObservationTracking {
|
||||
updater.update
|
||||
} onChange: { [updater, notifier] in
|
||||
notifier.notify(update: updater.update!) { release in
|
||||
Task {
|
||||
await updater.ignore(release: release)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,8 +4,9 @@ import AppKit
|
||||
import SecretKit
|
||||
import SecretAgentKit
|
||||
import Brief
|
||||
import Synchronization
|
||||
|
||||
class Notifier {
|
||||
final class Notifier: Sendable {
|
||||
|
||||
private let notificationDelegate = NotificationDelegate()
|
||||
|
||||
@ -34,7 +35,9 @@ class Notifier {
|
||||
guard let string = formatter.string(from: seconds)?.capitalized else { continue }
|
||||
let identifier = Constants.persistAuthenticationCategoryIdentitifier.appending("\(seconds)")
|
||||
let action = UNNotificationAction(identifier: identifier, title: string, options: [])
|
||||
notificationDelegate.persistOptions[identifier] = seconds
|
||||
notificationDelegate.state.withLock { state in
|
||||
state.persistOptions[identifier] = seconds
|
||||
}
|
||||
allPersistenceActions.append(action)
|
||||
}
|
||||
|
||||
@ -45,10 +48,12 @@ class Notifier {
|
||||
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
|
||||
UNUserNotificationCenter.current().delegate = notificationDelegate
|
||||
|
||||
notificationDelegate.persistAuthentication = { secret, store, duration in
|
||||
notificationDelegate.state.withLock { state in
|
||||
state.persistAuthentication = { secret, store, duration in
|
||||
guard let duration = duration else { return }
|
||||
try? await store.persistAuthentication(secret: secret, forDuration: duration)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -58,8 +63,10 @@ class Notifier {
|
||||
}
|
||||
|
||||
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async {
|
||||
notificationDelegate.pendingPersistableSecrets[secret.id.description] = secret
|
||||
notificationDelegate.pendingPersistableStores[store.id.description] = store
|
||||
notificationDelegate.state.withLock { state in
|
||||
state.pendingPersistableSecrets[secret.id.description] = secret
|
||||
state.pendingPersistableStores[store.id.description] = store
|
||||
}
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
let notificationContent = UNMutableNotificationContent()
|
||||
notificationContent.title = String(localized: "signed_notification_title_\(provenance.origin.displayName)")
|
||||
@ -74,12 +81,14 @@ class Notifier {
|
||||
notificationContent.attachments = [attachment]
|
||||
}
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
|
||||
notificationCenter.add(request, withCompletionHandler: nil)
|
||||
try? await notificationCenter.add(request)
|
||||
}
|
||||
|
||||
func notify(update: Release, ignore: ((Release) -> Void)?) {
|
||||
notificationDelegate.release = update
|
||||
notificationDelegate.ignore = ignore
|
||||
notificationDelegate.state.withLock { [update] state in
|
||||
state.release = update
|
||||
// state.ignore = ignore
|
||||
}
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
let notificationContent = UNMutableNotificationContent()
|
||||
if update.critical {
|
||||
@ -129,14 +138,20 @@ extension Notifier {
|
||||
|
||||
}
|
||||
|
||||
class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
||||
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
|
||||
|
||||
struct State {
|
||||
typealias PersistAuthentication = ((AnySecret, AnySecretStore, TimeInterval?) async -> Void)
|
||||
typealias Ignore = ((Release) -> Void)
|
||||
fileprivate var release: Release?
|
||||
fileprivate var ignore: ((Release) -> Void)?
|
||||
fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) async -> Void)?
|
||||
fileprivate var ignore: Ignore?
|
||||
fileprivate var persistAuthentication: PersistAuthentication?
|
||||
fileprivate var persistOptions: [String: TimeInterval] = [:]
|
||||
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
|
||||
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
|
||||
}
|
||||
|
||||
fileprivate let state: Mutex<State> = .init(.init())
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
||||
|
||||
@ -155,27 +170,34 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
||||
}
|
||||
|
||||
func handleUpdateResponse(response: UNNotificationResponse) {
|
||||
guard let update = release else { return }
|
||||
state.withLock { state in
|
||||
guard let update = state.release else { return }
|
||||
switch response.actionIdentifier {
|
||||
case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
|
||||
NSWorkspace.shared.open(update.html_url)
|
||||
case Notifier.Constants.ignoreActionIdentitifier:
|
||||
ignore?(update)
|
||||
state.ignore?(update)
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
|
||||
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]
|
||||
else { return }
|
||||
pendingPersistableSecrets[secretID] = nil
|
||||
await persistAuthentication?(secret, store, persistOptions[response.actionIdentifier])
|
||||
// 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, let secret = state.pendingPersistableSecrets[secretID],
|
||||
// let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String, let store = state.pendingPersistableStores[storeID]
|
||||
// else { return (nil, nil, nil, nil) }
|
||||
// state.pendingPersistableSecrets[secretID] = nil
|
||||
// return (secret, store, state.persistOptions[response.actionIdentifier], state.persistAuthentication)
|
||||
// }
|
||||
// guard let secret, let store, let persistOptions else { return }
|
||||
// await callback?(secret, store, persistOptions)
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
completionHandler([.list, .banner])
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
|
||||
[.list, .banner]
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,17 @@ import SecureEnclaveSecretKit
|
||||
import SmartCardSecretKit
|
||||
import Brief
|
||||
|
||||
extension EnvironmentValues {
|
||||
@Entry var secretStoreList: SecretStoreList = {
|
||||
let list = SecretStoreList()
|
||||
list.add(store: SecureEnclave.Store())
|
||||
list.add(store: SmartCard.Store())
|
||||
return list
|
||||
}()
|
||||
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = AgentStatusChecker()
|
||||
@Entry var updater: any UpdaterProtocol = Updater(checkOnLaunch: false)
|
||||
}
|
||||
|
||||
@main
|
||||
struct Secretive: App {
|
||||
|
||||
@ -23,10 +34,10 @@ struct Secretive: App {
|
||||
|
||||
@SceneBuilder var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView<Updater, AgentStatusChecker>(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
||||
.environmentObject(storeList)
|
||||
.environmentObject(Updater(checkOnLaunch: hasRunSetup))
|
||||
.environmentObject(agentStatusChecker)
|
||||
ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
||||
.environment(storeList)
|
||||
.environment(Updater(checkOnLaunch: hasRunSetup))
|
||||
.environment(agentStatusChecker)
|
||||
.onAppear {
|
||||
if !hasRunSetup {
|
||||
showingSetup = true
|
||||
|
@ -2,15 +2,16 @@ import Foundation
|
||||
import Combine
|
||||
import AppKit
|
||||
import SecretKit
|
||||
import Observation
|
||||
|
||||
protocol AgentStatusCheckerProtocol: ObservableObject {
|
||||
protocol AgentStatusCheckerProtocol: Observable {
|
||||
var running: Bool { get }
|
||||
var developmentBuild: Bool { get }
|
||||
}
|
||||
|
||||
class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol {
|
||||
@Observable class AgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
|
||||
@Published var running: Bool = false
|
||||
var running: Bool = false
|
||||
|
||||
init() {
|
||||
check()
|
||||
|
@ -4,7 +4,7 @@ import SecureEnclaveSecretKit
|
||||
import SmartCardSecretKit
|
||||
import Brief
|
||||
|
||||
struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentStatusCheckerProtocol>: View {
|
||||
struct ContentView: View {
|
||||
|
||||
@Binding var showingCreation: Bool
|
||||
@Binding var runningSetup: Bool
|
||||
@ -13,9 +13,9 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
|
||||
@State var activeSecret: AnySecret.ID?
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@EnvironmentObject private var storeList: SecretStoreList
|
||||
@EnvironmentObject private var updater: UpdaterType
|
||||
@EnvironmentObject private var agentStatusChecker: AgentStatusCheckerType
|
||||
@Environment(\.secretStoreList) private var storeList: SecretStoreList
|
||||
@Environment(\.updater) private var updater: any UpdaterProtocol
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
|
||||
@State private var selectedUpdate: Release?
|
||||
@State private var showingAppPathNotice = false
|
||||
|
@ -6,7 +6,7 @@ struct StoreListView: View {
|
||||
|
||||
@Binding var activeSecret: AnySecret.ID?
|
||||
|
||||
@EnvironmentObject private var storeList: SecretStoreList
|
||||
@Environment(SecretStoreList.self) private var storeList: SecretStoreList
|
||||
|
||||
private func secretDeleted(secret: AnySecret) {
|
||||
activeSecret = nextDefaultSecret
|
||||
|
@ -3,7 +3,7 @@ import Brief
|
||||
|
||||
struct UpdateDetailView<UpdaterType: Updater>: View {
|
||||
|
||||
@EnvironmentObject var updater: UpdaterType
|
||||
@Environment(UpdaterType.self) var updater: UpdaterType
|
||||
|
||||
let update: Release
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user