import Foundation import Security import CryptoTokenKit import LocalAuthentication import SecretKit // TODO: Might need to split this up into "sub-stores?" // ie, each token has its own Store. extension SmartCard { public class Store: SecretStore { @Published public var isAvailable: Bool = false public let id = UUID() public private(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card") @Published public private(set) var secrets: [Secret] = [] private let watcher = TKTokenWatcher() private var tokenID: String? public init() { tokenID = watcher.nonSecureEnclaveTokens.first watcher.setInsertionHandler { string in guard self.tokenID == nil else { return } guard !string.contains("setoken") else { return } self.tokenID = string self.reloadSecrets() self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string) } if let tokenID = tokenID { self.isAvailable = true self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID) } loadSecrets() } // MARK: Public API public func create(name: String) throws { fatalError("Keys must be created on the smart card.") } public func delete(secret: Secret) throws { fatalError("Keys must be deleted on the smart card.") } public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData { guard let tokenID = tokenID else { fatalError() } let context = LAContext() context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\"" context.localizedCancelTitle = "Deny" let attributes = [ kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrApplicationLabel: secret.id as CFData, kSecAttrTokenID: tokenID, kSecUseAuthenticationContext: context, 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? let signatureAlgorithm: SecKeyAlgorithm switch (secret.algorithm, secret.keySize) { case (.ellipticCurve, 256): signatureAlgorithm = .ecdsaSignatureMessageX962SHA256 case (.ellipticCurve, 384): signatureAlgorithm = .ecdsaSignatureMessageX962SHA384 default: fatalError() } guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else { throw SigningError(error: signError) } return SignedData(data: signature as Data, requiredAuthentication: false) } public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws { } } } extension SmartCard.Store { private func smartcardRemoved(for tokenID: String? = nil) { self.tokenID = nil reloadSecrets() } private func reloadSecrets() { DispatchQueue.main.async { self.isAvailable = self.tokenID != nil self.secrets.removeAll() self.loadSecrets() } } private func loadSecrets() { guard let tokenID = tokenID else { return } let fallbackName = NSLocalizedString("Smart Card", comment: "Smart Card") if #available(macOS 12.0, *) { if let driverName = watcher.tokenInfo(forTokenID: tokenID)?.driverName { name = driverName } else { name = fallbackName } } else { // Hack to read name if there's only one smart card let slotNames = TKSmartCardSlotManager().slotNames if watcher.nonSecureEnclaveTokens.count == 1 && slotNames.count == 1 { name = slotNames.first! } else { name = fallbackName } } let attributes = [ kSecClass: kSecClassKey, kSecAttrTokenID: tokenID, kSecAttrKeyType: kSecAttrKeyTypeEC, // Restrict to EC kSecReturnRef: true, kSecMatchLimit: kSecMatchLimitAll, kSecReturnAttributes: true ] as CFDictionary var untyped: CFTypeRef? SecItemCopyMatching(attributes, &untyped) guard let typed = untyped as? [[CFString: Any]] else { return } let wrapped: [SmartCard.Secret] = typed.map { let name = $0[kSecAttrLabel] as? String ?? "Unnamed" let tokenID = $0[kSecAttrApplicationLabel] as! Data let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber) let keySize = $0[kSecAttrKeySizeInBits] as! Int 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: tokenID, name: name, algorithm: algorithm, keySize: keySize, publicKey: publicKey) } secrets.append(contentsOf: wrapped) } } extension TKTokenWatcher { fileprivate var nonSecureEnclaveTokens: [String] { tokenIDs.filter { !$0.contains("setoken") } } } extension SmartCard { public struct KeychainError: Error { public let statusCode: OSStatus } public struct SigningError: Error { public let error: SecurityError? } } extension SmartCard { public typealias SecurityError = Unmanaged }