diff --git a/SecretAgent/AppDelegate.swift b/SecretAgent/AppDelegate.swift index 5421581..1791fa9 100644 --- a/SecretAgent/AppDelegate.swift +++ b/SecretAgent/AppDelegate.swift @@ -5,7 +5,7 @@ import OSLog @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { - let store = SecureEnclave.Store() + let store = SmartCard.Store() let notifier = Notifier() lazy var agent: Agent = { Agent(store: store, notifier: notifier) diff --git a/SecretAgent/SecretAgent.entitlements b/SecretAgent/SecretAgent.entitlements index 7f8f6ca..6d010b7 100644 --- a/SecretAgent/SecretAgent.entitlements +++ b/SecretAgent/SecretAgent.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.temporary-exception.mach-lookup.global-name + + com.apple.ctkd.watcher-client + keychain-access-groups $(AppIdentifierPrefix)com.maxgoedjen.Secretive diff --git a/SecretKit/SmartCard/SmartCardStore.swift b/SecretKit/SmartCard/SmartCardStore.swift index da243c6..c5e7f02 100644 --- a/SecretKit/SmartCard/SmartCardStore.swift +++ b/SecretKit/SmartCard/SmartCardStore.swift @@ -2,6 +2,8 @@ import Foundation import Security import CryptoTokenKit +// TODO: Might need to split this up into "sub-stores?" +// ie, each token has its own Store. extension SmartCard { public class Store: SecretStore { @@ -10,20 +12,102 @@ extension SmartCard { public let name = NSLocalizedString("Smart Card", comment: "Smart Card") @Published public fileprivate(set) var secrets: [Secret] = [] fileprivate let watcher = TKTokenWatcher() + fileprivate var id: String? public init() { - watcher.setInsertionHandler { (string) in - print(string) + id = watcher.tokenIDs.filter { !$0.contains("setoken") }.first + watcher.setInsertionHandler { string in + guard self.id == nil else { return } + guard !string.contains("setoken") else { return } + self.id = string + self.secrets.removeAll() + self.loadSecrets() } - print(watcher.tokenIDs) + loadSecrets() } - public func sign(data: Data, with secret: SmartCard.Secret) throws -> Data { - fatalError() + // MARK: Public API + + public func create(name: String) throws { + fatalError("Keys must be created on the smart card.") } - public func delete(secret: SmartCard.Secret) throws { + public func delete(secret: Secret) throws { + fatalError("Keys must be deleted on the smart card.") + } + + public func sign(data: Data, with secret: SecretType) throws -> Data { + guard let id = id else { fatalError() } + let attributes = [ + kSecClass: kSecClassKey, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + kSecAttrApplicationLabel: secret.id as CFData, + kSecAttrTokenID: id, + kSecReturnRef: true + ] as CFDictionary + var untyped: CFTypeRef? + let status = SecItemCopyMatching(attributes, &untyped) + if status != errSecSuccess { + throw KeychainError(statusCode: status) + } + guard let untypedSafe = untyped else { + throw KeychainError(statusCode: errSecSuccess) + } + let key = untypedSafe as! SecKey + var signError: SecurityError? + guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else { + throw SigningError(error: signError) + } + return signature as Data } } + +} + +extension SmartCard.Store { + + fileprivate func loadSecrets() { + guard let id = id else { return } + let attributes = [ + kSecClass: kSecClassKey, + kSecAttrTokenID: id, + kSecReturnRef: true, + kSecMatchLimit: kSecMatchLimitAll, + kSecReturnAttributes: true + ] as CFDictionary + var untyped: CFTypeRef? + let status = SecItemCopyMatching(attributes, &untyped) + print(status) + 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 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) + } + secrets.append(contentsOf: wrapped) + } + +} + +extension SmartCard { + + public struct KeychainError: Error { + public let statusCode: OSStatus + } + + public struct SigningError: Error { + public let error: SecurityError? + } + +} + +extension SmartCard { + + public typealias SecurityError = Unmanaged + } diff --git a/Secretive/AppDelegate.swift b/Secretive/AppDelegate.swift index ee03f86..420f4ae 100644 --- a/Secretive/AppDelegate.swift +++ b/Secretive/AppDelegate.swift @@ -12,7 +12,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { - let contentView = ContentView(store: secureEnclave) + let contentView = ContentView(store: yubikey) // Create the window and set the content view. window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), diff --git a/Secretive/Secretive.entitlements b/Secretive/Secretive.entitlements index 7f8f6ca..6d010b7 100644 --- a/Secretive/Secretive.entitlements +++ b/Secretive/Secretive.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.temporary-exception.mach-lookup.global-name + + com.apple.ctkd.watcher-client + keychain-access-groups $(AppIdentifierPrefix)com.maxgoedjen.Secretive