Availibilty
This commit is contained in:
parent
734df29065
commit
a9d7e7644e
|
@ -3,6 +3,7 @@ import Combine
|
|||
public protocol SecretStore: ObservableObject {
|
||||
|
||||
associatedtype SecretType: Secret
|
||||
var isAvailable: Bool { get }
|
||||
var name: String { get }
|
||||
var secrets: [SecretType] { get }
|
||||
|
||||
|
@ -20,6 +21,7 @@ extension NSNotification.Name {
|
|||
public class AnySecretStore: SecretStore {
|
||||
|
||||
fileprivate let base: Any
|
||||
fileprivate let _isAvailable: () -> Bool
|
||||
fileprivate let _name: () -> String
|
||||
fileprivate let _secrets: () -> [AnySecret]
|
||||
fileprivate let _sign: (Data, AnySecret) throws -> Data
|
||||
|
@ -27,12 +29,17 @@ public class AnySecretStore: SecretStore {
|
|||
|
||||
public init<T>(_ secretStore: T) where T: SecretStore {
|
||||
base = secretStore
|
||||
_isAvailable = { secretStore.isAvailable }
|
||||
_name = { secretStore.name }
|
||||
_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) }
|
||||
}
|
||||
|
||||
public var isAvailable: Bool {
|
||||
return _isAvailable()
|
||||
}
|
||||
|
||||
public var name: String {
|
||||
return _name()
|
||||
}
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import Foundation
|
||||
import Security
|
||||
import CryptoTokenKit
|
||||
|
||||
extension SecureEnclave {
|
||||
|
||||
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")
|
||||
@Published public fileprivate(set) var secrets: [Secret] = []
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ extension SmartCard {
|
|||
public class Store: SecretStore {
|
||||
|
||||
// TODO: Read actual smart card name, eg "YubiKey 5c"
|
||||
@Published public var isAvailable: Bool = false
|
||||
public let name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
||||
@Published public fileprivate(set) var secrets: [Secret] = []
|
||||
fileprivate let watcher = TKTokenWatcher()
|
||||
|
@ -21,10 +22,11 @@ extension SmartCard {
|
|||
guard !string.contains("setoken") else { return }
|
||||
self.id = string
|
||||
self.reloadSecrets()
|
||||
self.watcher.addRemovalHandler(self.reloadSecrets, forTokenID: string)
|
||||
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
|
||||
}
|
||||
if let id = id {
|
||||
self.watcher.addRemovalHandler(self.reloadSecrets, forTokenID: id)
|
||||
self.isAvailable = true
|
||||
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: id)
|
||||
}
|
||||
loadSecrets()
|
||||
}
|
||||
|
@ -70,8 +72,14 @@ extension SmartCard {
|
|||
|
||||
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 {
|
||||
self.isAvailable = self.id != nil
|
||||
self.secrets.removeAll()
|
||||
self.loadSecrets()
|
||||
}
|
||||
|
|
|
@ -27,10 +27,12 @@ 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 secureEnclave.isAvailable {
|
||||
let plus = NSTitlebarAccessoryViewController()
|
||||
plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:)))
|
||||
plus.layoutAttribute = .right
|
||||
window.addTitlebarAccessoryViewController(plus)
|
||||
}
|
||||
runSetupIfNeeded()
|
||||
}
|
||||
|
||||
|
|
|
@ -13,21 +13,25 @@ struct ContentView: View {
|
|||
var body: some View {
|
||||
NavigationView {
|
||||
List(selection: $active) {
|
||||
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")
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(header: Text(smartCard.name)) {
|
||||
ForEach(smartCard.secrets) { secret in
|
||||
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
|
||||
Text(secret.name)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ extension Preview {
|
|||
|
||||
class Store: SecretStore, ObservableObject {
|
||||
|
||||
let isAvailable = true
|
||||
let name = "Preview Store"
|
||||
@Published var secrets: [Secret] = []
|
||||
|
||||
|
|
Loading…
Reference in New Issue