More progress.

This commit is contained in:
Max Goedjen 2020-03-08 20:03:40 -07:00
parent a9d7e7644e
commit 376f26ef38
No known key found for this signature in database
GPG Key ID: E58C21DD77B9B8E8
11 changed files with 130 additions and 65 deletions

View File

@ -9,16 +9,26 @@ public protocol Secret: Identifiable, Hashable {
public struct AnySecret: Secret { public struct AnySecret: Secret {
let base: Any
fileprivate let hashable: AnyHashable fileprivate let hashable: AnyHashable
fileprivate let _id: () -> AnyHashable fileprivate let _id: () -> AnyHashable
fileprivate let _name: () -> String fileprivate let _name: () -> String
fileprivate let _publicKey: () -> Data fileprivate let _publicKey: () -> Data
public init<T>(_ secret: T) where T: Secret { public init<T>(_ secret: T) where T: Secret {
self.hashable = secret if let secret = secret as? AnySecret {
_id = { secret.id as AnyHashable } base = secret.base
_name = { secret.name } hashable = secret.hashable
_publicKey = { secret.publicKey } _id = secret._id
_name = secret._name
_publicKey = secret._publicKey
} else {
base = secret as Any
self.hashable = secret
_id = { secret.id as AnyHashable }
_name = { secret.name }
_publicKey = { secret.publicKey }
}
} }
public var id: AnyHashable { public var id: AnyHashable {

View File

@ -1,13 +1,21 @@
import Combine import Combine
public protocol SecretStore: ObservableObject { public protocol SecretStore: ObservableObject, Identifiable {
associatedtype SecretType: Secret associatedtype SecretType: Secret
var isAvailable: Bool { get } var isAvailable: Bool { get }
var id: UUID { get }
var name: String { get } var name: String { get }
var secrets: [SecretType] { get } var secrets: [SecretType] { get }
func sign(data: Data, with secret: SecretType) throws -> Data func sign(data: Data, with secret: SecretType) throws -> Data
}
public protocol SecretStoreModifiable: SecretStore {
func create(name: String, requiresAuthentication: Bool) throws
func delete(secret: SecretType) throws func delete(secret: SecretType) throws
} }
@ -20,26 +28,30 @@ extension NSNotification.Name {
public class AnySecretStore: SecretStore { public class AnySecretStore: SecretStore {
fileprivate let base: Any let base: Any
fileprivate let _isAvailable: () -> Bool fileprivate let _isAvailable: () -> Bool
fileprivate let _id: () -> UUID
fileprivate let _name: () -> String fileprivate let _name: () -> String
fileprivate let _secrets: () -> [AnySecret] fileprivate let _secrets: () -> [AnySecret]
fileprivate let _sign: (Data, AnySecret) throws -> Data fileprivate let _sign: (Data, AnySecret) throws -> Data
fileprivate let _delete: (AnySecret) throws -> Void
public init<T>(_ secretStore: T) where T: SecretStore { public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
base = secretStore base = secretStore
_isAvailable = { secretStore.isAvailable } _isAvailable = { secretStore.isAvailable }
_name = { secretStore.name } _name = { secretStore.name }
_id = { secretStore.id }
_secrets = { secretStore.secrets.map { AnySecret($0) } } _secrets = { secretStore.secrets.map { AnySecret($0) } }
_sign = { try secretStore.sign(data: $0, with: $1 as! T.SecretType) } _sign = { try secretStore.sign(data: $0, with: $1 as! SecretStoreType.SecretType) }
_delete = { try secretStore.delete(secret: $0 as! T.SecretType) }
} }
public var isAvailable: Bool { public var isAvailable: Bool {
return _isAvailable() return _isAvailable()
} }
public var id: UUID {
return _id()
}
public var name: String { public var name: String {
return _name() return _name()
} }
@ -52,6 +64,23 @@ public class AnySecretStore: SecretStore {
try _sign(data, secret) try _sign(data, secret)
} }
}
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
fileprivate let _create: (String, Bool) throws -> Void
fileprivate let _delete: (AnySecret) throws -> Void
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
_create = { try secretStore.create(name: $0, requiresAuthentication: $1) }
_delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
super.init(secretStore)
}
public func create(name: String, requiresAuthentication: Bool) throws {
try _create(name, requiresAuthentication)
}
public func delete(secret: AnySecret) throws { public func delete(secret: AnySecret) throws {
try _delete(secret) try _delete(secret)
} }

View File

@ -0,0 +1,22 @@
import Foundation
import Combine
public class SecretStoreList: ObservableObject {
@Published public var stores: [AnySecretStore] = []
@Published public var modifiableStore: AnySecretStoreModifiable?
public init() {
}
public func add<SecretStoreType: SecretStore>(store: SecretStoreType) {
stores.append(AnySecretStore(store))
}
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
let modifiable = AnySecretStoreModifiable(modifiable: store)
modifiableStore = modifiable
stores.append(modifiable)
}
}

View File

@ -4,7 +4,7 @@ import CryptoTokenKit
extension SecureEnclave { extension SecureEnclave {
public class Store: SecretStore { public class Store: SecretStoreModifiable {
public var isAvailable: Bool { public var isAvailable: Bool {
// For some reason, as of build time, CryptoKit.SecureEnclave.isAvailable always returns false // For some reason, as of build time, CryptoKit.SecureEnclave.isAvailable always returns false
@ -12,6 +12,7 @@ extension SecureEnclave {
// Verify it with TKTokenWatcher manually. // Verify it with TKTokenWatcher manually.
return TKTokenWatcher().tokenIDs.contains("com.apple.setoken") return TKTokenWatcher().tokenIDs.contains("com.apple.setoken")
} }
public let id = UUID()
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave") public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
@Published public fileprivate(set) var secrets: [Secret] = [] @Published public fileprivate(set) var secrets: [Secret] = []

View File

@ -10,23 +10,24 @@ extension SmartCard {
// TODO: Read actual smart card name, eg "YubiKey 5c" // TODO: Read actual smart card name, eg "YubiKey 5c"
@Published public var isAvailable: Bool = false @Published public var isAvailable: Bool = false
public let id = UUID()
public let name = NSLocalizedString("Smart Card", comment: "Smart Card") public let name = NSLocalizedString("Smart Card", comment: "Smart Card")
@Published public fileprivate(set) var secrets: [Secret] = [] @Published public fileprivate(set) var secrets: [Secret] = []
fileprivate let watcher = TKTokenWatcher() fileprivate let watcher = TKTokenWatcher()
fileprivate var id: String? fileprivate var tokenID: String?
public init() { public init() {
id = watcher.tokenIDs.filter { !$0.contains("setoken") }.first tokenID = watcher.tokenIDs.filter { !$0.contains("setoken") }.first
watcher.setInsertionHandler { string in watcher.setInsertionHandler { string in
guard self.id == nil else { return } guard self.tokenID == nil else { return }
guard !string.contains("setoken") else { return } guard !string.contains("setoken") else { return }
self.id = string self.tokenID = string
self.reloadSecrets() self.reloadSecrets()
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string) self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
} }
if let id = id { if let tokenID = tokenID {
self.isAvailable = true self.isAvailable = true
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: id) self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
} }
loadSecrets() loadSecrets()
} }
@ -42,12 +43,12 @@ extension SmartCard {
} }
public func sign(data: Data, with secret: SecretType) throws -> Data { public func sign(data: Data, with secret: SecretType) throws -> Data {
guard let id = id else { fatalError() } guard let tokenID = tokenID else { fatalError() }
let attributes = [ let attributes = [
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecAttrApplicationLabel: secret.id as CFData, kSecAttrApplicationLabel: secret.id as CFData,
kSecAttrTokenID: id, kSecAttrTokenID: tokenID,
kSecReturnRef: true kSecReturnRef: true
] as CFDictionary ] as CFDictionary
var untyped: CFTypeRef? var untyped: CFTypeRef?
@ -73,23 +74,23 @@ extension SmartCard {
extension SmartCard.Store { extension SmartCard.Store {
fileprivate func smartcardRemoved(for tokenID: String? = nil) { fileprivate func smartcardRemoved(for tokenID: String? = nil) {
id = nil self.tokenID = nil
reloadSecrets() reloadSecrets()
} }
fileprivate func reloadSecrets() { fileprivate func reloadSecrets() {
DispatchQueue.main.async { DispatchQueue.main.async {
self.isAvailable = self.id != nil self.isAvailable = self.tokenID != nil
self.secrets.removeAll() self.secrets.removeAll()
self.loadSecrets() self.loadSecrets()
} }
} }
fileprivate func loadSecrets() { fileprivate func loadSecrets() {
guard let id = id else { return } guard let tokenID = tokenID else { return }
let attributes = [ let attributes = [
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrTokenID: id, kSecAttrTokenID: tokenID,
kSecReturnRef: true, kSecReturnRef: true,
kSecMatchLimit: kSecMatchLimitAll, kSecMatchLimit: kSecMatchLimitAll,
kSecReturnAttributes: true kSecReturnAttributes: true
@ -99,12 +100,12 @@ extension SmartCard.Store {
guard let typed = untyped as? [[CFString: Any]] else { return } guard let typed = untyped as? [[CFString: Any]] else { return }
let wrapped: [SmartCard.Secret] = typed.map { let wrapped: [SmartCard.Secret] = typed.map {
let name = $0[kSecAttrLabel] as? String ?? "Unnamed" let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
let id = $0[kSecAttrApplicationLabel] as! Data let tokenID = $0[kSecAttrApplicationLabel] as! Data
let publicKeyRef = $0[kSecValueRef] as! SecKey let publicKeyRef = $0[kSecValueRef] as! SecKey
let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)! let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any] let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
let publicKey = publicKeyAttributes[kSecValueData] as! Data let publicKey = publicKeyAttributes[kSecValueData] as! Data
return SmartCard.Secret(id: id, name: name, publicKey: publicKey) return SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey)
} }
secrets.append(contentsOf: wrapped) secrets.append(contentsOf: wrapped)
} }

View File

@ -26,6 +26,7 @@
50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; }; 50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; };
50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; }; 50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; };
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; }; 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
5068389E241471CD00F55094 /* SecretStoreList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5068389D241471CD00F55094 /* SecretStoreList.swift */; };
506AB87E2412334700335D91 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 506AB87E2412334700335D91 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; }; 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; }; 5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; };
@ -172,6 +173,7 @@
50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = "<group>"; }; 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = "<group>"; };
50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = "<group>"; }; 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = "<group>"; };
50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = "<group>"; }; 50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = "<group>"; };
5068389D241471CD00F55094 /* SecretStoreList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStoreList.swift; sourceTree = "<group>"; };
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; }; 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; };
5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = "<group>"; }; 5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = "<group>"; };
5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = "<group>"; }; 5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = "<group>"; };
@ -327,6 +329,7 @@
50617DAA23FCE4AB0099B055 /* SecretKit.h */, 50617DAA23FCE4AB0099B055 /* SecretKit.h */,
50617DCA23FCECA10099B055 /* Secret.swift */, 50617DCA23FCECA10099B055 /* Secret.swift */,
50617DC623FCE4EA0099B055 /* SecretStore.swift */, 50617DC623FCE4EA0099B055 /* SecretStore.swift */,
5068389D241471CD00F55094 /* SecretStoreList.swift */,
5099A02C23FE56D70062B6F2 /* Common */, 5099A02C23FE56D70062B6F2 /* Common */,
50617DCC23FCECEE0099B055 /* SecureEnclave */, 50617DCC23FCECEE0099B055 /* SecureEnclave */,
5099A02523FE34DE0062B6F2 /* SmartCard */, 5099A02523FE34DE0062B6F2 /* SmartCard */,
@ -731,6 +734,7 @@
50617DC923FCE50E0099B055 /* SecureEnclaveStore.swift in Sources */, 50617DC923FCE50E0099B055 /* SecureEnclaveStore.swift in Sources */,
50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */, 50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */,
50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */, 50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */,
5068389E241471CD00F55094 /* SecretStoreList.swift in Sources */,
5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */, 5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */,
5099A02B23FE352C0062B6F2 /* SmartCardSecret.swift in Sources */, 5099A02B23FE352C0062B6F2 /* SmartCardSecret.swift in Sources */,
50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */, 50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */,

View File

@ -7,15 +7,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow! var window: NSWindow!
@IBOutlet var toolbar: NSToolbar! @IBOutlet var toolbar: NSToolbar!
let secureEnclave = SecureEnclave.Store() let storeList: SecretStoreList = {
let smartCard = SmartCard.Store() let list = SecretStoreList()
lazy var allStores: [AnySecretStore] = { list.add(store: SecureEnclave.Store())
[AnySecretStore(secureEnclave), AnySecretStore(smartCard)] list.add(store: SmartCard.Store())
return list
}() }()
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView(secureEnclave: secureEnclave, smartCard: smartCard) let contentView = ContentView(storeList: storeList)
// Create the window and set the content view. // Create the window and set the content view.
window = NSWindow( window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
@ -27,7 +28,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
window.titleVisibility = .hidden window.titleVisibility = .hidden
window.toolbar = toolbar window.toolbar = toolbar
if secureEnclave.isAvailable { if storeList.modifiableStore?.isAvailable ?? false {
let plus = NSTitlebarAccessoryViewController() let plus = NSTitlebarAccessoryViewController()
plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:))) plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:)))
plus.layoutAttribute = .right plus.layoutAttribute = .right
@ -38,7 +39,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@IBAction func add(sender: AnyObject?) { @IBAction func add(sender: AnyObject?) {
var addWindow: NSWindow! var addWindow: NSWindow!
let addView = CreateSecretView(store: secureEnclave) { let addView = CreateSecretView(store: storeList.modifiableStore!) {
self.window.endSheet(addWindow) self.window.endSheet(addWindow)
} }
addWindow = NSWindow( addWindow = NSWindow(

View File

@ -3,57 +3,53 @@ import SecretKit
struct ContentView: View { struct ContentView: View {
@ObservedObject var secureEnclave: SecureEnclave.Store @ObservedObject var storeList: SecretStoreList
@ObservedObject var smartCard: SmartCard.Store @State var active: AnySecret.ID?
@State var active: Data?
@State var showingDeletion = false @State var showingDeletion = false
@State var deletingSecret: SecureEnclave.Secret? @State var deletingSecret: AnySecret?
var body: some View { var body: some View {
NavigationView { NavigationView {
List(selection: $active) { List(selection: $active) {
if secureEnclave.isAvailable { ForEach(storeList.stores) { store in
Section(header: Text(secureEnclave.name)) { if store.isAvailable {
ForEach(secureEnclave.secrets) { secret in Section(header: Text(store.name)) {
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) { ForEach(store.secrets) { secret in
Text(secret.name) NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
}.contextMenu { Text(secret.name)
Button(action: { self.delete(secret: secret) }) { }.contextMenu {
Text("Delete") if store is AnySecretStoreModifiable {
Button(action: { self.delete(secret: secret) }) {
Text("Delete")
}
}
} }
} }
} }
} }
} }
if smartCard.isAvailable {
Section(header: Text(smartCard.name)) {
ForEach(smartCard.secrets) { secret in
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
Text(secret.name)
}
}
}
}
}.onAppear { }.onAppear {
self.active = self.secureEnclave.secrets.first?.id ?? self.smartCard.secrets.first?.id self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id
} }
.listStyle(SidebarListStyle()) .listStyle(SidebarListStyle())
.frame(minWidth: 100, idealWidth: 240) .frame(minWidth: 100, idealWidth: 240)
} }
.navigationViewStyle(DoubleColumnNavigationViewStyle()) .navigationViewStyle(DoubleColumnNavigationViewStyle())
.sheet(isPresented: $showingDeletion) { .sheet(isPresented: $showingDeletion) {
DeleteSecretView(secret: self.deletingSecret!, store: self.secureEnclave) { if self.storeList.modifiableStore != nil {
self.showingDeletion = false DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) {
self.showingDeletion = false
}
} }
} }
} }
func delete(secret: SecureEnclave.Secret) { func delete<SecretType: Secret>(secret: SecretType) {
deletingSecret = secret deletingSecret = AnySecret(secret)
showingDeletion = true self.showingDeletion = true
} }
} }

View File

@ -3,7 +3,7 @@ import SecretKit
struct CreateSecretView: View { struct CreateSecretView: View {
@ObservedObject var store: SecureEnclave.Store @ObservedObject var store: AnySecretStoreModifiable
@State var name = "" @State var name = ""
@State var requiresAuthentication = true @State var requiresAuthentication = true

View File

@ -1,16 +1,16 @@
import SwiftUI import SwiftUI
import SecretKit import SecretKit
struct DeleteSecretView: View { struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
let secret: SecureEnclave.Secret let secret: StoreType.SecretType
@ObservedObject var store: SecureEnclave.Store @ObservedObject var store: StoreType
@State var confirm = "" @State var confirm = ""
fileprivate var dismissalBlock: () -> () fileprivate var dismissalBlock: () -> ()
init(secret: SecureEnclave.Secret, store: SecureEnclave.Store, dismissalBlock: @escaping () -> ()) { init(secret: StoreType.SecretType, store: StoreType, dismissalBlock: @escaping () -> ()) {
self.secret = secret self.secret = secret
self.store = store self.store = store
self.dismissalBlock = dismissalBlock self.dismissalBlock = dismissalBlock

View File

@ -20,6 +20,7 @@ extension Preview {
class Store: SecretStore, ObservableObject { class Store: SecretStore, ObservableObject {
let isAvailable = true let isAvailable = true
let id = UUID()
let name = "Preview Store" let name = "Preview Store"
@Published var secrets: [Secret] = [] @Published var secrets: [Secret] = []