This commit is contained in:
Max Goedjen
2025-01-05 16:07:11 -08:00
parent 304741e019
commit 576e625b8f
14 changed files with 147 additions and 77 deletions

View File

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

View File

@@ -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?

View File

@@ -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:

View File

@@ -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``

View File

@@ -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() {

View File

@@ -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

View File

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