mirror of
				https://github.com/maxgoedjen/secretive.git
				synced 2025-10-31 07:20:57 +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,8 +21,10 @@ public final class OpenSSHCertificateHandler { | ||||
|             logger.log("No certificates, short circuiting") | ||||
|             return | ||||
|         } | ||||
|         keyBlobsAndNames = secrets.reduce(into: [:]) { partialResult, next in | ||||
|             partialResult[next] = try? loadKeyblobAndName(for: next) | ||||
|         keyBlobsAndNames.withLock { | ||||
|             $0 = secrets.reduce(into: [:]) { partialResult, next in | ||||
|                 partialResult[next] = try? loadKeyblobAndName(for: next) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -29,7 +32,10 @@ public final class OpenSSHCertificateHandler { | ||||
|     /// - 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,19 +32,28 @@ 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,9 +48,11 @@ class Notifier { | ||||
|         UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory]) | ||||
|         UNUserNotificationCenter.current().delegate = notificationDelegate | ||||
| 
 | ||||
|         notificationDelegate.persistAuthentication = { secret, store, duration in | ||||
|             guard let duration = duration else { return } | ||||
|             try? await store.persistAuthentication(secret: secret, forDuration: duration) | ||||
|         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,15 +138,21 @@ extension Notifier { | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { | ||||
| 
 | ||||
|     fileprivate var release: Release? | ||||
|     fileprivate var ignore: ((Release) -> Void)? | ||||
|     fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) async -> Void)? | ||||
|     fileprivate var persistOptions: [String: TimeInterval] = [:] | ||||
|     fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:] | ||||
|     fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:] | ||||
| 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: 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 } | ||||
|         switch response.actionIdentifier { | ||||
|         case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier: | ||||
|             NSWorkspace.shared.open(update.html_url) | ||||
|         case Notifier.Constants.ignoreActionIdentitifier: | ||||
|             ignore?(update) | ||||
|         default: | ||||
|             fatalError() | ||||
|         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: | ||||
|                 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,9 +5,20 @@ 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 { | ||||
| 
 | ||||
|      | ||||
|     private let storeList: SecretStoreList = { | ||||
|         let list = SecretStoreList() | ||||
|         list.add(store: SecureEnclave.Store()) | ||||
| @ -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