Availibilty

This commit is contained in:
Max Goedjen 2020-03-07 15:42:40 -08:00
parent 734df29065
commit a9d7e7644e
No known key found for this signature in database
GPG Key ID: E58C21DD77B9B8E8
6 changed files with 47 additions and 18 deletions

View File

@ -3,6 +3,7 @@ import Combine
public protocol SecretStore: ObservableObject { public protocol SecretStore: ObservableObject {
associatedtype SecretType: Secret associatedtype SecretType: Secret
var isAvailable: Bool { get }
var name: String { get } var name: String { get }
var secrets: [SecretType] { get } var secrets: [SecretType] { get }
@ -20,6 +21,7 @@ extension NSNotification.Name {
public class AnySecretStore: SecretStore { public class AnySecretStore: SecretStore {
fileprivate let base: Any fileprivate let base: Any
fileprivate let _isAvailable: () -> Bool
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
@ -27,11 +29,16 @@ public class AnySecretStore: SecretStore {
public init<T>(_ secretStore: T) where T: SecretStore { public init<T>(_ secretStore: T) where T: SecretStore {
base = secretStore base = secretStore
_isAvailable = { secretStore.isAvailable }
_name = { secretStore.name } _name = { secretStore.name }
_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! T.SecretType) }
_delete = { try secretStore.delete(secret: $0 as! T.SecretType) } _delete = { try secretStore.delete(secret: $0 as! T.SecretType) }
} }
public var isAvailable: Bool {
return _isAvailable()
}
public var name: String { public var name: String {
return _name() return _name()

View File

@ -1,10 +1,17 @@
import Foundation import Foundation
import Security import Security
import CryptoTokenKit
extension SecureEnclave { extension SecureEnclave {
public class Store: SecretStore { public class Store: SecretStore {
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 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

@ -9,6 +9,7 @@ extension SmartCard {
public class Store: SecretStore { public class Store: SecretStore {
// TODO: Read actual smart card name, eg "YubiKey 5c" // TODO: Read actual smart card name, eg "YubiKey 5c"
@Published public var isAvailable: Bool = false
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()
@ -21,10 +22,11 @@ extension SmartCard {
guard !string.contains("setoken") else { return } guard !string.contains("setoken") else { return }
self.id = string self.id = string
self.reloadSecrets() self.reloadSecrets()
self.watcher.addRemovalHandler(self.reloadSecrets, forTokenID: string) self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
} }
if let id = id { if let id = id {
self.watcher.addRemovalHandler(self.reloadSecrets, forTokenID: id) self.isAvailable = true
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: id)
} }
loadSecrets() loadSecrets()
} }
@ -70,8 +72,14 @@ extension SmartCard {
extension SmartCard.Store { extension SmartCard.Store {
fileprivate func reloadSecrets(for tokenID: String? = nil) { fileprivate func smartcardRemoved(for tokenID: String? = nil) {
id = nil
reloadSecrets()
}
fileprivate func reloadSecrets() {
DispatchQueue.main.async { DispatchQueue.main.async {
self.isAvailable = self.id != nil
self.secrets.removeAll() self.secrets.removeAll()
self.loadSecrets() self.loadSecrets()
} }

View File

@ -27,10 +27,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
window.titleVisibility = .hidden window.titleVisibility = .hidden
window.toolbar = toolbar window.toolbar = toolbar
let plus = NSTitlebarAccessoryViewController() if secureEnclave.isAvailable {
plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:))) let plus = NSTitlebarAccessoryViewController()
plus.layoutAttribute = .right plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:)))
window.addTitlebarAccessoryViewController(plus) plus.layoutAttribute = .right
window.addTitlebarAccessoryViewController(plus)
}
runSetupIfNeeded() runSetupIfNeeded()
} }

View File

@ -13,21 +13,25 @@ struct ContentView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
List(selection: $active) { List(selection: $active) {
Section(header: Text(secureEnclave.name)) { if secureEnclave.isAvailable {
ForEach(secureEnclave.secrets) { secret in Section(header: Text(secureEnclave.name)) {
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) { ForEach(secureEnclave.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") Button(action: { self.delete(secret: secret) }) {
Text("Delete")
}
} }
} }
} }
} }
Section(header: Text(smartCard.name)) { if smartCard.isAvailable {
ForEach(smartCard.secrets) { secret in Section(header: Text(smartCard.name)) {
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) { ForEach(smartCard.secrets) { secret in
Text(secret.name) NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
Text(secret.name)
}
} }
} }
} }

View File

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