More progress.

This commit is contained in:
Max Goedjen
2020-03-08 20:03:40 -07:00
parent a9d7e7644e
commit 376f26ef38
11 changed files with 130 additions and 65 deletions

View File

@@ -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<T>(_ 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 {

View File

@@ -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<T>(_ secretStore: T) where T: SecretStore {
public init<SecretStoreType>(_ 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<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 {
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 {
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] = []

View File

@@ -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)
}