diff --git a/SecretAgent/Agent.swift b/SecretAgent/Agent.swift index c00317a..fa94204 100644 --- a/SecretAgent/Agent.swift +++ b/SecretAgent/Agent.swift @@ -4,14 +4,14 @@ import OSLog import SecretKit import SecretAgentKit -class Agent { +class Agent { - fileprivate let store: StoreType + fileprivate let storeList: SecretStoreList fileprivate let notifier: Notifier - public init(store: StoreType, notifier: Notifier) { + public init(storeList: SecretStoreList, notifier: Notifier) { os_log(.debug, "Agent is running") - self.store = store + self.storeList = storeList self.notifier = notifier } @@ -57,17 +57,18 @@ extension Agent { extension Agent { func identities() throws -> Data { - var count = UInt32(store.secrets.count).bigEndian + let secrets = storeList.stores.flatMap(\.secrets) + var count = UInt32(secrets.count).bigEndian let countData = Data(bytes: &count, count: UInt32.bitWidth/8) var keyData = Data() let writer = OpenSSHKeyWriter() - for secret in store.secrets { + for secret in secrets { let keyBlob = writer.data(secret: secret) keyData.append(writer.lengthAndData(of: keyBlob)) let curveData = OpenSSHKeyWriter.Constants.curveType.data(using: .utf8)! keyData.append(writer.lengthAndData(of: curveData)) } - os_log(.debug, "Agent enumerated %@ identities", store.secrets.count as NSNumber) + os_log(.debug, "Agent enumerated %@ identities", secrets.count as NSNumber) return countData + keyData } @@ -75,10 +76,16 @@ extension Agent { let reader = OpenSSHReader(data: data) let writer = OpenSSHKeyWriter() let hash = try reader.readNextChunk() - let matching = store.secrets.filter { secret in - hash == writer.data(secret: secret) + let matching = storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in + let allMatching = store.secrets.filter { secret in + hash == writer.data(secret: secret) + } + if let matching = allMatching.first { + return (store, matching) + } + return nil } - guard let secret = matching.first else { + guard let (store, secret) = matching.first else { throw AgentError.noMatchingKey } let dataToSign = try reader.readNextChunk() diff --git a/SecretAgent/AppDelegate.swift b/SecretAgent/AppDelegate.swift index 5421581..4881158 100644 --- a/SecretAgent/AppDelegate.swift +++ b/SecretAgent/AppDelegate.swift @@ -5,10 +5,15 @@ import OSLog @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { - let store = SecureEnclave.Store() + let storeList: SecretStoreList = { + let list = SecretStoreList() + list.add(store: SecureEnclave.Store()) + list.add(store: SmartCard.Store()) + return list + }() let notifier = Notifier() lazy var agent: Agent = { - Agent(store: store, notifier: notifier) + Agent(storeList: storeList, notifier: notifier) }() lazy var socketController: SocketController = { let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String diff --git a/SecretKit/Common/Erasers/AnySecret.swift b/SecretKit/Common/Erasers/AnySecret.swift new file mode 100644 index 0000000..11587bc --- /dev/null +++ b/SecretKit/Common/Erasers/AnySecret.swift @@ -0,0 +1,48 @@ +import Foundation + +public struct AnySecret: Secret { + + let base: Any + fileprivate let hashable: AnyHashable + fileprivate let _id: () -> AnyHashable + fileprivate let _name: () -> String + fileprivate let _publicKey: () -> Data + + public init(_ secret: T) where T: Secret { + if let secret = secret as? AnySecret { + base = secret.base + hashable = secret.hashable + _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 { + return _id() + } + + public var name: String { + return _name() + } + + public var publicKey: Data { + return _publicKey() + } + + public static func == (lhs: AnySecret, rhs: AnySecret) -> Bool { + lhs.hashable == rhs.hashable + } + + public func hash(into hasher: inout Hasher) { + hashable.hash(into: &hasher) + } + +} + diff --git a/SecretKit/Common/Erasers/AnySecretStore.swift b/SecretKit/Common/Erasers/AnySecretStore.swift new file mode 100644 index 0000000..322c31b --- /dev/null +++ b/SecretKit/Common/Erasers/AnySecretStore.swift @@ -0,0 +1,67 @@ +import Foundation +import Combine + +public class AnySecretStore: SecretStore { + + let base: Any + fileprivate let _isAvailable: () -> Bool + fileprivate let _id: () -> UUID + fileprivate let _name: () -> String + fileprivate let _secrets: () -> [AnySecret] + fileprivate let _sign: (Data, AnySecret) throws -> Data + fileprivate var sink: AnyCancellable? + + public init(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore { + base = secretStore + _isAvailable = { secretStore.isAvailable } + _name = { secretStore.name } + _id = { secretStore.id } + _secrets = { secretStore.secrets.map { AnySecret($0) } } + _sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType) } + sink = secretStore.objectWillChange.sink { _ in + self.objectWillChange.send() + } + } + + public var isAvailable: Bool { + return _isAvailable() + } + + public var id: UUID { + return _id() + } + + public var name: String { + return _name() + } + + public var secrets: [AnySecret] { + return _secrets() + } + + public func sign(data: Data, with secret: AnySecret) throws -> Data { + try _sign(data, secret) + } + +} + +public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable { + + fileprivate let _create: (String, Bool) throws -> Void + fileprivate let _delete: (AnySecret) throws -> Void + + public init(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 { + try _delete(secret) + } + +} diff --git a/SecretKit/Common/OpenSSHKeyWriter.swift b/SecretKit/Common/OpenSSH/OpenSSHKeyWriter.swift similarity index 100% rename from SecretKit/Common/OpenSSHKeyWriter.swift rename to SecretKit/Common/OpenSSH/OpenSSHKeyWriter.swift diff --git a/SecretAgentKit/OpenSSHReader.swift b/SecretKit/Common/OpenSSH/OpenSSHReader.swift similarity index 100% rename from SecretAgentKit/OpenSSHReader.swift rename to SecretKit/Common/OpenSSH/OpenSSHReader.swift diff --git a/SecretKit/Common/SecretStoreList.swift b/SecretKit/Common/SecretStoreList.swift new file mode 100644 index 0000000..7becd40 --- /dev/null +++ b/SecretKit/Common/SecretStoreList.swift @@ -0,0 +1,35 @@ +import Foundation +import Combine + +public class SecretStoreList: ObservableObject { + + @Published public var stores: [AnySecretStore] = [] + @Published public var modifiableStore: AnySecretStoreModifiable? + fileprivate var sinks: [AnyCancellable] = [] + + public init() { + } + + public func add(store: SecretStoreType) { + addInternal(store: AnySecretStore(store)) + } + + public func add(store: SecretStoreType) { + let modifiable = AnySecretStoreModifiable(modifiable: store) + modifiableStore = modifiable + addInternal(store: modifiable) + } + +} + +extension SecretStoreList { + + fileprivate func addInternal(store: AnySecretStore) { + stores.append(store) + let sink = store.objectWillChange.sink { + self.objectWillChange.send() + } + sinks.append(sink) + } + +} diff --git a/SecretKit/SecretStore.swift b/SecretKit/SecretStore.swift index d944836..ef0488d 100644 --- a/SecretKit/SecretStore.swift +++ b/SecretKit/SecretStore.swift @@ -1,12 +1,21 @@ import Combine -public protocol SecretStore: ObservableObject { +public protocol SecretStore: ObservableObject, Identifiable { associatedtype SecretType: Secret + + var isAvailable: Bool { get } + var id: UUID { get } var name: String { get } var secrets: [SecretType] { get } 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 } diff --git a/SecretKit/SecureEnclave/SecureEnclaveStore.swift b/SecretKit/SecureEnclave/SecureEnclaveStore.swift index 8e9ac2e..7c56e00 100644 --- a/SecretKit/SecureEnclave/SecureEnclaveStore.swift +++ b/SecretKit/SecureEnclave/SecureEnclaveStore.swift @@ -1,10 +1,18 @@ import Foundation import Security +import CryptoTokenKit extension SecureEnclave { - public class Store: SecretStore { + public class Store: SecretStoreModifiable { + public var isAvailable: Bool { + // For some reason, as of build time, CryptoKit.SecureEnclave.isAvailable always returns false + // error msg "Received error sending GET UNIQUE DEVICE command" + // Verify it with TKTokenWatcher manually. + return TKTokenWatcher().tokenIDs.contains("com.apple.setoken") + } + public let id = UUID() public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave") @Published public fileprivate(set) var secrets: [Secret] = [] diff --git a/SecretKit/SmartCard/SmartCardStore.swift b/SecretKit/SmartCard/SmartCardStore.swift index 2f7361a..b1ed962 100644 --- a/SecretKit/SmartCard/SmartCardStore.swift +++ b/SecretKit/SmartCard/SmartCardStore.swift @@ -9,19 +9,25 @@ extension SmartCard { public class Store: SecretStore { // TODO: Read actual smart card name, eg "YubiKey 5c" + @Published public var isAvailable: Bool = false + public let id = UUID() public let name = NSLocalizedString("Smart Card", comment: "Smart Card") @Published public fileprivate(set) var secrets: [Secret] = [] fileprivate let watcher = TKTokenWatcher() - fileprivate var id: String? + fileprivate var tokenID: String? public init() { - id = watcher.tokenIDs.filter { !$0.contains("setoken") }.first + tokenID = watcher.tokenIDs.filter { !$0.contains("setoken") }.first watcher.setInsertionHandler { string in - guard self.id == nil else { return } + guard self.tokenID == nil else { return } guard !string.contains("setoken") else { return } - self.id = string - self.secrets.removeAll() - self.loadSecrets() + self.tokenID = string + self.reloadSecrets() + self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string) + } + if let tokenID = tokenID { + self.isAvailable = true + self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID) } loadSecrets() } @@ -37,12 +43,12 @@ extension SmartCard { } public func sign(data: Data, with secret: SecretType) throws -> Data { - guard let id = id else { fatalError() } + guard let tokenID = tokenID else { fatalError() } let attributes = [ kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrApplicationLabel: secret.id as CFData, - kSecAttrTokenID: id, + kSecAttrTokenID: tokenID, kSecReturnRef: true ] as CFDictionary var untyped: CFTypeRef? @@ -67,11 +73,26 @@ extension SmartCard { extension SmartCard.Store { + fileprivate func smartcardRemoved(for tokenID: String? = nil) { + self.tokenID = nil + reloadSecrets() + } + + fileprivate func reloadSecrets() { + DispatchQueue.main.async { + self.isAvailable = self.tokenID != nil + self.secrets.removeAll() + self.loadSecrets() + } + } + fileprivate func loadSecrets() { - guard let id = id else { return } + guard let tokenID = tokenID else { return } let attributes = [ kSecClass: kSecClassKey, - kSecAttrTokenID: id, + kSecAttrKeyType: kSecAttrKeyTypeEC, + kSecAttrKeySizeInBits: 256, + kSecAttrTokenID: tokenID, kSecReturnRef: true, kSecMatchLimit: kSecMatchLimitAll, kSecReturnAttributes: true @@ -81,12 +102,12 @@ extension SmartCard.Store { guard let typed = untyped as? [[CFString: Any]] else { return } let wrapped: [SmartCard.Secret] = typed.map { 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 publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)! let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any] 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) } diff --git a/Secretive.xcodeproj/project.pbxproj b/Secretive.xcodeproj/project.pbxproj index 35e10cf..28f48c4 100644 --- a/Secretive.xcodeproj/project.pbxproj +++ b/Secretive.xcodeproj/project.pbxproj @@ -26,6 +26,9 @@ 50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; }; 50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; }; 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; }; + 5068389E241471CD00F55094 /* SecretStoreList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5068389D241471CD00F55094 /* SecretStoreList.swift */; }; + 506838A12415EA5600F55094 /* AnySecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A02415EA5600F55094 /* AnySecret.swift */; }; + 506838A32415EA5D00F55094 /* AnySecretStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A22415EA5D00F55094 /* AnySecretStore.swift */; }; 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 */; }; 5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; }; @@ -172,6 +175,9 @@ 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = ""; }; 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = ""; }; 50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = ""; }; + 5068389D241471CD00F55094 /* SecretStoreList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStoreList.swift; sourceTree = ""; }; + 506838A02415EA5600F55094 /* AnySecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecret.swift; sourceTree = ""; }; + 506838A22415EA5D00F55094 /* AnySecretStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecretStore.swift; sourceTree = ""; }; 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = ""; }; 5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = ""; }; 5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = ""; }; @@ -193,7 +199,7 @@ 50A3B79D24026B9900D209EA /* SocketController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketController.swift; sourceTree = ""; }; 50A3B79F24026B9900D209EA /* Agent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Agent.swift; sourceTree = ""; }; 50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = ""; }; - 50C385A2240789E600AF2719 /* OpenSSHReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OpenSSHReader.swift; path = SecretAgentKit/OpenSSHReader.swift; sourceTree = SOURCE_ROOT; }; + 50C385A2240789E600AF2719 /* OpenSSHReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OpenSSHReader.swift; path = SecretKit/Common/OpenSSH/OpenSSHReader.swift; sourceTree = SOURCE_ROOT; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = ""; }; 50C385A8240B636500AF2719 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -354,6 +360,24 @@ path = SecureEnclave; sourceTree = ""; }; + 5068389F2415EA4F00F55094 /* Erasers */ = { + isa = PBXGroup; + children = ( + 506838A02415EA5600F55094 /* AnySecret.swift */, + 506838A22415EA5D00F55094 /* AnySecretStore.swift */, + ); + path = Erasers; + sourceTree = ""; + }; + 506838A42415EA6800F55094 /* OpenSSH */ = { + isa = PBXGroup; + children = ( + 5099A02D23FE56E10062B6F2 /* OpenSSHKeyWriter.swift */, + 50C385A2240789E600AF2719 /* OpenSSHReader.swift */, + ); + path = OpenSSH; + sourceTree = ""; + }; 5099A02523FE34DE0062B6F2 /* SmartCard */ = { isa = PBXGroup; children = ( @@ -367,8 +391,9 @@ 5099A02C23FE56D70062B6F2 /* Common */ = { isa = PBXGroup; children = ( - 5099A02D23FE56E10062B6F2 /* OpenSSHKeyWriter.swift */, - 50C385A2240789E600AF2719 /* OpenSSHReader.swift */, + 5068389F2415EA4F00F55094 /* Erasers */, + 506838A42415EA6800F55094 /* OpenSSH */, + 5068389D241471CD00F55094 /* SecretStoreList.swift */, ); path = Common; sourceTree = ""; @@ -729,8 +754,11 @@ 50617DCB23FCECA10099B055 /* Secret.swift in Sources */, 5099A02E23FE56E10062B6F2 /* OpenSSHKeyWriter.swift in Sources */, 50617DC923FCE50E0099B055 /* SecureEnclaveStore.swift in Sources */, + 506838A32415EA5D00F55094 /* AnySecretStore.swift in Sources */, 50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */, 50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */, + 5068389E241471CD00F55094 /* SecretStoreList.swift in Sources */, + 506838A12415EA5600F55094 /* AnySecret.swift in Sources */, 5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */, 5099A02B23FE352C0062B6F2 /* SmartCardSecret.swift in Sources */, 50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */, diff --git a/Secretive/AppDelegate.swift b/Secretive/AppDelegate.swift index 746dd9d..300b805 100644 --- a/Secretive/AppDelegate.swift +++ b/Secretive/AppDelegate.swift @@ -7,12 +7,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! @IBOutlet var toolbar: NSToolbar! - let secureEnclave = SecureEnclave.Store() - let smartCard = SmartCard.Store() + let storeList: SecretStoreList = { + let list = SecretStoreList() + list.add(store: SecureEnclave.Store()) + list.add(store: SmartCard.Store()) + return list + }() func applicationDidFinishLaunching(_ aNotification: Notification) { - let contentView = ContentView(store: secureEnclave) + let contentView = ContentView(storeList: storeList) // Create the window and set the content view. window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), @@ -24,16 +28,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { window.makeKeyAndOrderFront(nil) window.titleVisibility = .hidden window.toolbar = toolbar - let plus = NSTitlebarAccessoryViewController() - plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:))) - plus.layoutAttribute = .right - window.addTitlebarAccessoryViewController(plus) + if storeList.modifiableStore?.isAvailable ?? false { + let plus = NSTitlebarAccessoryViewController() + plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:))) + plus.layoutAttribute = .right + window.addTitlebarAccessoryViewController(plus) + } runSetupIfNeeded() } @IBAction func add(sender: AnyObject?) { var addWindow: NSWindow! - let addView = CreateSecretView(store: secureEnclave) { + let addView = CreateSecretView(store: storeList.modifiableStore!) { self.window.endSheet(addWindow) } addWindow = NSWindow( diff --git a/Secretive/ContentView.swift b/Secretive/ContentView.swift index f7bc4cc..951dfa6 100644 --- a/Secretive/ContentView.swift +++ b/Secretive/ContentView.swift @@ -3,45 +3,53 @@ import SecretKit struct ContentView: View { - @ObservedObject var store: SecureEnclave.Store - @State var active: SecureEnclave.Secret.ID? + @ObservedObject var storeList: SecretStoreList + @State var active: AnySecret.ID? @State var showingDeletion = false - @State var deletingSecret: SecureEnclave.Secret? + @State var deletingSecret: AnySecret? var body: some View { NavigationView { List(selection: $active) { - Section(header: Text(store.name)) { - ForEach(store.secrets) { secret in - NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) { - Text(secret.name) - }.contextMenu { - Button(action: { self.delete(secret: secret) }) { - Text("Delete") + ForEach(storeList.stores) { store in + if store.isAvailable { + Section(header: Text(store.name)) { + ForEach(store.secrets) { secret in + NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) { + Text(secret.name) + }.contextMenu { + if store is AnySecretStoreModifiable { + Button(action: { self.delete(secret: secret) }) { + Text("Delete") + } + } + } } } } } }.onAppear { - self.active = self.store.secrets.first?.id + self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id } .listStyle(SidebarListStyle()) .frame(minWidth: 100, idealWidth: 240) } .navigationViewStyle(DoubleColumnNavigationViewStyle()) .sheet(isPresented: $showingDeletion) { - DeleteSecretView(secret: self.deletingSecret!, store: self.store) { - self.showingDeletion = false + if self.storeList.modifiableStore != nil { + DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) { + self.showingDeletion = false + } } } } - func delete(secret: SecureEnclave.Secret) { - deletingSecret = secret - showingDeletion = true + func delete(secret: SecretType) { + deletingSecret = AnySecret(secret) + self.showingDeletion = true } } diff --git a/Secretive/CreateSecretView.swift b/Secretive/CreateSecretView.swift index e9cdb6a..88c1bd2 100644 --- a/Secretive/CreateSecretView.swift +++ b/Secretive/CreateSecretView.swift @@ -3,7 +3,7 @@ import SecretKit struct CreateSecretView: View { - @ObservedObject var store: SecureEnclave.Store + @ObservedObject var store: AnySecretStoreModifiable @State var name = "" @State var requiresAuthentication = true diff --git a/Secretive/DeleteSecretView.swift b/Secretive/DeleteSecretView.swift index 6004096..0efe7a1 100644 --- a/Secretive/DeleteSecretView.swift +++ b/Secretive/DeleteSecretView.swift @@ -1,16 +1,16 @@ import SwiftUI import SecretKit -struct DeleteSecretView: View { +struct DeleteSecretView: View { - let secret: SecureEnclave.Secret - @ObservedObject var store: SecureEnclave.Store + let secret: StoreType.SecretType + @ObservedObject var store: StoreType @State var confirm = "" fileprivate var dismissalBlock: () -> () - init(secret: SecureEnclave.Secret, store: SecureEnclave.Store, dismissalBlock: @escaping () -> ()) { + init(secret: StoreType.SecretType, store: StoreType, dismissalBlock: @escaping () -> ()) { self.secret = secret self.store = store self.dismissalBlock = dismissalBlock diff --git a/Secretive/Preview Content/PreviewStore.swift b/Secretive/Preview Content/PreviewStore.swift index cfc41d0..6567697 100644 --- a/Secretive/Preview Content/PreviewStore.swift +++ b/Secretive/Preview Content/PreviewStore.swift @@ -19,6 +19,8 @@ extension Preview { class Store: SecretStore, ObservableObject { + let isAvailable = true + let id = UUID() let name = "Preview Store" @Published var secrets: [Secret] = []