From 376f26ef38cf489595ffebb7e9086b49096aaffc Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 8 Mar 2020 20:03:40 -0700 Subject: [PATCH] More progress. --- SecretKit/Secret.swift | 18 +++++-- SecretKit/SecretStore.swift | 43 +++++++++++++--- SecretKit/SecretStoreList.swift | 22 ++++++++ .../SecureEnclave/SecureEnclaveStore.swift | 3 +- SecretKit/SmartCard/SmartCardStore.swift | 29 +++++------ Secretive.xcodeproj/project.pbxproj | 4 ++ Secretive/AppDelegate.swift | 15 +++--- Secretive/ContentView.swift | 50 +++++++++---------- Secretive/CreateSecretView.swift | 2 +- Secretive/DeleteSecretView.swift | 8 +-- Secretive/Preview Content/PreviewStore.swift | 1 + 11 files changed, 130 insertions(+), 65 deletions(-) create mode 100644 SecretKit/SecretStoreList.swift diff --git a/SecretKit/Secret.swift b/SecretKit/Secret.swift index 49a6536..2e486d4 100644 --- a/SecretKit/Secret.swift +++ b/SecretKit/Secret.swift @@ -9,16 +9,26 @@ public protocol Secret: Identifiable, Hashable { 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 { - self.hashable = secret - _id = { secret.id as AnyHashable } - _name = { secret.name } - _publicKey = { secret.publicKey } + 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 { diff --git a/SecretKit/SecretStore.swift b/SecretKit/SecretStore.swift index a2f6615..89130d6 100644 --- a/SecretKit/SecretStore.swift +++ b/SecretKit/SecretStore.swift @@ -1,13 +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 } @@ -20,26 +28,30 @@ extension NSNotification.Name { public class AnySecretStore: SecretStore { - fileprivate let base: Any + 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 let _delete: (AnySecret) throws -> Void - public init(_ secretStore: T) where T: SecretStore { + 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 as! T.SecretType) } - _delete = { try secretStore.delete(secret: $0 as! T.SecretType) } + _sign = { try secretStore.sign(data: $0, with: $1 as! SecretStoreType.SecretType) } } - + public var isAvailable: Bool { return _isAvailable() } + public var id: UUID { + return _id() + } + public var name: String { return _name() } @@ -52,6 +64,23 @@ public class AnySecretStore: SecretStore { 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/SecretStoreList.swift b/SecretKit/SecretStoreList.swift new file mode 100644 index 0000000..5810b97 --- /dev/null +++ b/SecretKit/SecretStoreList.swift @@ -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(store: SecretStoreType) { + stores.append(AnySecretStore(store)) + } + + public func add(store: SecretStoreType) { + let modifiable = AnySecretStoreModifiable(modifiable: store) + modifiableStore = modifiable + stores.append(modifiable) + } + +} diff --git a/SecretKit/SecureEnclave/SecureEnclaveStore.swift b/SecretKit/SecureEnclave/SecureEnclaveStore.swift index 4eb3bbd..7c56e00 100644 --- a/SecretKit/SecureEnclave/SecureEnclaveStore.swift +++ b/SecretKit/SecureEnclave/SecureEnclaveStore.swift @@ -4,7 +4,7 @@ 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 @@ -12,6 +12,7 @@ extension SecureEnclave { // 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 c95ea8a..0adfb0d 100644 --- a/SecretKit/SmartCard/SmartCardStore.swift +++ b/SecretKit/SmartCard/SmartCardStore.swift @@ -10,23 +10,24 @@ extension SmartCard { // 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.tokenID = string self.reloadSecrets() self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string) } - if let id = id { + if let tokenID = tokenID { self.isAvailable = true - self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: id) + self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID) } loadSecrets() } @@ -42,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? @@ -73,23 +74,23 @@ extension SmartCard { extension SmartCard.Store { fileprivate func smartcardRemoved(for tokenID: String? = nil) { - id = nil + self.tokenID = nil reloadSecrets() } fileprivate func reloadSecrets() { DispatchQueue.main.async { - self.isAvailable = self.id != nil + 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, + kSecAttrTokenID: tokenID, kSecReturnRef: true, kSecMatchLimit: kSecMatchLimitAll, kSecReturnAttributes: true @@ -99,12 +100,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..bce2499 100644 --- a/Secretive.xcodeproj/project.pbxproj +++ b/Secretive.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 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 */; }; 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 +173,7 @@ 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 = ""; }; 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 = ""; }; @@ -327,6 +329,7 @@ 50617DAA23FCE4AB0099B055 /* SecretKit.h */, 50617DCA23FCECA10099B055 /* Secret.swift */, 50617DC623FCE4EA0099B055 /* SecretStore.swift */, + 5068389D241471CD00F55094 /* SecretStoreList.swift */, 5099A02C23FE56D70062B6F2 /* Common */, 50617DCC23FCECEE0099B055 /* SecureEnclave */, 5099A02523FE34DE0062B6F2 /* SmartCard */, @@ -731,6 +734,7 @@ 50617DC923FCE50E0099B055 /* SecureEnclaveStore.swift in Sources */, 50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */, 50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */, + 5068389E241471CD00F55094 /* SecretStoreList.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 9caaae9..300b805 100644 --- a/Secretive/AppDelegate.swift +++ b/Secretive/AppDelegate.swift @@ -7,15 +7,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! @IBOutlet var toolbar: NSToolbar! - let secureEnclave = SecureEnclave.Store() - let smartCard = SmartCard.Store() - lazy var allStores: [AnySecretStore] = { - [AnySecretStore(secureEnclave), AnySecretStore(smartCard)] + 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(secureEnclave: secureEnclave, smartCard: smartCard) + 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), @@ -27,7 +28,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { window.makeKeyAndOrderFront(nil) window.titleVisibility = .hidden window.toolbar = toolbar - if secureEnclave.isAvailable { + 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 @@ -38,7 +39,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @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 0eb07cd..951dfa6 100644 --- a/Secretive/ContentView.swift +++ b/Secretive/ContentView.swift @@ -3,57 +3,53 @@ import SecretKit struct ContentView: View { - @ObservedObject var secureEnclave: SecureEnclave.Store - @ObservedObject var smartCard: SmartCard.Store - @State var active: Data? + @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) { - if secureEnclave.isAvailable { - Section(header: Text(secureEnclave.name)) { - ForEach(secureEnclave.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") + } + } } } } } } - 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 { - 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()) .frame(minWidth: 100, idealWidth: 240) } .navigationViewStyle(DoubleColumnNavigationViewStyle()) .sheet(isPresented: $showingDeletion) { - DeleteSecretView(secret: self.deletingSecret!, store: self.secureEnclave) { - 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 16f52a0..6567697 100644 --- a/Secretive/Preview Content/PreviewStore.swift +++ b/Secretive/Preview Content/PreviewStore.swift @@ -20,6 +20,7 @@ extension Preview { class Store: SecretStore, ObservableObject { let isAvailable = true + let id = UUID() let name = "Preview Store" @Published var secrets: [Secret] = []