mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-03-30 05:47:24 +02:00
Batch processing WIP
This commit is contained in:
@@ -14,6 +14,7 @@ public final class Agent: Sendable {
|
|||||||
private let signatureWriter = OpenSSHSignatureWriter()
|
private let signatureWriter = OpenSSHSignatureWriter()
|
||||||
private let certificateHandler = OpenSSHCertificateHandler()
|
private let certificateHandler = OpenSSHCertificateHandler()
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
|
||||||
|
private let authorizationCoordinator = AuthorizationCoordinator()
|
||||||
|
|
||||||
/// Initializes an agent with a store list and a witness.
|
/// Initializes an agent with a store list and a witness.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -102,8 +103,15 @@ extension Agent {
|
|||||||
throw NoMatchingKeyError()
|
throw NoMatchingKeyError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let decision = try await authorizationCoordinator.waitForAccessIfNeeded(to: secret, provenance: provenance)
|
||||||
|
switch decision {
|
||||||
|
case .proceed:
|
||||||
|
break
|
||||||
|
case .promptForSharedAuth:
|
||||||
|
try? await store.persistAuthentication(secret: secret, forProvenance: provenance)
|
||||||
|
try await authorizationCoordinator.completedPersistence()
|
||||||
|
}
|
||||||
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
||||||
|
|
||||||
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance)
|
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance)
|
||||||
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
|
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
|
||||||
|
|
||||||
@@ -111,6 +119,8 @@ extension Agent {
|
|||||||
|
|
||||||
logger.debug("Agent signed request")
|
logger.debug("Agent signed request")
|
||||||
|
|
||||||
|
try await authorizationCoordinator.completedAuthorization()
|
||||||
|
|
||||||
return signedData
|
return signedData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import Foundation
|
||||||
|
import SecretKit
|
||||||
|
import os
|
||||||
|
import LocalAuthentication
|
||||||
|
|
||||||
|
struct PendingRequest: Identifiable, Hashable, CustomStringConvertible {
|
||||||
|
let id: UUID = UUID()
|
||||||
|
let secret: AnySecret
|
||||||
|
let provenance: SigningRequestProvenance
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
"\(id.uuidString) - \(secret.name) \(provenance.origin.displayName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func batchable(with request: PendingRequest) -> Bool {
|
||||||
|
secret == request.secret &&
|
||||||
|
provenance.isSameProvenance(as: request.provenance)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Decision {
|
||||||
|
case proceed
|
||||||
|
case promptForSharedAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
actor RequestHolder {
|
||||||
|
|
||||||
|
var pending: [PendingRequest] = []
|
||||||
|
var active: PendingRequest?
|
||||||
|
var preauthorized: PendingRequest?
|
||||||
|
|
||||||
|
func shouldBlock(_ request: PendingRequest) -> Bool {
|
||||||
|
if let preauthorized, preauthorized.batchable(with: request) {
|
||||||
|
print("Batching: \(request)")
|
||||||
|
pending.removeAll(where: { $0 == request })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let isTurn = request.id == active?.id
|
||||||
|
if isTurn {
|
||||||
|
print("turn \(request)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if pending.isEmpty && active == nil {
|
||||||
|
active = request
|
||||||
|
return false
|
||||||
|
} else if !pending.contains(where: { $0.id == request.id }) {
|
||||||
|
pending.append(request)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear() {
|
||||||
|
if let preauthorized, allBatchable(with: preauthorized).isEmpty {
|
||||||
|
self.preauthorized = nil
|
||||||
|
}
|
||||||
|
if !pending.isEmpty {
|
||||||
|
let next = pending.removeFirst()
|
||||||
|
active = next
|
||||||
|
} else {
|
||||||
|
active = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func allBatchable(with request: PendingRequest) -> [PendingRequest] {
|
||||||
|
pending.filter { $0.batchable(with: request) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func completedPersistence() {
|
||||||
|
self.preauthorized = active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class AuthorizationCoordinator: Sendable {
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AuthorizationCoordinator")
|
||||||
|
private let holder = RequestHolder()
|
||||||
|
|
||||||
|
public func waitForAccessIfNeeded(to secret: AnySecret, provenance: SigningRequestProvenance) async throws -> Decision {
|
||||||
|
// Block on unknown, since we don't really have any way to check.
|
||||||
|
if secret.authenticationRequirement == .unknown {
|
||||||
|
logger.warning("\(secret.name) has unknown authentication requirement.")
|
||||||
|
}
|
||||||
|
guard secret.authenticationRequirement != .notRequired else {
|
||||||
|
logger.debug("\(secret.name) does not require authentication, continuing.")
|
||||||
|
return .proceed
|
||||||
|
}
|
||||||
|
logger.debug("\(secret.name) requires authentication.")
|
||||||
|
let pending = PendingRequest(secret: secret, provenance: provenance)
|
||||||
|
while !Task.isCancelled, await holder.shouldBlock(pending) {
|
||||||
|
logger.debug("\(pending) waiting.")
|
||||||
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
}
|
||||||
|
if await holder.preauthorized == nil, await holder.allBatchable(with: pending).count > 0 {
|
||||||
|
logger.debug("\(pending) batch suggestion.")
|
||||||
|
return .promptForSharedAuth
|
||||||
|
}
|
||||||
|
logger.debug("\(pending) continuing")
|
||||||
|
return .proceed
|
||||||
|
}
|
||||||
|
|
||||||
|
func completedAuthorization() async throws {
|
||||||
|
await holder.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
func completedPersistence() async throws {
|
||||||
|
await holder.completedPersistence()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import LocalAuthentication
|
import LocalAuthentication
|
||||||
|
|
||||||
/// A context describing a persisted authentication.
|
/// A context describing a persisted authentication.
|
||||||
package final class PersistentAuthenticationContext<SecretType: Secret>: PersistedAuthenticationContext {
|
package struct PersistentAuthenticationContext<SecretType: Secret>: PersistedAuthenticationContext {
|
||||||
|
|
||||||
/// The Secret to persist authentication for.
|
/// The Secret to persist authentication for.
|
||||||
let secret: SecretType
|
let secret: SecretType
|
||||||
@@ -35,16 +35,27 @@ package final class PersistentAuthenticationContext<SecretType: Secret>: Persist
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ScopedPersistentAuthenticationContext<SecretType: Secret>: Hashable {
|
||||||
|
let provenance: SigningRequestProvenance
|
||||||
|
let secret: SecretType
|
||||||
|
}
|
||||||
|
|
||||||
package actor PersistentAuthenticationHandler<SecretType: Secret>: Sendable {
|
package actor PersistentAuthenticationHandler<SecretType: Secret>: Sendable {
|
||||||
|
|
||||||
private var persistedAuthenticationContexts: [SecretType: PersistentAuthenticationContext<SecretType>] = [:]
|
private var unscopedPersistedAuthenticationContexts: [SecretType: PersistentAuthenticationContext<SecretType>] = [:]
|
||||||
|
private var scopedPersistedAuthenticationContexts: [ScopedPersistentAuthenticationContext<SecretType>: PersistentAuthenticationContext<SecretType>] = [:]
|
||||||
|
|
||||||
package init() {
|
package init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
package func existingPersistedAuthenticationContext(secret: SecretType) -> PersistentAuthenticationContext<SecretType>? {
|
package func existingPersistedAuthenticationContext(secret: SecretType, provenance: SigningRequestProvenance) -> PersistentAuthenticationContext<SecretType>? {
|
||||||
guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil }
|
if let unscopedPersistence = unscopedPersistedAuthenticationContexts[secret], unscopedPersistence.valid {
|
||||||
return persisted
|
return unscopedPersistence
|
||||||
|
}
|
||||||
|
if let scopedPersistence = scopedPersistedAuthenticationContexts[.init(provenance: provenance, secret: secret)], scopedPersistence.valid {
|
||||||
|
return scopedPersistence
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
package func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws {
|
package func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws {
|
||||||
@@ -62,7 +73,22 @@ package actor PersistentAuthenticationHandler<SecretType: Secret>: Sendable {
|
|||||||
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
|
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
|
||||||
guard success else { return }
|
guard success else { return }
|
||||||
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
|
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
|
||||||
persistedAuthenticationContexts[secret] = context
|
unscopedPersistedAuthenticationContexts[secret] = context
|
||||||
|
}
|
||||||
|
|
||||||
|
package func persistAuthentication(secret: SecretType, provenance: SigningRequestProvenance) async throws {
|
||||||
|
let newContext = LAContext()
|
||||||
|
|
||||||
|
// FIXME: TEMPORARY
|
||||||
|
let duration: TimeInterval = 10000
|
||||||
|
newContext.touchIDAuthenticationAllowableReuseDuration = duration
|
||||||
|
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
||||||
|
|
||||||
|
newContext.localizedReason = "Batch requests"
|
||||||
|
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
|
||||||
|
guard success else { return }
|
||||||
|
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
|
||||||
|
scopedPersistedAuthenticationContexts[.init(provenance: provenance, secret: secret)] = context
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
|
|||||||
private let _name: @MainActor @Sendable () -> String
|
private let _name: @MainActor @Sendable () -> String
|
||||||
private let _secrets: @MainActor @Sendable () -> [AnySecret]
|
private let _secrets: @MainActor @Sendable () -> [AnySecret]
|
||||||
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance) async throws -> Data
|
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance) async throws -> Data
|
||||||
private let _existingPersistedAuthenticationContext: @Sendable (AnySecret) async -> PersistedAuthenticationContext?
|
private let _existingPersistedAuthenticationContext: @Sendable (AnySecret, SigningRequestProvenance) async -> PersistedAuthenticationContext?
|
||||||
private let _persistAuthentication: @Sendable (AnySecret, TimeInterval) async throws -> Void
|
private let _persistAuthenticationForDuration: @Sendable (AnySecret, TimeInterval) async throws -> Void
|
||||||
|
private let _persistAuthenticationForProvenance: @Sendable (AnySecret, SigningRequestProvenance) async throws -> Void
|
||||||
private let _reloadSecrets: @Sendable () async -> Void
|
private let _reloadSecrets: @Sendable () async -> Void
|
||||||
|
|
||||||
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
|
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
|
||||||
@@ -20,8 +21,9 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
|
|||||||
_id = { secretStore.id }
|
_id = { secretStore.id }
|
||||||
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
||||||
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
||||||
_existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
|
_existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType, provenance: $1) }
|
||||||
_persistAuthentication = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
_persistAuthenticationForDuration = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
||||||
|
_persistAuthenticationForProvenance = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forProvenance: $1) }
|
||||||
_reloadSecrets = { await secretStore.reloadSecrets() }
|
_reloadSecrets = { await secretStore.reloadSecrets() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +47,16 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
|
|||||||
try await _sign(data, secret, provenance)
|
try await _sign(data, secret, provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: AnySecret) async -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: AnySecret, provenance: SigningRequestProvenance) async -> PersistedAuthenticationContext? {
|
||||||
await _existingPersistedAuthenticationContext(secret)
|
await _existingPersistedAuthenticationContext(secret, provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) async throws {
|
public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) async throws {
|
||||||
try await _persistAuthentication(secret, duration)
|
try await _persistAuthenticationForDuration(secret, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: AnySecret, forProvenance provenance: SigningRequestProvenance) async throws {
|
||||||
|
try await _persistAuthenticationForProvenance(secret, provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reloadSecrets() async {
|
public func reloadSecrets() async {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public protocol SecretStore<SecretType>: Identifiable, Sendable {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - secret: The ``Secret`` to check if there is a persisted authentication for.
|
/// - secret: The ``Secret`` to check if there is a persisted authentication for.
|
||||||
/// - Returns: A persisted authentication context, if a valid one exists.
|
/// - Returns: A persisted authentication context, if a valid one exists.
|
||||||
func existingPersistedAuthenticationContext(secret: SecretType) async -> PersistedAuthenticationContext?
|
func existingPersistedAuthenticationContext(secret: SecretType, provenance: SigningRequestProvenance) async -> PersistedAuthenticationContext?
|
||||||
|
|
||||||
/// Persists user authorization for access to a secret.
|
/// Persists user authorization for access to a secret.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -35,6 +35,8 @@ public protocol SecretStore<SecretType>: Identifiable, Sendable {
|
|||||||
/// - Note: This is used for temporarily unlocking access to a secret which would otherwise require authentication every single use. This is useful for situations where the user anticipates several rapid accesses to a authorization-guarded secret.
|
/// - Note: This is used for temporarily unlocking access to a secret which would otherwise require authentication every single use. This is useful for situations where the user anticipates several rapid accesses to a authorization-guarded secret.
|
||||||
func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws
|
func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws
|
||||||
|
|
||||||
|
func persistAuthentication(secret: SecretType, forProvenance provenance: SigningRequestProvenance) async throws
|
||||||
|
|
||||||
/// Requests that the store reload secrets from any backing store, if neccessary.
|
/// Requests that the store reload secrets from any backing store, if neccessary.
|
||||||
func reloadSecrets() async
|
func reloadSecrets() async
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Foundation
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
/// Describes the chain of applications that requested a signature operation.
|
/// Describes the chain of applications that requested a signature operation.
|
||||||
public struct SigningRequestProvenance: Equatable, Sendable {
|
public struct SigningRequestProvenance: Equatable, Sendable, Hashable {
|
||||||
|
|
||||||
/// A list of processes involved in the request.
|
/// A list of processes involved in the request.
|
||||||
/// - Note: A chain will typically consist of many elements even for a simple request. For example, running `git fetch` in Terminal.app would generate a request chain of `ssh` -> `git` -> `zsh` -> `login` -> `Terminal.app`
|
/// - Note: A chain will typically consist of many elements even for a simple request. For example, running `git fetch` in Terminal.app would generate a request chain of `ssh` -> `git` -> `zsh` -> `login` -> `Terminal.app`
|
||||||
@@ -25,12 +25,16 @@ extension SigningRequestProvenance {
|
|||||||
chain.allSatisfy { $0.validSignature }
|
chain.allSatisfy { $0.validSignature }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func isSameProvenance(as other: SigningRequestProvenance) -> Bool {
|
||||||
|
zip(chain, other.chain).allSatisfy { $0.isSameProcess(as: $1) }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SigningRequestProvenance {
|
extension SigningRequestProvenance {
|
||||||
|
|
||||||
/// Describes a process in a `SigningRequestProvenance` chain.
|
/// Describes a process in a `SigningRequestProvenance` chain.
|
||||||
public struct Process: Equatable, Sendable {
|
public struct Process: Equatable, Sendable, Hashable {
|
||||||
|
|
||||||
/// The pid of the process.
|
/// The pid of the process.
|
||||||
public let pid: Int32
|
public let pid: Int32
|
||||||
@@ -71,6 +75,15 @@ extension SigningRequestProvenance {
|
|||||||
appName ?? processName
|
appName ?? processName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether the
|
||||||
|
public func isSameProcess(as other: Process) -> Bool {
|
||||||
|
processName == other.processName &&
|
||||||
|
appName == other.appName &&
|
||||||
|
iconURL == other.iconURL &&
|
||||||
|
path == other.path &&
|
||||||
|
validSignature == other.validSignature
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
|
||||||
var context: LAContext
|
var context: LAContext
|
||||||
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
|
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret, provenance: provenance) {
|
||||||
context = unsafe existing.context
|
context = unsafe existing.context
|
||||||
} else {
|
} else {
|
||||||
let newContext = LAContext()
|
let newContext = LAContext()
|
||||||
@@ -88,14 +88,18 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: Secret, provenance: SigningRequestProvenance) async -> PersistedAuthenticationContext? {
|
||||||
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
|
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret, provenance: provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
|
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
|
||||||
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
|
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: SecureEnclave.Secret, forProvenance provenance: SigningRequestProvenance) async throws {
|
||||||
|
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, provenance: provenance)
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor public func reloadSecrets() {
|
@MainActor public func reloadSecrets() {
|
||||||
let before = secrets
|
let before = secrets
|
||||||
secrets.removeAll()
|
secrets.removeAll()
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ extension SmartCard {
|
|||||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
|
||||||
guard let tokenID = await state.tokenID else { fatalError() }
|
guard let tokenID = await state.tokenID else { fatalError() }
|
||||||
var context: LAContext
|
var context: LAContext
|
||||||
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
|
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret, provenance: provenance) {
|
||||||
context = unsafe existing.context
|
context = unsafe existing.context
|
||||||
} else {
|
} else {
|
||||||
let newContext = LAContext()
|
let newContext = LAContext()
|
||||||
@@ -93,14 +93,18 @@ extension SmartCard {
|
|||||||
return signature as Data
|
return signature as Data
|
||||||
}
|
}
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: Secret, provenance: SigningRequestProvenance) async -> PersistedAuthenticationContext? {
|
||||||
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
|
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret, provenance: provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
|
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
|
||||||
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
|
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: Secret, forProvenance provenance: SigningRequestProvenance) async throws {
|
||||||
|
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, provenance: provenance)
|
||||||
|
}
|
||||||
|
|
||||||
/// Reloads all secrets from the store.
|
/// Reloads all secrets from the store.
|
||||||
@MainActor public func reloadSecrets() {
|
@MainActor public func reloadSecrets() {
|
||||||
reloadSecretsInternal()
|
reloadSecretsInternal()
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ final class Notifier: Sendable {
|
|||||||
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
|
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
|
||||||
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
|
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
|
||||||
notificationContent.interruptionLevel = .timeSensitive
|
notificationContent.interruptionLevel = .timeSensitive
|
||||||
if await store.existingPersistedAuthenticationContext(secret: secret) == nil && secret.authenticationRequirement.required {
|
if await store.existingPersistedAuthenticationContext(secret: secret, provenance: provenance) == nil && secret.authenticationRequirement.required {
|
||||||
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
|
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
|
||||||
}
|
}
|
||||||
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
|
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
|
||||||
@@ -79,6 +79,25 @@ final class Notifier: Sendable {
|
|||||||
try? await notificationCenter.add(request)
|
try? await notificationCenter.add(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notify(pendingAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async {
|
||||||
|
await notificationDelegate.state.setPending(secret: secret, store: store)
|
||||||
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
|
let notificationContent = UNMutableNotificationContent()
|
||||||
|
notificationContent.title = "pending" //String(localized: .signedNotificationTitle(appName: provenance.origin.displayName))
|
||||||
|
notificationContent.subtitle = "pending" //String(localized: .signedNotificationDescription(secretName: secret.name))
|
||||||
|
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
|
||||||
|
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
|
||||||
|
notificationContent.interruptionLevel = .timeSensitive
|
||||||
|
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
|
||||||
|
notificationContent.threadIdentifier = "\(secret.id)_\(provenance.hashValue)"
|
||||||
|
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
|
||||||
|
notificationContent.attachments = [attachment]
|
||||||
|
}
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
|
||||||
|
try? await notificationCenter.add(request)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func notify(update: Release, ignore: (@Sendable (Release) async -> Void)?) async {
|
func notify(update: Release, ignore: (@Sendable (Release) async -> Void)?) async {
|
||||||
await notificationDelegate.state.prepareForNotification(release: update, ignoreAction: ignore)
|
await notificationDelegate.state.prepareForNotification(release: update, ignoreAction: ignore)
|
||||||
let notificationCenter = UNUserNotificationCenter.current()
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
@@ -103,6 +122,10 @@ extension Notifier: SigningWitness {
|
|||||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func witness(pendingAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
|
||||||
|
await notify(pendingAccessTo: secret, from: store, by: provenance)
|
||||||
|
}
|
||||||
|
|
||||||
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
|
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
|
||||||
await notify(accessTo: secret, from: store, by: provenance)
|
await notify(accessTo: secret, from: store, by: provenance)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user