This commit is contained in:
Max Goedjen 2024-12-26 19:28:30 -05:00
parent 2dc317d398
commit 970e407e29
No known key found for this signature in database
13 changed files with 281 additions and 257 deletions

View File

@ -2,21 +2,19 @@ import Foundation
import Combine import Combine
/// Type eraser for SecretStore. /// Type eraser for SecretStore.
public class AnySecretStore: SecretStore { public class AnySecretStore: SecretStore, @unchecked Sendable {
let base: Any private let _isAvailable: @Sendable () -> Bool
private let _isAvailable: () -> Bool private let _id: @Sendable () -> UUID
private let _id: () -> UUID private let _name: @Sendable () -> String
private let _name: () -> String private let _secrets: @Sendable () -> [AnySecret]
private let _secrets: () -> [AnySecret] private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance) async throws -> Data
private let _sign: (Data, AnySecret, SigningRequestProvenance) async throws -> Data private let _verify: @Sendable (Data, Data, AnySecret) async throws -> Bool
private let _verify: (Data, Data, AnySecret) async throws -> Bool private let _existingPersistedAuthenticationContext: @Sendable (AnySecret) async -> PersistedAuthenticationContext?
private let _existingPersistedAuthenticationContext: (AnySecret) async -> PersistedAuthenticationContext? private let _persistAuthentication: @Sendable (AnySecret, TimeInterval) async throws -> Void
private let _persistAuthentication: (AnySecret, TimeInterval) async throws -> Void private let _reloadSecrets: @Sendable () async -> Void
private let _reloadSecrets: () async -> Void
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore { public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
base = secretStore
_isAvailable = { secretStore.isAvailable } _isAvailable = { secretStore.isAvailable }
_name = { secretStore.name } _name = { secretStore.name }
_id = { secretStore.id } _id = { secretStore.id }
@ -66,11 +64,11 @@ public class AnySecretStore: SecretStore {
} }
public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable { public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable, @unchecked Sendable {
private let _create: (String, Bool) async throws -> Void private let _create: @Sendable (String, Bool) async throws -> Void
private let _delete: (AnySecret) async throws -> Void private let _delete: @Sendable (AnySecret) async throws -> Void
private let _update: (AnySecret, String) async throws -> Void private let _update: @Sendable (AnySecret, String) async throws -> Void
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable { public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
_create = { try await secretStore.create(name: $0, requiresAuthentication: $1) } _create = { try await secretStore.create(name: $0, requiresAuthentication: $1) }

View File

@ -15,14 +15,14 @@ import Observation
/// Adds a non-type-erased SecretStore to the list. /// Adds a non-type-erased SecretStore to the list.
public func add<SecretStoreType: SecretStore>(store: SecretStoreType) { public func add<SecretStoreType: SecretStore>(store: SecretStoreType) {
addInternal(store: AnySecretStore(store)) stores.append(AnySecretStore(store))
} }
/// Adds a non-type-erased modifiable SecretStore. /// Adds a non-type-erased modifiable SecretStore.
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) { public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
let modifiable = AnySecretStoreModifiable(modifiable: store) let modifiable = AnySecretStoreModifiable(modifiable: store)
modifiableStore = modifiable modifiableStore = modifiable
addInternal(store: modifiable) stores.append(modifiable)
} }
/// A boolean describing whether there are any Stores available. /// A boolean describing whether there are any Stores available.
@ -35,14 +35,3 @@ import Observation
} }
} }
extension SecretStoreList {
private func addInternal(store: AnySecretStore) {
stores.append(store)
// store.objectWillChange.sink {
// self.objectWillChange.send()
// }.store(in: &cancellables)
}
}

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// Protocol describing a persisted authentication context. This is an authorization that can be reused for multiple access to a secret that requires authentication for a specific period of time. /// Protocol describing a persisted authentication context. This is an authorization that can be reused for multiple access to a secret that requires authentication for a specific period of time.
public protocol PersistedAuthenticationContext { public protocol PersistedAuthenticationContext: Sendable {
/// Whether the context remains valid. /// Whether the context remains valid.
var valid: Bool { get } var valid: Bool { get }
/// The date at which the authorization expires and the context becomes invalid. /// The date at which the authorization expires and the context becomes invalid.

View File

@ -2,7 +2,7 @@ import Foundation
import Combine import Combine
/// Manages access to Secrets, and performs signature operations on data using those Secrets. /// Manages access to Secrets, and performs signature operations on data using those Secrets.
public protocol SecretStore: Identifiable { public protocol SecretStore: Identifiable, Sendable {
associatedtype SecretType: Secret associatedtype SecretType: Secret

View File

@ -21,16 +21,15 @@ extension SecureEnclave {
} }
private let _secrets: Mutex<[Secret]> = .init([]) private let _secrets: Mutex<[Secret]> = .init([])
private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:] private let persistedAuthenticationContexts: Mutex<[Secret: PersistentAuthenticationContext]> = .init([:])
/// Initializes a Store. /// Initializes a Store.
public init() { public init() {
// FIXME: THIS Task {
// Task { for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
// for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { await reloadSecretsInternal(notifyAgent: false)
// await reloadSecretsInternal(notifyAgent: false) }
// } }
// }
loadSecrets() loadSecrets()
} }
@ -106,14 +105,15 @@ extension SecureEnclave {
} }
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
let context: LAContext let context: Mutex<LAContext>
if let existing = persistedAuthenticationContexts[secret], existing.valid { // if let existing = persistedAuthenticationContexts.withLock({ $0 })[secret], existing.valid {
context = existing.context // context = existing.context
} else { // } else {
let newContext = LAContext() let newContext = LAContext()
newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button") newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
context = newContext context = .init(newContext)
} // }
return try context.withLock { context in
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)") context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
let attributes = KeychainDictionary([ let attributes = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
@ -141,6 +141,7 @@ extension SecureEnclave {
} }
return signature as Data return signature as Data
} }
}
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool { public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
let context = LAContext() let context = LAContext()
@ -178,7 +179,7 @@ extension SecureEnclave {
} }
public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? { public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil } guard let persisted = persistedAuthenticationContexts.withLock({ $0 })[secret], persisted.valid else { return nil }
return persisted return persisted
} }
@ -197,9 +198,11 @@ extension SecureEnclave {
newContext.localizedReason = String(localized: "auth_context_persist_for_duration_unknown_\(secret.name)") newContext.localizedReason = String(localized: "auth_context_persist_for_duration_unknown_\(secret.name)")
} }
newContext.evaluatePolicy(LAPolicy.deviceOwnerAuthentication, localizedReason: newContext.localizedReason) { [weak self] success, _ in newContext.evaluatePolicy(LAPolicy.deviceOwnerAuthentication, localizedReason: newContext.localizedReason) { [weak self] success, _ in
guard success else { return } guard success, let self else { return }
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration) let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
self?.persistedAuthenticationContexts[secret] = context self.persistedAuthenticationContexts.withLock {
$0[secret] = context
}
} }
} }
@ -322,12 +325,12 @@ extension SecureEnclave {
extension SecureEnclave { extension SecureEnclave {
/// A context describing a persisted authentication. /// A context describing a persisted authentication.
private struct PersistentAuthenticationContext: PersistedAuthenticationContext { private final class PersistentAuthenticationContext: PersistedAuthenticationContext {
/// The Secret to persist authentication for. /// The Secret to persist authentication for.
let secret: Secret let secret: Secret
/// The LAContext used to authorize the persistent context. /// The LAContext used to authorize the persistent context.
let context: LAContext nonisolated(unsafe) let context: LAContext
/// An expiration date for the context. /// An expiration date for the context.
/// - Note - Monotonic time instead of Date() to prevent people setting the clock back. /// - Note - Monotonic time instead of Date() to prevent people setting the clock back.
let monotonicExpiration: UInt64 let monotonicExpiration: UInt64

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import Combine import Synchronization
import Observation
import Security import Security
import CryptoTokenKit import CryptoTokenKit
import LocalAuthentication import LocalAuthentication
@ -8,32 +9,54 @@ import SecretKit
extension SmartCard { extension SmartCard {
/// An implementation of Store backed by a Smart Card. /// An implementation of Store backed by a Smart Card.
public final class Store: SecretStore { @Observable public final class Store: SecretStore {
public var isAvailable: Bool {
_isAvailable.withLock { $0 }
}
private let _isAvailable: Mutex<Bool> = .init(false)
@Published public var isAvailable: Bool = false
public let id = UUID() public let id = UUID()
public private(set) var name = String(localized: "smart_card") public var name: String {
@Published public private(set) var secrets: [Secret] = [] _name.withLock { $0 }
private let watcher = TKTokenWatcher() }
private var tokenID: String? private let _name: Mutex<String> = .init(String(localized: "smart_card"))
public var secrets: [Secret] {
_secrets.withLock { $0 }
}
private let _secrets: Mutex<[Secret]> = .init([])
private let watcher: Mutex<TKTokenWatcher> = .init(TKTokenWatcher())
private let tokenID: Mutex<String?> = .init(nil)
/// Initializes a Store. /// Initializes a Store.
public init() { public init() {
tokenID = watcher.nonSecureEnclaveTokens.first tokenID.withLock { tokenID in
// FIXME: THIS watcher.withLock { watcher in
let id = watcher.nonSecureEnclaveTokens.first
watcher.setInsertionHandler { string in watcher.setInsertionHandler { string in
guard self.tokenID == nil else { return } // guard self.tokenID == nil else { return }
guard !string.contains("setoken") else { return } // guard !string.contains("setoken") else { return }
//
self.tokenID = string //// self.tokenID.withLock {
// DispatchQueue.main.async { //// $0 = string
// reload() //// }
// } // // DispatchQueue.main.async {
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string) // // reload()
// // }
// watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
}
tokenID = id
}
}
// FIXME: THIS
if let tokenID = tokenID.withLock({ $0 }) {
_isAvailable.withLock {
$0 = true
}
watcher.withLock {
$0.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
} }
if let tokenID = tokenID {
self.isAvailable = true
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
} }
loadSecrets() loadSecrets()
} }
@ -49,7 +72,7 @@ extension SmartCard {
} }
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
guard let tokenID = tokenID else { fatalError() } guard let tokenID = tokenID.withLock({ $0 }) else { fatalError() }
let context = LAContext() let context = LAContext()
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)") context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") context.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
@ -119,9 +142,13 @@ extension SmartCard {
extension SmartCard.Store { extension SmartCard.Store {
private func reloadSecretsInternal() { private func reloadSecretsInternal() {
self.isAvailable = self.tokenID != nil _isAvailable.withLock {
$0 = tokenID.withLock({ $0 }) != nil
}
let before = self.secrets let before = self.secrets
self.secrets.removeAll() self._secrets.withLock {
$0.removeAll()
}
self.loadSecrets() self.loadSecrets()
if self.secrets != before { if self.secrets != before {
NotificationCenter.default.post(name: .secretStoreReloaded, object: self) NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
@ -131,19 +158,23 @@ extension SmartCard.Store {
/// Resets the token ID and reloads secrets. /// Resets the token ID and reloads secrets.
/// - Parameter tokenID: The ID of the token that was removed. /// - Parameter tokenID: The ID of the token that was removed.
private func smartcardRemoved(for tokenID: String? = nil) { private func smartcardRemoved(for tokenID: String? = nil) {
self.tokenID = nil self.tokenID.withLock {
$0 = nil
}
reloadSecrets() reloadSecrets()
} }
/// Loads all secrets from the store. /// Loads all secrets from the store.
private func loadSecrets() { private func loadSecrets() {
guard let tokenID = tokenID else { return } guard let tokenID = tokenID.withLock({ $0 }) else { return }
let fallbackName = String(localized: "smart_card") let fallbackName = String(localized: "smart_card")
if let driverName = watcher.tokenInfo(forTokenID: tokenID)?.driverName { _name.withLock {
name = driverName if let driverName = watcher.withLock({ $0.tokenInfo(forTokenID: tokenID)?.driverName }) {
$0 = driverName
} else { } else {
name = fallbackName $0 = fallbackName
}
} }
let attributes = KeychainDictionary([ let attributes = KeychainDictionary([
@ -167,7 +198,9 @@ extension SmartCard.Store {
let publicKey = publicKeyAttributes[kSecValueData] as! Data let publicKey = publicKeyAttributes[kSecValueData] as! Data
return SmartCard.Secret(id: tokenID, name: name, algorithm: algorithm, keySize: keySize, publicKey: publicKey) return SmartCard.Secret(id: tokenID, name: name, algorithm: algorithm, keySize: keySize, publicKey: publicKey)
} }
secrets.append(contentsOf: wrapped) _secrets.withLock {
$0.append(contentsOf: wrapped)
}
} }
} }
@ -211,7 +244,7 @@ extension SmartCard.Store {
/// - Returns: The decrypted data. /// - Returns: The decrypted data.
/// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged. /// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
public func decrypt(data: Data, with secret: SecretType) throws -> Data { public func decrypt(data: Data, with secret: SecretType) throws -> Data {
guard let tokenID = tokenID else { fatalError() } guard let tokenID = tokenID.withLock({ $0 }) else { fatalError() }
let context = LAContext() let context = LAContext()
context.localizedReason = String(localized: "auth_context_request_decrypt_description_\(secret.name)") context.localizedReason = String(localized: "auth_context_request_decrypt_description_\(secret.name)")
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") context.localizedCancelTitle = String(localized: "auth_context_request_deny_button")

View File

@ -69,26 +69,24 @@ struct Secretive: App {
extension Secretive { extension Secretive {
private func reinstallAgent() { private func reinstallAgent() {
// justUpdatedChecker.check() justUpdatedChecker.check()
// FIXME: THIS Task {
// LaunchAgentController().install { await LaunchAgentController().install()
// // Wait a second for launchd to kick in (next runloop isn't enough). try? await Task.sleep(for: .seconds(1))
// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { agentStatusChecker.check()
// agentStatusChecker.check() if !agentStatusChecker.running {
// if !agentStatusChecker.running { forceLaunchAgent()
// forceLaunchAgent() }
// } }
// }
// }
} }
private func forceLaunchAgent() { private func forceLaunchAgent() {
// We've run setup, we didn't just update, launchd is just not doing it's thing. // We've run setup, we didn't just update, launchd is just not doing it's thing.
// Force a launch directly. // Force a launch directly.
// FIXME: THIS Task {
// LaunchAgentController().forceLaunch { _ in _ = await LaunchAgentController().forceLaunch()
// agentStatusChecker.check() agentStatusChecker.check()
// } }
} }
} }

View File

@ -18,69 +18,69 @@ extension Preview {
} }
extension Preview { //extension Preview {
//
class Store: SecretStore, ObservableObject { // class Store: SecretStore, ObservableObject {
//
let isAvailable = true // let isAvailable = true
let id = UUID() // let id = UUID()
var name: String { "Preview Store" } // var name: String { "Preview Store" }
@Published var secrets: [Secret] = [] // @Published var secrets: [Secret] = []
//
init(secrets: [Secret]) { // init(secrets: [Secret]) {
self.secrets.append(contentsOf: secrets) // self.secrets.append(contentsOf: secrets)
} // }
//
init(numberOfRandomSecrets: Int = 5) { // init(numberOfRandomSecrets: Int = 5) {
let new = (0..<numberOfRandomSecrets).map { Secret(name: String(describing: $0)) } // let new = (0..<numberOfRandomSecrets).map { Secret(name: String(describing: $0)) }
self.secrets.append(contentsOf: new) // self.secrets.append(contentsOf: new)
} // }
//
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data { // func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data {
return data // return data
} // }
//
func verify(signature data: Data, for signature: Data, with secret: Preview.Secret) throws -> Bool { // func verify(signature data: Data, for signature: Data, with secret: Preview.Secret) throws -> Bool {
true // true
} // }
//
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? { // func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
nil // nil
} // }
//
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws { // func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
} // }
//
func reloadSecrets() { // func reloadSecrets() {
} // }
//
} // }
//
class StoreModifiable: Store, SecretStoreModifiable { // class StoreModifiable: Store, SecretStoreModifiable {
override var name: String { "Modifiable Preview Store" } // override var name: String { "Modifiable Preview Store" }
//
func create(name: String, requiresAuthentication: Bool) throws { // func create(name: String, requiresAuthentication: Bool) throws {
} // }
//
func delete(secret: Preview.Secret) throws { // func delete(secret: Preview.Secret) throws {
} // }
//
func update(secret: Preview.Secret, name: String) throws { // func update(secret: Preview.Secret, name: String) throws {
} // }
} // }
} //}
//
extension Preview { //extension Preview {
//
static func storeList(stores: [Store] = [], modifiableStores: [StoreModifiable] = []) -> SecretStoreList { // static func storeList(stores: [Store] = [], modifiableStores: [StoreModifiable] = []) -> SecretStoreList {
let list = SecretStoreList() // let list = SecretStoreList()
for store in stores { // for store in stores {
list.add(store: store) // list.add(store: store)
} // }
for storeModifiable in modifiableStores { // for storeModifiable in modifiableStores {
list.add(store: storeModifiable) // list.add(store: storeModifiable)
} // }
return list // return list
} // }
//
} //}

View File

@ -193,41 +193,41 @@ extension ContentView {
} }
#if DEBUG //#if DEBUG
//
struct ContentView_Previews: PreviewProvider { //struct ContentView_Previews: PreviewProvider {
//
private static let storeList: SecretStoreList = { // private static let storeList: SecretStoreList = {
let list = SecretStoreList() // let list = SecretStoreList()
list.add(store: SecureEnclave.Store()) // list.add(store: SecureEnclave.Store())
list.add(store: SmartCard.Store()) // list.add(store: SmartCard.Store())
return list // return list
}() // }()
private static let agentStatusChecker = AgentStatusChecker() // private static let agentStatusChecker = AgentStatusChecker()
private static let justUpdatedChecker = JustUpdatedChecker() // private static let justUpdatedChecker = JustUpdatedChecker()
//
@State var hasRunSetup = false // @State var hasRunSetup = false
@State private var showingSetup = false // @State private var showingSetup = false
@State private var showingCreation = false // @State private var showingCreation = false
//
static var previews: some View { // static var previews: some View {
Group { // Group {
// Empty on modifiable and nonmodifiable // // Empty on modifiable and nonmodifiable
ContentView<PreviewUpdater, AgentStatusChecker>(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) // ContentView<PreviewUpdater, AgentStatusChecker>(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
.environmentObject(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)])) // .environmentObject(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
.environmentObject(PreviewUpdater()) // .environmentObject(PreviewUpdater())
.environmentObject(agentStatusChecker) // .environmentObject(agentStatusChecker)
//
// 5 items on modifiable and nonmodifiable // // 5 items on modifiable and nonmodifiable
ContentView<PreviewUpdater, AgentStatusChecker>(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) // ContentView<PreviewUpdater, AgentStatusChecker>(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
.environmentObject(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()])) // .environmentObject(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
.environmentObject(PreviewUpdater()) // .environmentObject(PreviewUpdater())
.environmentObject(agentStatusChecker) // .environmentObject(agentStatusChecker)
} // }
.environmentObject(agentStatusChecker) // .environmentObject(agentStatusChecker)
//
} // }
} //}
//
#endif //#endif

View File

@ -45,10 +45,11 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
} }
func save() { func save() {
// FIXME: THIS Task {
// try! store.create(name: name, requiresAuthentication: requiresAuthentication) try! await store.create(name: name, requiresAuthentication: requiresAuthentication)
showing = false showing = false
} }
}
} }
@ -231,19 +232,19 @@ struct NotificationView: View {
} }
#if DEBUG //#if DEBUG
//
struct CreateSecretView_Previews: PreviewProvider { //struct CreateSecretView_Previews: PreviewProvider {
//
static var previews: some View { // static var previews: some View {
Group { // Group {
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true)) // CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true))
AuthenticationView().environment(\.colorScheme, .dark) // AuthenticationView().environment(\.colorScheme, .dark)
AuthenticationView().environment(\.colorScheme, .light) // AuthenticationView().environment(\.colorScheme, .light)
NotificationView().environment(\.colorScheme, .dark) // NotificationView().environment(\.colorScheme, .dark)
NotificationView().environment(\.colorScheme, .light) // NotificationView().environment(\.colorScheme, .light)
} // }
} // }
} //}
//
#endif //#endif

View File

@ -49,9 +49,10 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
} }
func delete() { func delete() {
// FIXME: THIS Task {
// try! store.delete(secret: secret) try! await store.delete(secret: secret)
dismissalBlock(true) dismissalBlock(true)
} }
}
} }

View File

@ -44,8 +44,9 @@ struct RenameSecretView<StoreType: SecretStoreModifiable>: View {
} }
func rename() { func rename() {
// FIXME: THIS Task {
// try? await store.update(secret: secret, name: newName) try? await store.update(secret: secret, name: newName)
dismissalBlock(true) dismissalBlock(true)
} }
}
} }

View File

@ -47,12 +47,12 @@ struct SecretDetailView<SecretType: Secret>: View {
} }
#if DEBUG //#if DEBUG
//
struct SecretDetailView_Previews: PreviewProvider { //struct SecretDetailView_Previews: PreviewProvider {
static var previews: some View { // static var previews: some View {
SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0]) // SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0])
} // }
} //}
//
#endif //#endif