Skip to content

Commit

Permalink
More
Browse files Browse the repository at this point in the history
  • Loading branch information
maxgoedjen committed Jan 6, 2025
1 parent 304741e commit 576e625
Show file tree
Hide file tree
Showing 14 changed files with 148 additions and 78 deletions.
2 changes: 1 addition & 1 deletion Sources/Packages/Sources/Brief/UpdaterProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion Sources/Packages/Sources/SecretAgentKit/Agent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -20,16 +21,21 @@ 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)
}
}
}

/// 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
}

}


Expand Down Expand Up @@ -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``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 26 additions & 8 deletions Sources/Packages/Sources/SecretKit/SecretStoreList.swift
Original file line number Diff line number Diff line change
@@ -1,37 +1,55 @@
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() {
}

/// 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)
}
}

}
32 changes: 21 additions & 11 deletions Sources/SecretAgent/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SecureEnclaveSecretKit
import SmartCardSecretKit
import SecretAgentKit
import Brief
import Observation

@main
class AppDelegate: NSObject, NSApplicationDelegate {
Expand All @@ -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)
}
}
}
}

}

90 changes: 56 additions & 34 deletions Sources/SecretAgent/Notifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import AppKit
import SecretKit
import SecretAgentKit
import Brief
import Synchronization

class Notifier {
final class Notifier: Sendable {

private let notificationDelegate = NotificationDelegate()

Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
}
}

}
Expand All @@ -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)")
Expand All @@ -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 {
Expand Down Expand Up @@ -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?) {

}
Expand All @@ -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]
}

}
21 changes: 16 additions & 5 deletions Sources/Secretive/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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
Expand Down
Loading

0 comments on commit 576e625

Please sign in to comment.